authvital-sdk 0.1.1-dev.3.cefb119.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +657 -0
- package/dist/chunk-ETJ5ICJ7.mjs +412 -0
- package/dist/chunk-FXVD4Y5G.js +412 -0
- package/dist/chunk-JNEJMHGA.mjs +235 -0
- package/dist/chunk-JPODZIZT.mjs +95 -0
- package/dist/chunk-QPYBK2J4.js +235 -0
- package/dist/chunk-R4OHZZQP.js +95 -0
- package/dist/index.d.mts +615 -0
- package/dist/index.d.ts +615 -0
- package/dist/index.js +1299 -0
- package/dist/index.mjs +1299 -0
- package/dist/invitations-EFJA5C6L.mjs +22 -0
- package/dist/invitations-LZHJ3AZY.js +22 -0
- package/dist/oauth-K7E7OCWI.js +34 -0
- package/dist/oauth-UAFXEKZ7.mjs +34 -0
- package/dist/server.d.mts +2656 -0
- package/dist/server.d.ts +2656 -0
- package/dist/server.js +2915 -0
- package/dist/server.mjs +2915 -0
- package/dist/webhook-router-DdfXLtHa.d.mts +461 -0
- package/dist/webhook-router-DdfXLtHa.d.ts +461 -0
- package/package.json +71 -0
package/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;
|