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/dist/server.js ADDED
@@ -0,0 +1,2915 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+ var _chunkFXVD4Y5Gjs = require('./chunk-FXVD4Y5G.js');
17
+
18
+ // src/server/jwt-validator.ts
19
+ var JwtValidator = class {
20
+ constructor(config) {
21
+ this.jwksCache = null;
22
+ this.jwksCacheTime = 0;
23
+ this.fetchPromise = null;
24
+ if (!config.authVitalHost) {
25
+ throw new Error(
26
+ "authVitalHost is required. Pass it in config or set AV_HOST environment variable."
27
+ );
28
+ }
29
+ this.config = {
30
+ authVitalHost: config.authVitalHost.replace(/\/$/, ""),
31
+ // Remove trailing slash
32
+ cacheTtl: _nullishCoalesce(config.cacheTtl, () => ( 3600)),
33
+ audience: config.audience,
34
+ issuer: _nullishCoalesce(config.issuer, () => ( config.authVitalHost.replace(/\/$/, "")))
35
+ };
36
+ }
37
+ /**
38
+ * Get the JWKS URL for this IDP
39
+ */
40
+ getJwksUrl() {
41
+ return `${this.config.authVitalHost}/.well-known/jwks.json`;
42
+ }
43
+ /**
44
+ * Get the OpenID Configuration URL
45
+ */
46
+ getOpenIdConfigUrl() {
47
+ return `${this.config.authVitalHost}/.well-known/openid-configuration`;
48
+ }
49
+ /**
50
+ * Fetch public keys from the JWKS endpoint
51
+ * Results are cached according to cacheTtl
52
+ */
53
+ async getPublicKeys(forceRefresh = false) {
54
+ const now = Date.now();
55
+ const cacheExpired = now - this.jwksCacheTime > this.config.cacheTtl * 1e3;
56
+ if (!forceRefresh && this.jwksCache && !cacheExpired) {
57
+ return this.jwksCache;
58
+ }
59
+ if (this.fetchPromise) {
60
+ return this.fetchPromise;
61
+ }
62
+ this.fetchPromise = this.fetchJwks();
63
+ try {
64
+ const jwks = await this.fetchPromise;
65
+ this.jwksCache = jwks;
66
+ this.jwksCacheTime = now;
67
+ return jwks;
68
+ } finally {
69
+ this.fetchPromise = null;
70
+ }
71
+ }
72
+ /**
73
+ * Fetch JWKS from the IDP
74
+ */
75
+ async fetchJwks() {
76
+ const response = await fetch(this.getJwksUrl());
77
+ if (!response.ok) {
78
+ throw new Error(`Failed to fetch JWKS: ${response.status} ${response.statusText}`);
79
+ }
80
+ const jwks = await response.json();
81
+ if (!jwks.keys || !Array.isArray(jwks.keys)) {
82
+ throw new Error("Invalid JWKS response: missing keys array");
83
+ }
84
+ return jwks;
85
+ }
86
+ /**
87
+ * Find a key by its ID (kid)
88
+ */
89
+ async findKey(kid, allowRefresh = true) {
90
+ const jwks = await this.getPublicKeys();
91
+ let key = jwks.keys.find((k) => k.kid === kid);
92
+ if (!key && allowRefresh) {
93
+ const refreshedJwks = await this.getPublicKeys(true);
94
+ key = refreshedJwks.keys.find((k) => k.kid === kid);
95
+ }
96
+ return key || null;
97
+ }
98
+ /**
99
+ * Decode a JWT without verification (to get header/payload)
100
+ */
101
+ decodeToken(token) {
102
+ try {
103
+ const parts = token.split(".");
104
+ if (parts.length !== 3) return null;
105
+ const header = JSON.parse(this.base64UrlDecode(parts[0]));
106
+ const payload = JSON.parse(this.base64UrlDecode(parts[1]));
107
+ return { header, payload };
108
+ } catch (e2) {
109
+ return null;
110
+ }
111
+ }
112
+ /**
113
+ * Validate a JWT token
114
+ *
115
+ * @param token - The JWT to validate
116
+ * @returns Validation result with payload if valid
117
+ */
118
+ async validateToken(token) {
119
+ try {
120
+ const decoded = this.decodeToken(token);
121
+ if (!decoded) {
122
+ return { valid: false, error: "Invalid token format" };
123
+ }
124
+ const { header, payload } = decoded;
125
+ if (header.alg !== "RS256") {
126
+ return { valid: false, error: `Unsupported algorithm: ${header.alg}` };
127
+ }
128
+ if (!header.kid) {
129
+ return { valid: false, error: "Token missing kid header" };
130
+ }
131
+ const key = await this.findKey(header.kid);
132
+ if (!key) {
133
+ return { valid: false, error: `Unknown signing key: ${header.kid}` };
134
+ }
135
+ const isValid = await this.verifySignature(token, key);
136
+ if (!isValid) {
137
+ return { valid: false, error: "Invalid signature" };
138
+ }
139
+ if (payload.exp && payload.exp < Math.floor(Date.now() / 1e3)) {
140
+ return { valid: false, error: "Token expired" };
141
+ }
142
+ if (payload.nbf && payload.nbf > Math.floor(Date.now() / 1e3)) {
143
+ return { valid: false, error: "Token not yet valid" };
144
+ }
145
+ if (payload.iss !== this.config.issuer) {
146
+ return { valid: false, error: `Invalid issuer: expected ${this.config.issuer}, got ${payload.iss}` };
147
+ }
148
+ if (this.config.audience) {
149
+ const audiences = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
150
+ if (!audiences.includes(this.config.audience)) {
151
+ return { valid: false, error: `Invalid audience: ${payload.aud}` };
152
+ }
153
+ }
154
+ return { valid: true, payload };
155
+ } catch (error) {
156
+ return {
157
+ valid: false,
158
+ error: error instanceof Error ? error.message : "Token validation failed"
159
+ };
160
+ }
161
+ }
162
+ /**
163
+ * Verify RS256 signature using Web Crypto API
164
+ */
165
+ async verifySignature(token, jwk) {
166
+ const parts = token.split(".");
167
+ if (parts.length !== 3) return false;
168
+ const [headerB64, payloadB64, signatureB64] = parts;
169
+ const data = new TextEncoder().encode(`${headerB64}.${payloadB64}`);
170
+ const signatureBytes = this.base64UrlDecodeToBuffer(signatureB64);
171
+ const signature = new Uint8Array(signatureBytes).buffer;
172
+ const cryptoKey = await crypto.subtle.importKey(
173
+ "jwk",
174
+ {
175
+ kty: jwk.kty,
176
+ n: jwk.n,
177
+ e: jwk.e,
178
+ alg: "RS256",
179
+ use: "sig"
180
+ },
181
+ {
182
+ name: "RSASSA-PKCS1-v1_5",
183
+ hash: "SHA-256"
184
+ },
185
+ false,
186
+ ["verify"]
187
+ );
188
+ return crypto.subtle.verify(
189
+ "RSASSA-PKCS1-v1_5",
190
+ cryptoKey,
191
+ signature,
192
+ data
193
+ );
194
+ }
195
+ /**
196
+ * Base64URL decode to string
197
+ */
198
+ base64UrlDecode(str) {
199
+ const padded = str + "===".slice(0, (4 - str.length % 4) % 4);
200
+ const base64 = padded.replace(/-/g, "+").replace(/_/g, "/");
201
+ if (typeof Buffer !== "undefined") {
202
+ return Buffer.from(base64, "base64").toString("utf-8");
203
+ }
204
+ return decodeURIComponent(
205
+ atob(base64).split("").map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2)).join("")
206
+ );
207
+ }
208
+ /**
209
+ * Base64URL decode to Uint8Array
210
+ */
211
+ base64UrlDecodeToBuffer(str) {
212
+ const padded = str + "===".slice(0, (4 - str.length % 4) % 4);
213
+ const base64 = padded.replace(/-/g, "+").replace(/_/g, "/");
214
+ if (typeof Buffer !== "undefined") {
215
+ return new Uint8Array(Buffer.from(base64, "base64"));
216
+ }
217
+ const binary = atob(base64);
218
+ const bytes = new Uint8Array(binary.length);
219
+ for (let i = 0; i < binary.length; i++) {
220
+ bytes[i] = binary.charCodeAt(i);
221
+ }
222
+ return bytes;
223
+ }
224
+ /**
225
+ * Clear the JWKS cache (useful for testing or forced refresh)
226
+ */
227
+ clearCache() {
228
+ this.jwksCache = null;
229
+ this.jwksCacheTime = 0;
230
+ }
231
+ // ===========================================================================
232
+ // PERMISSION HELPER METHODS
233
+ // ===========================================================================
234
+ /**
235
+ * Check if the JWT payload has a specific tenant permission
236
+ *
237
+ * @param payload - Decoded JWT payload
238
+ * @param permission - Permission to check (e.g., 'licenses:manage')
239
+ * @returns true if user has the permission (wildcards supported)
240
+ *
241
+ * @example
242
+ * ```ts
243
+ * const { user } = await validator.getCurrentUser(authHeader);
244
+ * if (validator.hasTenantPermission(user, 'licenses:manage')) {
245
+ * // User can manage licenses
246
+ * }
247
+ * ```
248
+ */
249
+ hasTenantPermission(payload, permission) {
250
+ const permissions = payload.tenant_permissions;
251
+ if (!permissions) return false;
252
+ return permissions.some(
253
+ (p) => p === permission || this.matchesWildcard(p, permission)
254
+ );
255
+ }
256
+ /**
257
+ * Check if the JWT payload has a specific app permission
258
+ *
259
+ * @param payload - Decoded JWT payload
260
+ * @param permission - Permission to check (e.g., 'projects:create')
261
+ * @returns true if user has the permission (wildcards supported)
262
+ *
263
+ * @example
264
+ * ```ts
265
+ * const { user } = await validator.getCurrentUser(authHeader);
266
+ * if (validator.hasAppPermission(user, 'projects:create')) {
267
+ * // User can create projects
268
+ * }
269
+ * ```
270
+ */
271
+ hasAppPermission(payload, permission) {
272
+ const permissions = payload.app_permissions;
273
+ if (!permissions) return false;
274
+ return permissions.some(
275
+ (p) => p === permission || this.matchesWildcard(p, permission)
276
+ );
277
+ }
278
+ /**
279
+ * Check if the JWT payload has a specific feature enabled
280
+ *
281
+ * This reads from the `license.features` array in the JWT.
282
+ * No API call needed - feature information is embedded in the token!
283
+ *
284
+ * @param payload - Decoded JWT payload
285
+ * @param featureKey - Feature to check (e.g., 'sso', 'audit_logs')
286
+ * @returns true if feature is enabled
287
+ *
288
+ * @example
289
+ * ```ts
290
+ * const { user } = await validator.getCurrentUser(authHeader);
291
+ * if (validator.hasFeature(user, 'sso')) {
292
+ * // User's tenant has SSO enabled
293
+ * }
294
+ * ```
295
+ */
296
+ hasFeature(payload, featureKey) {
297
+ const license = payload.license;
298
+ return _nullishCoalesce(_optionalChain([license, 'optionalAccess', _ => _.features, 'optionalAccess', _2 => _2.includes, 'call', _3 => _3(featureKey)]), () => ( false));
299
+ }
300
+ /**
301
+ * Get the license type from JWT payload
302
+ *
303
+ * @param payload - Decoded JWT payload
304
+ * @returns License type slug (e.g., 'pro', 'enterprise') or null
305
+ *
306
+ * @example
307
+ * ```ts
308
+ * const { user } = await validator.getCurrentUser(authHeader);
309
+ * const licenseType = validator.getLicenseType(user);
310
+ * if (licenseType === 'enterprise') {
311
+ * // Show enterprise features
312
+ * }
313
+ * ```
314
+ */
315
+ getLicenseType(payload) {
316
+ const license = payload.license;
317
+ return _nullishCoalesce(_optionalChain([license, 'optionalAccess', _4 => _4.type]), () => ( null));
318
+ }
319
+ /**
320
+ * Check if wildcard pattern matches permission
321
+ *
322
+ * @example
323
+ * - '*' matches everything
324
+ * - 'licenses:*' matches 'licenses:manage', 'licenses:view', etc.
325
+ * - 'licenses:manage' only matches 'licenses:manage'
326
+ */
327
+ matchesWildcard(pattern, permission) {
328
+ if (pattern === "*") return true;
329
+ if (!pattern.includes("*")) return pattern === permission;
330
+ const [patternResource] = pattern.split(":");
331
+ const [permResource] = permission.split(":");
332
+ if (pattern.endsWith(":*")) {
333
+ return patternResource === permResource;
334
+ }
335
+ return false;
336
+ }
337
+ /**
338
+ * Get the current user from an Authorization header.
339
+ * This is the helper for implementing `/api/auth/me` endpoints.
340
+ *
341
+ * - Does NOT call the IDP
342
+ * - Validates JWT signature using cached JWKS
343
+ * - Returns the decoded JWT payload
344
+ *
345
+ * @example
346
+ * ```ts
347
+ * const validator = createJwtValidator({ authVitalHost: process.env.AV_HOST });
348
+ *
349
+ * // GET /api/auth/me
350
+ * app.get('/api/auth/me', async (req, res) => {
351
+ * const result = await validator.getCurrentUser(req.headers.authorization);
352
+ *
353
+ * if (!result.authenticated) {
354
+ * return res.status(401).json({ error: result.error });
355
+ * }
356
+ *
357
+ * res.json(result.user);
358
+ * });
359
+ * ```
360
+ */
361
+ async getCurrentUser(authorizationHeader) {
362
+ if (!authorizationHeader) {
363
+ return {
364
+ authenticated: false,
365
+ user: null,
366
+ error: "Missing Authorization header"
367
+ };
368
+ }
369
+ if (!authorizationHeader.startsWith("Bearer ")) {
370
+ return {
371
+ authenticated: false,
372
+ user: null,
373
+ error: "Invalid Authorization header format (expected Bearer token)"
374
+ };
375
+ }
376
+ const token = authorizationHeader.slice(7);
377
+ if (!token) {
378
+ return {
379
+ authenticated: false,
380
+ user: null,
381
+ error: "Empty token"
382
+ };
383
+ }
384
+ const validationResult = await this.validateToken(token);
385
+ if (!validationResult.valid) {
386
+ return {
387
+ authenticated: false,
388
+ user: null,
389
+ error: validationResult.error
390
+ };
391
+ }
392
+ return {
393
+ authenticated: true,
394
+ user: validationResult.payload
395
+ };
396
+ }
397
+ };
398
+ function createJwtValidator(config) {
399
+ return new JwtValidator(config);
400
+ }
401
+ async function getCurrentUser(authorizationHeader, validator) {
402
+ if (!authorizationHeader) {
403
+ return {
404
+ authenticated: false,
405
+ user: null,
406
+ error: "Missing Authorization header"
407
+ };
408
+ }
409
+ if (!authorizationHeader.startsWith("Bearer ")) {
410
+ return {
411
+ authenticated: false,
412
+ user: null,
413
+ error: "Invalid Authorization header format (expected Bearer token)"
414
+ };
415
+ }
416
+ const token = authorizationHeader.slice(7);
417
+ if (!token) {
418
+ return {
419
+ authenticated: false,
420
+ user: null,
421
+ error: "Empty token"
422
+ };
423
+ }
424
+ const validationResult = await validator.validateToken(token);
425
+ if (!validationResult.valid) {
426
+ return {
427
+ authenticated: false,
428
+ user: null,
429
+ error: validationResult.error
430
+ };
431
+ }
432
+ return {
433
+ authenticated: true,
434
+ user: validationResult.payload
435
+ };
436
+ }
437
+ async function getCurrentUserFromConfig(authorizationHeader, config) {
438
+ const validator = createJwtValidator(config);
439
+ return getCurrentUser(authorizationHeader, validator);
440
+ }
441
+ function createJwtMiddleware(config) {
442
+ const validator = createJwtValidator(config);
443
+ return async (req, res, next) => {
444
+ const authHeader = _optionalChain([req, 'access', _5 => _5.headers, 'optionalAccess', _6 => _6.authorization]);
445
+ if (!_optionalChain([authHeader, 'optionalAccess', _7 => _7.startsWith, 'call', _8 => _8("Bearer ")])) {
446
+ return res.status(401).json({ error: "Missing or invalid Authorization header" });
447
+ }
448
+ const token = authHeader.slice(7);
449
+ const result = await validator.validateToken(token);
450
+ if (!result.valid) {
451
+ return res.status(401).json({ error: result.error || "Invalid token" });
452
+ }
453
+ req.user = result.payload;
454
+ next();
455
+ };
456
+ }
457
+ async function createPassportJwtOptions(config) {
458
+ if (!config.authVitalHost) {
459
+ throw new Error(
460
+ "authVitalHost is required. Pass it in config or set AV_HOST environment variable."
461
+ );
462
+ }
463
+ const validator = createJwtValidator(config);
464
+ return {
465
+ jwtFromRequest: (req) => {
466
+ const authHeader = _optionalChain([req, 'access', _9 => _9.headers, 'optionalAccess', _10 => _10.authorization]);
467
+ if (_optionalChain([authHeader, 'optionalAccess', _11 => _11.startsWith, 'call', _12 => _12("Bearer ")])) {
468
+ return authHeader.slice(7);
469
+ }
470
+ return null;
471
+ },
472
+ secretOrKeyProvider: async (_req, rawJwt, done) => {
473
+ try {
474
+ const decoded = validator.decodeToken(rawJwt);
475
+ if (!_optionalChain([decoded, 'optionalAccess', _13 => _13.header, 'access', _14 => _14.kid])) {
476
+ return done(new Error("Token missing kid header"));
477
+ }
478
+ const jwks = await validator.getPublicKeys();
479
+ const key = jwks.keys.find((k) => k.kid === decoded.header.kid);
480
+ if (!key) {
481
+ return done(new Error(`Unknown signing key: ${decoded.header.kid}`));
482
+ }
483
+ const pem = jwkToPem(key);
484
+ done(null, pem);
485
+ } catch (error) {
486
+ done(error);
487
+ }
488
+ },
489
+ issuer: _nullishCoalesce(config.issuer, () => ( config.authVitalHost.replace(/\/$/, ""))),
490
+ audience: config.audience,
491
+ algorithms: ["RS256"]
492
+ };
493
+ }
494
+ function jwkToPem(jwk) {
495
+ if (jwk.kty !== "RSA" || !jwk.n || !jwk.e) {
496
+ throw new Error("Only RSA keys are supported");
497
+ }
498
+ const n = base64UrlToBase64(jwk.n);
499
+ const e = base64UrlToBase64(jwk.e);
500
+ const nBytes = Buffer.from(n, "base64");
501
+ const eBytes = Buffer.from(e, "base64");
502
+ const rsaPublicKey = Buffer.concat([
503
+ Buffer.from([48]),
504
+ // SEQUENCE
505
+ encodeLength(nBytes.length + eBytes.length + 4 + (nBytes[0] & 128 ? 1 : 0) + (eBytes[0] & 128 ? 1 : 0)),
506
+ Buffer.from([2]),
507
+ // INTEGER (modulus)
508
+ encodeLength(nBytes.length + (nBytes[0] & 128 ? 1 : 0)),
509
+ nBytes[0] & 128 ? Buffer.from([0]) : Buffer.alloc(0),
510
+ nBytes,
511
+ Buffer.from([2]),
512
+ // INTEGER (exponent)
513
+ encodeLength(eBytes.length + (eBytes[0] & 128 ? 1 : 0)),
514
+ eBytes[0] & 128 ? Buffer.from([0]) : Buffer.alloc(0),
515
+ eBytes
516
+ ]);
517
+ const rsaOid = Buffer.from([48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0]);
518
+ const bitString = Buffer.concat([
519
+ Buffer.from([3]),
520
+ encodeLength(rsaPublicKey.length + 1),
521
+ Buffer.from([0]),
522
+ rsaPublicKey
523
+ ]);
524
+ const spki = Buffer.concat([
525
+ Buffer.from([48]),
526
+ encodeLength(rsaOid.length + bitString.length),
527
+ rsaOid,
528
+ bitString
529
+ ]);
530
+ const base64 = spki.toString("base64");
531
+ const lines = base64.match(/.{1,64}/g) || [];
532
+ return `-----BEGIN PUBLIC KEY-----
533
+ ${lines.join("\n")}
534
+ -----END PUBLIC KEY-----`;
535
+ }
536
+ function base64UrlToBase64(str) {
537
+ const padded = str + "===".slice(0, (4 - str.length % 4) % 4);
538
+ return padded.replace(/-/g, "+").replace(/_/g, "/");
539
+ }
540
+ function encodeLength(len) {
541
+ if (len < 128) {
542
+ return Buffer.from([len]);
543
+ }
544
+ const bytes = [];
545
+ while (len > 0) {
546
+ bytes.unshift(len & 255);
547
+ len >>= 8;
548
+ }
549
+ return Buffer.from([128 | bytes.length, ...bytes]);
550
+ }
551
+
552
+ // src/server/base-client.ts
553
+ function extractAuthorizationHeader(request) {
554
+ const headers = request.headers;
555
+ if (headers && typeof headers.get === "function") {
556
+ return headers.get("authorization") || headers.get("Authorization");
557
+ }
558
+ if (headers && typeof headers === "object") {
559
+ return headers.authorization || headers.Authorization || null;
560
+ }
561
+ return null;
562
+ }
563
+ function appendClientIdToUri(uri, clientId) {
564
+ if (!uri) return null;
565
+ const separator = uri.includes("?") ? "&" : "?";
566
+ return `${uri}${separator}client_id=${encodeURIComponent(clientId)}`;
567
+ }
568
+ var BaseClient = class {
569
+ constructor(config) {
570
+ this.accessToken = null;
571
+ this.tokenExpiresAt = 0;
572
+ if (!config.authVitalHost) {
573
+ throw new Error(
574
+ "authVitalHost is required. Pass it in config or set AV_HOST environment variable."
575
+ );
576
+ }
577
+ this.config = {
578
+ ...config,
579
+ authVitalHost: config.authVitalHost.replace(/\/$/, "")
580
+ // Remove trailing slash
581
+ };
582
+ this.jwtValidator = new JwtValidator({
583
+ authVitalHost: this.config.authVitalHost,
584
+ cacheTtl: config.jwksCacheTtl,
585
+ audience: _nullishCoalesce(config.audience, () => ( config.clientId))
586
+ });
587
+ }
588
+ // ===========================================================================
589
+ // GET CURRENT USER (JWT Validation)
590
+ // ===========================================================================
591
+ /**
592
+ * Validate JWT from an incoming request and return the decoded user.
593
+ *
594
+ * - Extracts Authorization header from the request
595
+ * - Validates JWT signature using cached JWKS (public endpoint, no auth needed)
596
+ * - Returns decoded JWT payload
597
+ * - Does NOT call the IDP
598
+ *
599
+ * @example
600
+ * ```ts
601
+ * // Express
602
+ * app.get('/api/auth/me', async (req, res) => {
603
+ * const { authenticated, user, error } = await authvital.getCurrentUser(req);
604
+ * if (!authenticated) return res.status(401).json({ error });
605
+ * res.json(user);
606
+ * });
607
+ * ```
608
+ */
609
+ async getCurrentUser(request) {
610
+ const authHeader = extractAuthorizationHeader(request);
611
+ if (!authHeader) {
612
+ return {
613
+ authenticated: false,
614
+ user: null,
615
+ error: "Missing Authorization header"
616
+ };
617
+ }
618
+ if (!authHeader.startsWith("Bearer ")) {
619
+ return {
620
+ authenticated: false,
621
+ user: null,
622
+ error: "Invalid Authorization header format (expected Bearer token)"
623
+ };
624
+ }
625
+ const token = authHeader.slice(7);
626
+ if (!token) {
627
+ return {
628
+ authenticated: false,
629
+ user: null,
630
+ error: "Empty token"
631
+ };
632
+ }
633
+ const result = await this.jwtValidator.validateToken(token);
634
+ if (!result.valid) {
635
+ return {
636
+ authenticated: false,
637
+ user: null,
638
+ error: result.error
639
+ };
640
+ }
641
+ return {
642
+ authenticated: true,
643
+ user: result.payload
644
+ };
645
+ }
646
+ // ===========================================================================
647
+ // VALIDATE REQUEST & EXTRACT CLAIMS
648
+ // ===========================================================================
649
+ /**
650
+ * Validate the JWT from an incoming request and extract key claims.
651
+ *
652
+ * This is the recommended way to get user/tenant context for API calls.
653
+ * Throws an error if the token is invalid or missing required claims.
654
+ *
655
+ * @param request - The incoming HTTP request (Express, Next.js, Fetch API, etc.)
656
+ * @returns Validated claims including sub (userId) and tenantId
657
+ * @throws Error if token is invalid or missing tenant_id claim
658
+ *
659
+ * @example
660
+ * ```ts
661
+ * // Express
662
+ * app.get('/api/members', async (req, res) => {
663
+ * const claims = await authvital.validateRequest(req);
664
+ * const members = await authvital.memberships.listForApplication(claims);
665
+ * res.json(members);
666
+ * });
667
+ * ```
668
+ */
669
+ async validateRequest(request) {
670
+ const { authenticated, user, error } = await this.getCurrentUser(request);
671
+ if (!authenticated || !user) {
672
+ throw new Error(error || "Unauthorized");
673
+ }
674
+ if (!user.sub) {
675
+ throw new Error("Invalid token: missing sub claim");
676
+ }
677
+ const tenantId = user.tenant_id;
678
+ if (!tenantId) {
679
+ throw new Error(
680
+ "Invalid token: missing tenant_id claim. Ensure the token was issued for a specific tenant."
681
+ );
682
+ }
683
+ return {
684
+ sub: user.sub,
685
+ tenantId,
686
+ tenantSubdomain: user.tenant_subdomain,
687
+ email: user.email,
688
+ payload: user
689
+ };
690
+ }
691
+ // ===========================================================================
692
+ // INTERNAL: Token Management for M2M calls
693
+ // ===========================================================================
694
+ /**
695
+ * Get M2M access token (client_credentials flow)
696
+ *
697
+ * Returns cached token if still valid, otherwise fetches a new one.
698
+ */
699
+ async getAccessToken() {
700
+ if (this.accessToken && Date.now() < this.tokenExpiresAt - 6e4) {
701
+ return this.accessToken;
702
+ }
703
+ const response = await fetch(`${this.config.authVitalHost}/oauth/token`, {
704
+ method: "POST",
705
+ headers: { "Content-Type": "application/json" },
706
+ body: JSON.stringify({
707
+ grant_type: "client_credentials",
708
+ client_id: this.config.clientId,
709
+ client_secret: this.config.clientSecret,
710
+ scope: "system:admin"
711
+ })
712
+ });
713
+ if (!response.ok) {
714
+ const error = await response.json().catch(() => ({}));
715
+ throw new Error(
716
+ error.message || `Token exchange failed: ${response.status}`
717
+ );
718
+ }
719
+ const data = await response.json();
720
+ this.accessToken = data.access_token;
721
+ this.tokenExpiresAt = Date.now() + data.expires_in * 1e3;
722
+ return this.accessToken;
723
+ }
724
+ /**
725
+ * Make an M2M authenticated request
726
+ *
727
+ * Uses client_credentials token for machine-to-machine calls.
728
+ */
729
+ async request(method, path, body, isRetry = false) {
730
+ const token = await this.getAccessToken();
731
+ const response = await fetch(`${this.config.authVitalHost}${path}`, {
732
+ method,
733
+ headers: {
734
+ Authorization: `Bearer ${token}`,
735
+ "Content-Type": "application/json"
736
+ },
737
+ body: body ? JSON.stringify(body) : void 0
738
+ });
739
+ if (response.status === 401 && !isRetry) {
740
+ this.accessToken = null;
741
+ this.tokenExpiresAt = 0;
742
+ return this.request(method, path, body, true);
743
+ }
744
+ if (!response.ok) {
745
+ const error = await response.json().catch(() => ({}));
746
+ throw new Error(
747
+ error.message || `Request failed: ${response.status}`
748
+ );
749
+ }
750
+ return response.json();
751
+ }
752
+ /**
753
+ * Make an authenticated request using the JWT from the original request
754
+ *
755
+ * This version forwards the user's JWT token instead of using the
756
+ * M2M (client_credentials) token. Used for endpoints that require
757
+ * user context and validate tenant permissions.
758
+ *
759
+ * @param originalRequest - The incoming request with JWT
760
+ * @param method - HTTP method
761
+ * @param path - API path
762
+ * @param body - Request body (for POST/PUT)
763
+ * @returns Parsed response
764
+ * @throws Error if request fails or JWT is required
765
+ */
766
+ async authenticatedRequest(originalRequest, method, path, body) {
767
+ const authHeader = extractAuthorizationHeader(originalRequest);
768
+ if (!authHeader) {
769
+ throw new Error("No Authorization header found in request");
770
+ }
771
+ const options = {
772
+ method,
773
+ headers: {
774
+ "Content-Type": "application/json",
775
+ Authorization: authHeader
776
+ }
777
+ };
778
+ if (body && method !== "GET") {
779
+ options.body = JSON.stringify(body);
780
+ }
781
+ const response = await fetch(`${this.config.authVitalHost}${path}`, options);
782
+ if (!response.ok) {
783
+ const error = await response.text();
784
+ try {
785
+ const json = JSON.parse(error);
786
+ throw new Error(
787
+ json.message || `Request failed: ${response.status} ${error}`
788
+ );
789
+ } catch (e3) {
790
+ throw new Error(`Request failed: ${response.status} ${error}`);
791
+ }
792
+ }
793
+ return response.json();
794
+ }
795
+ };
796
+
797
+ // src/server/namespaces/invitations.ts
798
+ function createInvitationsNamespace(client) {
799
+ return {
800
+ /**
801
+ * Send an invitation to join a tenant
802
+ *
803
+ * Automatically validates JWT and uses tenantId from it.
804
+ * If a user with this email already exists, uses that user.
805
+ * If not, creates a new user with the provided details.
806
+ * Returns only the user's sub (ID) and invitation expiry for security.
807
+ *
808
+ * @example
809
+ * ```ts
810
+ * // First, get the role ID from available roles
811
+ * const { roles } = await authvital.memberships.getTenantRoles();
812
+ * const adminRole = roles.find(r => r.slug === 'admin');
813
+ *
814
+ * const { sub, expiresAt } = await authvital.invitations.send(request, {
815
+ * email: 'newuser@example.com',
816
+ * givenName: 'John',
817
+ * familyName: 'Doe',
818
+ * roleId: adminRole?.id, // Use role ID, not slug
819
+ * });
820
+ * // sub = user's ID (can be used in your app's database)
821
+ * // expiresAt = when the invitation expires
822
+ * ```
823
+ */
824
+ send: async (request, params) => {
825
+ const claims = await client.validateRequest(request);
826
+ return client.request("POST", "/api/integration/invitations/send", {
827
+ ...params,
828
+ tenantId: claims.tenantId,
829
+ // Auto-include clientId from SDK config (allows redirect after invite acceptance)
830
+ // Can be overridden by explicitly passing clientId in params
831
+ clientId: _nullishCoalesce(params.clientId, () => ( client.config.clientId))
832
+ });
833
+ },
834
+ /**
835
+ * Get all pending invitations for a tenant
836
+ *
837
+ * Automatically validates JWT and uses tenantId from it.
838
+ *
839
+ * @example
840
+ * ```ts
841
+ * const { invitations, totalCount } = await authvital.invitations.listPending(request);
842
+ * ```
843
+ */
844
+ listPending: async (request) => {
845
+ const claims = await client.validateRequest(request);
846
+ const params = new URLSearchParams({ tenantId: claims.tenantId });
847
+ return client.request(
848
+ "GET",
849
+ `/api/integration/invitations/pending?${params.toString()}`
850
+ );
851
+ },
852
+ /**
853
+ * Resend an invitation (generates new token, extends expiry)
854
+ *
855
+ * Automatically validates JWT and uses tenantId from it.
856
+ * Returns the new expiration date
857
+ *
858
+ * @example
859
+ * ```ts
860
+ * const { expiresAt } = await authvital.invitations.resend(request, {
861
+ * invitationId: 'inv-123',
862
+ * expiresInDays: 7,
863
+ * });
864
+ * ```
865
+ */
866
+ resend: async (request, params) => {
867
+ await client.validateRequest(request);
868
+ return client.request(
869
+ "POST",
870
+ "/api/integration/invitations/resend",
871
+ params
872
+ );
873
+ },
874
+ /**
875
+ * Revoke an invitation
876
+ *
877
+ * @example
878
+ * ```ts
879
+ * await authvital.invitations.revoke(request, 'inv-123');
880
+ * ```
881
+ */
882
+ revoke: async (request, invitationId) => {
883
+ await client.validateRequest(request);
884
+ return client.request(
885
+ "DELETE",
886
+ `/api/integration/invitations/${encodeURIComponent(invitationId)}`
887
+ );
888
+ }
889
+ };
890
+ }
891
+
892
+ // src/server/namespaces/memberships.ts
893
+ function createMembershipsNamespace(client) {
894
+ return {
895
+ /**
896
+ * Get all memberships for the authenticated user's tenant
897
+ *
898
+ * Automatically validates JWT and uses tenantId from it.
899
+ *
900
+ * @param request - The incoming HTTP request
901
+ * @param options - Optional filters and configuration
902
+ *
903
+ * @example
904
+ * ```ts
905
+ * app.get('/api/team', async (req, res) => {
906
+ * const members = await authvital.memberships.listForTenant(req, {
907
+ * status: 'ACTIVE',
908
+ * });
909
+ * res.json(members);
910
+ * });
911
+ * ```
912
+ */
913
+ listForTenant: async (request, options) => {
914
+ const claims = await client.validateRequest(request);
915
+ const params = new URLSearchParams({ tenantId: claims.tenantId });
916
+ if (_optionalChain([options, 'optionalAccess', _15 => _15.status])) params.set("status", options.status);
917
+ if (_optionalChain([options, 'optionalAccess', _16 => _16.includeRoles]) !== void 0)
918
+ params.set("includeRoles", String(options.includeRoles));
919
+ const response = await client.request(
920
+ "GET",
921
+ `/api/integration/tenant-memberships?${params.toString()}`
922
+ );
923
+ if (_optionalChain([options, 'optionalAccess', _17 => _17.appendClientId]) && response.initiateLoginUri) {
924
+ response.initiateLoginUri = appendClientIdToUri(
925
+ response.initiateLoginUri,
926
+ client.config.clientId
927
+ );
928
+ }
929
+ return response;
930
+ },
931
+ /**
932
+ * Get all memberships for your application in the authenticated user's tenant
933
+ *
934
+ * Automatically uses clientId from SDK config and tenantId from the validated JWT.
935
+ *
936
+ * @param request - The incoming HTTP request
937
+ * @param options - Optional filters and configuration
938
+ *
939
+ * @example
940
+ * ```ts
941
+ * app.get('/api/members', async (req, res) => {
942
+ * const { memberships } = await authvital.memberships.listForApplication(req);
943
+ * res.json(memberships);
944
+ * });
945
+ * ```
946
+ */
947
+ listForApplication: async (request, options) => {
948
+ const claims = await client.validateRequest(request);
949
+ const params = new URLSearchParams({
950
+ clientId: client.config.clientId,
951
+ tenantId: claims.tenantId
952
+ });
953
+ if (_optionalChain([options, 'optionalAccess', _18 => _18.status])) params.set("status", options.status);
954
+ const response = await client.request(
955
+ "GET",
956
+ `/api/integration/application-memberships?${params.toString()}`
957
+ );
958
+ if (_optionalChain([options, 'optionalAccess', _19 => _19.appendClientId])) {
959
+ response.memberships = response.memberships.map((m) => ({
960
+ ...m,
961
+ tenant: {
962
+ ...m.tenant,
963
+ initiateLoginUri: appendClientIdToUri(m.tenant.initiateLoginUri, client.config.clientId)
964
+ }
965
+ }));
966
+ }
967
+ return response;
968
+ },
969
+ /**
970
+ * Validate that the authenticated user is a member of their tenant
971
+ *
972
+ * @param request - The incoming HTTP request
973
+ */
974
+ validate: async (request) => {
975
+ const claims = await client.validateRequest(request);
976
+ const params = new URLSearchParams({ userId: claims.sub, tenantId: claims.tenantId });
977
+ return client.request("GET", `/api/integration/validate-membership?${params.toString()}`);
978
+ },
979
+ /**
980
+ * Get all tenants for the authenticated user
981
+ *
982
+ * Returns all tenants the user is a member of, with optional role information.
983
+ *
984
+ * @param request - The incoming HTTP request
985
+ * @param options - Optional filters and configuration
986
+ *
987
+ * @example
988
+ * ```ts
989
+ * app.get('/api/my-tenants', async (req, res) => {
990
+ * const result = await authvital.memberships.listTenantsForUser(req, {
991
+ * status: 'ACTIVE',
992
+ * appendClientId: true,
993
+ * });
994
+ * res.json(result.memberships);
995
+ * });
996
+ * ```
997
+ */
998
+ listTenantsForUser: async (request, options) => {
999
+ const claims = await client.validateRequest(request);
1000
+ const params = new URLSearchParams({ userId: claims.sub });
1001
+ if (_optionalChain([options, 'optionalAccess', _20 => _20.status])) params.set("status", options.status);
1002
+ if (_optionalChain([options, 'optionalAccess', _21 => _21.includeRoles]) !== void 0)
1003
+ params.set("includeRoles", String(options.includeRoles));
1004
+ const response = await client.request(
1005
+ "GET",
1006
+ `/api/integration/user-tenants?${params.toString()}`
1007
+ );
1008
+ if (_optionalChain([options, 'optionalAccess', _22 => _22.appendClientId])) {
1009
+ response.memberships = response.memberships.map((m) => ({
1010
+ ...m,
1011
+ tenant: {
1012
+ ...m.tenant,
1013
+ initiateLoginUri: appendClientIdToUri(m.tenant.initiateLoginUri, client.config.clientId)
1014
+ }
1015
+ }));
1016
+ }
1017
+ return response;
1018
+ },
1019
+ /**
1020
+ * Get all available tenant roles (IDP-level)
1021
+ *
1022
+ * Returns the role definitions (owner, admin, member, etc.) that can be
1023
+ * assigned to memberships. These are instance-wide, not tenant-specific.
1024
+ * Use this to populate a role picker dropdown.
1025
+ *
1026
+ * @example
1027
+ * ```ts
1028
+ * app.get('/api/roles', async (req, res) => {
1029
+ * const { roles } = await authvital.memberships.getTenantRoles();
1030
+ * res.json(roles);
1031
+ * // [{ slug: 'owner', name: 'Owner', ... }, { slug: 'admin', ... }, ...]
1032
+ * });
1033
+ * ```
1034
+ */
1035
+ getTenantRoles: async () => {
1036
+ return client.request("GET", "/api/integration/tenant-roles");
1037
+ },
1038
+ /**
1039
+ * Get all roles for your application
1040
+ *
1041
+ * Returns the role definitions (admin, editor, viewer, etc.) specific to
1042
+ * your application. Uses the clientId from SDK config automatically.
1043
+ * Use this to populate a role picker for invite flows or role assignment.
1044
+ *
1045
+ * NOTE: These are APPLICATION-specific roles, different from tenant roles
1046
+ * (owner/admin/member). Application roles are used for fine-grained
1047
+ * permissions within your app.
1048
+ *
1049
+ * @example
1050
+ * ```ts
1051
+ * app.get('/api/app-roles', async (req, res) => {
1052
+ * const { roles } = await authvital.memberships.getApplicationRoles();
1053
+ * res.json(roles);
1054
+ * // [{ slug: 'admin', name: 'Admin', permissions: [...] }, ...]
1055
+ * });
1056
+ *
1057
+ * // Use in invite flow:
1058
+ * const { roles } = await authvital.memberships.getApplicationRoles();
1059
+ * const editorRole = roles.find(r => r.slug === 'editor');
1060
+ * await authvital.invitations.send(request, {
1061
+ * email: 'user@example.com',
1062
+ * roleId: editorRole?.id,
1063
+ * });
1064
+ * ```
1065
+ */
1066
+ getApplicationRoles: async () => {
1067
+ const params = new URLSearchParams({ clientId: client.config.clientId });
1068
+ return client.request(
1069
+ "GET",
1070
+ `/api/integration/application-roles?${params.toString()}`
1071
+ );
1072
+ },
1073
+ /**
1074
+ * Set a member's tenant role (replaces any existing roles)
1075
+ *
1076
+ * Performs a pre-flight check using the caller's JWT to catch obvious
1077
+ * permission violations before calling the IDP. The IDP then does the
1078
+ * full authoritative check (e.g., admin can't demote an owner).
1079
+ *
1080
+ * Role hierarchy: owner > admin > member
1081
+ * - Owners can change anyone's role
1082
+ * - Admins can change admins and members, but cannot touch owners or promote to owner
1083
+ * - Members cannot change roles
1084
+ *
1085
+ * @param request - The incoming HTTP request (used to read caller's JWT)
1086
+ * @param membershipId - The membership to update
1087
+ * @param roleSlug - The role slug to set (e.g., 'admin', 'member', 'owner')
1088
+ *
1089
+ * @example
1090
+ * ```ts
1091
+ * app.put('/api/team/:membershipId/role', async (req, res) => {
1092
+ * const result = await authvital.memberships.setMemberRole(
1093
+ * req,
1094
+ * req.params.membershipId,
1095
+ * req.body.role, // e.g., 'admin'
1096
+ * );
1097
+ * res.json(result.role); // { id, name, slug }
1098
+ * });
1099
+ * ```
1100
+ */
1101
+ setMemberRole: async (request, membershipId, roleSlug) => {
1102
+ const claims = await client.validateRequest(request);
1103
+ const callerRoles = _nullishCoalesce(claims.payload.tenant_roles, () => ( []));
1104
+ const isOwner = callerRoles.includes("owner");
1105
+ const isAdmin = callerRoles.includes("admin");
1106
+ if (!isOwner && !isAdmin) {
1107
+ throw new Error(
1108
+ "Insufficient permissions: only owners and admins can change member roles"
1109
+ );
1110
+ }
1111
+ if (!isOwner && roleSlug === "owner") {
1112
+ throw new Error("Insufficient permissions: only owners can promote to owner");
1113
+ }
1114
+ return client.request(
1115
+ "PUT",
1116
+ `/api/integration/memberships/${encodeURIComponent(membershipId)}/tenant-role`,
1117
+ { roleSlug, callerUserId: claims.sub }
1118
+ );
1119
+ }
1120
+ };
1121
+ }
1122
+
1123
+ // src/server/namespaces/permissions.ts
1124
+ function createPermissionsNamespace(client) {
1125
+ return {
1126
+ /**
1127
+ * Check if the authenticated user has a specific permission
1128
+ *
1129
+ * @param request - The incoming HTTP request
1130
+ * @param permission - The permission to check (e.g., 'users:write')
1131
+ *
1132
+ * @example
1133
+ * ```ts
1134
+ * app.post('/api/users', async (req, res) => {
1135
+ * const { allowed } = await authvital.permissions.check(req, 'users:write');
1136
+ * if (!allowed) return res.status(403).json({ error: 'Forbidden' });
1137
+ * // ... create user
1138
+ * });
1139
+ * ```
1140
+ */
1141
+ check: async (request, permission) => {
1142
+ const claims = await client.validateRequest(request);
1143
+ return client.request("POST", "/api/integration/check-permission", {
1144
+ userId: claims.sub,
1145
+ tenantId: claims.tenantId,
1146
+ permission
1147
+ });
1148
+ },
1149
+ /**
1150
+ * Check multiple permissions at once for the authenticated user
1151
+ *
1152
+ * @param request - The incoming HTTP request
1153
+ * @param permissions - Array of permissions to check
1154
+ *
1155
+ * @example
1156
+ * ```ts
1157
+ * const { results } = await authvital.permissions.checkMany(req, ['users:read', 'users:write']);
1158
+ * // results = { 'users:read': true, 'users:write': false }
1159
+ * ```
1160
+ */
1161
+ checkMany: async (request, permissions) => {
1162
+ const claims = await client.validateRequest(request);
1163
+ return client.request("POST", "/api/integration/check-permissions", {
1164
+ userId: claims.sub,
1165
+ tenantId: claims.tenantId,
1166
+ permissions
1167
+ });
1168
+ },
1169
+ /**
1170
+ * Get all permissions for the authenticated user
1171
+ *
1172
+ * @param request - The incoming HTTP request
1173
+ *
1174
+ * @example
1175
+ * ```ts
1176
+ * app.get('/api/my-permissions', async (req, res) => {
1177
+ * const perms = await authvital.permissions.list(req);
1178
+ * res.json(perms);
1179
+ * });
1180
+ * ```
1181
+ */
1182
+ list: async (request) => {
1183
+ const claims = await client.validateRequest(request);
1184
+ const params = new URLSearchParams({ userId: claims.sub, tenantId: claims.tenantId });
1185
+ return client.request(
1186
+ "GET",
1187
+ `/api/integration/user-permissions?${params.toString()}`
1188
+ );
1189
+ }
1190
+ };
1191
+ }
1192
+
1193
+ // src/server/namespaces/entitlements.ts
1194
+ function createEntitlementsNamespace(client) {
1195
+ return {
1196
+ /**
1197
+ * Check if a quota-based action is allowed
1198
+ *
1199
+ * This is the main "gatekeeper" function. Use it before any quota-consuming action.
1200
+ *
1201
+ * @param request - The incoming HTTP request
1202
+ * @param featureKey - The quota to check (e.g., 'seats', 'projects')
1203
+ * @param options - Optional: appScope and incrementCount
1204
+ *
1205
+ * @example
1206
+ * ```ts
1207
+ * // Before adding a team member
1208
+ * const check = await authvital.entitlements.canPerform(req, 'seats');
1209
+ * if (!check.allowed) {
1210
+ * return res.status(403).json({ error: check.reason });
1211
+ * }
1212
+ * // Add the member...
1213
+ * await authvital.entitlements.incrementUsage(req, 'seats');
1214
+ * ```
1215
+ */
1216
+ canPerform: async (request, featureKey, _options) => {
1217
+ const claims = await client.validateRequest(request);
1218
+ if (featureKey === "seats") {
1219
+ const params = new URLSearchParams({
1220
+ tenantId: claims.tenantId,
1221
+ clientId: client.config.clientId
1222
+ });
1223
+ const seatsResult = await client.request("GET", `/api/integration/check-seats?${params.toString()}`);
1224
+ if (seatsResult.unlimited) {
1225
+ return {
1226
+ allowed: true,
1227
+ reason: void 0,
1228
+ currentUsage: seatsResult.memberCount,
1229
+ limit: void 0
1230
+ };
1231
+ }
1232
+ const canAddSeat = seatsResult.totalSeatsAvailable > 0;
1233
+ return {
1234
+ allowed: canAddSeat,
1235
+ reason: canAddSeat ? void 0 : "No available seats. Upgrade your subscription to add more team members.",
1236
+ currentUsage: seatsResult.totalSeatsAssigned,
1237
+ limit: seatsResult.totalSeatsOwned
1238
+ };
1239
+ }
1240
+ return {
1241
+ allowed: true,
1242
+ reason: void 0,
1243
+ currentUsage: 0,
1244
+ limit: void 0
1245
+ };
1246
+ },
1247
+ // Note: The following methods reference billing endpoints that don't exist yet.
1248
+ // They should be implemented if/when billing features are added.
1249
+ // For now, we keep them for API compatibility but they'll throw errors.
1250
+ //
1251
+ // TODO: If billing is ever needed, implement:
1252
+ // - /billing/tenants/{tenantId}/check-feature/{featureKey}
1253
+ // - /billing/tenants/{tenantId}/check-app-access/{applicationId}
1254
+ // - /billing/tenants/{tenantId}/status
1255
+ // - /billing/tenants/{tenantId}/usage/increment
1256
+ /**
1257
+ * Decrement usage for a quota (call after removing resource)
1258
+ *
1259
+ * @param request - The incoming HTTP request
1260
+ * @param featureKey - The quota to decrement (e.g., 'seats')
1261
+ * @param options - Optional: appScope and amount to decrement by
1262
+ *
1263
+ * @example
1264
+ * ```ts
1265
+ * // After removing a team member
1266
+ * await authvital.entitlements.decrementUsage(req, 'seats');
1267
+ * ```
1268
+ */
1269
+ decrementUsage: async (request, featureKey, options) => {
1270
+ const claims = await client.validateRequest(request);
1271
+ return client.request("POST", `/billing/tenants/${claims.tenantId}/usage/decrement`, {
1272
+ featureKey,
1273
+ appScope: _optionalChain([options, 'optionalAccess', _23 => _23.appScope]) || "global",
1274
+ value: _optionalChain([options, 'optionalAccess', _24 => _24.by]) || 1
1275
+ });
1276
+ }
1277
+ };
1278
+ }
1279
+
1280
+ // src/server/namespaces/licenses-user.ts
1281
+ function createUserLicenseOperations(client) {
1282
+ return {
1283
+ /**
1284
+ * Grant a license to a user
1285
+ *
1286
+ * @param request - The incoming HTTP request
1287
+ * @param options - License grant options
1288
+ *
1289
+ * @example
1290
+ * ```ts
1291
+ * // Grant pro license to a team member
1292
+ * await authvital.licenses.grant(req, {
1293
+ * userId: 'user-123',
1294
+ * applicationId: 'app-456',
1295
+ * licenseTypeId: 'license-pro',
1296
+ * });
1297
+ * ```
1298
+ */
1299
+ grant: async (request, options) => {
1300
+ const claims = await client.validateRequest(request);
1301
+ return client.request("POST", "/api/integration/licenses/grant", {
1302
+ tenantId: claims.tenantId,
1303
+ userId: options.userId || claims.sub,
1304
+ applicationId: options.applicationId,
1305
+ licenseTypeId: options.licenseTypeId
1306
+ });
1307
+ },
1308
+ /**
1309
+ * Revoke a license from a user
1310
+ *
1311
+ * @param request - The incoming HTTP request
1312
+ * @param options - License revoke options
1313
+ *
1314
+ * @example
1315
+ * ```ts
1316
+ * await authvital.licenses.revoke(req, {
1317
+ * userId: 'user-123',
1318
+ * applicationId: 'app-456',
1319
+ * });
1320
+ * ```
1321
+ */
1322
+ revoke: async (request, options) => {
1323
+ const claims = await client.validateRequest(request);
1324
+ return client.request("POST", "/api/integration/licenses/revoke", {
1325
+ tenantId: claims.tenantId,
1326
+ userId: options.userId || claims.sub,
1327
+ applicationId: options.applicationId
1328
+ });
1329
+ },
1330
+ /**
1331
+ * Change a user's license type
1332
+ *
1333
+ * @param request - The incoming HTTP request
1334
+ * @param options - License change options
1335
+ *
1336
+ * @example
1337
+ * ```ts
1338
+ * // Upgrade user from basic to pro
1339
+ * await authvital.licenses.changeType(req, {
1340
+ * userId: 'user-123',
1341
+ * applicationId: 'app-456',
1342
+ * newLicenseTypeId: 'license-pro',
1343
+ * });
1344
+ * ```
1345
+ */
1346
+ changeType: async (request, options) => {
1347
+ const claims = await client.validateRequest(request);
1348
+ return client.request("POST", "/api/integration/licenses/change-type", {
1349
+ tenantId: claims.tenantId,
1350
+ userId: options.userId || claims.sub,
1351
+ applicationId: options.applicationId,
1352
+ newLicenseTypeId: options.newLicenseTypeId
1353
+ });
1354
+ },
1355
+ /**
1356
+ * Get all licenses for a user
1357
+ *
1358
+ * @param request - The incoming HTTP request
1359
+ * @param userId - User ID (optional, defaults to authenticated user)
1360
+ *
1361
+ * @returns List of license assignments with license type details
1362
+ *
1363
+ * @example
1364
+ * ```ts
1365
+ * const licenses = await authvital.licenses.listForUser(req);
1366
+ * // [{ id: 'assignment-1', licenseType: 'pro', applicationId: 'app-1', ... }]
1367
+ * ```
1368
+ */
1369
+ listForUser: async (request, userId) => {
1370
+ const claims = await client.validateRequest(request);
1371
+ return client.request(
1372
+ "GET",
1373
+ `/api/integration/licenses/tenants/${claims.tenantId}/users/${userId || claims.sub}`
1374
+ );
1375
+ },
1376
+ /**
1377
+ * Check if a user has a license (uses tenant from JWT)
1378
+ *
1379
+ * This endpoint validates the JWT and checks if the user
1380
+ * (or specified user) has a valid license for the application.
1381
+ *
1382
+ * @param request - The incoming HTTP request with JWT
1383
+ * @param userId - User to check (or omit to check authenticated user)
1384
+ * @param applicationId - Application to check
1385
+ *
1386
+ * @returns License check result with hasLicense flag and license details
1387
+ *
1388
+ * @example
1389
+ * ```ts
1390
+ * const result = await authvital.licenses.check(req, undefined, 'my-app-id');
1391
+ * if (result.hasLicense) {
1392
+ * console.log('User has', result.licenseType, 'license');
1393
+ * }
1394
+ * ```
1395
+ */
1396
+ check: async (request, userId, applicationId) => {
1397
+ await client.getCurrentUser(request);
1398
+ const params = new URLSearchParams();
1399
+ if (userId) params.set("userId", userId);
1400
+ params.set("applicationId", applicationId);
1401
+ return client.authenticatedRequest(
1402
+ request,
1403
+ "GET",
1404
+ `/api/integration/licenses/check?${params.toString()}`
1405
+ );
1406
+ },
1407
+ /**
1408
+ * Check if user has a specific feature enabled
1409
+ *
1410
+ * This validates the JWT and checks if the user's license
1411
+ * includes the specified feature.
1412
+ *
1413
+ * @param request - The incoming HTTP request with JWT
1414
+ * @param userId - User to check (or omit to check authenticated user)
1415
+ * @param applicationId - Application to check
1416
+ * @param featureKey - Feature to check (e.g., 'sso', 'audit_logs')
1417
+ *
1418
+ * @returns Result with hasFeature boolean
1419
+ *
1420
+ * @example
1421
+ * ```ts
1422
+ * const { hasFeature } = await authvital.licenses.hasFeature(req, undefined, 'my-app-id', 'sso');
1423
+ * if (hasFeature) {
1424
+ * // Show SSO option
1425
+ * }
1426
+ * ```
1427
+ */
1428
+ hasFeature: async (request, userId, applicationId, featureKey) => {
1429
+ await client.getCurrentUser(request);
1430
+ const params = new URLSearchParams();
1431
+ if (userId) params.set("userId", userId);
1432
+ params.set("applicationId", applicationId);
1433
+ params.set("featureKey", featureKey);
1434
+ return client.authenticatedRequest(
1435
+ request,
1436
+ "GET",
1437
+ `/api/integration/licenses/feature?${params.toString()}`
1438
+ );
1439
+ },
1440
+ /**
1441
+ * Get all licensed users for an app in the authenticated tenant
1442
+ *
1443
+ * Returns all users with valid licenses for the specified application.
1444
+ * The tenant is extracted from the JWT.
1445
+ *
1446
+ * @param request - The incoming HTTP request with JWT
1447
+ * @param applicationId - Application ID
1448
+ *
1449
+ * @returns List of licensed users with license details
1450
+ *
1451
+ * @example
1452
+ * ```ts
1453
+ * const users = await authvital.licenses.getAppLicensedUsers(req, 'my-app-id');
1454
+ * users.forEach(u => console.log(u.email, '-', u.licenseType));
1455
+ * ```
1456
+ */
1457
+ getAppLicensedUsers: async (request, applicationId) => {
1458
+ await client.getCurrentUser(request);
1459
+ return client.authenticatedRequest(
1460
+ request,
1461
+ "GET",
1462
+ `/api/integration/licenses/apps/${encodeURIComponent(applicationId)}/users`
1463
+ );
1464
+ },
1465
+ /**
1466
+ * Count licensed users for an app in the authenticated tenant
1467
+ *
1468
+ * @param request - The incoming HTTP request with JWT
1469
+ * @param applicationId - Application ID
1470
+ *
1471
+ * @returns Count of users with licenses
1472
+ *
1473
+ * @example
1474
+ * ```ts
1475
+ * const { count } = await authvital.licenses.countLicensedUsers(req, 'my-app-id');
1476
+ * console.log(`${count} users have licenses`);
1477
+ * ```
1478
+ */
1479
+ countLicensedUsers: async (request, applicationId) => {
1480
+ await client.getCurrentUser(request);
1481
+ return client.authenticatedRequest(
1482
+ request,
1483
+ "GET",
1484
+ `/api/integration/licenses/apps/${encodeURIComponent(applicationId)}/count`
1485
+ );
1486
+ },
1487
+ /**
1488
+ * Get the license type for a user (local check - API call)
1489
+ *
1490
+ * This is a convenience wrapper around `check` for getting just the license type.
1491
+ *
1492
+ * @param request - The incoming HTTP request with JWT
1493
+ * @param userId - User to check (or omit to check authenticated user)
1494
+ * @param applicationId - Application to check
1495
+ *
1496
+ * @returns License type slug or null
1497
+ *
1498
+ * @example
1499
+ * ```ts
1500
+ * const licenseType = await authvital.licenses.getUserLicenseType(req, undefined, 'my-app-id');
1501
+ * if (licenseType === 'enterprise') {
1502
+ * // Show enterprise features
1503
+ * }
1504
+ * ```
1505
+ */
1506
+ getUserLicenseType: async (request, userId, applicationId) => {
1507
+ const params = new URLSearchParams();
1508
+ if (userId) params.set("userId", userId);
1509
+ params.set("applicationId", applicationId);
1510
+ const result = await client.authenticatedRequest(
1511
+ request,
1512
+ "GET",
1513
+ `/api/integration/licenses/check?${params.toString()}`
1514
+ );
1515
+ return result.licenseType;
1516
+ },
1517
+ /**
1518
+ * Get all license holders for an application
1519
+ *
1520
+ * @param request - The incoming HTTP request
1521
+ * @param applicationId - Application ID
1522
+ *
1523
+ * @returns List of all users with licenses for the application
1524
+ *
1525
+ * @example
1526
+ * ```ts
1527
+ * const holders = await authvital.licenses.getHolders(req, 'app-456');
1528
+ * // [{ userId: 'user-1', licenseType: 'pro', ... }, ...]
1529
+ * ```
1530
+ */
1531
+ getHolders: async (request, applicationId) => {
1532
+ const claims = await client.validateRequest(request);
1533
+ return client.request(
1534
+ "GET",
1535
+ `/api/integration/licenses/tenants/${claims.tenantId}/applications/${applicationId}/holders`
1536
+ );
1537
+ },
1538
+ /**
1539
+ * Get license audit log
1540
+ *
1541
+ * @param request - The incoming HTTP request
1542
+ * @param options - Filter options
1543
+ *
1544
+ * @returns Audit log entries with pagination
1545
+ *
1546
+ * @example
1547
+ * ```ts
1548
+ * const auditLog = await authvital.licenses.getAuditLog(req, {
1549
+ * userId: 'user-123',
1550
+ * limit: 50,
1551
+ * });
1552
+ * ```
1553
+ */
1554
+ getAuditLog: async (request, options) => {
1555
+ const claims = await client.validateRequest(request);
1556
+ const params = new URLSearchParams({
1557
+ limit: (_optionalChain([options, 'optionalAccess', _25 => _25.limit]) || 50).toString(),
1558
+ offset: (_optionalChain([options, 'optionalAccess', _26 => _26.offset]) || 0).toString()
1559
+ });
1560
+ if (_optionalChain([options, 'optionalAccess', _27 => _27.userId])) params.append("userId", options.userId);
1561
+ if (_optionalChain([options, 'optionalAccess', _28 => _28.applicationId])) params.append("applicationId", options.applicationId);
1562
+ return client.request(
1563
+ "GET",
1564
+ `/api/integration/licenses/tenants/${claims.tenantId}/audit-log?${params.toString()}`
1565
+ );
1566
+ },
1567
+ /**
1568
+ * Get usage overview for tenant
1569
+ *
1570
+ * @param request - The incoming HTTP request
1571
+ *
1572
+ * @returns Usage overview with seat counts and utilization
1573
+ *
1574
+ * @example
1575
+ * ```ts
1576
+ * const usage = await authvital.licenses.getUsageOverview(req);
1577
+ * // { totalSeats: 10, seatsAssigned: 8, utilization: 80, ... }
1578
+ * ```
1579
+ */
1580
+ getUsageOverview: async (request) => {
1581
+ const claims = await client.validateRequest(request);
1582
+ return client.request(
1583
+ "GET",
1584
+ `/api/integration/licenses/tenants/${claims.tenantId}/usage-overview`
1585
+ );
1586
+ },
1587
+ /**
1588
+ * Get usage trends for tenant
1589
+ *
1590
+ * @param request - The incoming HTTP request
1591
+ * @param days - Number of days to look back (default: 30)
1592
+ *
1593
+ * @returns Daily usage data
1594
+ *
1595
+ * @example
1596
+ * ```ts
1597
+ * const trends = await authvital.licenses.getUsageTrends(req, 30);
1598
+ * // [{ date: '2024-01-01', seatsAssigned: 8, ... }, ...]
1599
+ * ```
1600
+ */
1601
+ getUsageTrends: async (request, days) => {
1602
+ const claims = await client.validateRequest(request);
1603
+ const params = new URLSearchParams({ days: (days || 30).toString() });
1604
+ return client.request(
1605
+ "GET",
1606
+ `/api/integration/licenses/tenants/${claims.tenantId}/usage-trends?${params.toString()}`
1607
+ );
1608
+ }
1609
+ };
1610
+ }
1611
+
1612
+ // src/server/namespaces/licenses-admin.ts
1613
+ function createAdminLicenseOperations(client) {
1614
+ return {
1615
+ /**
1616
+ * Get full license overview for a tenant
1617
+ *
1618
+ * Returns all subscriptions (inventory) and utilization stats.
1619
+ * Requires M2M authentication.
1620
+ *
1621
+ * @example
1622
+ * ```ts
1623
+ * const overview = await authvital.licenses.getTenantOverview('tenant-123');
1624
+ * console.log(`Using ${overview.totalSeatsAssigned} of ${overview.totalSeatsOwned} seats`);
1625
+ * ```
1626
+ */
1627
+ getTenantOverview: async (tenantId) => {
1628
+ return client.request(
1629
+ "GET",
1630
+ `/api/licensing/tenants/${encodeURIComponent(tenantId)}/license-overview`
1631
+ );
1632
+ },
1633
+ /**
1634
+ * Get all license assignments for a user in a tenant
1635
+ *
1636
+ * @example
1637
+ * ```ts
1638
+ * const licenses = await authvital.licenses.getUserLicenses('tenant-123', 'user-456');
1639
+ * licenses.forEach(l => console.log(`Has ${l.licenseTypeName} for ${l.applicationId}`));
1640
+ * ```
1641
+ */
1642
+ getUserLicenses: async (tenantId, userId) => {
1643
+ return client.request(
1644
+ "GET",
1645
+ `/api/licensing/tenants/${encodeURIComponent(tenantId)}/users/${encodeURIComponent(userId)}/licenses`
1646
+ );
1647
+ },
1648
+ /**
1649
+ * Get all subscriptions for a tenant
1650
+ *
1651
+ * Returns the tenant's "wallet" - all their purchased license seats.
1652
+ *
1653
+ * @example
1654
+ * ```ts
1655
+ * const subscriptions = await authvital.licenses.getTenantSubscriptions('tenant-123');
1656
+ * subscriptions.forEach(sub => {
1657
+ * console.log(`${sub.applicationName}: ${sub.quantityAvailable} seats available`);
1658
+ * });
1659
+ * ```
1660
+ */
1661
+ getTenantSubscriptions: async (tenantId) => {
1662
+ return client.request(
1663
+ "GET",
1664
+ `/api/licensing/tenants/${encodeURIComponent(tenantId)}/subscriptions`
1665
+ );
1666
+ },
1667
+ /**
1668
+ * Get tenant members with their license assignments
1669
+ *
1670
+ * Returns all members along with their license status for each application.
1671
+ * Useful for admin dashboards.
1672
+ *
1673
+ * @example
1674
+ * ```ts
1675
+ * const members = await authvital.licenses.getMembersWithLicenses('tenant-123');
1676
+ * members.forEach(member => {
1677
+ * console.log(`${member.user.email} has ${member.licenses.length} licenses`);
1678
+ * });
1679
+ * ```
1680
+ */
1681
+ getMembersWithLicenses: async (tenantId) => {
1682
+ return client.request(
1683
+ "GET",
1684
+ `/api/licensing/tenants/${encodeURIComponent(tenantId)}/members-with-licenses`
1685
+ );
1686
+ },
1687
+ /**
1688
+ * Get available license types for tenant provisioning
1689
+ *
1690
+ * Returns all ACTIVE license types across all applications that the tenant
1691
+ * could purchase/provision. Includes info about existing subscriptions.
1692
+ *
1693
+ * @example
1694
+ * ```ts
1695
+ * const available = await authvital.licenses.getAvailableLicenseTypes('tenant-123');
1696
+ * available.forEach(type => {
1697
+ * if (type.hasSubscription) {
1698
+ * console.log(`Already have: ${type.name} (${type.existingSubscription?.quantityPurchased} seats)`);
1699
+ * } else {
1700
+ * console.log(`Can add: ${type.name}`);
1701
+ * }
1702
+ * });
1703
+ * ```
1704
+ */
1705
+ getAvailableLicenseTypes: async (tenantId) => {
1706
+ return client.request(
1707
+ "GET",
1708
+ `/api/licensing/tenants/${encodeURIComponent(tenantId)}/available-license-types`
1709
+ );
1710
+ },
1711
+ /**
1712
+ * Grant a license to a user (M2M version)
1713
+ *
1714
+ * Assigns a seat from the tenant's subscription to a user.
1715
+ * Requires M2M authentication with licensing permissions.
1716
+ *
1717
+ * @throws Error if no seats available or user already has a license for this app
1718
+ *
1719
+ * @example
1720
+ * ```ts
1721
+ * const assignment = await authvital.licenses.grantToUser({
1722
+ * tenantId: 'tenant-123',
1723
+ * userId: 'user-456',
1724
+ * applicationId: 'app-789',
1725
+ * licenseTypeId: 'pro-license',
1726
+ * });
1727
+ * ```
1728
+ */
1729
+ grantToUser: async (params) => {
1730
+ return client.request("POST", "/api/licensing/licenses/grant", params);
1731
+ },
1732
+ /**
1733
+ * Revoke a license from a user (M2M version)
1734
+ *
1735
+ * Returns the seat to the tenant's pool.
1736
+ *
1737
+ * @example
1738
+ * ```ts
1739
+ * await authvital.licenses.revokeFromUser({
1740
+ * tenantId: 'tenant-123',
1741
+ * userId: 'user-456',
1742
+ * applicationId: 'app-789',
1743
+ * });
1744
+ * ```
1745
+ */
1746
+ revokeFromUser: async (params) => {
1747
+ await client.request("POST", "/api/licensing/licenses/revoke", params);
1748
+ },
1749
+ /**
1750
+ * Change a user's license type (M2M version)
1751
+ *
1752
+ * Moves a user from one license type to another for the same application.
1753
+ *
1754
+ * @example
1755
+ * ```ts
1756
+ * const newAssignment = await authvital.licenses.changeUserType({
1757
+ * tenantId: 'tenant-123',
1758
+ * userId: 'user-456',
1759
+ * applicationId: 'app-789',
1760
+ * newLicenseTypeId: 'enterprise-license',
1761
+ * });
1762
+ * ```
1763
+ */
1764
+ changeUserType: async (params) => {
1765
+ return client.request(
1766
+ "POST",
1767
+ "/api/licensing/licenses/change-type",
1768
+ params
1769
+ );
1770
+ },
1771
+ /**
1772
+ * Bulk grant licenses to multiple users
1773
+ *
1774
+ * @example
1775
+ * ```ts
1776
+ * const results = await authvital.licenses.grantBulk([
1777
+ * { tenantId: 'tenant-123', userId: 'user-1', applicationId: 'app-789', licenseTypeId: 'pro' },
1778
+ * { tenantId: 'tenant-123', userId: 'user-2', applicationId: 'app-789', licenseTypeId: 'pro' },
1779
+ * ]);
1780
+ * results.forEach(r => console.log(`${r.userId}: ${r.success ? 'Success' : r.error}`));
1781
+ * ```
1782
+ */
1783
+ grantBulk: async (assignments) => {
1784
+ return client.request("POST", "/api/licensing/licenses/grant-bulk", {
1785
+ assignments
1786
+ });
1787
+ },
1788
+ /**
1789
+ * Bulk revoke licenses from multiple users
1790
+ *
1791
+ * @example
1792
+ * ```ts
1793
+ * const result = await authvital.licenses.revokeBulk([
1794
+ * { tenantId: 'tenant-123', userId: 'user-1', applicationId: 'app-789' },
1795
+ * { tenantId: 'tenant-123', userId: 'user-2', applicationId: 'app-789' },
1796
+ * ]);
1797
+ * console.log(`Revoked ${result.revokedCount} licenses`);
1798
+ * result.failures.forEach(f => console.error(`Failed: ${f.error}`));
1799
+ * ```
1800
+ */
1801
+ revokeBulk: async (revocations) => {
1802
+ return client.request(
1803
+ "POST",
1804
+ "/api/licensing/licenses/revoke-bulk",
1805
+ {
1806
+ revocations
1807
+ }
1808
+ );
1809
+ }
1810
+ };
1811
+ }
1812
+
1813
+ // src/server/namespaces/licenses.ts
1814
+ function createLicensesNamespace(client) {
1815
+ const userOps = createUserLicenseOperations(client);
1816
+ const adminOps = createAdminLicenseOperations(client);
1817
+ return {
1818
+ // User-scoped operations (JWT auth)
1819
+ ...userOps,
1820
+ // Admin operations (M2M)
1821
+ ...adminOps
1822
+ };
1823
+ }
1824
+
1825
+ // src/server/namespaces/sessions.ts
1826
+ function createSessionsNamespace(client) {
1827
+ return {
1828
+ /**
1829
+ * Get all active sessions for the authenticated user
1830
+ *
1831
+ * Returns a list of active sessions with metadata (device info, location, etc.).
1832
+ * Useful for building "manage sessions" UI.
1833
+ *
1834
+ * @example
1835
+ * ```ts
1836
+ * app.get('/api/sessions', async (req, res) => {
1837
+ * const { sessions, count } = await authvital.sessions.list(req);
1838
+ * res.json(sessions);
1839
+ * });
1840
+ * ```
1841
+ */
1842
+ list: async (request, options) => {
1843
+ await client.validateRequest(request);
1844
+ const authHeader = extractAuthorizationHeader(request);
1845
+ const params = new URLSearchParams();
1846
+ if (_optionalChain([options, 'optionalAccess', _29 => _29.applicationId])) params.set("application_id", options.applicationId);
1847
+ const url = `/oauth/sessions${params.toString() ? `?${params.toString()}` : ""}`;
1848
+ const response = await fetch(`${client.config.authVitalHost}${url}`, {
1849
+ method: "GET",
1850
+ headers: {
1851
+ Authorization: authHeader,
1852
+ "Content-Type": "application/json"
1853
+ }
1854
+ });
1855
+ if (!response.ok) {
1856
+ const error = await response.json().catch(() => ({ message: response.statusText }));
1857
+ throw new Error(error.message || `Request failed: ${response.status}`);
1858
+ }
1859
+ return response.json();
1860
+ },
1861
+ /**
1862
+ * Revoke a specific session by ID
1863
+ *
1864
+ * Call this from "manage sessions" UI to logout a specific device.
1865
+ * User can only revoke their own sessions.
1866
+ *
1867
+ * @example
1868
+ * ```ts
1869
+ * app.post('/api/sessions/:id/revoke', async (req, res) => {
1870
+ * const result = await authvital.sessions.revoke(req, req.params.id);
1871
+ * res.json(result);
1872
+ * });
1873
+ * ```
1874
+ */
1875
+ revoke: async (request, sessionId) => {
1876
+ await client.validateRequest(request);
1877
+ const authHeader = extractAuthorizationHeader(request);
1878
+ const response = await fetch(
1879
+ `${client.config.authVitalHost}/oauth/sessions/${encodeURIComponent(sessionId)}/revoke`,
1880
+ {
1881
+ method: "POST",
1882
+ headers: {
1883
+ Authorization: authHeader,
1884
+ "Content-Type": "application/json"
1885
+ }
1886
+ }
1887
+ );
1888
+ if (!response.ok) {
1889
+ const error = await response.json().catch(() => ({ message: response.statusText }));
1890
+ throw new Error(error.message || `Request failed: ${response.status}`);
1891
+ }
1892
+ return response.json();
1893
+ },
1894
+ /**
1895
+ * Revoke ALL sessions for the authenticated user
1896
+ *
1897
+ * Call this when user clicks "logout everywhere".
1898
+ * Revokes all active sessions, forcing re-authentication on all devices.
1899
+ *
1900
+ * @example
1901
+ * ```ts
1902
+ * app.post('/api/logout-all', async (req, res) => {
1903
+ * const result = await authvital.sessions.revokeAll(req);
1904
+ * res.json({ message: `Logged out of ${result.count} devices` });
1905
+ * });
1906
+ * ```
1907
+ */
1908
+ revokeAll: async (request, options) => {
1909
+ await client.validateRequest(request);
1910
+ const authHeader = extractAuthorizationHeader(request);
1911
+ const response = await fetch(`${client.config.authVitalHost}/oauth/logout-all`, {
1912
+ method: "POST",
1913
+ headers: {
1914
+ Authorization: authHeader,
1915
+ "Content-Type": "application/json"
1916
+ },
1917
+ body: JSON.stringify({
1918
+ application_id: _optionalChain([options, 'optionalAccess', _30 => _30.applicationId])
1919
+ })
1920
+ });
1921
+ if (!response.ok) {
1922
+ const error = await response.json().catch(() => ({ message: response.statusText }));
1923
+ throw new Error(error.message || `Request failed: ${response.status}`);
1924
+ }
1925
+ return response.json();
1926
+ },
1927
+ /**
1928
+ * Logout current session
1929
+ *
1930
+ * Revokes the session associated with the current refresh token.
1931
+ * Call this for normal logout.
1932
+ *
1933
+ * Note: For browser apps, prefer redirecting to /oauth/logout which
1934
+ * handles cookie clearing automatically.
1935
+ *
1936
+ * @example
1937
+ * ```ts
1938
+ * app.post('/api/logout', async (req, res) => {
1939
+ * // Get refresh token from cookie or body
1940
+ * const refreshToken = req.cookies.refresh_token || req.body.refresh_token;
1941
+ * const result = await authvital.sessions.logout(refreshToken);
1942
+ * res.clearCookie('refresh_token');
1943
+ * res.json(result);
1944
+ * });
1945
+ * ```
1946
+ */
1947
+ logout: async (refreshToken) => {
1948
+ const response = await fetch(`${client.config.authVitalHost}/oauth/logout`, {
1949
+ method: "POST",
1950
+ headers: {
1951
+ "Content-Type": "application/json"
1952
+ },
1953
+ body: JSON.stringify({
1954
+ refresh_token: refreshToken
1955
+ })
1956
+ });
1957
+ if (!response.ok) {
1958
+ const error = await response.json().catch(() => ({ message: response.statusText }));
1959
+ throw new Error(error.message || `Request failed: ${response.status}`);
1960
+ }
1961
+ return response.json();
1962
+ }
1963
+ };
1964
+ }
1965
+
1966
+ // src/server/authvital.ts
1967
+ var AuthVital = class extends BaseClient {
1968
+ constructor() {
1969
+ super(...arguments);
1970
+ // ===========================================================================
1971
+ // NAMESPACED APIS
1972
+ // ===========================================================================
1973
+ this.invitations = createInvitationsNamespace(this);
1974
+ this.memberships = createMembershipsNamespace(this);
1975
+ this.permissions = createPermissionsNamespace(this);
1976
+ this.entitlements = createEntitlementsNamespace(this);
1977
+ this.licenses = createLicensesNamespace(this);
1978
+ this.sessions = createSessionsNamespace(this);
1979
+ }
1980
+ // ===========================================================================
1981
+ // PERMISSION HELPERS (Read from JWT - No API call!)
1982
+ // ===========================================================================
1983
+ /**
1984
+ * Check tenant permission from JWT (no API call)
1985
+ * Returns true if user has the specified tenant permission.
1986
+ *
1987
+ * Reads from the `tenant_permissions` claim in the JWT.
1988
+ * Wildcards are supported (e.g., `licenses:*` matches `licenses:manage`).
1989
+ *
1990
+ * @example
1991
+ * ```ts
1992
+ * if (await authvital.hasTenantPermission(req, 'licenses:manage')) {
1993
+ * // User can manage licenses - show admin UI
1994
+ * }
1995
+ * ```
1996
+ */
1997
+ async hasTenantPermission(request, permission) {
1998
+ const { user } = await this.getCurrentUser(request);
1999
+ if (!user) return false;
2000
+ return this.jwtValidator.hasTenantPermission(user, permission);
2001
+ }
2002
+ /**
2003
+ * Check app permission from JWT (no API call)
2004
+ * Returns true if user has the specified app permission.
2005
+ *
2006
+ * Reads from the `app_permissions` claim in the JWT.
2007
+ *
2008
+ * @example
2009
+ * ```ts
2010
+ * if (await authvital.hasAppPermission(req, 'projects:create')) {
2011
+ * // User can create projects
2012
+ * }
2013
+ * ```
2014
+ */
2015
+ async hasAppPermission(request, permission) {
2016
+ const { user } = await this.getCurrentUser(request);
2017
+ if (!user) return false;
2018
+ return this.jwtValidator.hasAppPermission(user, permission);
2019
+ }
2020
+ /**
2021
+ * Check feature from JWT license claim (no API call)
2022
+ * Returns true if the feature is enabled in the user's license.
2023
+ *
2024
+ * Reads from the `license.features` array in the JWT.
2025
+ *
2026
+ * @example
2027
+ * ```ts
2028
+ * if (await authvital.hasFeatureFromJwt(req, 'sso')) {
2029
+ * // User's tenant has SSO enabled
2030
+ * }
2031
+ * ```
2032
+ */
2033
+ async hasFeatureFromJwt(request, featureKey) {
2034
+ const { user } = await this.getCurrentUser(request);
2035
+ if (!user) return false;
2036
+ return this.jwtValidator.hasFeature(user, featureKey);
2037
+ }
2038
+ /**
2039
+ * Get license type from JWT (no API call)
2040
+ * Returns the license type slug (e.g., 'pro', 'enterprise') or null.
2041
+ *
2042
+ * Reads from the `license.type` claim in the JWT.
2043
+ *
2044
+ * @example
2045
+ * ```ts
2046
+ * const licenseType = await authvital.getLicenseTypeFromJwt(req);
2047
+ * if (licenseType === 'enterprise') {
2048
+ * // Show enterprise features
2049
+ * }
2050
+ * ```
2051
+ */
2052
+ async getLicenseTypeFromJwt(request) {
2053
+ const { user } = await this.getCurrentUser(request);
2054
+ if (!user) return null;
2055
+ return this.jwtValidator.getLicenseType(user);
2056
+ }
2057
+ /**
2058
+ * Get all tenant permissions from JWT (no API call)
2059
+ * Returns the array of tenant permissions granted to the user.
2060
+ *
2061
+ * @example
2062
+ * ```ts
2063
+ * const permissions = await authvital.getTenantPermissions(req);
2064
+ * console.log(permissions); // ['licenses:view', 'members:invite', ...]
2065
+ * ```
2066
+ */
2067
+ async getTenantPermissions(request) {
2068
+ const { user } = await this.getCurrentUser(request);
2069
+ return _nullishCoalesce(_optionalChain([user, 'optionalAccess', _31 => _31.tenant_permissions]), () => ( []));
2070
+ }
2071
+ /**
2072
+ * Get all app permissions from JWT (no API call)
2073
+ * Returns the array of app permissions granted to the user.
2074
+ *
2075
+ * @example
2076
+ * ```ts
2077
+ * const permissions = await authvital.getAppPermissions(req);
2078
+ * console.log(permissions); // ['projects:create', 'datasets:read', ...]
2079
+ * ```
2080
+ */
2081
+ async getAppPermissions(request) {
2082
+ const { user } = await this.getCurrentUser(request);
2083
+ return _nullishCoalesce(_optionalChain([user, 'optionalAccess', _32 => _32.app_permissions]), () => ( []));
2084
+ }
2085
+ /**
2086
+ * Get all tenant roles from JWT (no API call)
2087
+ * Returns the array of tenant role slugs for the user.
2088
+ *
2089
+ * @example
2090
+ * ```ts
2091
+ * const roles = await authvital.getTenantRoles(req);
2092
+ * console.log(roles); // ['owner', 'admin']
2093
+ * ```
2094
+ */
2095
+ async getTenantRoles(request) {
2096
+ const { user } = await this.getCurrentUser(request);
2097
+ return _nullishCoalesce(_optionalChain([user, 'optionalAccess', _33 => _33.tenant_roles]), () => ( []));
2098
+ }
2099
+ /**
2100
+ * Get all app roles from JWT (no API call)
2101
+ * Returns the array of app role slugs for the user.
2102
+ *
2103
+ * @example
2104
+ * ```ts
2105
+ * const roles = await authvital.getAppRoles(req);
2106
+ * console.log(roles); // ['editor', 'viewer']
2107
+ * ```
2108
+ */
2109
+ async getAppRoles(request) {
2110
+ const { user } = await this.getCurrentUser(request);
2111
+ return _nullishCoalesce(_optionalChain([user, 'optionalAccess', _34 => _34.app_roles]), () => ( []));
2112
+ }
2113
+ // ===========================================================================
2114
+ // MANAGEMENT URLs (extract tenantId from JWT)
2115
+ // ===========================================================================
2116
+ /**
2117
+ * Get URL for tenant members management page
2118
+ * Extracts tenantId from the request JWT
2119
+ */
2120
+ async getMembersUrl(req) {
2121
+ const { tenantId } = await this.validateRequest(req);
2122
+ return `${this.config.authVitalHost}/tenant/${tenantId}/members`;
2123
+ }
2124
+ /**
2125
+ * Get URL for tenant applications management page
2126
+ * Extracts tenantId from the request JWT
2127
+ */
2128
+ async getApplicationsUrl(req) {
2129
+ const { tenantId } = await this.validateRequest(req);
2130
+ return `${this.config.authVitalHost}/tenant/${tenantId}/applications`;
2131
+ }
2132
+ /**
2133
+ * Get URL for tenant settings page
2134
+ * Extracts tenantId from the request JWT
2135
+ */
2136
+ async getSettingsUrl(req) {
2137
+ const { tenantId } = await this.validateRequest(req);
2138
+ return `${this.config.authVitalHost}/tenant/${tenantId}/settings`;
2139
+ }
2140
+ /**
2141
+ * Get URL for tenant overview page
2142
+ * Extracts tenantId from the request JWT
2143
+ */
2144
+ async getOverviewUrl(req) {
2145
+ const { tenantId } = await this.validateRequest(req);
2146
+ return `${this.config.authVitalHost}/tenant/${tenantId}/overview`;
2147
+ }
2148
+ /**
2149
+ * Get URL for user account settings page
2150
+ * (Does not require tenantId)
2151
+ */
2152
+ getAccountSettingsUrl() {
2153
+ return `${this.config.authVitalHost}/account/settings`;
2154
+ }
2155
+ /**
2156
+ * Get all management URLs at once
2157
+ * Extracts tenantId from the request JWT
2158
+ *
2159
+ * @example
2160
+ * ```typescript
2161
+ * const urls = await authvital.getManagementUrls(req);
2162
+ * res.json({ urls });
2163
+ * // {
2164
+ * // overview: 'https://auth.example.com/tenant/abc/overview',
2165
+ * // members: 'https://auth.example.com/tenant/abc/members',
2166
+ * // applications: 'https://auth.example.com/tenant/abc/applications',
2167
+ * // settings: 'https://auth.example.com/tenant/abc/settings',
2168
+ * // accountSettings: 'https://auth.example.com/account/settings',
2169
+ * // }
2170
+ * ```
2171
+ */
2172
+ async getManagementUrls(req) {
2173
+ const { tenantId } = await this.validateRequest(req);
2174
+ const base = this.config.authVitalHost;
2175
+ return {
2176
+ overview: `${base}/tenant/${tenantId}/overview`,
2177
+ members: `${base}/tenant/${tenantId}/members`,
2178
+ applications: `${base}/tenant/${tenantId}/applications`,
2179
+ settings: `${base}/tenant/${tenantId}/settings`,
2180
+ accountSettings: `${base}/account/settings`
2181
+ };
2182
+ }
2183
+ };
2184
+ function createAuthVital(config) {
2185
+ return new AuthVital(config);
2186
+ }
2187
+
2188
+ // src/server/oauth-flow.ts
2189
+ var _crypto = require('crypto'); var crypto2 = _interopRequireWildcard(_crypto);
2190
+ function generateCodeVerifier() {
2191
+ return crypto2.randomBytes(32).toString("base64url");
2192
+ }
2193
+ function generateCodeChallenge(verifier) {
2194
+ const hash = crypto2.createHash("sha256").update(verifier).digest();
2195
+ return hash.toString("base64url");
2196
+ }
2197
+ function generatePKCE() {
2198
+ const codeVerifier = generateCodeVerifier();
2199
+ const codeChallenge = generateCodeChallenge(codeVerifier);
2200
+ return { codeVerifier, codeChallenge };
2201
+ }
2202
+ function generateState() {
2203
+ return crypto2.randomBytes(16).toString("base64url");
2204
+ }
2205
+ function encodeState(csrf, appState) {
2206
+ const payload = { csrf, appState };
2207
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
2208
+ }
2209
+ function decodeState(state) {
2210
+ try {
2211
+ const json = Buffer.from(state, "base64url").toString("utf-8");
2212
+ return JSON.parse(json);
2213
+ } catch (e4) {
2214
+ return null;
2215
+ }
2216
+ }
2217
+ function encodeStateWithVerifier(csrf, codeVerifier) {
2218
+ const encodedVerifier = Buffer.from(codeVerifier).toString("base64url");
2219
+ return `${csrf}:${encodedVerifier}`;
2220
+ }
2221
+ function decodeStateWithVerifier(state) {
2222
+ const colonIndex = state.indexOf(":");
2223
+ if (colonIndex === -1) return null;
2224
+ const csrf = state.substring(0, colonIndex);
2225
+ const encodedVerifier = state.substring(colonIndex + 1);
2226
+ try {
2227
+ const codeVerifier = Buffer.from(encodedVerifier, "base64url").toString("utf-8");
2228
+ return { csrf, codeVerifier };
2229
+ } catch (e5) {
2230
+ return null;
2231
+ }
2232
+ }
2233
+ function buildAuthorizeUrl(params) {
2234
+ const url = new URL(`${params.authVitalHost}/oauth/authorize`);
2235
+ url.searchParams.set("client_id", params.clientId);
2236
+ url.searchParams.set("redirect_uri", params.redirectUri);
2237
+ url.searchParams.set("response_type", "code");
2238
+ url.searchParams.set("scope", params.scope || "openid profile email");
2239
+ url.searchParams.set("state", params.state);
2240
+ url.searchParams.set("code_challenge", params.codeChallenge);
2241
+ url.searchParams.set("code_challenge_method", "S256");
2242
+ if (params.nonce) {
2243
+ url.searchParams.set("nonce", params.nonce);
2244
+ }
2245
+ return url.toString();
2246
+ }
2247
+ async function exchangeCodeForTokens(params) {
2248
+ const body = {
2249
+ grant_type: "authorization_code",
2250
+ code: params.code,
2251
+ redirect_uri: params.redirectUri,
2252
+ client_id: params.clientId,
2253
+ code_verifier: params.codeVerifier
2254
+ };
2255
+ if (params.clientSecret) {
2256
+ body.client_secret = params.clientSecret;
2257
+ }
2258
+ const response = await fetch(`${params.authVitalHost}/oauth/token`, {
2259
+ method: "POST",
2260
+ headers: { "Content-Type": "application/json" },
2261
+ body: JSON.stringify(body)
2262
+ });
2263
+ if (!response.ok) {
2264
+ const error = await response.json().catch(() => ({}));
2265
+ throw new Error(error.message || `Token exchange failed: ${response.status}`);
2266
+ }
2267
+ return response.json();
2268
+ }
2269
+ async function refreshAccessToken(params) {
2270
+ const body = {
2271
+ grant_type: "refresh_token",
2272
+ refresh_token: params.refreshToken,
2273
+ client_id: params.clientId
2274
+ };
2275
+ if (params.clientSecret) {
2276
+ body.client_secret = params.clientSecret;
2277
+ }
2278
+ const response = await fetch(`${params.authVitalHost}/oauth/token`, {
2279
+ method: "POST",
2280
+ headers: { "Content-Type": "application/json" },
2281
+ body: JSON.stringify(body)
2282
+ });
2283
+ if (!response.ok) {
2284
+ const error = await response.json().catch(() => ({}));
2285
+ throw new Error(error.message || `Token refresh failed: ${response.status}`);
2286
+ }
2287
+ return response.json();
2288
+ }
2289
+ function decodeJwt(token) {
2290
+ try {
2291
+ const parts = token.split(".");
2292
+ if (parts.length !== 3) return null;
2293
+ const payload = Buffer.from(parts[1], "base64url").toString("utf-8");
2294
+ return JSON.parse(payload);
2295
+ } catch (e6) {
2296
+ return null;
2297
+ }
2298
+ }
2299
+ var OAuthFlow = class {
2300
+ constructor(config) {
2301
+ this.config = config;
2302
+ }
2303
+ /**
2304
+ * Start the OAuth flow - generates PKCE, state, and authorize URL
2305
+ *
2306
+ * @param options.appState - Optional app-specific state to pass through OAuth (e.g., return URL)
2307
+ */
2308
+ startFlow(options) {
2309
+ const { codeVerifier, codeChallenge } = generatePKCE();
2310
+ const csrfNonce = generateState();
2311
+ const state = encodeState(csrfNonce, _optionalChain([options, 'optionalAccess', _35 => _35.appState]));
2312
+ const authorizeUrl = buildAuthorizeUrl({
2313
+ authVitalHost: this.config.authVitalHost,
2314
+ clientId: this.config.clientId,
2315
+ redirectUri: this.config.redirectUri,
2316
+ state,
2317
+ codeChallenge,
2318
+ scope: this.config.scope
2319
+ });
2320
+ return { authorizeUrl, state, codeVerifier, codeChallenge };
2321
+ }
2322
+ /**
2323
+ * Handle the OAuth callback - verify state and exchange code for tokens
2324
+ *
2325
+ * @param code - Authorization code from callback
2326
+ * @param receivedState - State parameter from callback URL
2327
+ * @param expectedState - State that was stored when starting the flow
2328
+ * @param codeVerifier - PKCE code verifier that was stored when starting the flow
2329
+ * @returns Token response with optional appState that was passed through
2330
+ * @throws Error if state doesn't match (CSRF) or token exchange fails
2331
+ */
2332
+ async handleCallback(code, receivedState, expectedState, codeVerifier) {
2333
+ const receivedPayload = decodeState(receivedState);
2334
+ const expectedPayload = decodeState(expectedState);
2335
+ if (!receivedPayload || !expectedPayload) {
2336
+ if (receivedState !== expectedState) {
2337
+ throw new Error("State mismatch - possible CSRF attack");
2338
+ }
2339
+ } else {
2340
+ if (receivedPayload.csrf !== expectedPayload.csrf) {
2341
+ throw new Error("State mismatch - possible CSRF attack");
2342
+ }
2343
+ }
2344
+ const tokens = await exchangeCodeForTokens({
2345
+ authVitalHost: this.config.authVitalHost,
2346
+ clientId: this.config.clientId,
2347
+ clientSecret: this.config.clientSecret,
2348
+ code,
2349
+ codeVerifier,
2350
+ redirectUri: this.config.redirectUri
2351
+ });
2352
+ return {
2353
+ ...tokens,
2354
+ appState: _optionalChain([receivedPayload, 'optionalAccess', _36 => _36.appState])
2355
+ };
2356
+ }
2357
+ /**
2358
+ * Refresh tokens using refresh token
2359
+ */
2360
+ async refreshTokens(refreshToken) {
2361
+ return refreshAccessToken({
2362
+ authVitalHost: this.config.authVitalHost,
2363
+ clientId: this.config.clientId,
2364
+ clientSecret: this.config.clientSecret,
2365
+ refreshToken
2366
+ });
2367
+ }
2368
+ };
2369
+
2370
+ // src/server/urls.ts
2371
+ function getSignupUrl(options) {
2372
+ const url = new URL(`${options.authVitalHost}/auth/signup`);
2373
+ url.searchParams.set("client_id", options.clientId);
2374
+ if (options.redirectUri) {
2375
+ url.searchParams.set("redirect_uri", options.redirectUri);
2376
+ }
2377
+ if (options.email) {
2378
+ url.searchParams.set("email", options.email);
2379
+ }
2380
+ if (options.inviteToken) {
2381
+ url.searchParams.set("invite_token", options.inviteToken);
2382
+ }
2383
+ return url.toString();
2384
+ }
2385
+ function getLoginUrl(options) {
2386
+ const url = new URL(`${options.authVitalHost}/auth/login`);
2387
+ url.searchParams.set("client_id", options.clientId);
2388
+ if (options.redirectUri) {
2389
+ url.searchParams.set("redirect_uri", options.redirectUri);
2390
+ }
2391
+ if (options.email) {
2392
+ url.searchParams.set("email", options.email);
2393
+ }
2394
+ if (options.tenantHint) {
2395
+ url.searchParams.set("tenant_hint", options.tenantHint);
2396
+ }
2397
+ return url.toString();
2398
+ }
2399
+ function getPasswordResetUrl(options) {
2400
+ const url = new URL(`${options.authVitalHost}/auth/reset-password`);
2401
+ url.searchParams.set("client_id", options.clientId);
2402
+ if (options.redirectUri) {
2403
+ url.searchParams.set("redirect_uri", options.redirectUri);
2404
+ }
2405
+ if (options.email) {
2406
+ url.searchParams.set("email", options.email);
2407
+ }
2408
+ return url.toString();
2409
+ }
2410
+ function getInviteAcceptUrl(options) {
2411
+ const url = new URL(`${options.authVitalHost}/auth/accept-invite`);
2412
+ url.searchParams.set("client_id", options.clientId);
2413
+ url.searchParams.set("token", options.inviteToken);
2414
+ if (options.redirectUri) {
2415
+ url.searchParams.set("redirect_uri", options.redirectUri);
2416
+ }
2417
+ return url.toString();
2418
+ }
2419
+ function getLogoutUrl(options) {
2420
+ const url = new URL(`${options.authVitalHost}/api/auth/logout/redirect`);
2421
+ if (options.postLogoutRedirectUri) {
2422
+ url.searchParams.set("post_logout_redirect_uri", options.postLogoutRedirectUri);
2423
+ }
2424
+ return url.toString();
2425
+ }
2426
+ function getAccountSettingsUrl(authVitalHost) {
2427
+ return `${authVitalHost.replace(/\/$/, "")}/account/settings`;
2428
+ }
2429
+
2430
+ // src/sync/prisma-schema.ts
2431
+ var IDENTITY_SCHEMA = `
2432
+ // =============================================================================
2433
+ // AUTHVITAL IDENTITY (synced from IDP)
2434
+ // =============================================================================
2435
+ // Copy this into your schema.prisma and customize as needed.
2436
+ // The sync handler only touches these base fields.
2437
+
2438
+ model Identity {
2439
+ // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2440
+ // CORE IDENTITY (synced from AuthVital - OIDC Standard Claims)
2441
+ // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2442
+ id String @id // AuthVital subject ID (sub claim)
2443
+ username String? @unique // Unique handle (@janesmith)
2444
+ displayName String? @map("display_name") // Full name (OIDC: name)
2445
+ givenName String? @map("given_name") // First name
2446
+ familyName String? @map("family_name") // Last name
2447
+ middleName String? @map("middle_name") // Middle name(s)
2448
+ nickname String? // Casual name
2449
+ pictureUrl String? @map("picture_url") // Profile picture URL
2450
+ website String? // Personal URL
2451
+ gender String? // Gender identity
2452
+ birthdate String? // YYYY-MM-DD
2453
+ zoneinfo String? // IANA timezone
2454
+ locale String? // Language (e.g., en-US)
2455
+
2456
+ // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2457
+ // EMAIL SCOPE (OIDC Standard)
2458
+ // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2459
+ email String? @unique
2460
+ emailVerified Boolean @default(false) @map("email_verified")
2461
+
2462
+ // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2463
+ // PHONE SCOPE (OIDC Standard)
2464
+ // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2465
+ phone String? @unique
2466
+ phoneVerified Boolean @default(false) @map("phone_verified")
2467
+
2468
+ // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2469
+ // TENANT CONTEXT (for multi-tenant apps)
2470
+ // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2471
+ tenantId String? @map("tenant_id") // Current tenant ID
2472
+ appRole String? @map("app_role") // Role slug (e.g., "admin")
2473
+ groups String[] @default([]) // Group slugs in tenant
2474
+
2475
+ // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2476
+ // STATUS
2477
+ // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2478
+ isActive Boolean @default(true) @map("is_active") // IDP-level: can user log in at all?
2479
+ hasAppAccess Boolean @default(true) @map("has_app_access") // App-level: does user have access to THIS app?
2480
+
2481
+ // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2482
+ // SYNC METADATA
2483
+ // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2484
+ syncedAt DateTime @default(now()) @map("synced_at")
2485
+ createdAt DateTime @default(now()) @map("created_at")
2486
+ updatedAt DateTime @updatedAt @map("updated_at")
2487
+
2488
+ // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2489
+ // RELATIONS
2490
+ // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2491
+ sessions IdentitySession[]
2492
+
2493
+ // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2494
+ // ADD YOUR APP-SPECIFIC RELATIONS BELOW
2495
+ // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2496
+ // profile UserProfile?
2497
+ // posts Post[]
2498
+
2499
+ @@index([tenantId])
2500
+ @@index([email])
2501
+ @@index([username])
2502
+ @@map("av_identities")
2503
+ }
2504
+ `;
2505
+ var IDENTITY_SESSION_SCHEMA = `
2506
+ // =============================================================================
2507
+ // AUTHVITAL IDENTITY SESSION (optional - for session management)
2508
+ // =============================================================================
2509
+
2510
+ model IdentitySession {
2511
+ id String @id @default(cuid())
2512
+ identityId String @map("identity_id")
2513
+ identity Identity @relation(fields: [identityId], references: [id], onDelete: Cascade)
2514
+
2515
+ authSessionId String? @map("auth_session_id") // AuthVital session ID
2516
+ deviceInfo String? @map("device_info") // Parsed device info
2517
+ ipAddress String? @map("ip_address")
2518
+ userAgent String? @map("user_agent")
2519
+
2520
+ createdAt DateTime @default(now()) @map("created_at")
2521
+ lastActiveAt DateTime @default(now()) @map("last_active_at")
2522
+ expiresAt DateTime @map("expires_at")
2523
+ revokedAt DateTime? @map("revoked_at")
2524
+
2525
+ @@index([identityId])
2526
+ @@index([authSessionId])
2527
+ @@map("av_identity_sessions")
2528
+ }
2529
+ `;
2530
+ var FULL_SCHEMA = `${IDENTITY_SCHEMA}
2531
+
2532
+ ${IDENTITY_SESSION_SCHEMA}`;
2533
+ function printSchema() {
2534
+ console.log("// ============================================================================");
2535
+ console.log("// AUTHVITAL SDK - PRISMA SCHEMA SNIPPET");
2536
+ console.log("// Copy the following into your schema.prisma file");
2537
+ console.log("// ============================================================================");
2538
+ console.log(FULL_SCHEMA);
2539
+ }
2540
+
2541
+ // src/sync/identity-sync-handler.ts
2542
+ function extractOidcFields(data) {
2543
+ const fields = {};
2544
+ if (data.preferred_username !== void 0) fields.username = data.preferred_username;
2545
+ if (data.name !== void 0) fields.displayName = data.name;
2546
+ if (data.given_name !== void 0) fields.givenName = data.given_name;
2547
+ if (data.family_name !== void 0) fields.familyName = data.family_name;
2548
+ if (data.middle_name !== void 0) fields.middleName = data.middle_name;
2549
+ if (data.nickname !== void 0) fields.nickname = data.nickname;
2550
+ if (data.picture !== void 0) fields.pictureUrl = data.picture;
2551
+ if (data.website !== void 0) fields.website = data.website;
2552
+ if (data.gender !== void 0) fields.gender = data.gender;
2553
+ if (data.birthdate !== void 0) fields.birthdate = data.birthdate;
2554
+ if (data.zoneinfo !== void 0) fields.zoneinfo = data.zoneinfo;
2555
+ if (data.locale !== void 0) fields.locale = data.locale;
2556
+ if (data.email !== void 0) fields.email = data.email;
2557
+ if (data.email_verified !== void 0) fields.emailVerified = data.email_verified;
2558
+ if (data.phone_number !== void 0) fields.phone = data.phone_number;
2559
+ if (data.phone_number_verified !== void 0) fields.phoneVerified = data.phone_number_verified;
2560
+ if (data.groups !== void 0) fields.groups = data.groups;
2561
+ return fields;
2562
+ }
2563
+ var IdentitySyncHandler = class extends _chunkFXVD4Y5Gjs.AuthVitalEventHandler {
2564
+ /**
2565
+ * Create a new identity sync handler
2566
+ *
2567
+ * @param prismaOrResolver - Either a Prisma client (shared DB) or a resolver function (tenant-isolated DBs)
2568
+ *
2569
+ * @example Shared database
2570
+ * ```typescript
2571
+ * new IdentitySyncHandler(prisma)
2572
+ * ```
2573
+ *
2574
+ * @example Tenant-isolated databases
2575
+ * ```typescript
2576
+ * new IdentitySyncHandler((tenantId) => getTenantPrisma(tenantId))
2577
+ * ```
2578
+ */
2579
+ constructor(prismaOrResolver) {
2580
+ super();
2581
+ this.prismaOrResolver = prismaOrResolver;
2582
+ this.isResolver = typeof prismaOrResolver === "function";
2583
+ }
2584
+ /**
2585
+ * Get the Prisma client for the given tenant
2586
+ * - Shared DB mode: Returns the single client (tenantId ignored)
2587
+ * - Tenant-isolated mode: Calls resolver with tenantId
2588
+ */
2589
+ async getClient(tenantId) {
2590
+ if (this.isResolver) {
2591
+ if (!tenantId) {
2592
+ throw new Error(
2593
+ "[IdentitySyncHandler] Tenant-isolated mode requires tenant_id in webhook event. Ensure webhooks are configured with tenant context."
2594
+ );
2595
+ }
2596
+ return this.prismaOrResolver(tenantId);
2597
+ }
2598
+ return this.prismaOrResolver;
2599
+ }
2600
+ // ===========================================================================
2601
+ // SUBJECT EVENTS (identity lifecycle)
2602
+ // ===========================================================================
2603
+ async onSubjectCreated(event) {
2604
+ const prisma = await this.getClient(event.tenant_id);
2605
+ const data = event.data;
2606
+ const oidcFields = extractOidcFields(data);
2607
+ const identityData = {
2608
+ id: data.sub,
2609
+ ...oidcFields,
2610
+ tenantId: _nullishCoalesce(event.tenant_id, () => ( null)),
2611
+ isActive: true
2612
+ };
2613
+ await prisma.identity.upsert({
2614
+ where: { id: data.sub },
2615
+ create: identityData,
2616
+ update: {
2617
+ ...oidcFields,
2618
+ syncedAt: /* @__PURE__ */ new Date()
2619
+ }
2620
+ });
2621
+ }
2622
+ async onSubjectUpdated(event) {
2623
+ const prisma = await this.getClient(event.tenant_id);
2624
+ const data = event.data;
2625
+ const oidcFields = extractOidcFields(data);
2626
+ const updateData = {
2627
+ syncedAt: /* @__PURE__ */ new Date()
2628
+ };
2629
+ const changedFields = _nullishCoalesce(data.changed_fields, () => ( []));
2630
+ if (changedFields.length === 0) {
2631
+ Object.assign(updateData, oidcFields);
2632
+ } else {
2633
+ const fieldMap = {
2634
+ "preferred_username": "username",
2635
+ "username": "username",
2636
+ "name": "displayName",
2637
+ "display_name": "displayName",
2638
+ "given_name": "givenName",
2639
+ "family_name": "familyName",
2640
+ "middle_name": "middleName",
2641
+ "nickname": "nickname",
2642
+ "picture": "pictureUrl",
2643
+ "website": "website",
2644
+ "gender": "gender",
2645
+ "birthdate": "birthdate",
2646
+ "zoneinfo": "zoneinfo",
2647
+ "locale": "locale",
2648
+ "email": "email",
2649
+ "email_verified": "emailVerified",
2650
+ "phone_number": "phone",
2651
+ "phone_verified": "phoneVerified",
2652
+ "groups": "groups"
2653
+ };
2654
+ for (const field of changedFields) {
2655
+ const mappedField = fieldMap[field];
2656
+ if (mappedField && mappedField in oidcFields) {
2657
+ updateData[mappedField] = oidcFields[mappedField];
2658
+ }
2659
+ }
2660
+ }
2661
+ await prisma.identity.upsert({
2662
+ where: { id: data.sub },
2663
+ create: {
2664
+ id: data.sub,
2665
+ ...oidcFields,
2666
+ isActive: true
2667
+ },
2668
+ update: updateData
2669
+ });
2670
+ }
2671
+ async onSubjectDeleted(event) {
2672
+ const prisma = await this.getClient(event.tenant_id);
2673
+ const { sub } = event.data;
2674
+ try {
2675
+ await prisma.identity.delete({
2676
+ where: { id: sub }
2677
+ });
2678
+ } catch (e7) {
2679
+ console.warn(`[IdentitySyncHandler] Identity ${sub} not found for deletion`);
2680
+ }
2681
+ }
2682
+ async onSubjectDeactivated(event) {
2683
+ const prisma = await this.getClient(event.tenant_id);
2684
+ const { sub } = event.data;
2685
+ await prisma.identity.update({
2686
+ where: { id: sub },
2687
+ data: {
2688
+ isActive: false,
2689
+ syncedAt: /* @__PURE__ */ new Date()
2690
+ }
2691
+ });
2692
+ }
2693
+ // ===========================================================================
2694
+ // MEMBER EVENTS (tenant context)
2695
+ // ===========================================================================
2696
+ async onMemberJoined(event) {
2697
+ const prisma = await this.getClient(event.tenant_id);
2698
+ const data = event.data;
2699
+ const tenantId = event.tenant_id;
2700
+ const oidcFields = extractOidcFields(data);
2701
+ const primaryRole = _nullishCoalesce(_optionalChain([data, 'access', _37 => _37.tenant_roles, 'optionalAccess', _38 => _38[0]]), () => ( null));
2702
+ await prisma.identity.upsert({
2703
+ where: { id: data.sub },
2704
+ create: {
2705
+ id: data.sub,
2706
+ ...oidcFields,
2707
+ tenantId: _nullishCoalesce(tenantId, () => ( null)),
2708
+ appRole: primaryRole,
2709
+ isActive: true
2710
+ },
2711
+ update: {
2712
+ ...oidcFields,
2713
+ tenantId: _nullishCoalesce(tenantId, () => ( null)),
2714
+ appRole: primaryRole,
2715
+ groups: _nullishCoalesce(data.groups, () => ( [])),
2716
+ syncedAt: /* @__PURE__ */ new Date()
2717
+ }
2718
+ });
2719
+ }
2720
+ async onMemberLeft(event) {
2721
+ const prisma = await this.getClient(event.tenant_id);
2722
+ const { sub } = event.data;
2723
+ await prisma.identity.update({
2724
+ where: { id: sub },
2725
+ data: {
2726
+ tenantId: null,
2727
+ appRole: null,
2728
+ groups: [],
2729
+ syncedAt: /* @__PURE__ */ new Date()
2730
+ }
2731
+ });
2732
+ }
2733
+ async onMemberRoleChanged(event) {
2734
+ const prisma = await this.getClient(event.tenant_id);
2735
+ const data = event.data;
2736
+ const oidcFields = extractOidcFields(data);
2737
+ const primaryRole = _nullishCoalesce(_optionalChain([data, 'access', _39 => _39.tenant_roles, 'optionalAccess', _40 => _40[0]]), () => ( null));
2738
+ await prisma.identity.update({
2739
+ where: { id: data.sub },
2740
+ data: {
2741
+ ...oidcFields,
2742
+ appRole: primaryRole,
2743
+ groups: _nullishCoalesce(data.groups, () => ( [])),
2744
+ syncedAt: /* @__PURE__ */ new Date()
2745
+ }
2746
+ });
2747
+ }
2748
+ // ===========================================================================
2749
+ // APP ACCESS EVENTS (app-specific role)
2750
+ // ===========================================================================
2751
+ async onAppAccessGranted(event) {
2752
+ const prisma = await this.getClient(event.tenant_id);
2753
+ const data = event.data;
2754
+ const tenantId = event.tenant_id;
2755
+ const oidcFields = extractOidcFields(data);
2756
+ await prisma.identity.upsert({
2757
+ where: { id: data.sub },
2758
+ create: {
2759
+ id: data.sub,
2760
+ ...oidcFields,
2761
+ tenantId: _nullishCoalesce(tenantId, () => ( null)),
2762
+ appRole: _nullishCoalesce(data.role_slug, () => ( null)),
2763
+ isActive: true,
2764
+ hasAppAccess: true
2765
+ },
2766
+ update: {
2767
+ ...oidcFields,
2768
+ tenantId: _nullishCoalesce(tenantId, () => ( null)),
2769
+ appRole: _nullishCoalesce(data.role_slug, () => ( null)),
2770
+ hasAppAccess: true,
2771
+ groups: _nullishCoalesce(data.groups, () => ( [])),
2772
+ syncedAt: /* @__PURE__ */ new Date()
2773
+ }
2774
+ });
2775
+ }
2776
+ async onAppAccessRevoked(event) {
2777
+ const prisma = await this.getClient(event.tenant_id);
2778
+ const { sub } = event.data;
2779
+ await prisma.identity.update({
2780
+ where: { id: sub },
2781
+ data: {
2782
+ appRole: null,
2783
+ hasAppAccess: false,
2784
+ syncedAt: /* @__PURE__ */ new Date()
2785
+ }
2786
+ });
2787
+ }
2788
+ async onAppAccessRoleChanged(event) {
2789
+ const prisma = await this.getClient(event.tenant_id);
2790
+ const data = event.data;
2791
+ const oidcFields = extractOidcFields(data);
2792
+ await prisma.identity.update({
2793
+ where: { id: data.sub },
2794
+ data: {
2795
+ ...oidcFields,
2796
+ appRole: _nullishCoalesce(data.role_slug, () => ( null)),
2797
+ groups: _nullishCoalesce(data.groups, () => ( [])),
2798
+ syncedAt: /* @__PURE__ */ new Date()
2799
+ }
2800
+ });
2801
+ }
2802
+ };
2803
+
2804
+ // src/sync/session-cleanup.ts
2805
+ async function cleanupSessions(prisma, options = {}) {
2806
+ const {
2807
+ expiredOlderThanDays = 30,
2808
+ deleteRevoked = false,
2809
+ dryRun = false
2810
+ } = options;
2811
+ const cutoffDate = /* @__PURE__ */ new Date();
2812
+ cutoffDate.setDate(cutoffDate.getDate() - expiredOlderThanDays);
2813
+ const whereConditions = [
2814
+ // Expired sessions older than cutoff
2815
+ { expiresAt: { lt: cutoffDate } }
2816
+ ];
2817
+ if (deleteRevoked) {
2818
+ whereConditions.push({ revokedAt: { lt: cutoffDate } });
2819
+ }
2820
+ if (dryRun) {
2821
+ console.log("[SessionCleanup] DRY RUN - Would delete sessions:");
2822
+ console.log(` - Expired before: ${cutoffDate.toISOString()}`);
2823
+ if (deleteRevoked) {
2824
+ console.log(` - Revoked before: ${cutoffDate.toISOString()}`);
2825
+ }
2826
+ return {
2827
+ deletedCount: 0,
2828
+ // Can't know without actually querying
2829
+ dryRun: true
2830
+ };
2831
+ }
2832
+ const result = await prisma.identitySession.deleteMany({
2833
+ where: {
2834
+ OR: whereConditions
2835
+ }
2836
+ });
2837
+ console.log(`[SessionCleanup] Deleted ${result.count} sessions`);
2838
+ return {
2839
+ deletedCount: result.count,
2840
+ dryRun: false
2841
+ };
2842
+ }
2843
+ function getCleanupSQL(options = {}) {
2844
+ const { expiredOlderThanDays = 30, deleteRevoked = false } = options;
2845
+ let sql = `-- AuthVital SDK: Session Cleanup
2846
+ -- Run this periodically (e.g., daily via pg_cron)
2847
+
2848
+ DELETE FROM av_identity_sessions
2849
+ WHERE expires_at < NOW() - INTERVAL '${expiredOlderThanDays} days'`;
2850
+ if (deleteRevoked) {
2851
+ sql += `
2852
+ OR revoked_at < NOW() - INTERVAL '${expiredOlderThanDays} days'`;
2853
+ }
2854
+ sql += ";";
2855
+ return sql;
2856
+ }
2857
+
2858
+
2859
+
2860
+
2861
+
2862
+
2863
+
2864
+
2865
+
2866
+
2867
+
2868
+
2869
+
2870
+
2871
+
2872
+
2873
+
2874
+
2875
+
2876
+
2877
+
2878
+
2879
+
2880
+
2881
+
2882
+
2883
+
2884
+
2885
+
2886
+
2887
+
2888
+
2889
+
2890
+
2891
+
2892
+
2893
+
2894
+
2895
+
2896
+
2897
+
2898
+
2899
+
2900
+
2901
+
2902
+
2903
+
2904
+
2905
+
2906
+
2907
+
2908
+
2909
+
2910
+
2911
+
2912
+
2913
+
2914
+
2915
+ exports.AppAccessEventHandler = _chunkFXVD4Y5Gjs.AppAccessEventHandler; exports.AuthVital = AuthVital; exports.AuthVitalEventHandler = _chunkFXVD4Y5Gjs.AuthVitalEventHandler; exports.BaseClient = BaseClient; exports.FULL_SCHEMA = FULL_SCHEMA; exports.IDENTITY_SCHEMA = IDENTITY_SCHEMA; exports.IDENTITY_SESSION_SCHEMA = IDENTITY_SESSION_SCHEMA; exports.IdentitySyncHandler = IdentitySyncHandler; exports.InviteEventHandler = _chunkFXVD4Y5Gjs.InviteEventHandler; exports.JwtValidator = JwtValidator; exports.LicenseEventHandler = _chunkFXVD4Y5Gjs.LicenseEventHandler; exports.MemberEventHandler = _chunkFXVD4Y5Gjs.MemberEventHandler; exports.OAuthFlow = OAuthFlow; exports.SYNC_EVENT_TYPES = _chunkFXVD4Y5Gjs.SYNC_EVENT_TYPES; exports.SubjectEventHandler = _chunkFXVD4Y5Gjs.SubjectEventHandler; exports.WebhookRouter = _chunkFXVD4Y5Gjs.WebhookRouter; exports.WebhookVerifier = _chunkFXVD4Y5Gjs.AuthVitalWebhooks; exports.appendClientIdToUri = appendClientIdToUri; exports.buildAuthorizeUrl = buildAuthorizeUrl; exports.cleanupSessions = cleanupSessions; exports.createAuthVital = createAuthVital; exports.createEntitlementsNamespace = createEntitlementsNamespace; exports.createInvitationsNamespace = createInvitationsNamespace; exports.createJwtMiddleware = createJwtMiddleware; exports.createJwtValidator = createJwtValidator; exports.createLicensesNamespace = createLicensesNamespace; exports.createMembershipsNamespace = createMembershipsNamespace; exports.createPassportJwtOptions = createPassportJwtOptions; exports.createPermissionsNamespace = createPermissionsNamespace; exports.createSessionsNamespace = createSessionsNamespace; exports.decodeJwt = decodeJwt; exports.decodeState = decodeState; exports.decodeStateWithVerifier = decodeStateWithVerifier; exports.encodeState = encodeState; exports.encodeStateWithVerifier = encodeStateWithVerifier; exports.exchangeCodeForTokens = exchangeCodeForTokens; exports.extractAuthorizationHeader = extractAuthorizationHeader; exports.generateCodeChallenge = generateCodeChallenge; exports.generateCodeVerifier = generateCodeVerifier; exports.generatePKCE = generatePKCE; exports.generateState = generateState; exports.getAccountSettingsUrl = getAccountSettingsUrl; exports.getCleanupSQL = getCleanupSQL; exports.getCurrentUser = getCurrentUser; exports.getCurrentUserFromConfig = getCurrentUserFromConfig; exports.getInviteAcceptUrl = getInviteAcceptUrl; exports.getLoginUrl = getLoginUrl; exports.getLogoutUrl = getLogoutUrl; exports.getPasswordResetUrl = getPasswordResetUrl; exports.getSignupUrl = getSignupUrl; exports.isAppAccessEvent = _chunkFXVD4Y5Gjs.isAppAccessEvent; exports.isInviteEvent = _chunkFXVD4Y5Gjs.isInviteEvent; exports.isLicenseEvent = _chunkFXVD4Y5Gjs.isLicenseEvent; exports.isMemberEvent = _chunkFXVD4Y5Gjs.isMemberEvent; exports.isSubjectEvent = _chunkFXVD4Y5Gjs.isSubjectEvent; exports.printSchema = printSchema; exports.refreshAccessToken = refreshAccessToken;