@happyvertical/smrt-users 0.37.2 → 0.37.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/TerminalAuthService-BXAAuaXf.js +4531 -0
- package/dist/chunks/TerminalAuthService-BXAAuaXf.js.map +1 -0
- package/dist/index.js +1564 -2024
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +2 -2
- package/dist/playground.js +114 -136
- package/dist/playground.js.map +1 -1
- package/dist/smrt-knowledge.json +12 -12
- package/dist/sveltekit.js +685 -881
- package/dist/sveltekit.js.map +1 -1
- package/dist/ui.js +79 -73
- package/dist/ui.js.map +1 -1
- package/package.json +19 -19
- package/dist/chunks/TerminalAuthService-DcgimQYo.js +0 -5261
- package/dist/chunks/TerminalAuthService-DcgimQYo.js.map +0 -1
- package/dist/chunks/index-C3E55ikp.js +0 -169
- package/dist/chunks/index-C3E55ikp.js.map +0 -1
|
@@ -0,0 +1,4531 @@
|
|
|
1
|
+
import { ObjectRegistry, SmrtCollection, SmrtObject, crossPackageRef, field, foreignKey, smrt } from "@happyvertical/smrt-core";
|
|
2
|
+
import { AccessRequestStatus, MembershipStatus, OverrideEffect, SessionStatus, TenantPermissionEffect, TenantStatus, UserStatus } from "@happyvertical/smrt-types";
|
|
3
|
+
import { getPackageConfig } from "@happyvertical/smrt-config";
|
|
4
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
5
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
|
+
//#region \0rolldown/runtime.js
|
|
7
|
+
var __defProp$12 = Object.defineProperty;
|
|
8
|
+
var __exportAll = (all, no_symbols) => {
|
|
9
|
+
let target = {};
|
|
10
|
+
for (var name in all) __defProp$12(target, name, {
|
|
11
|
+
get: all[name],
|
|
12
|
+
enumerable: true
|
|
13
|
+
});
|
|
14
|
+
if (!no_symbols) __defProp$12(target, Symbol.toStringTag, { value: "Module" });
|
|
15
|
+
return target;
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/__smrt-register__.ts
|
|
19
|
+
ObjectRegistry.registerPackageManifest(new URL("./manifest.json", "" + import.meta.url));
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/types/index.ts
|
|
22
|
+
var DEFAULT_ROLE_SLUGS = {
|
|
23
|
+
OWNER: "owner",
|
|
24
|
+
ADMIN: "admin",
|
|
25
|
+
MEMBER: "member",
|
|
26
|
+
VIEWER: "viewer"
|
|
27
|
+
};
|
|
28
|
+
var DEFAULT_ROLES = [
|
|
29
|
+
{
|
|
30
|
+
slug: DEFAULT_ROLE_SLUGS.OWNER,
|
|
31
|
+
name: "Owner",
|
|
32
|
+
description: "Full access to all resources"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
slug: DEFAULT_ROLE_SLUGS.ADMIN,
|
|
36
|
+
name: "Administrator",
|
|
37
|
+
description: "Manage users and settings"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
slug: DEFAULT_ROLE_SLUGS.MEMBER,
|
|
41
|
+
name: "Member",
|
|
42
|
+
description: "Standard access"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
slug: DEFAULT_ROLE_SLUGS.VIEWER,
|
|
46
|
+
name: "Viewer",
|
|
47
|
+
description: "Read-only access"
|
|
48
|
+
}
|
|
49
|
+
];
|
|
50
|
+
var DEFAULT_TENANT_POLICY = {
|
|
51
|
+
mode: "flexible",
|
|
52
|
+
maxTenants: 0,
|
|
53
|
+
defaultName: "Default Workspace"
|
|
54
|
+
};
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/models/User.ts
|
|
57
|
+
var __defProp$11 = Object.defineProperty;
|
|
58
|
+
var __getOwnPropDesc$11 = Object.getOwnPropertyDescriptor;
|
|
59
|
+
var __decorateClass$11 = (decorators, target, key, kind) => {
|
|
60
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$11(target, key) : target;
|
|
61
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
62
|
+
if (kind && result) __defProp$11(target, key, result);
|
|
63
|
+
return result;
|
|
64
|
+
};
|
|
65
|
+
var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
66
|
+
function isValidEmail(email) {
|
|
67
|
+
if (!email || typeof email !== "string") return false;
|
|
68
|
+
return EMAIL_REGEX.test(email.trim());
|
|
69
|
+
}
|
|
70
|
+
function normalizeEmail(email) {
|
|
71
|
+
if (!email || typeof email !== "string") return "";
|
|
72
|
+
return email.trim().toLowerCase();
|
|
73
|
+
}
|
|
74
|
+
var User = class extends SmrtObject {
|
|
75
|
+
profileId = "";
|
|
76
|
+
/**
|
|
77
|
+
* User's email address (unique, used for lookup)
|
|
78
|
+
*/
|
|
79
|
+
email = "";
|
|
80
|
+
status = UserStatus.ACTIVE;
|
|
81
|
+
/**
|
|
82
|
+
* Last login timestamp
|
|
83
|
+
*/
|
|
84
|
+
lastLoginAt = null;
|
|
85
|
+
constructor(options = {}) {
|
|
86
|
+
super(options);
|
|
87
|
+
if (options.profileId !== void 0) this.profileId = options.profileId;
|
|
88
|
+
if (options.email !== void 0) this.email = normalizeEmail(options.email);
|
|
89
|
+
if (options.status !== void 0) this.status = options.status;
|
|
90
|
+
if (options.lastLoginAt !== void 0) this.lastLoginAt = options.lastLoginAt;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Validate the user's email format.
|
|
94
|
+
* @returns true if email is valid
|
|
95
|
+
*/
|
|
96
|
+
hasValidEmail() {
|
|
97
|
+
return isValidEmail(this.email);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Check if user is active
|
|
101
|
+
*/
|
|
102
|
+
isActive() {
|
|
103
|
+
return this.status === UserStatus.ACTIVE;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Check if user is suspended
|
|
107
|
+
*/
|
|
108
|
+
isSuspended() {
|
|
109
|
+
return this.status === UserStatus.SUSPENDED;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Check if user is pending verification
|
|
113
|
+
*/
|
|
114
|
+
isPending() {
|
|
115
|
+
return this.status === UserStatus.PENDING;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Record a login event
|
|
119
|
+
*/
|
|
120
|
+
recordLogin() {
|
|
121
|
+
this.lastLoginAt = /* @__PURE__ */ new Date();
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
__decorateClass$11([crossPackageRef("@happyvertical/smrt-profiles:Profile")], User.prototype, "profileId", 2);
|
|
125
|
+
__decorateClass$11([field({ type: "text" })], User.prototype, "status", 2);
|
|
126
|
+
User = __decorateClass$11([smrt({
|
|
127
|
+
api: { include: ["list", "get"] },
|
|
128
|
+
mcp: { include: ["list", "get"] },
|
|
129
|
+
cli: true
|
|
130
|
+
})], User);
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/models/CliAuthRequest.ts
|
|
133
|
+
var __defProp$10 = Object.defineProperty;
|
|
134
|
+
var __getOwnPropDesc$10 = Object.getOwnPropertyDescriptor;
|
|
135
|
+
var __decorateClass$10 = (decorators, target, key, kind) => {
|
|
136
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$10(target, key) : target;
|
|
137
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
138
|
+
if (kind && result) __defProp$10(target, key, result);
|
|
139
|
+
return result;
|
|
140
|
+
};
|
|
141
|
+
var UsersCliAuthRequest = class extends SmrtObject {
|
|
142
|
+
userCode = "";
|
|
143
|
+
deviceCodeHash = "";
|
|
144
|
+
status = "pending";
|
|
145
|
+
userId = "";
|
|
146
|
+
tenantId = "";
|
|
147
|
+
sessionId = "";
|
|
148
|
+
expiresAt = /* @__PURE__ */ new Date();
|
|
149
|
+
approvedAt = null;
|
|
150
|
+
};
|
|
151
|
+
__decorateClass$10([field({ type: "text" })], UsersCliAuthRequest.prototype, "userCode", 2);
|
|
152
|
+
__decorateClass$10([field({ type: "text" })], UsersCliAuthRequest.prototype, "deviceCodeHash", 2);
|
|
153
|
+
__decorateClass$10([field({ type: "text" })], UsersCliAuthRequest.prototype, "status", 2);
|
|
154
|
+
__decorateClass$10([field({ type: "text" })], UsersCliAuthRequest.prototype, "userId", 2);
|
|
155
|
+
__decorateClass$10([field({ type: "text" })], UsersCliAuthRequest.prototype, "tenantId", 2);
|
|
156
|
+
__decorateClass$10([field({ type: "text" })], UsersCliAuthRequest.prototype, "sessionId", 2);
|
|
157
|
+
__decorateClass$10([field({ type: "datetime" })], UsersCliAuthRequest.prototype, "expiresAt", 2);
|
|
158
|
+
__decorateClass$10([field({
|
|
159
|
+
type: "datetime",
|
|
160
|
+
nullable: true
|
|
161
|
+
})], UsersCliAuthRequest.prototype, "approvedAt", 2);
|
|
162
|
+
UsersCliAuthRequest = __decorateClass$10([smrt({
|
|
163
|
+
tableName: "users_cli_auth_requests",
|
|
164
|
+
api: { include: [] },
|
|
165
|
+
cli: { include: [] },
|
|
166
|
+
mcp: { include: [] }
|
|
167
|
+
})], UsersCliAuthRequest);
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region src/collections/CliAuthRequestCollection.ts
|
|
170
|
+
var UsersCliAuthRequestCollection = class extends SmrtCollection {
|
|
171
|
+
static _itemClass = UsersCliAuthRequest;
|
|
172
|
+
/**
|
|
173
|
+
* Look up a pending or completed request by the short user code shown in the CLI.
|
|
174
|
+
*/
|
|
175
|
+
async findByUserCode(userCode) {
|
|
176
|
+
const [request] = await this.list({
|
|
177
|
+
limit: 1,
|
|
178
|
+
where: { userCode: userCode.trim().toUpperCase() }
|
|
179
|
+
});
|
|
180
|
+
return request ?? null;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Look up a request by the hash of its device code (the CLI's polling key).
|
|
184
|
+
*/
|
|
185
|
+
async findByDeviceCodeHash(deviceCodeHash) {
|
|
186
|
+
const [request] = await this.list({
|
|
187
|
+
limit: 1,
|
|
188
|
+
where: { deviceCodeHash }
|
|
189
|
+
});
|
|
190
|
+
return request ?? null;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Delete expired pending requests (cleanup job).
|
|
194
|
+
*/
|
|
195
|
+
async deleteExpired() {
|
|
196
|
+
const requests = await this.list({ where: {
|
|
197
|
+
status: "pending",
|
|
198
|
+
"expiresAt <": (/* @__PURE__ */ new Date()).toISOString()
|
|
199
|
+
} });
|
|
200
|
+
let count = 0;
|
|
201
|
+
for (const request of requests) {
|
|
202
|
+
await request.delete();
|
|
203
|
+
count++;
|
|
204
|
+
}
|
|
205
|
+
return count;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
//#endregion
|
|
209
|
+
//#region src/models/Group.ts
|
|
210
|
+
var __defProp$9 = Object.defineProperty;
|
|
211
|
+
var __getOwnPropDesc$9 = Object.getOwnPropertyDescriptor;
|
|
212
|
+
var __decorateClass$9 = (decorators, target, key, kind) => {
|
|
213
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$9(target, key) : target;
|
|
214
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
215
|
+
if (kind && result) __defProp$9(target, key, result);
|
|
216
|
+
return result;
|
|
217
|
+
};
|
|
218
|
+
var Group = class extends SmrtObject {
|
|
219
|
+
tenantId;
|
|
220
|
+
/**
|
|
221
|
+
* Display name for the group
|
|
222
|
+
*/
|
|
223
|
+
name = "";
|
|
224
|
+
/**
|
|
225
|
+
* Description of the group
|
|
226
|
+
*/
|
|
227
|
+
description = "";
|
|
228
|
+
constructor(options = {}) {
|
|
229
|
+
super(options);
|
|
230
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
231
|
+
if (options.name !== void 0) this.name = options.name;
|
|
232
|
+
if (options.description !== void 0) this.description = options.description;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
__decorateClass$9([foreignKey("Tenant", { required: true })], Group.prototype, "tenantId", 2);
|
|
236
|
+
Group = __decorateClass$9([smrt({
|
|
237
|
+
api: { include: ["list", "get"] },
|
|
238
|
+
mcp: { include: ["list", "get"] },
|
|
239
|
+
cli: true
|
|
240
|
+
})], Group);
|
|
241
|
+
//#endregion
|
|
242
|
+
//#region src/collections/GroupCollection.ts
|
|
243
|
+
var GroupCollection = class extends SmrtCollection {
|
|
244
|
+
static _itemClass = Group;
|
|
245
|
+
/**
|
|
246
|
+
* Find all groups in a tenant
|
|
247
|
+
*/
|
|
248
|
+
async findByTenant(tenantId) {
|
|
249
|
+
return await this.list({
|
|
250
|
+
where: { tenantId },
|
|
251
|
+
orderBy: "name ASC"
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Find group by slug within a tenant
|
|
256
|
+
*/
|
|
257
|
+
async findBySlug(slug, tenantId) {
|
|
258
|
+
const results = await this.list({
|
|
259
|
+
where: {
|
|
260
|
+
slug,
|
|
261
|
+
tenantId
|
|
262
|
+
},
|
|
263
|
+
limit: 1
|
|
264
|
+
});
|
|
265
|
+
return results.length > 0 ? results[0] : null;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
//#endregion
|
|
269
|
+
//#region src/models/GroupMember.ts
|
|
270
|
+
var __defProp$8 = Object.defineProperty;
|
|
271
|
+
var __getOwnPropDesc$8 = Object.getOwnPropertyDescriptor;
|
|
272
|
+
var __decorateClass$8 = (decorators, target, key, kind) => {
|
|
273
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$8(target, key) : target;
|
|
274
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
275
|
+
if (kind && result) __defProp$8(target, key, result);
|
|
276
|
+
return result;
|
|
277
|
+
};
|
|
278
|
+
var GroupMember = class extends SmrtObject {
|
|
279
|
+
groupId;
|
|
280
|
+
userId;
|
|
281
|
+
constructor(options = {}) {
|
|
282
|
+
super(options);
|
|
283
|
+
if (options.groupId !== void 0) this.groupId = options.groupId;
|
|
284
|
+
if (options.userId !== void 0) this.userId = options.userId;
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
__decorateClass$8([foreignKey("Group", { required: true })], GroupMember.prototype, "groupId", 2);
|
|
288
|
+
__decorateClass$8([foreignKey("User", { required: true })], GroupMember.prototype, "userId", 2);
|
|
289
|
+
GroupMember = __decorateClass$8([smrt({
|
|
290
|
+
api: { include: ["list", "get"] },
|
|
291
|
+
mcp: { include: ["list", "get"] },
|
|
292
|
+
cli: true
|
|
293
|
+
})], GroupMember);
|
|
294
|
+
//#endregion
|
|
295
|
+
//#region src/collections/GroupMemberCollection.ts
|
|
296
|
+
var GroupMemberCollection = class extends SmrtCollection {
|
|
297
|
+
static _itemClass = GroupMember;
|
|
298
|
+
/** Memoized Group collection, used to resolve the Group table name. */
|
|
299
|
+
groupCollection;
|
|
300
|
+
/**
|
|
301
|
+
* Resolve the database table name for the `Group` model from the registry
|
|
302
|
+
* (via a shared-connection GroupCollection) rather than hardcoding `groups`.
|
|
303
|
+
* A `@smrt({ tableName })` override or table prefix on Group would otherwise
|
|
304
|
+
* make raw joins reference a non-existent or foreign table.
|
|
305
|
+
*/
|
|
306
|
+
async getGroupTableName() {
|
|
307
|
+
if (!this.groupCollection) this.groupCollection = await GroupCollection.create({ db: this.options.db });
|
|
308
|
+
return this.groupCollection.tableName;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Find all members of a group
|
|
312
|
+
*/
|
|
313
|
+
async findByGroup(groupId) {
|
|
314
|
+
return await this.list({ where: { groupId } });
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Find all groups a user belongs to
|
|
318
|
+
*/
|
|
319
|
+
async findByUser(userId) {
|
|
320
|
+
return await this.list({ where: { userId } });
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Check if a user is in a group
|
|
324
|
+
*/
|
|
325
|
+
async isMember(groupId, userId) {
|
|
326
|
+
return (await this.list({
|
|
327
|
+
where: {
|
|
328
|
+
groupId,
|
|
329
|
+
userId
|
|
330
|
+
},
|
|
331
|
+
limit: 1
|
|
332
|
+
})).length > 0;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Add user to a group
|
|
336
|
+
*/
|
|
337
|
+
async addMember(groupId, userId) {
|
|
338
|
+
const existing = await this.list({
|
|
339
|
+
where: {
|
|
340
|
+
groupId,
|
|
341
|
+
userId
|
|
342
|
+
},
|
|
343
|
+
limit: 1
|
|
344
|
+
});
|
|
345
|
+
if (existing.length > 0) return existing[0];
|
|
346
|
+
const member = await this.create({
|
|
347
|
+
groupId,
|
|
348
|
+
userId
|
|
349
|
+
});
|
|
350
|
+
await member.save();
|
|
351
|
+
return member;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Remove user from a group
|
|
355
|
+
*/
|
|
356
|
+
async removeMember(groupId, userId) {
|
|
357
|
+
const existing = await this.list({
|
|
358
|
+
where: {
|
|
359
|
+
groupId,
|
|
360
|
+
userId
|
|
361
|
+
},
|
|
362
|
+
limit: 1
|
|
363
|
+
});
|
|
364
|
+
if (existing.length === 0) return false;
|
|
365
|
+
await existing[0].delete();
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Get group IDs for a user
|
|
370
|
+
* @deprecated Use getGroupIdsForTenant to prevent cross-tenant leakage
|
|
371
|
+
*/
|
|
372
|
+
async getGroupIds(userId) {
|
|
373
|
+
return (await this.findByUser(userId)).map((m) => m.groupId);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Get group IDs for a user within a specific tenant
|
|
377
|
+
* This prevents cross-tenant permission leakage by filtering groups by tenant
|
|
378
|
+
*/
|
|
379
|
+
async getGroupIdsForTenant(userId, tenantId) {
|
|
380
|
+
const groupTable = await this.getGroupTableName();
|
|
381
|
+
const sql = `
|
|
382
|
+
SELECT gm.group_id
|
|
383
|
+
FROM ${this.tableName} gm
|
|
384
|
+
INNER JOIN ${groupTable} g ON g.id = gm.group_id
|
|
385
|
+
WHERE gm.user_id = ? AND g.tenant_id = ?
|
|
386
|
+
`;
|
|
387
|
+
return (await this.db.query(sql, userId, tenantId)).rows.map((r) => r.group_id);
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Get user IDs in a group
|
|
391
|
+
*/
|
|
392
|
+
async getUserIds(groupId) {
|
|
393
|
+
return (await this.findByGroup(groupId)).map((m) => m.userId);
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
//#endregion
|
|
397
|
+
//#region src/models/GroupRole.ts
|
|
398
|
+
var __defProp$7 = Object.defineProperty;
|
|
399
|
+
var __getOwnPropDesc$7 = Object.getOwnPropertyDescriptor;
|
|
400
|
+
var __decorateClass$7 = (decorators, target, key, kind) => {
|
|
401
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$7(target, key) : target;
|
|
402
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
403
|
+
if (kind && result) __defProp$7(target, key, result);
|
|
404
|
+
return result;
|
|
405
|
+
};
|
|
406
|
+
var GroupRole = class extends SmrtObject {
|
|
407
|
+
groupId;
|
|
408
|
+
roleId;
|
|
409
|
+
constructor(options = {}) {
|
|
410
|
+
super(options);
|
|
411
|
+
if (options.groupId !== void 0) this.groupId = options.groupId;
|
|
412
|
+
if (options.roleId !== void 0) this.roleId = options.roleId;
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
__decorateClass$7([foreignKey("Group", { required: true })], GroupRole.prototype, "groupId", 2);
|
|
416
|
+
__decorateClass$7([foreignKey("Role", { required: true })], GroupRole.prototype, "roleId", 2);
|
|
417
|
+
GroupRole = __decorateClass$7([smrt({
|
|
418
|
+
api: { include: ["list", "get"] },
|
|
419
|
+
mcp: { include: ["list", "get"] },
|
|
420
|
+
cli: true
|
|
421
|
+
})], GroupRole);
|
|
422
|
+
//#endregion
|
|
423
|
+
//#region src/collections/GroupRoleCollection.ts
|
|
424
|
+
var GroupRoleCollection = class extends SmrtCollection {
|
|
425
|
+
static _itemClass = GroupRole;
|
|
426
|
+
/**
|
|
427
|
+
* Find all roles for a group
|
|
428
|
+
*/
|
|
429
|
+
async findByGroup(groupId) {
|
|
430
|
+
return await this.list({ where: { groupId } });
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Find all groups that have a role
|
|
434
|
+
*/
|
|
435
|
+
async findByRole(roleId) {
|
|
436
|
+
return await this.list({ where: { roleId } });
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Check if a group has a role
|
|
440
|
+
*/
|
|
441
|
+
async hasRole(groupId, roleId) {
|
|
442
|
+
return (await this.list({
|
|
443
|
+
where: {
|
|
444
|
+
groupId,
|
|
445
|
+
roleId
|
|
446
|
+
},
|
|
447
|
+
limit: 1
|
|
448
|
+
})).length > 0;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Add role to a group
|
|
452
|
+
*/
|
|
453
|
+
async addRole(groupId, roleId) {
|
|
454
|
+
const existing = await this.list({
|
|
455
|
+
where: {
|
|
456
|
+
groupId,
|
|
457
|
+
roleId
|
|
458
|
+
},
|
|
459
|
+
limit: 1
|
|
460
|
+
});
|
|
461
|
+
if (existing.length > 0) return existing[0];
|
|
462
|
+
const groupRole = await this.create({
|
|
463
|
+
groupId,
|
|
464
|
+
roleId
|
|
465
|
+
});
|
|
466
|
+
await groupRole.save();
|
|
467
|
+
return groupRole;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Remove role from a group
|
|
471
|
+
*/
|
|
472
|
+
async removeRole(groupId, roleId) {
|
|
473
|
+
const existing = await this.list({
|
|
474
|
+
where: {
|
|
475
|
+
groupId,
|
|
476
|
+
roleId
|
|
477
|
+
},
|
|
478
|
+
limit: 1
|
|
479
|
+
});
|
|
480
|
+
if (existing.length === 0) return false;
|
|
481
|
+
await existing[0].delete();
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Get role IDs for a group
|
|
486
|
+
*/
|
|
487
|
+
async getRoleIds(groupId) {
|
|
488
|
+
return (await this.findByGroup(groupId)).map((gr) => gr.roleId);
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
//#endregion
|
|
492
|
+
//#region src/models/Membership.ts
|
|
493
|
+
var __defProp$6 = Object.defineProperty;
|
|
494
|
+
var __getOwnPropDesc$6 = Object.getOwnPropertyDescriptor;
|
|
495
|
+
var __decorateClass$6 = (decorators, target, key, kind) => {
|
|
496
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$6(target, key) : target;
|
|
497
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
498
|
+
if (kind && result) __defProp$6(target, key, result);
|
|
499
|
+
return result;
|
|
500
|
+
};
|
|
501
|
+
var Membership = class extends SmrtObject {
|
|
502
|
+
userId;
|
|
503
|
+
tenantId;
|
|
504
|
+
roleId;
|
|
505
|
+
status = MembershipStatus.ACTIVE;
|
|
506
|
+
constructor(options = {}) {
|
|
507
|
+
super(options);
|
|
508
|
+
if (options.userId !== void 0) this.userId = options.userId;
|
|
509
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
510
|
+
if (options.roleId !== void 0) this.roleId = options.roleId;
|
|
511
|
+
if (options.status !== void 0) this.status = options.status;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Check if membership is active
|
|
515
|
+
*/
|
|
516
|
+
isActive() {
|
|
517
|
+
return this.status === MembershipStatus.ACTIVE;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Check if membership is pending (invitation not yet accepted)
|
|
521
|
+
*/
|
|
522
|
+
isPending() {
|
|
523
|
+
return this.status === MembershipStatus.PENDING;
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
__decorateClass$6([foreignKey("User", { required: true })], Membership.prototype, "userId", 2);
|
|
527
|
+
__decorateClass$6([foreignKey("Tenant", { required: true })], Membership.prototype, "tenantId", 2);
|
|
528
|
+
__decorateClass$6([foreignKey("Role", { required: true })], Membership.prototype, "roleId", 2);
|
|
529
|
+
__decorateClass$6([field({ type: "text" })], Membership.prototype, "status", 2);
|
|
530
|
+
Membership = __decorateClass$6([smrt({
|
|
531
|
+
api: { include: ["list", "get"] },
|
|
532
|
+
mcp: { include: ["list", "get"] },
|
|
533
|
+
cli: true
|
|
534
|
+
})], Membership);
|
|
535
|
+
//#endregion
|
|
536
|
+
//#region src/collections/MembershipCollection.ts
|
|
537
|
+
var MembershipCollection = class extends SmrtCollection {
|
|
538
|
+
static _itemClass = Membership;
|
|
539
|
+
/**
|
|
540
|
+
* Find all memberships for a user
|
|
541
|
+
*/
|
|
542
|
+
async findByUser(userId) {
|
|
543
|
+
return await this.list({
|
|
544
|
+
where: { userId },
|
|
545
|
+
orderBy: "created_at DESC"
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Find all active memberships for a user
|
|
550
|
+
*/
|
|
551
|
+
async findActiveByUser(userId) {
|
|
552
|
+
return await this.list({
|
|
553
|
+
where: {
|
|
554
|
+
userId,
|
|
555
|
+
status: MembershipStatus.ACTIVE
|
|
556
|
+
},
|
|
557
|
+
orderBy: "created_at DESC"
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Find all memberships in a tenant
|
|
562
|
+
*/
|
|
563
|
+
async findByTenant(tenantId) {
|
|
564
|
+
return await this.list({
|
|
565
|
+
where: { tenantId },
|
|
566
|
+
orderBy: "created_at DESC"
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Find active memberships in a tenant
|
|
571
|
+
*/
|
|
572
|
+
async findActiveByTenant(tenantId) {
|
|
573
|
+
return await this.list({
|
|
574
|
+
where: {
|
|
575
|
+
tenantId,
|
|
576
|
+
status: MembershipStatus.ACTIVE
|
|
577
|
+
},
|
|
578
|
+
orderBy: "created_at DESC"
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Find a specific user's membership in a tenant
|
|
583
|
+
*/
|
|
584
|
+
async findByUserAndTenant(userId, tenantId) {
|
|
585
|
+
const results = await this.list({
|
|
586
|
+
where: {
|
|
587
|
+
userId,
|
|
588
|
+
tenantId
|
|
589
|
+
},
|
|
590
|
+
limit: 1
|
|
591
|
+
});
|
|
592
|
+
return results.length > 0 ? results[0] : null;
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Find memberships by role
|
|
596
|
+
*/
|
|
597
|
+
async findByRole(roleId) {
|
|
598
|
+
return await this.list({
|
|
599
|
+
where: { roleId },
|
|
600
|
+
orderBy: "created_at DESC"
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Find memberships by status
|
|
605
|
+
*/
|
|
606
|
+
async findByStatus(status) {
|
|
607
|
+
return await this.list({
|
|
608
|
+
where: { status },
|
|
609
|
+
orderBy: "created_at DESC"
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
//#endregion
|
|
614
|
+
//#region src/models/MembershipOverride.ts
|
|
615
|
+
var __defProp$5 = Object.defineProperty;
|
|
616
|
+
var __getOwnPropDesc$5 = Object.getOwnPropertyDescriptor;
|
|
617
|
+
var __decorateClass$5 = (decorators, target, key, kind) => {
|
|
618
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$5(target, key) : target;
|
|
619
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
620
|
+
if (kind && result) __defProp$5(target, key, result);
|
|
621
|
+
return result;
|
|
622
|
+
};
|
|
623
|
+
var MembershipOverride = class extends SmrtObject {
|
|
624
|
+
membershipId;
|
|
625
|
+
permissionId;
|
|
626
|
+
effect = OverrideEffect.GRANT;
|
|
627
|
+
constructor(options = {}) {
|
|
628
|
+
super(options);
|
|
629
|
+
if (options.membershipId !== void 0) this.membershipId = options.membershipId;
|
|
630
|
+
if (options.permissionId !== void 0) this.permissionId = options.permissionId;
|
|
631
|
+
if (options.effect !== void 0) this.effect = options.effect;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Check if this override grants the permission
|
|
635
|
+
*/
|
|
636
|
+
isGrant() {
|
|
637
|
+
return this.effect === OverrideEffect.GRANT;
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Check if this override denies the permission
|
|
641
|
+
*/
|
|
642
|
+
isDeny() {
|
|
643
|
+
return this.effect === OverrideEffect.DENY;
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
__decorateClass$5([foreignKey("Membership", { required: true })], MembershipOverride.prototype, "membershipId", 2);
|
|
647
|
+
__decorateClass$5([foreignKey("Permission", { required: true })], MembershipOverride.prototype, "permissionId", 2);
|
|
648
|
+
__decorateClass$5([field({ type: "text" })], MembershipOverride.prototype, "effect", 2);
|
|
649
|
+
MembershipOverride = __decorateClass$5([smrt({
|
|
650
|
+
api: { include: ["list", "get"] },
|
|
651
|
+
mcp: { include: ["list", "get"] },
|
|
652
|
+
cli: true
|
|
653
|
+
})], MembershipOverride);
|
|
654
|
+
//#endregion
|
|
655
|
+
//#region src/collections/MembershipOverrideCollection.ts
|
|
656
|
+
var MembershipOverrideCollection = class extends SmrtCollection {
|
|
657
|
+
static _itemClass = MembershipOverride;
|
|
658
|
+
/**
|
|
659
|
+
* Find all overrides for a membership
|
|
660
|
+
*/
|
|
661
|
+
async findByMembership(membershipId) {
|
|
662
|
+
return await this.list({ where: { membershipId } });
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Find grant overrides for a membership.
|
|
666
|
+
*
|
|
667
|
+
* Filters in memory because the `effect` column is JSON-typed and
|
|
668
|
+
* Postgres rejects bare `json = text` comparisons. A single
|
|
669
|
+
* `findByMembership` call is reused for both grant and deny lookups
|
|
670
|
+
* within the same request (see `_overridesByMembership` cache).
|
|
671
|
+
*/
|
|
672
|
+
async findGrants(membershipId) {
|
|
673
|
+
return (await this.findByMembership(membershipId)).filter((o) => o.effect === OverrideEffect.GRANT);
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Find deny overrides for a membership.
|
|
677
|
+
*
|
|
678
|
+
* See `findGrants` for rationale on in-memory filtering.
|
|
679
|
+
*/
|
|
680
|
+
async findDenies(membershipId) {
|
|
681
|
+
return (await this.findByMembership(membershipId)).filter((o) => o.effect === OverrideEffect.DENY);
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Get grant permission IDs for a membership
|
|
685
|
+
*/
|
|
686
|
+
async getGrantedPermissionIds(membershipId) {
|
|
687
|
+
return (await this.findGrants(membershipId)).map((o) => o.permissionId);
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Get denied permission IDs for a membership
|
|
691
|
+
*/
|
|
692
|
+
async getDeniedPermissionIds(membershipId) {
|
|
693
|
+
return (await this.findDenies(membershipId)).map((o) => o.permissionId);
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Set an override for a membership
|
|
697
|
+
*/
|
|
698
|
+
async setOverride(membershipId, permissionId, effect) {
|
|
699
|
+
const existing = await this.list({
|
|
700
|
+
where: {
|
|
701
|
+
membershipId,
|
|
702
|
+
permissionId
|
|
703
|
+
},
|
|
704
|
+
limit: 1
|
|
705
|
+
});
|
|
706
|
+
if (existing.length > 0) {
|
|
707
|
+
existing[0].effect = effect;
|
|
708
|
+
await existing[0].save();
|
|
709
|
+
return existing[0];
|
|
710
|
+
}
|
|
711
|
+
const override = await this.create({
|
|
712
|
+
membershipId,
|
|
713
|
+
permissionId,
|
|
714
|
+
effect
|
|
715
|
+
});
|
|
716
|
+
await override.save();
|
|
717
|
+
return override;
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Remove an override
|
|
721
|
+
*/
|
|
722
|
+
async removeOverride(membershipId, permissionId) {
|
|
723
|
+
const existing = await this.list({
|
|
724
|
+
where: {
|
|
725
|
+
membershipId,
|
|
726
|
+
permissionId
|
|
727
|
+
},
|
|
728
|
+
limit: 1
|
|
729
|
+
});
|
|
730
|
+
if (existing.length === 0) return false;
|
|
731
|
+
await existing[0].delete();
|
|
732
|
+
return true;
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Grant a permission to a membership
|
|
736
|
+
* Convenience method for setOverride with GRANT effect
|
|
737
|
+
*/
|
|
738
|
+
async grantPermission(membershipId, permissionId) {
|
|
739
|
+
return await this.setOverride(membershipId, permissionId, OverrideEffect.GRANT);
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Deny a permission for a membership
|
|
743
|
+
* Convenience method for setOverride with DENY effect
|
|
744
|
+
*/
|
|
745
|
+
async denyPermission(membershipId, permissionId) {
|
|
746
|
+
return await this.setOverride(membershipId, permissionId, OverrideEffect.DENY);
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
//#endregion
|
|
750
|
+
//#region src/models/Permission.ts
|
|
751
|
+
var __defProp$4 = Object.defineProperty;
|
|
752
|
+
var __getOwnPropDesc$4 = Object.getOwnPropertyDescriptor;
|
|
753
|
+
var __decorateClass$4 = (decorators, target, key, kind) => {
|
|
754
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$4(target, key) : target;
|
|
755
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
756
|
+
if (kind && result) __defProp$4(target, key, result);
|
|
757
|
+
return result;
|
|
758
|
+
};
|
|
759
|
+
function parsePermissionSlug(slug) {
|
|
760
|
+
const parts = slug.split(".");
|
|
761
|
+
if (parts.length === 2 && parts[0] && parts[1]) return {
|
|
762
|
+
resource: parts[0],
|
|
763
|
+
action: parts[1],
|
|
764
|
+
isValid: true
|
|
765
|
+
};
|
|
766
|
+
return {
|
|
767
|
+
resource: slug,
|
|
768
|
+
action: "",
|
|
769
|
+
isValid: false
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
function isValidPermissionSlug(slug) {
|
|
773
|
+
return parsePermissionSlug(slug).isValid;
|
|
774
|
+
}
|
|
775
|
+
var Permission = class extends SmrtObject {
|
|
776
|
+
/**
|
|
777
|
+
* Display name for the permission
|
|
778
|
+
*/
|
|
779
|
+
name = "";
|
|
780
|
+
/**
|
|
781
|
+
* Description of what this permission allows
|
|
782
|
+
*/
|
|
783
|
+
description = "";
|
|
784
|
+
/**
|
|
785
|
+
* Category for grouping in UI (e.g., 'articles', 'users', 'settings')
|
|
786
|
+
*/
|
|
787
|
+
category = "";
|
|
788
|
+
constructor(options = {}) {
|
|
789
|
+
super(options);
|
|
790
|
+
if (options.name !== void 0) this.name = options.name;
|
|
791
|
+
if (options.description !== void 0) this.description = options.description;
|
|
792
|
+
if (options.category !== void 0) this.category = options.category;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Parse the permission slug into resource and action components.
|
|
796
|
+
* @returns Parsed slug with resource, action, and validation status
|
|
797
|
+
*/
|
|
798
|
+
parseSlug() {
|
|
799
|
+
return parsePermissionSlug(this.slug ?? "");
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Get the resource part of the permission slug
|
|
803
|
+
* e.g., 'articles.create' -> 'articles'
|
|
804
|
+
*/
|
|
805
|
+
getResource() {
|
|
806
|
+
return this.parseSlug().resource;
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Get the action part of the permission slug
|
|
810
|
+
* e.g., 'articles.create' -> 'create'
|
|
811
|
+
*/
|
|
812
|
+
getAction() {
|
|
813
|
+
return this.parseSlug().action;
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Check if the permission slug is valid (follows 'resource.action' pattern)
|
|
817
|
+
*/
|
|
818
|
+
isValidSlug() {
|
|
819
|
+
return this.parseSlug().isValid;
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
Permission = __decorateClass$4([smrt({
|
|
823
|
+
api: { include: ["list", "get"] },
|
|
824
|
+
mcp: { include: ["list", "get"] },
|
|
825
|
+
cli: true
|
|
826
|
+
})], Permission);
|
|
827
|
+
//#endregion
|
|
828
|
+
//#region src/collections/PermissionCollection.ts
|
|
829
|
+
var PermissionCollection = class extends SmrtCollection {
|
|
830
|
+
static _itemClass = Permission;
|
|
831
|
+
/**
|
|
832
|
+
* Find permissions by category
|
|
833
|
+
*/
|
|
834
|
+
async findByCategory(category) {
|
|
835
|
+
return await this.list({
|
|
836
|
+
where: { category },
|
|
837
|
+
orderBy: "slug ASC"
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Find permission by slug
|
|
842
|
+
*/
|
|
843
|
+
async findBySlug(slug) {
|
|
844
|
+
const results = await this.list({
|
|
845
|
+
where: { slug },
|
|
846
|
+
limit: 1
|
|
847
|
+
});
|
|
848
|
+
return results.length > 0 ? results[0] : null;
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Batch fetch permissions by IDs
|
|
852
|
+
* Returns a Map of id -> Permission for efficient lookup
|
|
853
|
+
*/
|
|
854
|
+
async findByIds(ids) {
|
|
855
|
+
if (ids.length === 0) return /* @__PURE__ */ new Map();
|
|
856
|
+
const uniqueIds = [...new Set(ids)];
|
|
857
|
+
const placeholders = uniqueIds.map(() => "?").join(", ");
|
|
858
|
+
const results = await this.query(`SELECT * FROM ${this.tableName} WHERE id IN (${placeholders})`, uniqueIds);
|
|
859
|
+
const map = /* @__PURE__ */ new Map();
|
|
860
|
+
for (const perm of results) if (perm.id) map.set(perm.id, perm);
|
|
861
|
+
return map;
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Get all unique categories
|
|
865
|
+
*/
|
|
866
|
+
async getCategories() {
|
|
867
|
+
return (await this.query(`SELECT DISTINCT category FROM ${this.tableName} WHERE category != '' ORDER BY category ASC`)).map((r) => r.category);
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Find or create a permission by slug
|
|
871
|
+
*/
|
|
872
|
+
async findOrCreate(slug, defaults = {}) {
|
|
873
|
+
const existing = await this.findBySlug(slug);
|
|
874
|
+
if (existing) return existing;
|
|
875
|
+
const permission = await this.create({
|
|
876
|
+
slug,
|
|
877
|
+
name: defaults.name ?? slug,
|
|
878
|
+
description: defaults.description ?? "",
|
|
879
|
+
category: defaults.category ?? slug.split(".")[0]
|
|
880
|
+
});
|
|
881
|
+
await permission.save();
|
|
882
|
+
return permission;
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
//#endregion
|
|
886
|
+
//#region src/models/RolePermission.ts
|
|
887
|
+
var __defProp$3 = Object.defineProperty;
|
|
888
|
+
var __getOwnPropDesc$3 = Object.getOwnPropertyDescriptor;
|
|
889
|
+
var __decorateClass$3 = (decorators, target, key, kind) => {
|
|
890
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$3(target, key) : target;
|
|
891
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
892
|
+
if (kind && result) __defProp$3(target, key, result);
|
|
893
|
+
return result;
|
|
894
|
+
};
|
|
895
|
+
var RolePermission = class extends SmrtObject {
|
|
896
|
+
roleId;
|
|
897
|
+
permissionId;
|
|
898
|
+
constructor(options = {}) {
|
|
899
|
+
super(options);
|
|
900
|
+
if (options.roleId !== void 0) this.roleId = options.roleId;
|
|
901
|
+
if (options.permissionId !== void 0) this.permissionId = options.permissionId;
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
__decorateClass$3([foreignKey("Role", { required: true })], RolePermission.prototype, "roleId", 2);
|
|
905
|
+
__decorateClass$3([foreignKey("Permission", { required: true })], RolePermission.prototype, "permissionId", 2);
|
|
906
|
+
RolePermission = __decorateClass$3([smrt({
|
|
907
|
+
api: { include: ["list", "get"] },
|
|
908
|
+
mcp: { include: ["list", "get"] },
|
|
909
|
+
cli: true
|
|
910
|
+
})], RolePermission);
|
|
911
|
+
//#endregion
|
|
912
|
+
//#region src/collections/RolePermissionCollection.ts
|
|
913
|
+
var RolePermissionCollection = class extends SmrtCollection {
|
|
914
|
+
static _itemClass = RolePermission;
|
|
915
|
+
/**
|
|
916
|
+
* Find all permissions for a role
|
|
917
|
+
*/
|
|
918
|
+
async findByRole(roleId) {
|
|
919
|
+
return await this.list({ where: { roleId } });
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Find all roles that have a permission
|
|
923
|
+
*/
|
|
924
|
+
async findByPermission(permissionId) {
|
|
925
|
+
return await this.list({ where: { permissionId } });
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Check if a role has a specific permission
|
|
929
|
+
*/
|
|
930
|
+
async hasPermission(roleId, permissionId) {
|
|
931
|
+
return (await this.list({
|
|
932
|
+
where: {
|
|
933
|
+
roleId,
|
|
934
|
+
permissionId
|
|
935
|
+
},
|
|
936
|
+
limit: 1
|
|
937
|
+
})).length > 0;
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Add a permission to a role
|
|
941
|
+
*/
|
|
942
|
+
async addPermission(roleId, permissionId) {
|
|
943
|
+
const existing = await this.list({
|
|
944
|
+
where: {
|
|
945
|
+
roleId,
|
|
946
|
+
permissionId
|
|
947
|
+
},
|
|
948
|
+
limit: 1
|
|
949
|
+
});
|
|
950
|
+
if (existing.length > 0) return existing[0];
|
|
951
|
+
const rolePermission = await this.create({
|
|
952
|
+
roleId,
|
|
953
|
+
permissionId
|
|
954
|
+
});
|
|
955
|
+
await rolePermission.save();
|
|
956
|
+
return rolePermission;
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Remove a permission from a role
|
|
960
|
+
*/
|
|
961
|
+
async removePermission(roleId, permissionId) {
|
|
962
|
+
const existing = await this.list({
|
|
963
|
+
where: {
|
|
964
|
+
roleId,
|
|
965
|
+
permissionId
|
|
966
|
+
},
|
|
967
|
+
limit: 1
|
|
968
|
+
});
|
|
969
|
+
if (existing.length === 0) return false;
|
|
970
|
+
await existing[0].delete();
|
|
971
|
+
return true;
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Get permission IDs for a role
|
|
975
|
+
*/
|
|
976
|
+
async getPermissionIds(roleId) {
|
|
977
|
+
return (await this.findByRole(roleId)).map((rp) => rp.permissionId);
|
|
978
|
+
}
|
|
979
|
+
};
|
|
980
|
+
//#endregion
|
|
981
|
+
//#region src/models/Session.ts
|
|
982
|
+
var __defProp$2 = Object.defineProperty;
|
|
983
|
+
var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
|
|
984
|
+
var __decorateClass$2 = (decorators, target, key, kind) => {
|
|
985
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
|
|
986
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
987
|
+
if (kind && result) __defProp$2(target, key, result);
|
|
988
|
+
return result;
|
|
989
|
+
};
|
|
990
|
+
var DEFAULT_SESSION_TTL = 10080 * 60;
|
|
991
|
+
function generateSessionId() {
|
|
992
|
+
return crypto.randomUUID();
|
|
993
|
+
}
|
|
994
|
+
var Session = class extends SmrtObject {
|
|
995
|
+
userId = "";
|
|
996
|
+
tenantId = null;
|
|
997
|
+
status = SessionStatus.ACTIVE;
|
|
998
|
+
/**
|
|
999
|
+
* Session expiration time
|
|
1000
|
+
*/
|
|
1001
|
+
expiresAt = /* @__PURE__ */ new Date();
|
|
1002
|
+
/**
|
|
1003
|
+
* User agent string from the browser
|
|
1004
|
+
*/
|
|
1005
|
+
userAgent = "";
|
|
1006
|
+
/**
|
|
1007
|
+
* IP address of the client
|
|
1008
|
+
*/
|
|
1009
|
+
ipAddress = "";
|
|
1010
|
+
/**
|
|
1011
|
+
* Last activity timestamp (updated on each request)
|
|
1012
|
+
*/
|
|
1013
|
+
lastAccessedAt = /* @__PURE__ */ new Date();
|
|
1014
|
+
/**
|
|
1015
|
+
* Custom session data (JSON serializable)
|
|
1016
|
+
*/
|
|
1017
|
+
data = {};
|
|
1018
|
+
constructor(options = {}) {
|
|
1019
|
+
super(options);
|
|
1020
|
+
if (options.userId !== void 0) this.userId = options.userId;
|
|
1021
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
1022
|
+
if (options.status !== void 0) this.status = options.status;
|
|
1023
|
+
if (options.expiresAt !== void 0) this.expiresAt = options.expiresAt instanceof Date ? options.expiresAt : new Date(options.expiresAt);
|
|
1024
|
+
if (options.userAgent !== void 0) this.userAgent = options.userAgent;
|
|
1025
|
+
if (options.ipAddress !== void 0) this.ipAddress = options.ipAddress;
|
|
1026
|
+
if (options.lastAccessedAt !== void 0) this.lastAccessedAt = options.lastAccessedAt instanceof Date ? options.lastAccessedAt : new Date(options.lastAccessedAt);
|
|
1027
|
+
if (options.data !== void 0) this.data = options.data;
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Check if the session is currently valid (active and not expired)
|
|
1031
|
+
*/
|
|
1032
|
+
isValid() {
|
|
1033
|
+
return this.status === SessionStatus.ACTIVE && /* @__PURE__ */ new Date() < this.expiresAt;
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Check if the session has expired
|
|
1037
|
+
*/
|
|
1038
|
+
isExpired() {
|
|
1039
|
+
return /* @__PURE__ */ new Date() >= this.expiresAt;
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Check if the session was revoked
|
|
1043
|
+
*/
|
|
1044
|
+
isRevoked() {
|
|
1045
|
+
return this.status === SessionStatus.REVOKED;
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Update the last accessed timestamp
|
|
1049
|
+
*/
|
|
1050
|
+
touch() {
|
|
1051
|
+
this.lastAccessedAt = /* @__PURE__ */ new Date();
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Extend the session expiration by the given TTL (in seconds)
|
|
1055
|
+
*/
|
|
1056
|
+
extend(ttlSeconds = DEFAULT_SESSION_TTL) {
|
|
1057
|
+
this.expiresAt = new Date(Date.now() + ttlSeconds * 1e3);
|
|
1058
|
+
this.touch();
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Revoke the session
|
|
1062
|
+
*/
|
|
1063
|
+
revoke() {
|
|
1064
|
+
this.status = SessionStatus.REVOKED;
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* Set the tenant context for this session
|
|
1068
|
+
*/
|
|
1069
|
+
setTenant(tenantId) {
|
|
1070
|
+
this.tenantId = tenantId;
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Get or set custom session data
|
|
1074
|
+
*/
|
|
1075
|
+
getData(key) {
|
|
1076
|
+
return this.data[key];
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Set custom session data
|
|
1080
|
+
*/
|
|
1081
|
+
setData(key, value) {
|
|
1082
|
+
this.data[key] = value;
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* Remove custom session data
|
|
1086
|
+
*/
|
|
1087
|
+
removeData(key) {
|
|
1088
|
+
delete this.data[key];
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
__decorateClass$2([foreignKey("User")], Session.prototype, "userId", 2);
|
|
1092
|
+
__decorateClass$2([foreignKey("Tenant", { nullable: true })], Session.prototype, "tenantId", 2);
|
|
1093
|
+
__decorateClass$2([field({ type: "text" })], Session.prototype, "status", 2);
|
|
1094
|
+
Session = __decorateClass$2([smrt({
|
|
1095
|
+
api: { include: ["get", "delete"] },
|
|
1096
|
+
mcp: { include: [] },
|
|
1097
|
+
cli: true
|
|
1098
|
+
})], Session);
|
|
1099
|
+
//#endregion
|
|
1100
|
+
//#region src/collections/SessionCollection.ts
|
|
1101
|
+
var SessionCollection = class extends SmrtCollection {
|
|
1102
|
+
static _itemClass = Session;
|
|
1103
|
+
/**
|
|
1104
|
+
* Create a new session with a secure ID
|
|
1105
|
+
*/
|
|
1106
|
+
async createSession(options) {
|
|
1107
|
+
const ttl = options.ttl ?? 604800;
|
|
1108
|
+
const expiresAt = new Date(Date.now() + ttl * 1e3);
|
|
1109
|
+
const session = await this.create({
|
|
1110
|
+
id: generateSessionId(),
|
|
1111
|
+
userId: options.userId,
|
|
1112
|
+
tenantId: options.tenantId ?? null,
|
|
1113
|
+
status: SessionStatus.ACTIVE,
|
|
1114
|
+
expiresAt,
|
|
1115
|
+
userAgent: options.userAgent ?? "",
|
|
1116
|
+
ipAddress: options.ipAddress ?? "",
|
|
1117
|
+
lastAccessedAt: /* @__PURE__ */ new Date(),
|
|
1118
|
+
data: options.data ?? {}
|
|
1119
|
+
});
|
|
1120
|
+
await session.save();
|
|
1121
|
+
return session;
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Find a valid session by ID
|
|
1125
|
+
* Returns null if session doesn't exist, is expired, or is revoked
|
|
1126
|
+
*/
|
|
1127
|
+
async findValidSession(sessionId) {
|
|
1128
|
+
const session = await this.get(sessionId);
|
|
1129
|
+
if (!session) return null;
|
|
1130
|
+
if (!session.isValid()) {
|
|
1131
|
+
if (session.isExpired() && session.status === SessionStatus.ACTIVE) {
|
|
1132
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1133
|
+
await this.db.query(`UPDATE ${this.tableName}
|
|
1134
|
+
SET status = ?, updated_at = ?
|
|
1135
|
+
WHERE id = ? AND status = ? AND expires_at < ?`, SessionStatus.EXPIRED, now, sessionId, SessionStatus.ACTIVE, now);
|
|
1136
|
+
}
|
|
1137
|
+
return null;
|
|
1138
|
+
}
|
|
1139
|
+
return session;
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Update last accessed time and optionally extend session
|
|
1143
|
+
*/
|
|
1144
|
+
async touch(sessionId, extendTtl = false, ttl = DEFAULT_SESSION_TTL) {
|
|
1145
|
+
const session = await this.findValidSession(sessionId);
|
|
1146
|
+
if (!session) return false;
|
|
1147
|
+
session.touch();
|
|
1148
|
+
if (extendTtl) session.extend(ttl);
|
|
1149
|
+
await session.save();
|
|
1150
|
+
return true;
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Find all active sessions for a user
|
|
1154
|
+
*/
|
|
1155
|
+
async findByUser(userId) {
|
|
1156
|
+
return (await this.list({
|
|
1157
|
+
where: {
|
|
1158
|
+
userId,
|
|
1159
|
+
status: SessionStatus.ACTIVE
|
|
1160
|
+
},
|
|
1161
|
+
orderBy: "last_accessed_at DESC"
|
|
1162
|
+
})).filter((session) => session.isValid());
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Delete all sessions for a user (logout from all devices)
|
|
1166
|
+
*/
|
|
1167
|
+
async deleteUserSessions(userId) {
|
|
1168
|
+
const sessions = await this.list({ where: { userId } });
|
|
1169
|
+
let count = 0;
|
|
1170
|
+
for (const session of sessions) {
|
|
1171
|
+
await session.delete();
|
|
1172
|
+
count++;
|
|
1173
|
+
}
|
|
1174
|
+
return count;
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* Revoke all sessions for a user (soft delete)
|
|
1178
|
+
*/
|
|
1179
|
+
async revokeUserSessions(userId) {
|
|
1180
|
+
const sessions = await this.list({ where: {
|
|
1181
|
+
userId,
|
|
1182
|
+
status: SessionStatus.ACTIVE
|
|
1183
|
+
} });
|
|
1184
|
+
let count = 0;
|
|
1185
|
+
for (const session of sessions) {
|
|
1186
|
+
session.revoke();
|
|
1187
|
+
await session.save();
|
|
1188
|
+
count++;
|
|
1189
|
+
}
|
|
1190
|
+
return count;
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Revoke a specific session
|
|
1194
|
+
*/
|
|
1195
|
+
async revokeSession(sessionId) {
|
|
1196
|
+
const session = await this.get(sessionId);
|
|
1197
|
+
if (!session) return false;
|
|
1198
|
+
session.revoke();
|
|
1199
|
+
await session.save();
|
|
1200
|
+
return true;
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Delete expired sessions (cleanup job)
|
|
1204
|
+
* Returns the number of deleted sessions
|
|
1205
|
+
*/
|
|
1206
|
+
async deleteExpired() {
|
|
1207
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1208
|
+
const { rowCount } = await this.db.query(`DELETE FROM ${this.tableName}
|
|
1209
|
+
WHERE expires_at < ? OR status = ?`, now, SessionStatus.REVOKED);
|
|
1210
|
+
return rowCount ?? 0;
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Count active sessions for a user
|
|
1214
|
+
*/
|
|
1215
|
+
async countUserSessions(userId) {
|
|
1216
|
+
return (await this.findByUser(userId)).length;
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Update tenant context for a session (low-level primitive).
|
|
1220
|
+
*
|
|
1221
|
+
* SECURITY (#1400): this does NOT verify that the session's user is a member
|
|
1222
|
+
* of `tenantId` — it is the unguarded storage primitive. Application/route
|
|
1223
|
+
* code must go through {@link SessionService.switchTenant}, which fail-closes
|
|
1224
|
+
* on a missing/inactive membership before calling this. Calling it directly
|
|
1225
|
+
* with an untrusted `tenantId` reintroduces the cross-tenant access bug.
|
|
1226
|
+
*/
|
|
1227
|
+
async setSessionTenant(sessionId, tenantId) {
|
|
1228
|
+
const session = await this.findValidSession(sessionId);
|
|
1229
|
+
if (!session) return false;
|
|
1230
|
+
session.setTenant(tenantId);
|
|
1231
|
+
await session.save();
|
|
1232
|
+
return true;
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Set custom session data
|
|
1236
|
+
*/
|
|
1237
|
+
async setSessionData(sessionId, key, value) {
|
|
1238
|
+
const session = await this.findValidSession(sessionId);
|
|
1239
|
+
if (!session) return false;
|
|
1240
|
+
session.setData(key, value);
|
|
1241
|
+
await session.save();
|
|
1242
|
+
return true;
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Get custom session data
|
|
1246
|
+
*/
|
|
1247
|
+
async getSessionData(sessionId, key) {
|
|
1248
|
+
const session = await this.findValidSession(sessionId);
|
|
1249
|
+
if (!session) return void 0;
|
|
1250
|
+
return session.getData(key);
|
|
1251
|
+
}
|
|
1252
|
+
};
|
|
1253
|
+
//#endregion
|
|
1254
|
+
//#region src/models/Tenant.ts
|
|
1255
|
+
var __defProp$1 = Object.defineProperty;
|
|
1256
|
+
var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
|
|
1257
|
+
var __decorateClass$1 = (decorators, target, key, kind) => {
|
|
1258
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
|
|
1259
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
1260
|
+
if (kind && result) __defProp$1(target, key, result);
|
|
1261
|
+
return result;
|
|
1262
|
+
};
|
|
1263
|
+
var MAX_TENANT_HIERARCHY_DEPTH = 10;
|
|
1264
|
+
var Tenant = class extends SmrtObject {
|
|
1265
|
+
/**
|
|
1266
|
+
* Display name for the tenant
|
|
1267
|
+
*/
|
|
1268
|
+
name = "";
|
|
1269
|
+
status = TenantStatus.ACTIVE;
|
|
1270
|
+
/**
|
|
1271
|
+
* Optional description
|
|
1272
|
+
*/
|
|
1273
|
+
description = "";
|
|
1274
|
+
parentTenantId;
|
|
1275
|
+
/**
|
|
1276
|
+
* Depth in the hierarchy tree (0 = root, 1 = first level child, etc.)
|
|
1277
|
+
* Automatically managed by TenantCollection methods.
|
|
1278
|
+
*/
|
|
1279
|
+
hierarchyLevel = 0;
|
|
1280
|
+
/**
|
|
1281
|
+
* Materialized path for efficient tree traversal.
|
|
1282
|
+
* Format: "ancestor-id/parent-id" (path to parent; does not include this tenant's id)
|
|
1283
|
+
* Empty string for root tenants.
|
|
1284
|
+
* Automatically managed by TenantCollection methods.
|
|
1285
|
+
*/
|
|
1286
|
+
hierarchyPath = "";
|
|
1287
|
+
/**
|
|
1288
|
+
* If true, this tenant's permissions cascade DOWN to child tenants.
|
|
1289
|
+
* Children can still opt-out by setting inheritPermissions: false.
|
|
1290
|
+
* Default: true
|
|
1291
|
+
*/
|
|
1292
|
+
cascadePermissions = true;
|
|
1293
|
+
/**
|
|
1294
|
+
* If true, this tenant ACCEPTS permissions from its parent tenant.
|
|
1295
|
+
* Parent must also have cascadePermissions: true for inheritance to work.
|
|
1296
|
+
* Default: true
|
|
1297
|
+
*/
|
|
1298
|
+
inheritPermissions = true;
|
|
1299
|
+
constructor(options = {}) {
|
|
1300
|
+
super(options);
|
|
1301
|
+
if (options.name !== void 0) this.name = options.name;
|
|
1302
|
+
if (options.status !== void 0) this.status = options.status;
|
|
1303
|
+
if (options.description !== void 0) this.description = options.description;
|
|
1304
|
+
if (options.parentTenantId !== void 0) this.parentTenantId = options.parentTenantId;
|
|
1305
|
+
if (options.hierarchyLevel !== void 0) this.hierarchyLevel = options.hierarchyLevel;
|
|
1306
|
+
if (options.hierarchyPath !== void 0) this.hierarchyPath = options.hierarchyPath;
|
|
1307
|
+
if (options.cascadePermissions !== void 0) this.cascadePermissions = options.cascadePermissions;
|
|
1308
|
+
if (options.inheritPermissions !== void 0) this.inheritPermissions = options.inheritPermissions;
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Check if tenant is active
|
|
1312
|
+
*/
|
|
1313
|
+
isActive() {
|
|
1314
|
+
return this.status === TenantStatus.ACTIVE;
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Check if tenant is suspended
|
|
1318
|
+
*/
|
|
1319
|
+
isSuspended() {
|
|
1320
|
+
return this.status === TenantStatus.SUSPENDED;
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* Check if this is a root-level tenant (no parent)
|
|
1324
|
+
*/
|
|
1325
|
+
isRoot() {
|
|
1326
|
+
return !this.parentTenantId;
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* Check if this tenant is configured to cascade permissions to children.
|
|
1330
|
+
*
|
|
1331
|
+
* Note: This does NOT indicate whether any child tenants actually exist.
|
|
1332
|
+
* Use TenantCollection.findChildren() for accurate child lookup.
|
|
1333
|
+
*/
|
|
1334
|
+
canCascadeToChildren() {
|
|
1335
|
+
return this.cascadePermissions;
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Check if permission inheritance is active for this tenant.
|
|
1339
|
+
* Inheritance is active if:
|
|
1340
|
+
* - This tenant has a parent AND
|
|
1341
|
+
* - This tenant has inheritPermissions: true
|
|
1342
|
+
*
|
|
1343
|
+
* Note: The parent must also have cascadePermissions: true
|
|
1344
|
+
* for actual inheritance to occur. Use PermissionResolver
|
|
1345
|
+
* for accurate permission calculation.
|
|
1346
|
+
*/
|
|
1347
|
+
acceptsInheritance() {
|
|
1348
|
+
return !!this.parentTenantId && this.inheritPermissions;
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* Get ancestor IDs from the hierarchy path.
|
|
1352
|
+
* Returns an array of tenant IDs from root to immediate parent.
|
|
1353
|
+
* Empty array for root tenants.
|
|
1354
|
+
*/
|
|
1355
|
+
getAncestorIds() {
|
|
1356
|
+
if (!this.hierarchyPath) return [];
|
|
1357
|
+
return this.hierarchyPath.split("/").filter((id) => id.length > 0);
|
|
1358
|
+
}
|
|
1359
|
+
};
|
|
1360
|
+
__decorateClass$1([field({ type: "text" })], Tenant.prototype, "status", 2);
|
|
1361
|
+
__decorateClass$1([foreignKey("Tenant", { nullable: true })], Tenant.prototype, "parentTenantId", 2);
|
|
1362
|
+
Tenant = __decorateClass$1([smrt({
|
|
1363
|
+
tableStrategy: "sti",
|
|
1364
|
+
api: { include: ["list", "get"] },
|
|
1365
|
+
mcp: { include: ["list", "get"] },
|
|
1366
|
+
cli: true
|
|
1367
|
+
})], Tenant);
|
|
1368
|
+
//#endregion
|
|
1369
|
+
//#region src/collections/TenantCollection.ts
|
|
1370
|
+
var TenantHierarchyError = class extends Error {
|
|
1371
|
+
constructor(message, code) {
|
|
1372
|
+
super(message);
|
|
1373
|
+
this.code = code;
|
|
1374
|
+
this.name = "TenantHierarchyError";
|
|
1375
|
+
}
|
|
1376
|
+
code;
|
|
1377
|
+
};
|
|
1378
|
+
var TenantCollection = class extends SmrtCollection {
|
|
1379
|
+
static _itemClass = Tenant;
|
|
1380
|
+
/**
|
|
1381
|
+
* Find tenants by status
|
|
1382
|
+
*/
|
|
1383
|
+
async findByStatus(status) {
|
|
1384
|
+
return await this.list({
|
|
1385
|
+
where: { status },
|
|
1386
|
+
orderBy: "name ASC"
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Find all active tenants
|
|
1391
|
+
*/
|
|
1392
|
+
async findActive() {
|
|
1393
|
+
return await this.findByStatus(TenantStatus.ACTIVE);
|
|
1394
|
+
}
|
|
1395
|
+
/**
|
|
1396
|
+
* Find tenant by slug
|
|
1397
|
+
*/
|
|
1398
|
+
async findBySlug(slug) {
|
|
1399
|
+
const results = await this.list({
|
|
1400
|
+
where: { slug },
|
|
1401
|
+
limit: 1
|
|
1402
|
+
});
|
|
1403
|
+
return results.length > 0 ? results[0] : null;
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Find all root tenants (tenants with no parent)
|
|
1407
|
+
*/
|
|
1408
|
+
async findRoots() {
|
|
1409
|
+
return await this.list({
|
|
1410
|
+
where: { parentTenantId: null },
|
|
1411
|
+
orderBy: "name ASC"
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Find direct children of a tenant
|
|
1416
|
+
*/
|
|
1417
|
+
async findChildren(parentTenantId) {
|
|
1418
|
+
return await this.list({
|
|
1419
|
+
where: { parentTenantId },
|
|
1420
|
+
orderBy: "name ASC"
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* Find the parent tenant of a given tenant
|
|
1425
|
+
*/
|
|
1426
|
+
async findParent(tenantId) {
|
|
1427
|
+
const tenant = await this.get({ id: tenantId });
|
|
1428
|
+
if (!tenant || !tenant.parentTenantId) return null;
|
|
1429
|
+
return await this.get({ id: tenant.parentTenantId });
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Get all ancestors of a tenant, from immediate parent to root.
|
|
1433
|
+
* Uses the hierarchyPath for efficient lookup.
|
|
1434
|
+
*/
|
|
1435
|
+
async getAncestors(tenantId) {
|
|
1436
|
+
const tenant = await this.get({ id: tenantId });
|
|
1437
|
+
if (!tenant) return [];
|
|
1438
|
+
const ancestorIds = tenant.getAncestorIds();
|
|
1439
|
+
if (ancestorIds.length === 0) return [];
|
|
1440
|
+
const ancestorsList = await this.listByIds(ancestorIds);
|
|
1441
|
+
const ancestorsMap = new Map(ancestorsList.map((a) => [a.id, a]));
|
|
1442
|
+
const ancestors = [];
|
|
1443
|
+
for (let i = ancestorIds.length - 1; i >= 0; i--) {
|
|
1444
|
+
const ancestor = ancestorsMap.get(ancestorIds[i]);
|
|
1445
|
+
if (ancestor) ancestors.push(ancestor);
|
|
1446
|
+
}
|
|
1447
|
+
return ancestors;
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Get all ancestors in order from root to immediate parent.
|
|
1451
|
+
* Reverse of getAncestors.
|
|
1452
|
+
*/
|
|
1453
|
+
async getAncestorsFromRoot(tenantId) {
|
|
1454
|
+
return (await this.getAncestors(tenantId)).reverse();
|
|
1455
|
+
}
|
|
1456
|
+
/**
|
|
1457
|
+
* Get all descendants of a tenant (all children, grandchildren, etc.)
|
|
1458
|
+
* Uses hierarchyPath prefix matching for efficient lookup.
|
|
1459
|
+
*/
|
|
1460
|
+
async getDescendants(tenantId) {
|
|
1461
|
+
const tenant = await this.get({ id: tenantId });
|
|
1462
|
+
if (!tenant || !tenant.id) return [];
|
|
1463
|
+
const pathPrefix = tenant.hierarchyPath ? `${tenant.hierarchyPath}/${tenant.id}` : tenant.id;
|
|
1464
|
+
return (await this.list({ where: { "hierarchyPath like": `${pathPrefix}%` } })).filter((t) => t.hierarchyPath === pathPrefix || t.hierarchyPath?.startsWith(`${pathPrefix}/`));
|
|
1465
|
+
}
|
|
1466
|
+
/**
|
|
1467
|
+
* Get siblings of a tenant (other tenants with the same parent)
|
|
1468
|
+
*/
|
|
1469
|
+
async getSiblings(tenantId) {
|
|
1470
|
+
const tenant = await this.get({ id: tenantId });
|
|
1471
|
+
if (!tenant) return [];
|
|
1472
|
+
return (await this.list({
|
|
1473
|
+
where: { parentTenantId: tenant.parentTenantId ?? null },
|
|
1474
|
+
orderBy: "name ASC"
|
|
1475
|
+
})).filter((s) => s.id !== tenantId);
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* Check if a tenant is an ancestor of another tenant
|
|
1479
|
+
*/
|
|
1480
|
+
async isAncestorOf(potentialAncestorId, tenantId) {
|
|
1481
|
+
const tenant = await this.get({ id: tenantId });
|
|
1482
|
+
if (!tenant) return false;
|
|
1483
|
+
return tenant.getAncestorIds().includes(potentialAncestorId);
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* Check if a tenant is a descendant of another tenant
|
|
1487
|
+
*/
|
|
1488
|
+
async isDescendantOf(potentialDescendantId, tenantId) {
|
|
1489
|
+
return await this.isAncestorOf(tenantId, potentialDescendantId);
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Create a child tenant under a parent.
|
|
1493
|
+
* Automatically sets hierarchyLevel and hierarchyPath.
|
|
1494
|
+
*/
|
|
1495
|
+
async createChild(parentTenantId, options) {
|
|
1496
|
+
const parent = await this.get({ id: parentTenantId });
|
|
1497
|
+
if (!parent?.id) throw new TenantHierarchyError(`Parent tenant not found: ${parentTenantId}`, "PARENT_NOT_FOUND");
|
|
1498
|
+
const newLevel = parent.hierarchyLevel + 1;
|
|
1499
|
+
if (newLevel >= 10) throw new TenantHierarchyError(`Maximum hierarchy depth (10) exceeded`, "MAX_DEPTH_EXCEEDED");
|
|
1500
|
+
const newPath = parent.hierarchyPath ? `${parent.hierarchyPath}/${parent.id}` : parent.id;
|
|
1501
|
+
const child = await this.create({
|
|
1502
|
+
name: options.name,
|
|
1503
|
+
slug: options.slug,
|
|
1504
|
+
description: options.description ?? "",
|
|
1505
|
+
status: options.status ?? TenantStatus.ACTIVE,
|
|
1506
|
+
parentTenantId,
|
|
1507
|
+
hierarchyLevel: newLevel,
|
|
1508
|
+
hierarchyPath: newPath,
|
|
1509
|
+
cascadePermissions: options.cascadePermissions ?? true,
|
|
1510
|
+
inheritPermissions: options.inheritPermissions ?? true
|
|
1511
|
+
});
|
|
1512
|
+
await child.save();
|
|
1513
|
+
return child;
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* Move a tenant to a new parent.
|
|
1517
|
+
* Updates hierarchyLevel and hierarchyPath for the tenant and all descendants.
|
|
1518
|
+
*/
|
|
1519
|
+
async moveToParent(tenantId, newParentId) {
|
|
1520
|
+
const tenant = await this.get({ id: tenantId });
|
|
1521
|
+
if (!tenant || !tenant.id) throw new TenantHierarchyError(`Tenant not found: ${tenantId}`, "INVALID_OPERATION");
|
|
1522
|
+
if (newParentId === tenantId) throw new TenantHierarchyError("Cannot move tenant to itself", "CIRCULAR_REFERENCE");
|
|
1523
|
+
if (newParentId) {
|
|
1524
|
+
if (await this.isDescendantOf(newParentId, tenantId)) throw new TenantHierarchyError("Cannot move tenant to one of its descendants", "CIRCULAR_REFERENCE");
|
|
1525
|
+
}
|
|
1526
|
+
const descendants = await this.getDescendants(tenantId);
|
|
1527
|
+
let newLevel;
|
|
1528
|
+
let newPath;
|
|
1529
|
+
if (newParentId === null) {
|
|
1530
|
+
newLevel = 0;
|
|
1531
|
+
newPath = "";
|
|
1532
|
+
} else {
|
|
1533
|
+
const newParent = await this.get({ id: newParentId });
|
|
1534
|
+
if (!newParent) throw new TenantHierarchyError(`New parent tenant not found: ${newParentId}`, "PARENT_NOT_FOUND");
|
|
1535
|
+
newLevel = newParent.hierarchyLevel + 1;
|
|
1536
|
+
const maxDescendantDepth = descendants.reduce((max, d) => Math.max(max, d.hierarchyLevel - tenant.hierarchyLevel), 0);
|
|
1537
|
+
if (newLevel + maxDescendantDepth >= 10) throw new TenantHierarchyError(`Moving would exceed maximum hierarchy depth (10)`, "MAX_DEPTH_EXCEEDED");
|
|
1538
|
+
newPath = newParent.hierarchyPath ? `${newParent.hierarchyPath}/${newParent.id}` : newParent.id;
|
|
1539
|
+
}
|
|
1540
|
+
const oldPath = tenant.hierarchyPath ? `${tenant.hierarchyPath}/${tenant.id}` : tenant.id;
|
|
1541
|
+
const newPathForDescendants = newPath ? `${newPath}/${tenant.id}` : tenant.id;
|
|
1542
|
+
const levelDelta = newLevel - tenant.hierarchyLevel;
|
|
1543
|
+
tenant.parentTenantId = newParentId;
|
|
1544
|
+
tenant.hierarchyLevel = newLevel;
|
|
1545
|
+
tenant.hierarchyPath = newPath;
|
|
1546
|
+
await tenant.save();
|
|
1547
|
+
for (const descendant of descendants) {
|
|
1548
|
+
if (!descendant.hierarchyPath) throw new TenantHierarchyError(`Descendant tenant ${descendant.id} has no hierarchyPath while updating hierarchy from ${oldPath} to ${newPathForDescendants}`, "INVALID_OPERATION");
|
|
1549
|
+
if (!descendant.hierarchyPath.startsWith(oldPath)) throw new TenantHierarchyError(`Descendant tenant ${descendant.id} has hierarchyPath "${descendant.hierarchyPath}" which does not start with expected prefix "${oldPath}"`, "INVALID_OPERATION");
|
|
1550
|
+
descendant.hierarchyPath = newPathForDescendants + descendant.hierarchyPath.substring(oldPath.length);
|
|
1551
|
+
descendant.hierarchyLevel += levelDelta;
|
|
1552
|
+
await descendant.save();
|
|
1553
|
+
}
|
|
1554
|
+
return tenant;
|
|
1555
|
+
}
|
|
1556
|
+
/**
|
|
1557
|
+
* Make a tenant a root tenant (remove from hierarchy)
|
|
1558
|
+
*/
|
|
1559
|
+
async makeRoot(tenantId) {
|
|
1560
|
+
return await this.moveToParent(tenantId, null);
|
|
1561
|
+
}
|
|
1562
|
+
/**
|
|
1563
|
+
* Validate that a tenant hierarchy is consistent.
|
|
1564
|
+
* Returns validation errors if any.
|
|
1565
|
+
*/
|
|
1566
|
+
async validateHierarchy(tenantId) {
|
|
1567
|
+
const errors = [];
|
|
1568
|
+
const tenant = await this.get({ id: tenantId });
|
|
1569
|
+
if (!tenant) return [`Tenant not found: ${tenantId}`];
|
|
1570
|
+
if (tenant.parentTenantId) {
|
|
1571
|
+
const parent = await this.get({ id: tenant.parentTenantId });
|
|
1572
|
+
if (!parent) errors.push(`Parent tenant not found: ${tenant.parentTenantId}`);
|
|
1573
|
+
else {
|
|
1574
|
+
if (tenant.hierarchyLevel !== parent.hierarchyLevel + 1) errors.push(`Hierarchy level mismatch: expected ${parent.hierarchyLevel + 1}, got ${tenant.hierarchyLevel}`);
|
|
1575
|
+
const expectedPath = parent.hierarchyPath ? `${parent.hierarchyPath}/${parent.id}` : parent.id;
|
|
1576
|
+
if (tenant.hierarchyPath !== expectedPath) errors.push(`Hierarchy path mismatch: expected "${expectedPath}", got "${tenant.hierarchyPath}"`);
|
|
1577
|
+
}
|
|
1578
|
+
} else {
|
|
1579
|
+
if (tenant.hierarchyLevel !== 0) errors.push(`Root tenant should have hierarchyLevel 0, got ${tenant.hierarchyLevel}`);
|
|
1580
|
+
if (tenant.hierarchyPath !== "") errors.push(`Root tenant should have empty hierarchyPath, got "${tenant.hierarchyPath}"`);
|
|
1581
|
+
}
|
|
1582
|
+
if (tenant.parentTenantId && tenant.id) {
|
|
1583
|
+
if (tenant.getAncestorIds().includes(tenant.id)) errors.push("Circular reference detected in hierarchy");
|
|
1584
|
+
}
|
|
1585
|
+
return errors;
|
|
1586
|
+
}
|
|
1587
|
+
/**
|
|
1588
|
+
* Get the full hierarchy tree starting from a tenant.
|
|
1589
|
+
* Returns a nested structure useful for UI rendering.
|
|
1590
|
+
*/
|
|
1591
|
+
async getTree(rootTenantId) {
|
|
1592
|
+
const roots = rootTenantId ? [await this.get({ id: rootTenantId })] : await this.findRoots();
|
|
1593
|
+
const buildTree = async (tenant) => {
|
|
1594
|
+
if (!tenant) return null;
|
|
1595
|
+
const children = await this.findChildren(tenant.id);
|
|
1596
|
+
const childTrees = await Promise.all(children.map(buildTree));
|
|
1597
|
+
return Object.assign(tenant, { children: childTrees.filter(Boolean) });
|
|
1598
|
+
};
|
|
1599
|
+
return (await Promise.all(roots.map(buildTree))).filter(Boolean);
|
|
1600
|
+
}
|
|
1601
|
+
/**
|
|
1602
|
+
* Override create to automatically set hierarchy fields for new tenants.
|
|
1603
|
+
* Only calculates them when the caller hasn't already supplied
|
|
1604
|
+
* `hierarchyLevel`/`hierarchyPath` in the create input — an explicitly
|
|
1605
|
+
* provided value is preserved as-is.
|
|
1606
|
+
*/
|
|
1607
|
+
async create(options) {
|
|
1608
|
+
if (options.parentTenantId) {
|
|
1609
|
+
if (options.hierarchyLevel === void 0 || options.hierarchyPath === void 0) {
|
|
1610
|
+
const parent = await this.get({ id: options.parentTenantId });
|
|
1611
|
+
if (!parent?.id) throw new TenantHierarchyError(`Parent tenant not found: ${options.parentTenantId}`, "PARENT_NOT_FOUND");
|
|
1612
|
+
const newLevel = parent.hierarchyLevel + 1;
|
|
1613
|
+
if (newLevel >= 10) throw new TenantHierarchyError(`Maximum hierarchy depth (10) exceeded`, "MAX_DEPTH_EXCEEDED");
|
|
1614
|
+
if (options.hierarchyLevel === void 0) options.hierarchyLevel = newLevel;
|
|
1615
|
+
if (options.hierarchyPath === void 0) options.hierarchyPath = parent.hierarchyPath ? `${parent.hierarchyPath}/${parent.id}` : parent.id;
|
|
1616
|
+
}
|
|
1617
|
+
} else {
|
|
1618
|
+
options.hierarchyLevel = options.hierarchyLevel ?? 0;
|
|
1619
|
+
options.hierarchyPath = options.hierarchyPath ?? "";
|
|
1620
|
+
}
|
|
1621
|
+
return super.create(options);
|
|
1622
|
+
}
|
|
1623
|
+
};
|
|
1624
|
+
//#endregion
|
|
1625
|
+
//#region src/models/TenantPermissionOverride.ts
|
|
1626
|
+
var __defProp = Object.defineProperty;
|
|
1627
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
1628
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
1629
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
1630
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
1631
|
+
if (kind && result) __defProp(target, key, result);
|
|
1632
|
+
return result;
|
|
1633
|
+
};
|
|
1634
|
+
var TenantPermissionOverride = class extends SmrtObject {
|
|
1635
|
+
tenantId;
|
|
1636
|
+
permissionId;
|
|
1637
|
+
effect = TenantPermissionEffect.INHERIT;
|
|
1638
|
+
constructor(options = {}) {
|
|
1639
|
+
super(options);
|
|
1640
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
1641
|
+
if (options.permissionId !== void 0) this.permissionId = options.permissionId;
|
|
1642
|
+
if (options.effect !== void 0) this.effect = options.effect;
|
|
1643
|
+
}
|
|
1644
|
+
/**
|
|
1645
|
+
* Check if this override inherits from parent
|
|
1646
|
+
*/
|
|
1647
|
+
isInherit() {
|
|
1648
|
+
return this.effect === TenantPermissionEffect.INHERIT;
|
|
1649
|
+
}
|
|
1650
|
+
/**
|
|
1651
|
+
* Check if this override grants the permission
|
|
1652
|
+
*/
|
|
1653
|
+
isGrant() {
|
|
1654
|
+
return this.effect === TenantPermissionEffect.GRANT;
|
|
1655
|
+
}
|
|
1656
|
+
/**
|
|
1657
|
+
* Check if this override denies the permission
|
|
1658
|
+
*/
|
|
1659
|
+
isDeny() {
|
|
1660
|
+
return this.effect === TenantPermissionEffect.DENY;
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* Check if this override has an explicit effect (not inherit)
|
|
1664
|
+
*/
|
|
1665
|
+
hasExplicitEffect() {
|
|
1666
|
+
return this.effect !== TenantPermissionEffect.INHERIT;
|
|
1667
|
+
}
|
|
1668
|
+
};
|
|
1669
|
+
__decorateClass([foreignKey("Tenant", { required: true })], TenantPermissionOverride.prototype, "tenantId", 2);
|
|
1670
|
+
__decorateClass([foreignKey("Permission", { required: true })], TenantPermissionOverride.prototype, "permissionId", 2);
|
|
1671
|
+
__decorateClass([field({ type: "text" })], TenantPermissionOverride.prototype, "effect", 2);
|
|
1672
|
+
TenantPermissionOverride = __decorateClass([smrt({
|
|
1673
|
+
api: { include: ["list", "get"] },
|
|
1674
|
+
mcp: { include: ["list", "get"] },
|
|
1675
|
+
cli: true
|
|
1676
|
+
})], TenantPermissionOverride);
|
|
1677
|
+
//#endregion
|
|
1678
|
+
//#region src/collections/TenantPermissionOverrideCollection.ts
|
|
1679
|
+
var TenantPermissionOverrideCollection = class extends SmrtCollection {
|
|
1680
|
+
static _itemClass = TenantPermissionOverride;
|
|
1681
|
+
/**
|
|
1682
|
+
* Find all overrides for a tenant
|
|
1683
|
+
*/
|
|
1684
|
+
async findByTenant(tenantId) {
|
|
1685
|
+
return await this.list({ where: { tenantId } });
|
|
1686
|
+
}
|
|
1687
|
+
/**
|
|
1688
|
+
* Find grant overrides for a tenant.
|
|
1689
|
+
*
|
|
1690
|
+
* Filters in memory because the `effect` column is JSON-typed and
|
|
1691
|
+
* Postgres rejects bare `json = text` comparisons. A single
|
|
1692
|
+
* `findByTenant` call is reused for grant, deny, and inherit lookups.
|
|
1693
|
+
*/
|
|
1694
|
+
async findGrants(tenantId) {
|
|
1695
|
+
return (await this.findByTenant(tenantId)).filter((o) => o.effect === TenantPermissionEffect.GRANT);
|
|
1696
|
+
}
|
|
1697
|
+
/**
|
|
1698
|
+
* Find deny overrides for a tenant.
|
|
1699
|
+
*
|
|
1700
|
+
* See `findGrants` for rationale on in-memory filtering.
|
|
1701
|
+
*/
|
|
1702
|
+
async findDenies(tenantId) {
|
|
1703
|
+
return (await this.findByTenant(tenantId)).filter((o) => o.effect === TenantPermissionEffect.DENY);
|
|
1704
|
+
}
|
|
1705
|
+
/**
|
|
1706
|
+
* Find inherit overrides for a tenant.
|
|
1707
|
+
*
|
|
1708
|
+
* See `findGrants` for rationale on in-memory filtering.
|
|
1709
|
+
*/
|
|
1710
|
+
async findInherits(tenantId) {
|
|
1711
|
+
return (await this.findByTenant(tenantId)).filter((o) => o.effect === TenantPermissionEffect.INHERIT);
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Get granted permission IDs for a tenant
|
|
1715
|
+
*/
|
|
1716
|
+
async getGrantedPermissionIds(tenantId) {
|
|
1717
|
+
return (await this.findGrants(tenantId)).map((o) => o.permissionId);
|
|
1718
|
+
}
|
|
1719
|
+
/**
|
|
1720
|
+
* Get denied permission IDs for a tenant
|
|
1721
|
+
*/
|
|
1722
|
+
async getDeniedPermissionIds(tenantId) {
|
|
1723
|
+
return (await this.findDenies(tenantId)).map((o) => o.permissionId);
|
|
1724
|
+
}
|
|
1725
|
+
/**
|
|
1726
|
+
* Get all overrides for a tenant, organized by effect
|
|
1727
|
+
*/
|
|
1728
|
+
async getOverridesByEffect(tenantId) {
|
|
1729
|
+
const overrides = await this.findByTenant(tenantId);
|
|
1730
|
+
const result = {
|
|
1731
|
+
grantedPermissionIds: [],
|
|
1732
|
+
deniedPermissionIds: [],
|
|
1733
|
+
inheritedPermissionIds: []
|
|
1734
|
+
};
|
|
1735
|
+
for (const override of overrides) {
|
|
1736
|
+
const permId = override.permissionId;
|
|
1737
|
+
switch (override.effect) {
|
|
1738
|
+
case TenantPermissionEffect.GRANT:
|
|
1739
|
+
result.grantedPermissionIds.push(permId);
|
|
1740
|
+
break;
|
|
1741
|
+
case TenantPermissionEffect.DENY:
|
|
1742
|
+
result.deniedPermissionIds.push(permId);
|
|
1743
|
+
break;
|
|
1744
|
+
case TenantPermissionEffect.INHERIT:
|
|
1745
|
+
result.inheritedPermissionIds.push(permId);
|
|
1746
|
+
break;
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
return result;
|
|
1750
|
+
}
|
|
1751
|
+
/**
|
|
1752
|
+
* Batch get all overrides for multiple tenants, organized by effect.
|
|
1753
|
+
* Fetches all overrides in a single query to avoid N+1 query problem.
|
|
1754
|
+
*
|
|
1755
|
+
* @param tenantIds - Array of tenant IDs to fetch overrides for
|
|
1756
|
+
* @returns Map of tenant ID to their permission override results
|
|
1757
|
+
*/
|
|
1758
|
+
async getOverridesByEffectBatch(tenantIds) {
|
|
1759
|
+
const resultMap = /* @__PURE__ */ new Map();
|
|
1760
|
+
for (const tenantId of tenantIds) resultMap.set(tenantId, {
|
|
1761
|
+
grantedPermissionIds: [],
|
|
1762
|
+
deniedPermissionIds: [],
|
|
1763
|
+
inheritedPermissionIds: []
|
|
1764
|
+
});
|
|
1765
|
+
if (tenantIds.length === 0) return resultMap;
|
|
1766
|
+
const allOverrides = await this.list({ where: { tenantId: tenantIds } });
|
|
1767
|
+
for (const override of allOverrides) {
|
|
1768
|
+
const tenantId = override.tenantId;
|
|
1769
|
+
const permId = override.permissionId;
|
|
1770
|
+
const result = resultMap.get(tenantId);
|
|
1771
|
+
if (result) switch (override.effect) {
|
|
1772
|
+
case TenantPermissionEffect.GRANT:
|
|
1773
|
+
result.grantedPermissionIds.push(permId);
|
|
1774
|
+
break;
|
|
1775
|
+
case TenantPermissionEffect.DENY:
|
|
1776
|
+
result.deniedPermissionIds.push(permId);
|
|
1777
|
+
break;
|
|
1778
|
+
case TenantPermissionEffect.INHERIT:
|
|
1779
|
+
result.inheritedPermissionIds.push(permId);
|
|
1780
|
+
break;
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
return resultMap;
|
|
1784
|
+
}
|
|
1785
|
+
/**
|
|
1786
|
+
* Set an override for a tenant permission
|
|
1787
|
+
*/
|
|
1788
|
+
async setOverride(tenantId, permissionId, effect) {
|
|
1789
|
+
const existing = await this.list({
|
|
1790
|
+
where: {
|
|
1791
|
+
tenantId,
|
|
1792
|
+
permissionId
|
|
1793
|
+
},
|
|
1794
|
+
limit: 1
|
|
1795
|
+
});
|
|
1796
|
+
if (existing.length > 0) {
|
|
1797
|
+
existing[0].effect = effect;
|
|
1798
|
+
await existing[0].save();
|
|
1799
|
+
return existing[0];
|
|
1800
|
+
}
|
|
1801
|
+
const override = await this.create({
|
|
1802
|
+
tenantId,
|
|
1803
|
+
permissionId,
|
|
1804
|
+
effect
|
|
1805
|
+
});
|
|
1806
|
+
await override.save();
|
|
1807
|
+
return override;
|
|
1808
|
+
}
|
|
1809
|
+
/**
|
|
1810
|
+
* Remove an override (permission will use default behavior)
|
|
1811
|
+
*/
|
|
1812
|
+
async removeOverride(tenantId, permissionId) {
|
|
1813
|
+
const existing = await this.list({
|
|
1814
|
+
where: {
|
|
1815
|
+
tenantId,
|
|
1816
|
+
permissionId
|
|
1817
|
+
},
|
|
1818
|
+
limit: 1
|
|
1819
|
+
});
|
|
1820
|
+
if (existing.length === 0) return false;
|
|
1821
|
+
await existing[0].delete();
|
|
1822
|
+
return true;
|
|
1823
|
+
}
|
|
1824
|
+
/**
|
|
1825
|
+
* Remove all overrides for a tenant
|
|
1826
|
+
*/
|
|
1827
|
+
async removeAllOverrides(tenantId) {
|
|
1828
|
+
const overrides = await this.findByTenant(tenantId);
|
|
1829
|
+
await Promise.all(overrides.map((override) => override.delete()));
|
|
1830
|
+
return overrides.length;
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1833
|
+
* Grant a permission at the tenant level.
|
|
1834
|
+
* Convenience method for setOverride with GRANT effect.
|
|
1835
|
+
*/
|
|
1836
|
+
async grantPermission(tenantId, permissionId) {
|
|
1837
|
+
return await this.setOverride(tenantId, permissionId, TenantPermissionEffect.GRANT);
|
|
1838
|
+
}
|
|
1839
|
+
/**
|
|
1840
|
+
* Deny a permission at the tenant level.
|
|
1841
|
+
* Convenience method for setOverride with DENY effect.
|
|
1842
|
+
* This blocks inheritance from parent tenants.
|
|
1843
|
+
*/
|
|
1844
|
+
async denyPermission(tenantId, permissionId) {
|
|
1845
|
+
return await this.setOverride(tenantId, permissionId, TenantPermissionEffect.DENY);
|
|
1846
|
+
}
|
|
1847
|
+
/**
|
|
1848
|
+
* Set a permission to inherit from parent.
|
|
1849
|
+
* Convenience method for setOverride with INHERIT effect.
|
|
1850
|
+
* This is the default behavior, so mainly useful to document intent
|
|
1851
|
+
* or to reset a previous grant/deny.
|
|
1852
|
+
*/
|
|
1853
|
+
async inheritPermission(tenantId, permissionId) {
|
|
1854
|
+
return await this.setOverride(tenantId, permissionId, TenantPermissionEffect.INHERIT);
|
|
1855
|
+
}
|
|
1856
|
+
/**
|
|
1857
|
+
* Bulk set overrides for a tenant.
|
|
1858
|
+
* Useful for importing or copying permission configurations.
|
|
1859
|
+
*/
|
|
1860
|
+
async bulkSetOverrides(tenantId, overrides) {
|
|
1861
|
+
const results = [];
|
|
1862
|
+
for (const { permissionId, effect } of overrides) {
|
|
1863
|
+
const override = await this.setOverride(tenantId, permissionId, effect);
|
|
1864
|
+
results.push(override);
|
|
1865
|
+
}
|
|
1866
|
+
return results;
|
|
1867
|
+
}
|
|
1868
|
+
/**
|
|
1869
|
+
* Copy overrides from one tenant to another.
|
|
1870
|
+
* Useful when creating child tenants or templates.
|
|
1871
|
+
*/
|
|
1872
|
+
async copyOverrides(fromTenantId, toTenantId) {
|
|
1873
|
+
const sourceOverrides = await this.findByTenant(fromTenantId);
|
|
1874
|
+
const results = [];
|
|
1875
|
+
for (const source of sourceOverrides) {
|
|
1876
|
+
const override = await this.setOverride(toTenantId, source.permissionId, source.effect);
|
|
1877
|
+
results.push(override);
|
|
1878
|
+
}
|
|
1879
|
+
return results;
|
|
1880
|
+
}
|
|
1881
|
+
};
|
|
1882
|
+
//#endregion
|
|
1883
|
+
//#region src/collections/UserCollection.ts
|
|
1884
|
+
var UserCollection = class extends SmrtCollection {
|
|
1885
|
+
static _itemClass = User;
|
|
1886
|
+
/**
|
|
1887
|
+
* Find user by email address
|
|
1888
|
+
*/
|
|
1889
|
+
async findByEmail(email) {
|
|
1890
|
+
const results = await this.list({
|
|
1891
|
+
where: { email },
|
|
1892
|
+
limit: 1
|
|
1893
|
+
});
|
|
1894
|
+
return results.length > 0 ? results[0] : null;
|
|
1895
|
+
}
|
|
1896
|
+
/**
|
|
1897
|
+
* Find user by profile ID
|
|
1898
|
+
*/
|
|
1899
|
+
async findByProfile(profileId) {
|
|
1900
|
+
const results = await this.list({
|
|
1901
|
+
where: { profileId },
|
|
1902
|
+
limit: 1
|
|
1903
|
+
});
|
|
1904
|
+
return results.length > 0 ? results[0] : null;
|
|
1905
|
+
}
|
|
1906
|
+
/**
|
|
1907
|
+
* Find users by status
|
|
1908
|
+
*/
|
|
1909
|
+
async findByStatus(status) {
|
|
1910
|
+
return await this.list({
|
|
1911
|
+
where: { status },
|
|
1912
|
+
orderBy: "created_at DESC"
|
|
1913
|
+
});
|
|
1914
|
+
}
|
|
1915
|
+
/**
|
|
1916
|
+
* Find all active users
|
|
1917
|
+
*/
|
|
1918
|
+
async findActive() {
|
|
1919
|
+
return await this.findByStatus(UserStatus.ACTIVE);
|
|
1920
|
+
}
|
|
1921
|
+
/**
|
|
1922
|
+
* Find all pending users
|
|
1923
|
+
*/
|
|
1924
|
+
async findPending() {
|
|
1925
|
+
return await this.findByStatus(UserStatus.PENDING);
|
|
1926
|
+
}
|
|
1927
|
+
/**
|
|
1928
|
+
* Get or create user for a profile
|
|
1929
|
+
*/
|
|
1930
|
+
async getOrCreateForProfile(profileId, email, defaults = {}) {
|
|
1931
|
+
const existing = await this.findByProfile(profileId);
|
|
1932
|
+
if (existing) return existing;
|
|
1933
|
+
const user = await this.create({
|
|
1934
|
+
profileId,
|
|
1935
|
+
email,
|
|
1936
|
+
status: defaults.status ?? UserStatus.ACTIVE
|
|
1937
|
+
});
|
|
1938
|
+
await user.save();
|
|
1939
|
+
return user;
|
|
1940
|
+
}
|
|
1941
|
+
/**
|
|
1942
|
+
* Get or create user from OIDC claims
|
|
1943
|
+
*
|
|
1944
|
+
* This is the primary method for resolving identity from an OIDC login.
|
|
1945
|
+
* It handles the full flow:
|
|
1946
|
+
* 1. Find or create Profile from OIDC claims (via smrt-profiles)
|
|
1947
|
+
* 2. Link OidcIdentity to the Profile
|
|
1948
|
+
* 3. Find or create User linked to the Profile
|
|
1949
|
+
*
|
|
1950
|
+
* @param claims - OIDC token claims (sub, iss, email, name)
|
|
1951
|
+
* @param provider - Provider name (e.g., 'kanidm', 'keycloak', 'google')
|
|
1952
|
+
* @param options - Optional settings (recordLogin)
|
|
1953
|
+
* @returns User, Profile, OidcIdentity, and whether profile was created
|
|
1954
|
+
*
|
|
1955
|
+
* @example
|
|
1956
|
+
* ```typescript
|
|
1957
|
+
* const userCollection = await UserCollection.create({ db: dbConfig });
|
|
1958
|
+
*
|
|
1959
|
+
* // In your OIDC callback handler:
|
|
1960
|
+
* const { user, profile } = await userCollection.getOrCreateFromOidc(
|
|
1961
|
+
* {
|
|
1962
|
+
* sub: tokenClaims.sub,
|
|
1963
|
+
* iss: tokenClaims.iss,
|
|
1964
|
+
* email: tokenClaims.email,
|
|
1965
|
+
* name: tokenClaims.name,
|
|
1966
|
+
* },
|
|
1967
|
+
* 'kanidm'
|
|
1968
|
+
* );
|
|
1969
|
+
*
|
|
1970
|
+
* // User and profile are now available
|
|
1971
|
+
* // Login was auto-recorded; pass { recordLogin: false } to skip
|
|
1972
|
+
* ```
|
|
1973
|
+
*/
|
|
1974
|
+
async getOrCreateFromOidc(claims, provider, options) {
|
|
1975
|
+
const sub = claims?.sub?.trim();
|
|
1976
|
+
const iss = claims?.iss?.trim();
|
|
1977
|
+
if (!sub || !iss) throw new Error("Invalid OIDC claims: both \"sub\" and \"iss\" must be non-empty strings.");
|
|
1978
|
+
if (!claims.email) throw new Error("OIDC claims missing required \"email\" for user creation.");
|
|
1979
|
+
const allowUnverified = options?.allowUnverifiedEmail === true;
|
|
1980
|
+
if (claims.email_verified === false && !allowUnverified) throw new Error("OIDC claims report an unverified email; refusing to provision a user.");
|
|
1981
|
+
const { createProfileFromOidc } = await import("@happyvertical/smrt-profiles");
|
|
1982
|
+
const { profile, oidcIdentity, created } = await createProfileFromOidc(claims, provider, { db: this.options.db });
|
|
1983
|
+
const shouldRecordLogin = options?.recordLogin !== false;
|
|
1984
|
+
const existingUser = await this.findByProfile(profile.id);
|
|
1985
|
+
const user = existingUser ?? await this.create({
|
|
1986
|
+
email: claims.email,
|
|
1987
|
+
...shouldRecordLogin ? { lastLoginAt: /* @__PURE__ */ new Date() } : {},
|
|
1988
|
+
profileId: profile.id,
|
|
1989
|
+
status: UserStatus.ACTIVE
|
|
1990
|
+
});
|
|
1991
|
+
if (existingUser && shouldRecordLogin) {
|
|
1992
|
+
existingUser.recordLogin();
|
|
1993
|
+
await existingUser.save();
|
|
1994
|
+
}
|
|
1995
|
+
return {
|
|
1996
|
+
user,
|
|
1997
|
+
profile,
|
|
1998
|
+
oidcIdentity,
|
|
1999
|
+
created
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
};
|
|
2003
|
+
//#endregion
|
|
2004
|
+
//#region ../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/lib/buffer_utils.js
|
|
2005
|
+
var encoder = new TextEncoder();
|
|
2006
|
+
var decoder = new TextDecoder();
|
|
2007
|
+
function concat(...buffers) {
|
|
2008
|
+
const size = buffers.reduce((acc, { length }) => acc + length, 0);
|
|
2009
|
+
const buf = new Uint8Array(size);
|
|
2010
|
+
let i = 0;
|
|
2011
|
+
for (const buffer of buffers) {
|
|
2012
|
+
buf.set(buffer, i);
|
|
2013
|
+
i += buffer.length;
|
|
2014
|
+
}
|
|
2015
|
+
return buf;
|
|
2016
|
+
}
|
|
2017
|
+
function encode$1(string) {
|
|
2018
|
+
const bytes = new Uint8Array(string.length);
|
|
2019
|
+
for (let i = 0; i < string.length; i++) {
|
|
2020
|
+
const code = string.charCodeAt(i);
|
|
2021
|
+
if (code > 127) throw new TypeError("non-ASCII string encountered in encode()");
|
|
2022
|
+
bytes[i] = code;
|
|
2023
|
+
}
|
|
2024
|
+
return bytes;
|
|
2025
|
+
}
|
|
2026
|
+
//#endregion
|
|
2027
|
+
//#region ../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/lib/base64.js
|
|
2028
|
+
function encodeBase64(input) {
|
|
2029
|
+
if (Uint8Array.prototype.toBase64) return input.toBase64();
|
|
2030
|
+
const CHUNK_SIZE = 32768;
|
|
2031
|
+
const arr = [];
|
|
2032
|
+
for (let i = 0; i < input.length; i += CHUNK_SIZE) arr.push(String.fromCharCode.apply(null, input.subarray(i, i + CHUNK_SIZE)));
|
|
2033
|
+
return btoa(arr.join(""));
|
|
2034
|
+
}
|
|
2035
|
+
function decodeBase64(encoded) {
|
|
2036
|
+
if (Uint8Array.fromBase64) return Uint8Array.fromBase64(encoded);
|
|
2037
|
+
const binary = atob(encoded);
|
|
2038
|
+
const bytes = new Uint8Array(binary.length);
|
|
2039
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
2040
|
+
return bytes;
|
|
2041
|
+
}
|
|
2042
|
+
//#endregion
|
|
2043
|
+
//#region ../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/util/base64url.js
|
|
2044
|
+
function decode(input) {
|
|
2045
|
+
if (Uint8Array.fromBase64) return Uint8Array.fromBase64(typeof input === "string" ? input : decoder.decode(input), { alphabet: "base64url" });
|
|
2046
|
+
let encoded = input;
|
|
2047
|
+
if (encoded instanceof Uint8Array) encoded = decoder.decode(encoded);
|
|
2048
|
+
encoded = encoded.replace(/-/g, "+").replace(/_/g, "/");
|
|
2049
|
+
try {
|
|
2050
|
+
return decodeBase64(encoded);
|
|
2051
|
+
} catch {
|
|
2052
|
+
throw new TypeError("The input to be decoded is not correctly encoded.");
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
function encode(input) {
|
|
2056
|
+
let unencoded = input;
|
|
2057
|
+
if (typeof unencoded === "string") unencoded = encoder.encode(unencoded);
|
|
2058
|
+
if (Uint8Array.prototype.toBase64) return unencoded.toBase64({
|
|
2059
|
+
alphabet: "base64url",
|
|
2060
|
+
omitPadding: true
|
|
2061
|
+
});
|
|
2062
|
+
return encodeBase64(unencoded).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
2063
|
+
}
|
|
2064
|
+
//#endregion
|
|
2065
|
+
//#region ../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/lib/crypto_key.js
|
|
2066
|
+
var unusable = (name, prop = "algorithm.name") => /* @__PURE__ */ new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`);
|
|
2067
|
+
var isAlgorithm = (algorithm, name) => algorithm.name === name;
|
|
2068
|
+
function getHashLength(hash) {
|
|
2069
|
+
return parseInt(hash.name.slice(4), 10);
|
|
2070
|
+
}
|
|
2071
|
+
function checkHashLength(algorithm, expected) {
|
|
2072
|
+
if (getHashLength(algorithm.hash) !== expected) throw unusable(`SHA-${expected}`, "algorithm.hash");
|
|
2073
|
+
}
|
|
2074
|
+
function getNamedCurve(alg) {
|
|
2075
|
+
switch (alg) {
|
|
2076
|
+
case "ES256": return "P-256";
|
|
2077
|
+
case "ES384": return "P-384";
|
|
2078
|
+
case "ES512": return "P-521";
|
|
2079
|
+
default: throw new Error("unreachable");
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
function checkUsage(key, usage) {
|
|
2083
|
+
if (usage && !key.usages.includes(usage)) throw new TypeError(`CryptoKey does not support this operation, its usages must include ${usage}.`);
|
|
2084
|
+
}
|
|
2085
|
+
function checkSigCryptoKey(key, alg, usage) {
|
|
2086
|
+
switch (alg) {
|
|
2087
|
+
case "HS256":
|
|
2088
|
+
case "HS384":
|
|
2089
|
+
case "HS512":
|
|
2090
|
+
if (!isAlgorithm(key.algorithm, "HMAC")) throw unusable("HMAC");
|
|
2091
|
+
checkHashLength(key.algorithm, parseInt(alg.slice(2), 10));
|
|
2092
|
+
break;
|
|
2093
|
+
case "RS256":
|
|
2094
|
+
case "RS384":
|
|
2095
|
+
case "RS512":
|
|
2096
|
+
if (!isAlgorithm(key.algorithm, "RSASSA-PKCS1-v1_5")) throw unusable("RSASSA-PKCS1-v1_5");
|
|
2097
|
+
checkHashLength(key.algorithm, parseInt(alg.slice(2), 10));
|
|
2098
|
+
break;
|
|
2099
|
+
case "PS256":
|
|
2100
|
+
case "PS384":
|
|
2101
|
+
case "PS512":
|
|
2102
|
+
if (!isAlgorithm(key.algorithm, "RSA-PSS")) throw unusable("RSA-PSS");
|
|
2103
|
+
checkHashLength(key.algorithm, parseInt(alg.slice(2), 10));
|
|
2104
|
+
break;
|
|
2105
|
+
case "Ed25519":
|
|
2106
|
+
case "EdDSA":
|
|
2107
|
+
if (!isAlgorithm(key.algorithm, "Ed25519")) throw unusable("Ed25519");
|
|
2108
|
+
break;
|
|
2109
|
+
case "ML-DSA-44":
|
|
2110
|
+
case "ML-DSA-65":
|
|
2111
|
+
case "ML-DSA-87":
|
|
2112
|
+
if (!isAlgorithm(key.algorithm, alg)) throw unusable(alg);
|
|
2113
|
+
break;
|
|
2114
|
+
case "ES256":
|
|
2115
|
+
case "ES384":
|
|
2116
|
+
case "ES512": {
|
|
2117
|
+
if (!isAlgorithm(key.algorithm, "ECDSA")) throw unusable("ECDSA");
|
|
2118
|
+
const expected = getNamedCurve(alg);
|
|
2119
|
+
if (key.algorithm.namedCurve !== expected) throw unusable(expected, "algorithm.namedCurve");
|
|
2120
|
+
break;
|
|
2121
|
+
}
|
|
2122
|
+
default: throw new TypeError("CryptoKey does not support this operation");
|
|
2123
|
+
}
|
|
2124
|
+
checkUsage(key, usage);
|
|
2125
|
+
}
|
|
2126
|
+
//#endregion
|
|
2127
|
+
//#region ../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/lib/invalid_key_input.js
|
|
2128
|
+
function message(msg, actual, ...types) {
|
|
2129
|
+
types = types.filter(Boolean);
|
|
2130
|
+
if (types.length > 2) {
|
|
2131
|
+
const last = types.pop();
|
|
2132
|
+
msg += `one of type ${types.join(", ")}, or ${last}.`;
|
|
2133
|
+
} else if (types.length === 2) msg += `one of type ${types[0]} or ${types[1]}.`;
|
|
2134
|
+
else msg += `of type ${types[0]}.`;
|
|
2135
|
+
if (actual == null) msg += ` Received ${actual}`;
|
|
2136
|
+
else if (typeof actual === "function" && actual.name) msg += ` Received function ${actual.name}`;
|
|
2137
|
+
else if (typeof actual === "object" && actual != null) {
|
|
2138
|
+
if (actual.constructor?.name) msg += ` Received an instance of ${actual.constructor.name}`;
|
|
2139
|
+
}
|
|
2140
|
+
return msg;
|
|
2141
|
+
}
|
|
2142
|
+
var invalidKeyInput = (actual, ...types) => message("Key must be ", actual, ...types);
|
|
2143
|
+
var withAlg = (alg, actual, ...types) => message(`Key for the ${alg} algorithm must be `, actual, ...types);
|
|
2144
|
+
//#endregion
|
|
2145
|
+
//#region ../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/util/errors.js
|
|
2146
|
+
var errors_exports = /* @__PURE__ */ __exportAll({
|
|
2147
|
+
JOSEAlgNotAllowed: () => JOSEAlgNotAllowed,
|
|
2148
|
+
JOSEError: () => JOSEError,
|
|
2149
|
+
JOSENotSupported: () => JOSENotSupported,
|
|
2150
|
+
JWEDecryptionFailed: () => JWEDecryptionFailed,
|
|
2151
|
+
JWEInvalid: () => JWEInvalid,
|
|
2152
|
+
JWKInvalid: () => JWKInvalid,
|
|
2153
|
+
JWKSInvalid: () => JWKSInvalid,
|
|
2154
|
+
JWKSMultipleMatchingKeys: () => JWKSMultipleMatchingKeys,
|
|
2155
|
+
JWKSNoMatchingKey: () => JWKSNoMatchingKey,
|
|
2156
|
+
JWKSTimeout: () => JWKSTimeout,
|
|
2157
|
+
JWSInvalid: () => JWSInvalid,
|
|
2158
|
+
JWSSignatureVerificationFailed: () => JWSSignatureVerificationFailed,
|
|
2159
|
+
JWTClaimValidationFailed: () => JWTClaimValidationFailed,
|
|
2160
|
+
JWTExpired: () => JWTExpired,
|
|
2161
|
+
JWTInvalid: () => JWTInvalid
|
|
2162
|
+
});
|
|
2163
|
+
var JOSEError = class extends Error {
|
|
2164
|
+
static code = "ERR_JOSE_GENERIC";
|
|
2165
|
+
code = "ERR_JOSE_GENERIC";
|
|
2166
|
+
constructor(message, options) {
|
|
2167
|
+
super(message, options);
|
|
2168
|
+
this.name = this.constructor.name;
|
|
2169
|
+
Error.captureStackTrace?.(this, this.constructor);
|
|
2170
|
+
}
|
|
2171
|
+
};
|
|
2172
|
+
var JWTClaimValidationFailed = class extends JOSEError {
|
|
2173
|
+
static code = "ERR_JWT_CLAIM_VALIDATION_FAILED";
|
|
2174
|
+
code = "ERR_JWT_CLAIM_VALIDATION_FAILED";
|
|
2175
|
+
claim;
|
|
2176
|
+
reason;
|
|
2177
|
+
payload;
|
|
2178
|
+
constructor(message, payload, claim = "unspecified", reason = "unspecified") {
|
|
2179
|
+
super(message, { cause: {
|
|
2180
|
+
claim,
|
|
2181
|
+
reason,
|
|
2182
|
+
payload
|
|
2183
|
+
} });
|
|
2184
|
+
this.claim = claim;
|
|
2185
|
+
this.reason = reason;
|
|
2186
|
+
this.payload = payload;
|
|
2187
|
+
}
|
|
2188
|
+
};
|
|
2189
|
+
var JWTExpired = class extends JOSEError {
|
|
2190
|
+
static code = "ERR_JWT_EXPIRED";
|
|
2191
|
+
code = "ERR_JWT_EXPIRED";
|
|
2192
|
+
claim;
|
|
2193
|
+
reason;
|
|
2194
|
+
payload;
|
|
2195
|
+
constructor(message, payload, claim = "unspecified", reason = "unspecified") {
|
|
2196
|
+
super(message, { cause: {
|
|
2197
|
+
claim,
|
|
2198
|
+
reason,
|
|
2199
|
+
payload
|
|
2200
|
+
} });
|
|
2201
|
+
this.claim = claim;
|
|
2202
|
+
this.reason = reason;
|
|
2203
|
+
this.payload = payload;
|
|
2204
|
+
}
|
|
2205
|
+
};
|
|
2206
|
+
var JOSEAlgNotAllowed = class extends JOSEError {
|
|
2207
|
+
static code = "ERR_JOSE_ALG_NOT_ALLOWED";
|
|
2208
|
+
code = "ERR_JOSE_ALG_NOT_ALLOWED";
|
|
2209
|
+
};
|
|
2210
|
+
var JOSENotSupported = class extends JOSEError {
|
|
2211
|
+
static code = "ERR_JOSE_NOT_SUPPORTED";
|
|
2212
|
+
code = "ERR_JOSE_NOT_SUPPORTED";
|
|
2213
|
+
};
|
|
2214
|
+
var JWEDecryptionFailed = class extends JOSEError {
|
|
2215
|
+
static code = "ERR_JWE_DECRYPTION_FAILED";
|
|
2216
|
+
code = "ERR_JWE_DECRYPTION_FAILED";
|
|
2217
|
+
constructor(message = "decryption operation failed", options) {
|
|
2218
|
+
super(message, options);
|
|
2219
|
+
}
|
|
2220
|
+
};
|
|
2221
|
+
var JWEInvalid = class extends JOSEError {
|
|
2222
|
+
static code = "ERR_JWE_INVALID";
|
|
2223
|
+
code = "ERR_JWE_INVALID";
|
|
2224
|
+
};
|
|
2225
|
+
var JWSInvalid = class extends JOSEError {
|
|
2226
|
+
static code = "ERR_JWS_INVALID";
|
|
2227
|
+
code = "ERR_JWS_INVALID";
|
|
2228
|
+
};
|
|
2229
|
+
var JWTInvalid = class extends JOSEError {
|
|
2230
|
+
static code = "ERR_JWT_INVALID";
|
|
2231
|
+
code = "ERR_JWT_INVALID";
|
|
2232
|
+
};
|
|
2233
|
+
var JWKInvalid = class extends JOSEError {
|
|
2234
|
+
static code = "ERR_JWK_INVALID";
|
|
2235
|
+
code = "ERR_JWK_INVALID";
|
|
2236
|
+
};
|
|
2237
|
+
var JWKSInvalid = class extends JOSEError {
|
|
2238
|
+
static code = "ERR_JWKS_INVALID";
|
|
2239
|
+
code = "ERR_JWKS_INVALID";
|
|
2240
|
+
};
|
|
2241
|
+
var JWKSNoMatchingKey = class extends JOSEError {
|
|
2242
|
+
static code = "ERR_JWKS_NO_MATCHING_KEY";
|
|
2243
|
+
code = "ERR_JWKS_NO_MATCHING_KEY";
|
|
2244
|
+
constructor(message = "no applicable key found in the JSON Web Key Set", options) {
|
|
2245
|
+
super(message, options);
|
|
2246
|
+
}
|
|
2247
|
+
};
|
|
2248
|
+
var JWKSMultipleMatchingKeys = class extends JOSEError {
|
|
2249
|
+
[Symbol.asyncIterator];
|
|
2250
|
+
static code = "ERR_JWKS_MULTIPLE_MATCHING_KEYS";
|
|
2251
|
+
code = "ERR_JWKS_MULTIPLE_MATCHING_KEYS";
|
|
2252
|
+
constructor(message = "multiple matching keys found in the JSON Web Key Set", options) {
|
|
2253
|
+
super(message, options);
|
|
2254
|
+
}
|
|
2255
|
+
};
|
|
2256
|
+
var JWKSTimeout = class extends JOSEError {
|
|
2257
|
+
static code = "ERR_JWKS_TIMEOUT";
|
|
2258
|
+
code = "ERR_JWKS_TIMEOUT";
|
|
2259
|
+
constructor(message = "request timed out", options) {
|
|
2260
|
+
super(message, options);
|
|
2261
|
+
}
|
|
2262
|
+
};
|
|
2263
|
+
var JWSSignatureVerificationFailed = class extends JOSEError {
|
|
2264
|
+
static code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED";
|
|
2265
|
+
code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED";
|
|
2266
|
+
constructor(message = "signature verification failed", options) {
|
|
2267
|
+
super(message, options);
|
|
2268
|
+
}
|
|
2269
|
+
};
|
|
2270
|
+
//#endregion
|
|
2271
|
+
//#region ../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/lib/is_key_like.js
|
|
2272
|
+
var isCryptoKey = (key) => {
|
|
2273
|
+
if (key?.[Symbol.toStringTag] === "CryptoKey") return true;
|
|
2274
|
+
try {
|
|
2275
|
+
return key instanceof CryptoKey;
|
|
2276
|
+
} catch {
|
|
2277
|
+
return false;
|
|
2278
|
+
}
|
|
2279
|
+
};
|
|
2280
|
+
var isKeyObject = (key) => key?.[Symbol.toStringTag] === "KeyObject";
|
|
2281
|
+
var isKeyLike = (key) => isCryptoKey(key) || isKeyObject(key);
|
|
2282
|
+
//#endregion
|
|
2283
|
+
//#region ../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/lib/helpers.js
|
|
2284
|
+
function assertNotSet(value, name) {
|
|
2285
|
+
if (value) throw new TypeError(`${name} can only be called once`);
|
|
2286
|
+
}
|
|
2287
|
+
function decodeBase64url(value, label, ErrorClass) {
|
|
2288
|
+
try {
|
|
2289
|
+
return decode(value);
|
|
2290
|
+
} catch {
|
|
2291
|
+
throw new ErrorClass(`Failed to base64url decode the ${label}`);
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
//#endregion
|
|
2295
|
+
//#region ../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/lib/type_checks.js
|
|
2296
|
+
var isObjectLike = (value) => typeof value === "object" && value !== null;
|
|
2297
|
+
function isObject(input) {
|
|
2298
|
+
if (!isObjectLike(input) || Object.prototype.toString.call(input) !== "[object Object]") return false;
|
|
2299
|
+
if (Object.getPrototypeOf(input) === null) return true;
|
|
2300
|
+
let proto = input;
|
|
2301
|
+
while (Object.getPrototypeOf(proto) !== null) proto = Object.getPrototypeOf(proto);
|
|
2302
|
+
return Object.getPrototypeOf(input) === proto;
|
|
2303
|
+
}
|
|
2304
|
+
function isDisjoint(...headers) {
|
|
2305
|
+
const sources = headers.filter(Boolean);
|
|
2306
|
+
if (sources.length === 0 || sources.length === 1) return true;
|
|
2307
|
+
let acc;
|
|
2308
|
+
for (const header of sources) {
|
|
2309
|
+
const parameters = Object.keys(header);
|
|
2310
|
+
if (!acc || acc.size === 0) {
|
|
2311
|
+
acc = new Set(parameters);
|
|
2312
|
+
continue;
|
|
2313
|
+
}
|
|
2314
|
+
for (const parameter of parameters) {
|
|
2315
|
+
if (acc.has(parameter)) return false;
|
|
2316
|
+
acc.add(parameter);
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
return true;
|
|
2320
|
+
}
|
|
2321
|
+
var isJWK = (key) => isObject(key) && typeof key.kty === "string";
|
|
2322
|
+
var isPrivateJWK = (key) => key.kty !== "oct" && (key.kty === "AKP" && typeof key.priv === "string" || typeof key.d === "string");
|
|
2323
|
+
var isPublicJWK = (key) => key.kty !== "oct" && key.d === void 0 && key.priv === void 0;
|
|
2324
|
+
var isSecretJWK = (key) => key.kty === "oct" && typeof key.k === "string";
|
|
2325
|
+
//#endregion
|
|
2326
|
+
//#region ../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/lib/signing.js
|
|
2327
|
+
function checkKeyLength(alg, key) {
|
|
2328
|
+
if (alg.startsWith("RS") || alg.startsWith("PS")) {
|
|
2329
|
+
const { modulusLength } = key.algorithm;
|
|
2330
|
+
if (typeof modulusLength !== "number" || modulusLength < 2048) throw new TypeError(`${alg} requires key modulusLength to be 2048 bits or larger`);
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
function subtleAlgorithm(alg, algorithm) {
|
|
2334
|
+
const hash = `SHA-${alg.slice(-3)}`;
|
|
2335
|
+
switch (alg) {
|
|
2336
|
+
case "HS256":
|
|
2337
|
+
case "HS384":
|
|
2338
|
+
case "HS512": return {
|
|
2339
|
+
hash,
|
|
2340
|
+
name: "HMAC"
|
|
2341
|
+
};
|
|
2342
|
+
case "PS256":
|
|
2343
|
+
case "PS384":
|
|
2344
|
+
case "PS512": return {
|
|
2345
|
+
hash,
|
|
2346
|
+
name: "RSA-PSS",
|
|
2347
|
+
saltLength: parseInt(alg.slice(-3), 10) >> 3
|
|
2348
|
+
};
|
|
2349
|
+
case "RS256":
|
|
2350
|
+
case "RS384":
|
|
2351
|
+
case "RS512": return {
|
|
2352
|
+
hash,
|
|
2353
|
+
name: "RSASSA-PKCS1-v1_5"
|
|
2354
|
+
};
|
|
2355
|
+
case "ES256":
|
|
2356
|
+
case "ES384":
|
|
2357
|
+
case "ES512": return {
|
|
2358
|
+
hash,
|
|
2359
|
+
name: "ECDSA",
|
|
2360
|
+
namedCurve: algorithm.namedCurve
|
|
2361
|
+
};
|
|
2362
|
+
case "Ed25519":
|
|
2363
|
+
case "EdDSA": return { name: "Ed25519" };
|
|
2364
|
+
case "ML-DSA-44":
|
|
2365
|
+
case "ML-DSA-65":
|
|
2366
|
+
case "ML-DSA-87": return { name: alg };
|
|
2367
|
+
default: throw new JOSENotSupported(`alg ${alg} is not supported either by JOSE or your javascript runtime`);
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
async function getSigKey(alg, key, usage) {
|
|
2371
|
+
if (key instanceof Uint8Array) {
|
|
2372
|
+
if (!alg.startsWith("HS")) throw new TypeError(invalidKeyInput(key, "CryptoKey", "KeyObject", "JSON Web Key"));
|
|
2373
|
+
return crypto.subtle.importKey("raw", key, {
|
|
2374
|
+
hash: `SHA-${alg.slice(-3)}`,
|
|
2375
|
+
name: "HMAC"
|
|
2376
|
+
}, false, [usage]);
|
|
2377
|
+
}
|
|
2378
|
+
checkSigCryptoKey(key, alg, usage);
|
|
2379
|
+
return key;
|
|
2380
|
+
}
|
|
2381
|
+
async function sign(alg, key, data) {
|
|
2382
|
+
const cryptoKey = await getSigKey(alg, key, "sign");
|
|
2383
|
+
checkKeyLength(alg, cryptoKey);
|
|
2384
|
+
const signature = await crypto.subtle.sign(subtleAlgorithm(alg, cryptoKey.algorithm), cryptoKey, data);
|
|
2385
|
+
return new Uint8Array(signature);
|
|
2386
|
+
}
|
|
2387
|
+
async function verify(alg, key, signature, data) {
|
|
2388
|
+
const cryptoKey = await getSigKey(alg, key, "verify");
|
|
2389
|
+
checkKeyLength(alg, cryptoKey);
|
|
2390
|
+
const algorithm = subtleAlgorithm(alg, cryptoKey.algorithm);
|
|
2391
|
+
try {
|
|
2392
|
+
return await crypto.subtle.verify(algorithm, cryptoKey, signature, data);
|
|
2393
|
+
} catch {
|
|
2394
|
+
return false;
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
//#endregion
|
|
2398
|
+
//#region ../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/lib/jwk_to_key.js
|
|
2399
|
+
var unsupportedAlg = "Invalid or unsupported JWK \"alg\" (Algorithm) Parameter value";
|
|
2400
|
+
function subtleMapping(jwk) {
|
|
2401
|
+
let algorithm;
|
|
2402
|
+
let keyUsages;
|
|
2403
|
+
switch (jwk.kty) {
|
|
2404
|
+
case "AKP":
|
|
2405
|
+
switch (jwk.alg) {
|
|
2406
|
+
case "ML-DSA-44":
|
|
2407
|
+
case "ML-DSA-65":
|
|
2408
|
+
case "ML-DSA-87":
|
|
2409
|
+
algorithm = { name: jwk.alg };
|
|
2410
|
+
keyUsages = jwk.priv ? ["sign"] : ["verify"];
|
|
2411
|
+
break;
|
|
2412
|
+
default: throw new JOSENotSupported(unsupportedAlg);
|
|
2413
|
+
}
|
|
2414
|
+
break;
|
|
2415
|
+
case "RSA":
|
|
2416
|
+
switch (jwk.alg) {
|
|
2417
|
+
case "PS256":
|
|
2418
|
+
case "PS384":
|
|
2419
|
+
case "PS512":
|
|
2420
|
+
algorithm = {
|
|
2421
|
+
name: "RSA-PSS",
|
|
2422
|
+
hash: `SHA-${jwk.alg.slice(-3)}`
|
|
2423
|
+
};
|
|
2424
|
+
keyUsages = jwk.d ? ["sign"] : ["verify"];
|
|
2425
|
+
break;
|
|
2426
|
+
case "RS256":
|
|
2427
|
+
case "RS384":
|
|
2428
|
+
case "RS512":
|
|
2429
|
+
algorithm = {
|
|
2430
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
2431
|
+
hash: `SHA-${jwk.alg.slice(-3)}`
|
|
2432
|
+
};
|
|
2433
|
+
keyUsages = jwk.d ? ["sign"] : ["verify"];
|
|
2434
|
+
break;
|
|
2435
|
+
case "RSA-OAEP":
|
|
2436
|
+
case "RSA-OAEP-256":
|
|
2437
|
+
case "RSA-OAEP-384":
|
|
2438
|
+
case "RSA-OAEP-512":
|
|
2439
|
+
algorithm = {
|
|
2440
|
+
name: "RSA-OAEP",
|
|
2441
|
+
hash: `SHA-${parseInt(jwk.alg.slice(-3), 10) || 1}`
|
|
2442
|
+
};
|
|
2443
|
+
keyUsages = jwk.d ? ["decrypt", "unwrapKey"] : ["encrypt", "wrapKey"];
|
|
2444
|
+
break;
|
|
2445
|
+
default: throw new JOSENotSupported(unsupportedAlg);
|
|
2446
|
+
}
|
|
2447
|
+
break;
|
|
2448
|
+
case "EC":
|
|
2449
|
+
switch (jwk.alg) {
|
|
2450
|
+
case "ES256":
|
|
2451
|
+
case "ES384":
|
|
2452
|
+
case "ES512":
|
|
2453
|
+
algorithm = {
|
|
2454
|
+
name: "ECDSA",
|
|
2455
|
+
namedCurve: {
|
|
2456
|
+
ES256: "P-256",
|
|
2457
|
+
ES384: "P-384",
|
|
2458
|
+
ES512: "P-521"
|
|
2459
|
+
}[jwk.alg]
|
|
2460
|
+
};
|
|
2461
|
+
keyUsages = jwk.d ? ["sign"] : ["verify"];
|
|
2462
|
+
break;
|
|
2463
|
+
case "ECDH-ES":
|
|
2464
|
+
case "ECDH-ES+A128KW":
|
|
2465
|
+
case "ECDH-ES+A192KW":
|
|
2466
|
+
case "ECDH-ES+A256KW":
|
|
2467
|
+
algorithm = {
|
|
2468
|
+
name: "ECDH",
|
|
2469
|
+
namedCurve: jwk.crv
|
|
2470
|
+
};
|
|
2471
|
+
keyUsages = jwk.d ? ["deriveBits"] : [];
|
|
2472
|
+
break;
|
|
2473
|
+
default: throw new JOSENotSupported(unsupportedAlg);
|
|
2474
|
+
}
|
|
2475
|
+
break;
|
|
2476
|
+
case "OKP":
|
|
2477
|
+
switch (jwk.alg) {
|
|
2478
|
+
case "Ed25519":
|
|
2479
|
+
case "EdDSA":
|
|
2480
|
+
algorithm = { name: "Ed25519" };
|
|
2481
|
+
keyUsages = jwk.d ? ["sign"] : ["verify"];
|
|
2482
|
+
break;
|
|
2483
|
+
case "ECDH-ES":
|
|
2484
|
+
case "ECDH-ES+A128KW":
|
|
2485
|
+
case "ECDH-ES+A192KW":
|
|
2486
|
+
case "ECDH-ES+A256KW":
|
|
2487
|
+
algorithm = { name: jwk.crv };
|
|
2488
|
+
keyUsages = jwk.d ? ["deriveBits"] : [];
|
|
2489
|
+
break;
|
|
2490
|
+
default: throw new JOSENotSupported(unsupportedAlg);
|
|
2491
|
+
}
|
|
2492
|
+
break;
|
|
2493
|
+
default: throw new JOSENotSupported("Invalid or unsupported JWK \"kty\" (Key Type) Parameter value");
|
|
2494
|
+
}
|
|
2495
|
+
return {
|
|
2496
|
+
algorithm,
|
|
2497
|
+
keyUsages
|
|
2498
|
+
};
|
|
2499
|
+
}
|
|
2500
|
+
async function jwkToKey(jwk) {
|
|
2501
|
+
if (!jwk.alg) throw new TypeError("\"alg\" argument is required when \"jwk.alg\" is not present");
|
|
2502
|
+
const { algorithm, keyUsages } = subtleMapping(jwk);
|
|
2503
|
+
const keyData = { ...jwk };
|
|
2504
|
+
if (keyData.kty !== "AKP") delete keyData.alg;
|
|
2505
|
+
delete keyData.use;
|
|
2506
|
+
return crypto.subtle.importKey("jwk", keyData, algorithm, jwk.ext ?? (jwk.d || jwk.priv ? false : true), jwk.key_ops ?? keyUsages);
|
|
2507
|
+
}
|
|
2508
|
+
//#endregion
|
|
2509
|
+
//#region ../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/lib/normalize_key.js
|
|
2510
|
+
var unusableForAlg = "given KeyObject instance cannot be used for this algorithm";
|
|
2511
|
+
var cache;
|
|
2512
|
+
var handleJWK = async (key, jwk, alg, freeze = false) => {
|
|
2513
|
+
cache ||= /* @__PURE__ */ new WeakMap();
|
|
2514
|
+
let cached = cache.get(key);
|
|
2515
|
+
if (cached?.[alg]) return cached[alg];
|
|
2516
|
+
const cryptoKey = await jwkToKey({
|
|
2517
|
+
...jwk,
|
|
2518
|
+
alg
|
|
2519
|
+
});
|
|
2520
|
+
if (freeze) Object.freeze(key);
|
|
2521
|
+
if (!cached) cache.set(key, { [alg]: cryptoKey });
|
|
2522
|
+
else cached[alg] = cryptoKey;
|
|
2523
|
+
return cryptoKey;
|
|
2524
|
+
};
|
|
2525
|
+
var handleKeyObject = (keyObject, alg) => {
|
|
2526
|
+
cache ||= /* @__PURE__ */ new WeakMap();
|
|
2527
|
+
let cached = cache.get(keyObject);
|
|
2528
|
+
if (cached?.[alg]) return cached[alg];
|
|
2529
|
+
const isPublic = keyObject.type === "public";
|
|
2530
|
+
const extractable = isPublic ? true : false;
|
|
2531
|
+
let cryptoKey;
|
|
2532
|
+
if (keyObject.asymmetricKeyType === "x25519") {
|
|
2533
|
+
switch (alg) {
|
|
2534
|
+
case "ECDH-ES":
|
|
2535
|
+
case "ECDH-ES+A128KW":
|
|
2536
|
+
case "ECDH-ES+A192KW":
|
|
2537
|
+
case "ECDH-ES+A256KW": break;
|
|
2538
|
+
default: throw new TypeError(unusableForAlg);
|
|
2539
|
+
}
|
|
2540
|
+
cryptoKey = keyObject.toCryptoKey(keyObject.asymmetricKeyType, extractable, isPublic ? [] : ["deriveBits"]);
|
|
2541
|
+
}
|
|
2542
|
+
if (keyObject.asymmetricKeyType === "ed25519") {
|
|
2543
|
+
if (alg !== "EdDSA" && alg !== "Ed25519") throw new TypeError(unusableForAlg);
|
|
2544
|
+
cryptoKey = keyObject.toCryptoKey(keyObject.asymmetricKeyType, extractable, [isPublic ? "verify" : "sign"]);
|
|
2545
|
+
}
|
|
2546
|
+
switch (keyObject.asymmetricKeyType) {
|
|
2547
|
+
case "ml-dsa-44":
|
|
2548
|
+
case "ml-dsa-65":
|
|
2549
|
+
case "ml-dsa-87":
|
|
2550
|
+
if (alg !== keyObject.asymmetricKeyType.toUpperCase()) throw new TypeError(unusableForAlg);
|
|
2551
|
+
cryptoKey = keyObject.toCryptoKey(keyObject.asymmetricKeyType, extractable, [isPublic ? "verify" : "sign"]);
|
|
2552
|
+
}
|
|
2553
|
+
if (keyObject.asymmetricKeyType === "rsa") {
|
|
2554
|
+
let hash;
|
|
2555
|
+
switch (alg) {
|
|
2556
|
+
case "RSA-OAEP":
|
|
2557
|
+
hash = "SHA-1";
|
|
2558
|
+
break;
|
|
2559
|
+
case "RS256":
|
|
2560
|
+
case "PS256":
|
|
2561
|
+
case "RSA-OAEP-256":
|
|
2562
|
+
hash = "SHA-256";
|
|
2563
|
+
break;
|
|
2564
|
+
case "RS384":
|
|
2565
|
+
case "PS384":
|
|
2566
|
+
case "RSA-OAEP-384":
|
|
2567
|
+
hash = "SHA-384";
|
|
2568
|
+
break;
|
|
2569
|
+
case "RS512":
|
|
2570
|
+
case "PS512":
|
|
2571
|
+
case "RSA-OAEP-512":
|
|
2572
|
+
hash = "SHA-512";
|
|
2573
|
+
break;
|
|
2574
|
+
default: throw new TypeError(unusableForAlg);
|
|
2575
|
+
}
|
|
2576
|
+
if (alg.startsWith("RSA-OAEP")) return keyObject.toCryptoKey({
|
|
2577
|
+
name: "RSA-OAEP",
|
|
2578
|
+
hash
|
|
2579
|
+
}, extractable, isPublic ? ["encrypt"] : ["decrypt"]);
|
|
2580
|
+
cryptoKey = keyObject.toCryptoKey({
|
|
2581
|
+
name: alg.startsWith("PS") ? "RSA-PSS" : "RSASSA-PKCS1-v1_5",
|
|
2582
|
+
hash
|
|
2583
|
+
}, extractable, [isPublic ? "verify" : "sign"]);
|
|
2584
|
+
}
|
|
2585
|
+
if (keyObject.asymmetricKeyType === "ec") {
|
|
2586
|
+
const namedCurve = (/* @__PURE__ */ new Map([
|
|
2587
|
+
["prime256v1", "P-256"],
|
|
2588
|
+
["secp384r1", "P-384"],
|
|
2589
|
+
["secp521r1", "P-521"]
|
|
2590
|
+
])).get(keyObject.asymmetricKeyDetails?.namedCurve);
|
|
2591
|
+
if (!namedCurve) throw new TypeError(unusableForAlg);
|
|
2592
|
+
const expectedCurve = {
|
|
2593
|
+
ES256: "P-256",
|
|
2594
|
+
ES384: "P-384",
|
|
2595
|
+
ES512: "P-521"
|
|
2596
|
+
};
|
|
2597
|
+
if (expectedCurve[alg] && namedCurve === expectedCurve[alg]) cryptoKey = keyObject.toCryptoKey({
|
|
2598
|
+
name: "ECDSA",
|
|
2599
|
+
namedCurve
|
|
2600
|
+
}, extractable, [isPublic ? "verify" : "sign"]);
|
|
2601
|
+
if (alg.startsWith("ECDH-ES")) cryptoKey = keyObject.toCryptoKey({
|
|
2602
|
+
name: "ECDH",
|
|
2603
|
+
namedCurve
|
|
2604
|
+
}, extractable, isPublic ? [] : ["deriveBits"]);
|
|
2605
|
+
}
|
|
2606
|
+
if (!cryptoKey) throw new TypeError(unusableForAlg);
|
|
2607
|
+
if (!cached) cache.set(keyObject, { [alg]: cryptoKey });
|
|
2608
|
+
else cached[alg] = cryptoKey;
|
|
2609
|
+
return cryptoKey;
|
|
2610
|
+
};
|
|
2611
|
+
async function normalizeKey(key, alg) {
|
|
2612
|
+
if (key instanceof Uint8Array) return key;
|
|
2613
|
+
if (isCryptoKey(key)) return key;
|
|
2614
|
+
if (isKeyObject(key)) {
|
|
2615
|
+
if (key.type === "secret") return key.export();
|
|
2616
|
+
if ("toCryptoKey" in key && typeof key.toCryptoKey === "function") try {
|
|
2617
|
+
return handleKeyObject(key, alg);
|
|
2618
|
+
} catch (err) {
|
|
2619
|
+
if (err instanceof TypeError) throw err;
|
|
2620
|
+
}
|
|
2621
|
+
return handleJWK(key, key.export({ format: "jwk" }), alg);
|
|
2622
|
+
}
|
|
2623
|
+
if (isJWK(key)) {
|
|
2624
|
+
if (key.k) return decode(key.k);
|
|
2625
|
+
return handleJWK(key, key, alg, true);
|
|
2626
|
+
}
|
|
2627
|
+
throw new Error("unreachable");
|
|
2628
|
+
}
|
|
2629
|
+
//#endregion
|
|
2630
|
+
//#region ../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/key/import.js
|
|
2631
|
+
async function importJWK(jwk, alg, options) {
|
|
2632
|
+
if (!isObject(jwk)) throw new TypeError("JWK must be an object");
|
|
2633
|
+
let ext;
|
|
2634
|
+
alg ??= jwk.alg;
|
|
2635
|
+
ext ??= options?.extractable ?? jwk.ext;
|
|
2636
|
+
switch (jwk.kty) {
|
|
2637
|
+
case "oct":
|
|
2638
|
+
if (typeof jwk.k !== "string" || !jwk.k) throw new TypeError("missing \"k\" (Key Value) Parameter value");
|
|
2639
|
+
return decode(jwk.k);
|
|
2640
|
+
case "RSA":
|
|
2641
|
+
if ("oth" in jwk && jwk.oth !== void 0) throw new JOSENotSupported("RSA JWK \"oth\" (Other Primes Info) Parameter value is not supported");
|
|
2642
|
+
return jwkToKey({
|
|
2643
|
+
...jwk,
|
|
2644
|
+
alg,
|
|
2645
|
+
ext
|
|
2646
|
+
});
|
|
2647
|
+
case "AKP":
|
|
2648
|
+
if (typeof jwk.alg !== "string" || !jwk.alg) throw new TypeError("missing \"alg\" (Algorithm) Parameter value");
|
|
2649
|
+
if (alg !== void 0 && alg !== jwk.alg) throw new TypeError("JWK alg and alg option value mismatch");
|
|
2650
|
+
return jwkToKey({
|
|
2651
|
+
...jwk,
|
|
2652
|
+
ext
|
|
2653
|
+
});
|
|
2654
|
+
case "EC":
|
|
2655
|
+
case "OKP": return jwkToKey({
|
|
2656
|
+
...jwk,
|
|
2657
|
+
alg,
|
|
2658
|
+
ext
|
|
2659
|
+
});
|
|
2660
|
+
default: throw new JOSENotSupported("Unsupported \"kty\" (Key Type) Parameter value");
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
//#endregion
|
|
2664
|
+
//#region ../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/lib/validate_crit.js
|
|
2665
|
+
function validateCrit(Err, recognizedDefault, recognizedOption, protectedHeader, joseHeader) {
|
|
2666
|
+
if (joseHeader.crit !== void 0 && protectedHeader?.crit === void 0) throw new Err("\"crit\" (Critical) Header Parameter MUST be integrity protected");
|
|
2667
|
+
if (!protectedHeader || protectedHeader.crit === void 0) return /* @__PURE__ */ new Set();
|
|
2668
|
+
if (!Array.isArray(protectedHeader.crit) || protectedHeader.crit.length === 0 || protectedHeader.crit.some((input) => typeof input !== "string" || input.length === 0)) throw new Err("\"crit\" (Critical) Header Parameter MUST be an array of non-empty strings when present");
|
|
2669
|
+
let recognized;
|
|
2670
|
+
if (recognizedOption !== void 0) recognized = new Map([...Object.entries(recognizedOption), ...recognizedDefault.entries()]);
|
|
2671
|
+
else recognized = recognizedDefault;
|
|
2672
|
+
for (const parameter of protectedHeader.crit) {
|
|
2673
|
+
if (!recognized.has(parameter)) throw new JOSENotSupported(`Extension Header Parameter "${parameter}" is not recognized`);
|
|
2674
|
+
if (joseHeader[parameter] === void 0) throw new Err(`Extension Header Parameter "${parameter}" is missing`);
|
|
2675
|
+
if (recognized.get(parameter) && protectedHeader[parameter] === void 0) throw new Err(`Extension Header Parameter "${parameter}" MUST be integrity protected`);
|
|
2676
|
+
}
|
|
2677
|
+
return new Set(protectedHeader.crit);
|
|
2678
|
+
}
|
|
2679
|
+
//#endregion
|
|
2680
|
+
//#region ../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/lib/validate_algorithms.js
|
|
2681
|
+
function validateAlgorithms(option, algorithms) {
|
|
2682
|
+
if (algorithms !== void 0 && (!Array.isArray(algorithms) || algorithms.some((s) => typeof s !== "string"))) throw new TypeError(`"${option}" option must be an array of strings`);
|
|
2683
|
+
if (!algorithms) return;
|
|
2684
|
+
return new Set(algorithms);
|
|
2685
|
+
}
|
|
2686
|
+
//#endregion
|
|
2687
|
+
//#region ../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/lib/check_key_type.js
|
|
2688
|
+
var tag = (key) => key?.[Symbol.toStringTag];
|
|
2689
|
+
var jwkMatchesOp = (alg, key, usage) => {
|
|
2690
|
+
if (key.use !== void 0) {
|
|
2691
|
+
let expected;
|
|
2692
|
+
switch (usage) {
|
|
2693
|
+
case "sign":
|
|
2694
|
+
case "verify":
|
|
2695
|
+
expected = "sig";
|
|
2696
|
+
break;
|
|
2697
|
+
case "encrypt":
|
|
2698
|
+
case "decrypt":
|
|
2699
|
+
expected = "enc";
|
|
2700
|
+
break;
|
|
2701
|
+
}
|
|
2702
|
+
if (key.use !== expected) throw new TypeError(`Invalid key for this operation, its "use" must be "${expected}" when present`);
|
|
2703
|
+
}
|
|
2704
|
+
if (key.alg !== void 0 && key.alg !== alg) throw new TypeError(`Invalid key for this operation, its "alg" must be "${alg}" when present`);
|
|
2705
|
+
if (Array.isArray(key.key_ops)) {
|
|
2706
|
+
let expectedKeyOp;
|
|
2707
|
+
switch (true) {
|
|
2708
|
+
case usage === "sign" || usage === "verify":
|
|
2709
|
+
case alg === "dir":
|
|
2710
|
+
case alg.includes("CBC-HS"):
|
|
2711
|
+
expectedKeyOp = usage;
|
|
2712
|
+
break;
|
|
2713
|
+
case alg.startsWith("PBES2"):
|
|
2714
|
+
expectedKeyOp = "deriveBits";
|
|
2715
|
+
break;
|
|
2716
|
+
case /^A\d{3}(?:GCM)?(?:KW)?$/.test(alg):
|
|
2717
|
+
if (!alg.includes("GCM") && alg.endsWith("KW")) expectedKeyOp = usage === "encrypt" ? "wrapKey" : "unwrapKey";
|
|
2718
|
+
else expectedKeyOp = usage;
|
|
2719
|
+
break;
|
|
2720
|
+
case usage === "encrypt" && alg.startsWith("RSA"):
|
|
2721
|
+
expectedKeyOp = "wrapKey";
|
|
2722
|
+
break;
|
|
2723
|
+
case usage === "decrypt":
|
|
2724
|
+
expectedKeyOp = alg.startsWith("RSA") ? "unwrapKey" : "deriveBits";
|
|
2725
|
+
break;
|
|
2726
|
+
}
|
|
2727
|
+
if (expectedKeyOp && key.key_ops?.includes?.(expectedKeyOp) === false) throw new TypeError(`Invalid key for this operation, its "key_ops" must include "${expectedKeyOp}" when present`);
|
|
2728
|
+
}
|
|
2729
|
+
return true;
|
|
2730
|
+
};
|
|
2731
|
+
var symmetricTypeCheck = (alg, key, usage) => {
|
|
2732
|
+
if (key instanceof Uint8Array) return;
|
|
2733
|
+
if (isJWK(key)) {
|
|
2734
|
+
if (isSecretJWK(key) && jwkMatchesOp(alg, key, usage)) return;
|
|
2735
|
+
throw new TypeError(`JSON Web Key for symmetric algorithms must have JWK "kty" (Key Type) equal to "oct" and the JWK "k" (Key Value) present`);
|
|
2736
|
+
}
|
|
2737
|
+
if (!isKeyLike(key)) throw new TypeError(withAlg(alg, key, "CryptoKey", "KeyObject", "JSON Web Key", "Uint8Array"));
|
|
2738
|
+
if (key.type !== "secret") throw new TypeError(`${tag(key)} instances for symmetric algorithms must be of type "secret"`);
|
|
2739
|
+
};
|
|
2740
|
+
var asymmetricTypeCheck = (alg, key, usage) => {
|
|
2741
|
+
if (isJWK(key)) switch (usage) {
|
|
2742
|
+
case "decrypt":
|
|
2743
|
+
case "sign":
|
|
2744
|
+
if (isPrivateJWK(key) && jwkMatchesOp(alg, key, usage)) return;
|
|
2745
|
+
throw new TypeError(`JSON Web Key for this operation must be a private JWK`);
|
|
2746
|
+
case "encrypt":
|
|
2747
|
+
case "verify":
|
|
2748
|
+
if (isPublicJWK(key) && jwkMatchesOp(alg, key, usage)) return;
|
|
2749
|
+
throw new TypeError(`JSON Web Key for this operation must be a public JWK`);
|
|
2750
|
+
}
|
|
2751
|
+
if (!isKeyLike(key)) throw new TypeError(withAlg(alg, key, "CryptoKey", "KeyObject", "JSON Web Key"));
|
|
2752
|
+
if (key.type === "secret") throw new TypeError(`${tag(key)} instances for asymmetric algorithms must not be of type "secret"`);
|
|
2753
|
+
if (key.type === "public") switch (usage) {
|
|
2754
|
+
case "sign": throw new TypeError(`${tag(key)} instances for asymmetric algorithm signing must be of type "private"`);
|
|
2755
|
+
case "decrypt": throw new TypeError(`${tag(key)} instances for asymmetric algorithm decryption must be of type "private"`);
|
|
2756
|
+
}
|
|
2757
|
+
if (key.type === "private") switch (usage) {
|
|
2758
|
+
case "verify": throw new TypeError(`${tag(key)} instances for asymmetric algorithm verifying must be of type "public"`);
|
|
2759
|
+
case "encrypt": throw new TypeError(`${tag(key)} instances for asymmetric algorithm encryption must be of type "public"`);
|
|
2760
|
+
}
|
|
2761
|
+
};
|
|
2762
|
+
function checkKeyType(alg, key, usage) {
|
|
2763
|
+
switch (alg.substring(0, 2)) {
|
|
2764
|
+
case "A1":
|
|
2765
|
+
case "A2":
|
|
2766
|
+
case "di":
|
|
2767
|
+
case "HS":
|
|
2768
|
+
case "PB":
|
|
2769
|
+
symmetricTypeCheck(alg, key, usage);
|
|
2770
|
+
break;
|
|
2771
|
+
default: asymmetricTypeCheck(alg, key, usage);
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
//#endregion
|
|
2775
|
+
//#region ../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/jws/flattened/verify.js
|
|
2776
|
+
async function flattenedVerify(jws, key, options) {
|
|
2777
|
+
if (!isObject(jws)) throw new JWSInvalid("Flattened JWS must be an object");
|
|
2778
|
+
if (jws.protected === void 0 && jws.header === void 0) throw new JWSInvalid("Flattened JWS must have either of the \"protected\" or \"header\" members");
|
|
2779
|
+
if (jws.protected !== void 0 && typeof jws.protected !== "string") throw new JWSInvalid("JWS Protected Header incorrect type");
|
|
2780
|
+
if (jws.payload === void 0) throw new JWSInvalid("JWS Payload missing");
|
|
2781
|
+
if (typeof jws.signature !== "string") throw new JWSInvalid("JWS Signature missing or incorrect type");
|
|
2782
|
+
if (jws.header !== void 0 && !isObject(jws.header)) throw new JWSInvalid("JWS Unprotected Header incorrect type");
|
|
2783
|
+
let parsedProt = {};
|
|
2784
|
+
if (jws.protected) try {
|
|
2785
|
+
const protectedHeader = decode(jws.protected);
|
|
2786
|
+
parsedProt = JSON.parse(decoder.decode(protectedHeader));
|
|
2787
|
+
} catch {
|
|
2788
|
+
throw new JWSInvalid("JWS Protected Header is invalid");
|
|
2789
|
+
}
|
|
2790
|
+
if (!isDisjoint(parsedProt, jws.header)) throw new JWSInvalid("JWS Protected and JWS Unprotected Header Parameter names must be disjoint");
|
|
2791
|
+
const joseHeader = {
|
|
2792
|
+
...parsedProt,
|
|
2793
|
+
...jws.header
|
|
2794
|
+
};
|
|
2795
|
+
const extensions = validateCrit(JWSInvalid, /* @__PURE__ */ new Map([["b64", true]]), options?.crit, parsedProt, joseHeader);
|
|
2796
|
+
let b64 = true;
|
|
2797
|
+
if (extensions.has("b64")) {
|
|
2798
|
+
b64 = parsedProt.b64;
|
|
2799
|
+
if (typeof b64 !== "boolean") throw new JWSInvalid("The \"b64\" (base64url-encode payload) Header Parameter must be a boolean");
|
|
2800
|
+
}
|
|
2801
|
+
const { alg } = joseHeader;
|
|
2802
|
+
if (typeof alg !== "string" || !alg) throw new JWSInvalid("JWS \"alg\" (Algorithm) Header Parameter missing or invalid");
|
|
2803
|
+
const algorithms = options && validateAlgorithms("algorithms", options.algorithms);
|
|
2804
|
+
if (algorithms && !algorithms.has(alg)) throw new JOSEAlgNotAllowed("\"alg\" (Algorithm) Header Parameter value not allowed");
|
|
2805
|
+
if (b64) {
|
|
2806
|
+
if (typeof jws.payload !== "string") throw new JWSInvalid("JWS Payload must be a string");
|
|
2807
|
+
} else if (typeof jws.payload !== "string" && !(jws.payload instanceof Uint8Array)) throw new JWSInvalid("JWS Payload must be a string or an Uint8Array instance");
|
|
2808
|
+
let resolvedKey = false;
|
|
2809
|
+
if (typeof key === "function") {
|
|
2810
|
+
key = await key(parsedProt, jws);
|
|
2811
|
+
resolvedKey = true;
|
|
2812
|
+
}
|
|
2813
|
+
checkKeyType(alg, key, "verify");
|
|
2814
|
+
const data = concat(jws.protected !== void 0 ? encode$1(jws.protected) : /* @__PURE__ */ new Uint8Array(), encode$1("."), typeof jws.payload === "string" ? b64 ? encode$1(jws.payload) : encoder.encode(jws.payload) : jws.payload);
|
|
2815
|
+
const signature = decodeBase64url(jws.signature, "signature", JWSInvalid);
|
|
2816
|
+
const k = await normalizeKey(key, alg);
|
|
2817
|
+
if (!await verify(alg, k, signature, data)) throw new JWSSignatureVerificationFailed();
|
|
2818
|
+
let payload;
|
|
2819
|
+
if (b64) payload = decodeBase64url(jws.payload, "payload", JWSInvalid);
|
|
2820
|
+
else if (typeof jws.payload === "string") payload = encoder.encode(jws.payload);
|
|
2821
|
+
else payload = jws.payload;
|
|
2822
|
+
const result = { payload };
|
|
2823
|
+
if (jws.protected !== void 0) result.protectedHeader = parsedProt;
|
|
2824
|
+
if (jws.header !== void 0) result.unprotectedHeader = jws.header;
|
|
2825
|
+
if (resolvedKey) return {
|
|
2826
|
+
...result,
|
|
2827
|
+
key: k
|
|
2828
|
+
};
|
|
2829
|
+
return result;
|
|
2830
|
+
}
|
|
2831
|
+
//#endregion
|
|
2832
|
+
//#region ../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/jws/compact/verify.js
|
|
2833
|
+
async function compactVerify(jws, key, options) {
|
|
2834
|
+
if (jws instanceof Uint8Array) jws = decoder.decode(jws);
|
|
2835
|
+
if (typeof jws !== "string") throw new JWSInvalid("Compact JWS must be a string or Uint8Array");
|
|
2836
|
+
const { 0: protectedHeader, 1: payload, 2: signature, length } = jws.split(".");
|
|
2837
|
+
if (length !== 3) throw new JWSInvalid("Invalid Compact JWS");
|
|
2838
|
+
const verified = await flattenedVerify({
|
|
2839
|
+
payload,
|
|
2840
|
+
protected: protectedHeader,
|
|
2841
|
+
signature
|
|
2842
|
+
}, key, options);
|
|
2843
|
+
const result = {
|
|
2844
|
+
payload: verified.payload,
|
|
2845
|
+
protectedHeader: verified.protectedHeader
|
|
2846
|
+
};
|
|
2847
|
+
if (typeof key === "function") return {
|
|
2848
|
+
...result,
|
|
2849
|
+
key: verified.key
|
|
2850
|
+
};
|
|
2851
|
+
return result;
|
|
2852
|
+
}
|
|
2853
|
+
//#endregion
|
|
2854
|
+
//#region ../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/lib/jwt_claims_set.js
|
|
2855
|
+
var epoch = (date) => Math.floor(date.getTime() / 1e3);
|
|
2856
|
+
var minute = 60;
|
|
2857
|
+
var hour = minute * 60;
|
|
2858
|
+
var day = hour * 24;
|
|
2859
|
+
var week = day * 7;
|
|
2860
|
+
var year = day * 365.25;
|
|
2861
|
+
var REGEX = /^(\+|\-)? ?(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)(?: (ago|from now))?$/i;
|
|
2862
|
+
function secs(str) {
|
|
2863
|
+
const matched = REGEX.exec(str);
|
|
2864
|
+
if (!matched || matched[4] && matched[1]) throw new TypeError("Invalid time period format");
|
|
2865
|
+
const value = parseFloat(matched[2]);
|
|
2866
|
+
const unit = matched[3].toLowerCase();
|
|
2867
|
+
let numericDate;
|
|
2868
|
+
switch (unit) {
|
|
2869
|
+
case "sec":
|
|
2870
|
+
case "secs":
|
|
2871
|
+
case "second":
|
|
2872
|
+
case "seconds":
|
|
2873
|
+
case "s":
|
|
2874
|
+
numericDate = Math.round(value);
|
|
2875
|
+
break;
|
|
2876
|
+
case "minute":
|
|
2877
|
+
case "minutes":
|
|
2878
|
+
case "min":
|
|
2879
|
+
case "mins":
|
|
2880
|
+
case "m":
|
|
2881
|
+
numericDate = Math.round(value * minute);
|
|
2882
|
+
break;
|
|
2883
|
+
case "hour":
|
|
2884
|
+
case "hours":
|
|
2885
|
+
case "hr":
|
|
2886
|
+
case "hrs":
|
|
2887
|
+
case "h":
|
|
2888
|
+
numericDate = Math.round(value * hour);
|
|
2889
|
+
break;
|
|
2890
|
+
case "day":
|
|
2891
|
+
case "days":
|
|
2892
|
+
case "d":
|
|
2893
|
+
numericDate = Math.round(value * day);
|
|
2894
|
+
break;
|
|
2895
|
+
case "week":
|
|
2896
|
+
case "weeks":
|
|
2897
|
+
case "w":
|
|
2898
|
+
numericDate = Math.round(value * week);
|
|
2899
|
+
break;
|
|
2900
|
+
default:
|
|
2901
|
+
numericDate = Math.round(value * year);
|
|
2902
|
+
break;
|
|
2903
|
+
}
|
|
2904
|
+
if (matched[1] === "-" || matched[4] === "ago") return -numericDate;
|
|
2905
|
+
return numericDate;
|
|
2906
|
+
}
|
|
2907
|
+
function validateInput(label, input) {
|
|
2908
|
+
if (!Number.isFinite(input)) throw new TypeError(`Invalid ${label} input`);
|
|
2909
|
+
return input;
|
|
2910
|
+
}
|
|
2911
|
+
var normalizeTyp = (value) => {
|
|
2912
|
+
if (value.includes("/")) return value.toLowerCase();
|
|
2913
|
+
return `application/${value.toLowerCase()}`;
|
|
2914
|
+
};
|
|
2915
|
+
var checkAudiencePresence = (audPayload, audOption) => {
|
|
2916
|
+
if (typeof audPayload === "string") return audOption.includes(audPayload);
|
|
2917
|
+
if (Array.isArray(audPayload)) return audOption.some(Set.prototype.has.bind(new Set(audPayload)));
|
|
2918
|
+
return false;
|
|
2919
|
+
};
|
|
2920
|
+
function validateClaimsSet(protectedHeader, encodedPayload, options = {}) {
|
|
2921
|
+
let payload;
|
|
2922
|
+
try {
|
|
2923
|
+
payload = JSON.parse(decoder.decode(encodedPayload));
|
|
2924
|
+
} catch {}
|
|
2925
|
+
if (!isObject(payload)) throw new JWTInvalid("JWT Claims Set must be a top-level JSON object");
|
|
2926
|
+
const { typ } = options;
|
|
2927
|
+
if (typ && (typeof protectedHeader.typ !== "string" || normalizeTyp(protectedHeader.typ) !== normalizeTyp(typ))) throw new JWTClaimValidationFailed("unexpected \"typ\" JWT header value", payload, "typ", "check_failed");
|
|
2928
|
+
const { requiredClaims = [], issuer, subject, audience, maxTokenAge } = options;
|
|
2929
|
+
const presenceCheck = [...requiredClaims];
|
|
2930
|
+
if (maxTokenAge !== void 0) presenceCheck.push("iat");
|
|
2931
|
+
if (audience !== void 0) presenceCheck.push("aud");
|
|
2932
|
+
if (subject !== void 0) presenceCheck.push("sub");
|
|
2933
|
+
if (issuer !== void 0) presenceCheck.push("iss");
|
|
2934
|
+
for (const claim of new Set(presenceCheck.reverse())) if (!(claim in payload)) throw new JWTClaimValidationFailed(`missing required "${claim}" claim`, payload, claim, "missing");
|
|
2935
|
+
if (issuer && !(Array.isArray(issuer) ? issuer : [issuer]).includes(payload.iss)) throw new JWTClaimValidationFailed("unexpected \"iss\" claim value", payload, "iss", "check_failed");
|
|
2936
|
+
if (subject && payload.sub !== subject) throw new JWTClaimValidationFailed("unexpected \"sub\" claim value", payload, "sub", "check_failed");
|
|
2937
|
+
if (audience && !checkAudiencePresence(payload.aud, typeof audience === "string" ? [audience] : audience)) throw new JWTClaimValidationFailed("unexpected \"aud\" claim value", payload, "aud", "check_failed");
|
|
2938
|
+
let tolerance;
|
|
2939
|
+
switch (typeof options.clockTolerance) {
|
|
2940
|
+
case "string":
|
|
2941
|
+
tolerance = secs(options.clockTolerance);
|
|
2942
|
+
break;
|
|
2943
|
+
case "number":
|
|
2944
|
+
tolerance = options.clockTolerance;
|
|
2945
|
+
break;
|
|
2946
|
+
case "undefined":
|
|
2947
|
+
tolerance = 0;
|
|
2948
|
+
break;
|
|
2949
|
+
default: throw new TypeError("Invalid clockTolerance option type");
|
|
2950
|
+
}
|
|
2951
|
+
const { currentDate } = options;
|
|
2952
|
+
const now = epoch(currentDate || /* @__PURE__ */ new Date());
|
|
2953
|
+
if ((payload.iat !== void 0 || maxTokenAge) && typeof payload.iat !== "number") throw new JWTClaimValidationFailed("\"iat\" claim must be a number", payload, "iat", "invalid");
|
|
2954
|
+
if (payload.nbf !== void 0) {
|
|
2955
|
+
if (typeof payload.nbf !== "number") throw new JWTClaimValidationFailed("\"nbf\" claim must be a number", payload, "nbf", "invalid");
|
|
2956
|
+
if (payload.nbf > now + tolerance) throw new JWTClaimValidationFailed("\"nbf\" claim timestamp check failed", payload, "nbf", "check_failed");
|
|
2957
|
+
}
|
|
2958
|
+
if (payload.exp !== void 0) {
|
|
2959
|
+
if (typeof payload.exp !== "number") throw new JWTClaimValidationFailed("\"exp\" claim must be a number", payload, "exp", "invalid");
|
|
2960
|
+
if (payload.exp <= now - tolerance) throw new JWTExpired("\"exp\" claim timestamp check failed", payload, "exp", "check_failed");
|
|
2961
|
+
}
|
|
2962
|
+
if (maxTokenAge) {
|
|
2963
|
+
const age = now - payload.iat;
|
|
2964
|
+
const max = typeof maxTokenAge === "number" ? maxTokenAge : secs(maxTokenAge);
|
|
2965
|
+
if (age - tolerance > max) throw new JWTExpired("\"iat\" claim timestamp check failed (too far in the past)", payload, "iat", "check_failed");
|
|
2966
|
+
if (age < 0 - tolerance) throw new JWTClaimValidationFailed("\"iat\" claim timestamp check failed (it should be in the past)", payload, "iat", "check_failed");
|
|
2967
|
+
}
|
|
2968
|
+
return payload;
|
|
2969
|
+
}
|
|
2970
|
+
var JWTClaimsBuilder = class {
|
|
2971
|
+
#payload;
|
|
2972
|
+
constructor(payload) {
|
|
2973
|
+
if (!isObject(payload)) throw new TypeError("JWT Claims Set MUST be an object");
|
|
2974
|
+
this.#payload = structuredClone(payload);
|
|
2975
|
+
}
|
|
2976
|
+
data() {
|
|
2977
|
+
return encoder.encode(JSON.stringify(this.#payload));
|
|
2978
|
+
}
|
|
2979
|
+
get iss() {
|
|
2980
|
+
return this.#payload.iss;
|
|
2981
|
+
}
|
|
2982
|
+
set iss(value) {
|
|
2983
|
+
this.#payload.iss = value;
|
|
2984
|
+
}
|
|
2985
|
+
get sub() {
|
|
2986
|
+
return this.#payload.sub;
|
|
2987
|
+
}
|
|
2988
|
+
set sub(value) {
|
|
2989
|
+
this.#payload.sub = value;
|
|
2990
|
+
}
|
|
2991
|
+
get aud() {
|
|
2992
|
+
return this.#payload.aud;
|
|
2993
|
+
}
|
|
2994
|
+
set aud(value) {
|
|
2995
|
+
this.#payload.aud = value;
|
|
2996
|
+
}
|
|
2997
|
+
set jti(value) {
|
|
2998
|
+
this.#payload.jti = value;
|
|
2999
|
+
}
|
|
3000
|
+
set nbf(value) {
|
|
3001
|
+
if (typeof value === "number") this.#payload.nbf = validateInput("setNotBefore", value);
|
|
3002
|
+
else if (value instanceof Date) this.#payload.nbf = validateInput("setNotBefore", epoch(value));
|
|
3003
|
+
else this.#payload.nbf = epoch(/* @__PURE__ */ new Date()) + secs(value);
|
|
3004
|
+
}
|
|
3005
|
+
set exp(value) {
|
|
3006
|
+
if (typeof value === "number") this.#payload.exp = validateInput("setExpirationTime", value);
|
|
3007
|
+
else if (value instanceof Date) this.#payload.exp = validateInput("setExpirationTime", epoch(value));
|
|
3008
|
+
else this.#payload.exp = epoch(/* @__PURE__ */ new Date()) + secs(value);
|
|
3009
|
+
}
|
|
3010
|
+
set iat(value) {
|
|
3011
|
+
if (value === void 0) this.#payload.iat = epoch(/* @__PURE__ */ new Date());
|
|
3012
|
+
else if (value instanceof Date) this.#payload.iat = validateInput("setIssuedAt", epoch(value));
|
|
3013
|
+
else if (typeof value === "string") this.#payload.iat = validateInput("setIssuedAt", epoch(/* @__PURE__ */ new Date()) + secs(value));
|
|
3014
|
+
else this.#payload.iat = validateInput("setIssuedAt", value);
|
|
3015
|
+
}
|
|
3016
|
+
};
|
|
3017
|
+
//#endregion
|
|
3018
|
+
//#region ../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/jwt/verify.js
|
|
3019
|
+
async function jwtVerify(jwt, key, options) {
|
|
3020
|
+
const verified = await compactVerify(jwt, key, options);
|
|
3021
|
+
if (verified.protectedHeader.crit?.includes("b64") && verified.protectedHeader.b64 === false) throw new JWTInvalid("JWTs MUST NOT use unencoded payload");
|
|
3022
|
+
const result = {
|
|
3023
|
+
payload: validateClaimsSet(verified.protectedHeader, verified.payload, options),
|
|
3024
|
+
protectedHeader: verified.protectedHeader
|
|
3025
|
+
};
|
|
3026
|
+
if (typeof key === "function") return {
|
|
3027
|
+
...result,
|
|
3028
|
+
key: verified.key
|
|
3029
|
+
};
|
|
3030
|
+
return result;
|
|
3031
|
+
}
|
|
3032
|
+
//#endregion
|
|
3033
|
+
//#region ../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/jws/flattened/sign.js
|
|
3034
|
+
var FlattenedSign = class {
|
|
3035
|
+
#payload;
|
|
3036
|
+
#protectedHeader;
|
|
3037
|
+
#unprotectedHeader;
|
|
3038
|
+
constructor(payload) {
|
|
3039
|
+
if (!(payload instanceof Uint8Array)) throw new TypeError("payload must be an instance of Uint8Array");
|
|
3040
|
+
this.#payload = payload;
|
|
3041
|
+
}
|
|
3042
|
+
setProtectedHeader(protectedHeader) {
|
|
3043
|
+
assertNotSet(this.#protectedHeader, "setProtectedHeader");
|
|
3044
|
+
this.#protectedHeader = protectedHeader;
|
|
3045
|
+
return this;
|
|
3046
|
+
}
|
|
3047
|
+
setUnprotectedHeader(unprotectedHeader) {
|
|
3048
|
+
assertNotSet(this.#unprotectedHeader, "setUnprotectedHeader");
|
|
3049
|
+
this.#unprotectedHeader = unprotectedHeader;
|
|
3050
|
+
return this;
|
|
3051
|
+
}
|
|
3052
|
+
async sign(key, options) {
|
|
3053
|
+
if (!this.#protectedHeader && !this.#unprotectedHeader) throw new JWSInvalid("either setProtectedHeader or setUnprotectedHeader must be called before #sign()");
|
|
3054
|
+
if (!isDisjoint(this.#protectedHeader, this.#unprotectedHeader)) throw new JWSInvalid("JWS Protected and JWS Unprotected Header Parameter names must be disjoint");
|
|
3055
|
+
const joseHeader = {
|
|
3056
|
+
...this.#protectedHeader,
|
|
3057
|
+
...this.#unprotectedHeader
|
|
3058
|
+
};
|
|
3059
|
+
const extensions = validateCrit(JWSInvalid, /* @__PURE__ */ new Map([["b64", true]]), options?.crit, this.#protectedHeader, joseHeader);
|
|
3060
|
+
let b64 = true;
|
|
3061
|
+
if (extensions.has("b64")) {
|
|
3062
|
+
b64 = this.#protectedHeader.b64;
|
|
3063
|
+
if (typeof b64 !== "boolean") throw new JWSInvalid("The \"b64\" (base64url-encode payload) Header Parameter must be a boolean");
|
|
3064
|
+
}
|
|
3065
|
+
const { alg } = joseHeader;
|
|
3066
|
+
if (typeof alg !== "string" || !alg) throw new JWSInvalid("JWS \"alg\" (Algorithm) Header Parameter missing or invalid");
|
|
3067
|
+
checkKeyType(alg, key, "sign");
|
|
3068
|
+
let payloadS;
|
|
3069
|
+
let payloadB;
|
|
3070
|
+
if (b64) {
|
|
3071
|
+
payloadS = encode(this.#payload);
|
|
3072
|
+
payloadB = encode$1(payloadS);
|
|
3073
|
+
} else {
|
|
3074
|
+
payloadB = this.#payload;
|
|
3075
|
+
payloadS = "";
|
|
3076
|
+
}
|
|
3077
|
+
let protectedHeaderString;
|
|
3078
|
+
let protectedHeaderBytes;
|
|
3079
|
+
if (this.#protectedHeader) {
|
|
3080
|
+
protectedHeaderString = encode(JSON.stringify(this.#protectedHeader));
|
|
3081
|
+
protectedHeaderBytes = encode$1(protectedHeaderString);
|
|
3082
|
+
} else {
|
|
3083
|
+
protectedHeaderString = "";
|
|
3084
|
+
protectedHeaderBytes = /* @__PURE__ */ new Uint8Array();
|
|
3085
|
+
}
|
|
3086
|
+
const data = concat(protectedHeaderBytes, encode$1("."), payloadB);
|
|
3087
|
+
const jws = {
|
|
3088
|
+
signature: encode(await sign(alg, await normalizeKey(key, alg), data)),
|
|
3089
|
+
payload: payloadS
|
|
3090
|
+
};
|
|
3091
|
+
if (this.#unprotectedHeader) jws.header = this.#unprotectedHeader;
|
|
3092
|
+
if (this.#protectedHeader) jws.protected = protectedHeaderString;
|
|
3093
|
+
return jws;
|
|
3094
|
+
}
|
|
3095
|
+
};
|
|
3096
|
+
//#endregion
|
|
3097
|
+
//#region ../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/jws/compact/sign.js
|
|
3098
|
+
var CompactSign = class {
|
|
3099
|
+
#flattened;
|
|
3100
|
+
constructor(payload) {
|
|
3101
|
+
this.#flattened = new FlattenedSign(payload);
|
|
3102
|
+
}
|
|
3103
|
+
setProtectedHeader(protectedHeader) {
|
|
3104
|
+
this.#flattened.setProtectedHeader(protectedHeader);
|
|
3105
|
+
return this;
|
|
3106
|
+
}
|
|
3107
|
+
async sign(key, options) {
|
|
3108
|
+
const jws = await this.#flattened.sign(key, options);
|
|
3109
|
+
if (jws.payload === void 0) throw new TypeError("use the flattened module for creating JWS with b64: false");
|
|
3110
|
+
return `${jws.protected}.${jws.payload}.${jws.signature}`;
|
|
3111
|
+
}
|
|
3112
|
+
};
|
|
3113
|
+
//#endregion
|
|
3114
|
+
//#region ../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/jwt/sign.js
|
|
3115
|
+
var SignJWT = class {
|
|
3116
|
+
#protectedHeader;
|
|
3117
|
+
#jwt;
|
|
3118
|
+
constructor(payload = {}) {
|
|
3119
|
+
this.#jwt = new JWTClaimsBuilder(payload);
|
|
3120
|
+
}
|
|
3121
|
+
setIssuer(issuer) {
|
|
3122
|
+
this.#jwt.iss = issuer;
|
|
3123
|
+
return this;
|
|
3124
|
+
}
|
|
3125
|
+
setSubject(subject) {
|
|
3126
|
+
this.#jwt.sub = subject;
|
|
3127
|
+
return this;
|
|
3128
|
+
}
|
|
3129
|
+
setAudience(audience) {
|
|
3130
|
+
this.#jwt.aud = audience;
|
|
3131
|
+
return this;
|
|
3132
|
+
}
|
|
3133
|
+
setJti(jwtId) {
|
|
3134
|
+
this.#jwt.jti = jwtId;
|
|
3135
|
+
return this;
|
|
3136
|
+
}
|
|
3137
|
+
setNotBefore(input) {
|
|
3138
|
+
this.#jwt.nbf = input;
|
|
3139
|
+
return this;
|
|
3140
|
+
}
|
|
3141
|
+
setExpirationTime(input) {
|
|
3142
|
+
this.#jwt.exp = input;
|
|
3143
|
+
return this;
|
|
3144
|
+
}
|
|
3145
|
+
setIssuedAt(input) {
|
|
3146
|
+
this.#jwt.iat = input;
|
|
3147
|
+
return this;
|
|
3148
|
+
}
|
|
3149
|
+
setProtectedHeader(protectedHeader) {
|
|
3150
|
+
this.#protectedHeader = protectedHeader;
|
|
3151
|
+
return this;
|
|
3152
|
+
}
|
|
3153
|
+
async sign(key, options) {
|
|
3154
|
+
const sig = new CompactSign(this.#jwt.data());
|
|
3155
|
+
sig.setProtectedHeader(this.#protectedHeader);
|
|
3156
|
+
if (Array.isArray(this.#protectedHeader?.crit) && this.#protectedHeader.crit.includes("b64") && this.#protectedHeader.b64 === false) throw new JWTInvalid("JWTs MUST NOT use unencoded payload");
|
|
3157
|
+
return sig.sign(key, options);
|
|
3158
|
+
}
|
|
3159
|
+
};
|
|
3160
|
+
//#endregion
|
|
3161
|
+
//#region ../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/jwks/local.js
|
|
3162
|
+
function getKtyFromAlg(alg) {
|
|
3163
|
+
switch (typeof alg === "string" && alg.slice(0, 2)) {
|
|
3164
|
+
case "RS":
|
|
3165
|
+
case "PS": return "RSA";
|
|
3166
|
+
case "ES": return "EC";
|
|
3167
|
+
case "Ed": return "OKP";
|
|
3168
|
+
case "ML": return "AKP";
|
|
3169
|
+
default: throw new JOSENotSupported("Unsupported \"alg\" value for a JSON Web Key Set");
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
function isJWKSLike(jwks) {
|
|
3173
|
+
return jwks && typeof jwks === "object" && Array.isArray(jwks.keys) && jwks.keys.every(isJWKLike);
|
|
3174
|
+
}
|
|
3175
|
+
function isJWKLike(key) {
|
|
3176
|
+
return isObject(key);
|
|
3177
|
+
}
|
|
3178
|
+
var LocalJWKSet = class {
|
|
3179
|
+
#jwks;
|
|
3180
|
+
#cached = /* @__PURE__ */ new WeakMap();
|
|
3181
|
+
constructor(jwks) {
|
|
3182
|
+
if (!isJWKSLike(jwks)) throw new JWKSInvalid("JSON Web Key Set malformed");
|
|
3183
|
+
this.#jwks = structuredClone(jwks);
|
|
3184
|
+
}
|
|
3185
|
+
jwks() {
|
|
3186
|
+
return this.#jwks;
|
|
3187
|
+
}
|
|
3188
|
+
async getKey(protectedHeader, token) {
|
|
3189
|
+
const { alg, kid } = {
|
|
3190
|
+
...protectedHeader,
|
|
3191
|
+
...token?.header
|
|
3192
|
+
};
|
|
3193
|
+
const kty = getKtyFromAlg(alg);
|
|
3194
|
+
const candidates = this.#jwks.keys.filter((jwk) => {
|
|
3195
|
+
let candidate = kty === jwk.kty;
|
|
3196
|
+
if (candidate && typeof kid === "string") candidate = kid === jwk.kid;
|
|
3197
|
+
if (candidate && (typeof jwk.alg === "string" || kty === "AKP")) candidate = alg === jwk.alg;
|
|
3198
|
+
if (candidate && typeof jwk.use === "string") candidate = jwk.use === "sig";
|
|
3199
|
+
if (candidate && Array.isArray(jwk.key_ops)) candidate = jwk.key_ops.includes("verify");
|
|
3200
|
+
if (candidate) switch (alg) {
|
|
3201
|
+
case "ES256":
|
|
3202
|
+
candidate = jwk.crv === "P-256";
|
|
3203
|
+
break;
|
|
3204
|
+
case "ES384":
|
|
3205
|
+
candidate = jwk.crv === "P-384";
|
|
3206
|
+
break;
|
|
3207
|
+
case "ES512":
|
|
3208
|
+
candidate = jwk.crv === "P-521";
|
|
3209
|
+
break;
|
|
3210
|
+
case "Ed25519":
|
|
3211
|
+
case "EdDSA":
|
|
3212
|
+
candidate = jwk.crv === "Ed25519";
|
|
3213
|
+
break;
|
|
3214
|
+
}
|
|
3215
|
+
return candidate;
|
|
3216
|
+
});
|
|
3217
|
+
const { 0: jwk, length } = candidates;
|
|
3218
|
+
if (length === 0) throw new JWKSNoMatchingKey();
|
|
3219
|
+
if (length !== 1) {
|
|
3220
|
+
const error = new JWKSMultipleMatchingKeys();
|
|
3221
|
+
const _cached = this.#cached;
|
|
3222
|
+
error[Symbol.asyncIterator] = async function* () {
|
|
3223
|
+
for (const jwk of candidates) try {
|
|
3224
|
+
yield await importWithAlgCache(_cached, jwk, alg);
|
|
3225
|
+
} catch {}
|
|
3226
|
+
};
|
|
3227
|
+
throw error;
|
|
3228
|
+
}
|
|
3229
|
+
return importWithAlgCache(this.#cached, jwk, alg);
|
|
3230
|
+
}
|
|
3231
|
+
};
|
|
3232
|
+
async function importWithAlgCache(cache, jwk, alg) {
|
|
3233
|
+
const cached = cache.get(jwk) || cache.set(jwk, {}).get(jwk);
|
|
3234
|
+
if (cached[alg] === void 0) {
|
|
3235
|
+
const key = await importJWK({
|
|
3236
|
+
...jwk,
|
|
3237
|
+
ext: true
|
|
3238
|
+
}, alg);
|
|
3239
|
+
if (key instanceof Uint8Array || key.type !== "public") throw new JWKSInvalid("JSON Web Key Set members must be public keys");
|
|
3240
|
+
cached[alg] = key;
|
|
3241
|
+
}
|
|
3242
|
+
return cached[alg];
|
|
3243
|
+
}
|
|
3244
|
+
function createLocalJWKSet(jwks) {
|
|
3245
|
+
const set = new LocalJWKSet(jwks);
|
|
3246
|
+
const localJWKSet = async (protectedHeader, token) => set.getKey(protectedHeader, token);
|
|
3247
|
+
Object.defineProperties(localJWKSet, { jwks: {
|
|
3248
|
+
value: () => structuredClone(set.jwks()),
|
|
3249
|
+
enumerable: false,
|
|
3250
|
+
configurable: false,
|
|
3251
|
+
writable: false
|
|
3252
|
+
} });
|
|
3253
|
+
return localJWKSet;
|
|
3254
|
+
}
|
|
3255
|
+
//#endregion
|
|
3256
|
+
//#region ../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/jwks/remote.js
|
|
3257
|
+
function isCloudflareWorkers() {
|
|
3258
|
+
return typeof WebSocketPair !== "undefined" || typeof navigator !== "undefined" && navigator.userAgent === "Cloudflare-Workers" || typeof EdgeRuntime !== "undefined" && EdgeRuntime === "vercel";
|
|
3259
|
+
}
|
|
3260
|
+
var USER_AGENT;
|
|
3261
|
+
if (typeof navigator === "undefined" || !navigator.userAgent?.startsWith?.("Mozilla/5.0 ")) USER_AGENT = `jose/v6.2.3`;
|
|
3262
|
+
var customFetch = Symbol();
|
|
3263
|
+
async function fetchJwks(url, headers, signal, fetchImpl = fetch) {
|
|
3264
|
+
const response = await fetchImpl(url, {
|
|
3265
|
+
method: "GET",
|
|
3266
|
+
signal,
|
|
3267
|
+
redirect: "manual",
|
|
3268
|
+
headers
|
|
3269
|
+
}).catch((err) => {
|
|
3270
|
+
if (err.name === "TimeoutError") throw new JWKSTimeout();
|
|
3271
|
+
throw err;
|
|
3272
|
+
});
|
|
3273
|
+
if (response.status !== 200) throw new JOSEError("Expected 200 OK from the JSON Web Key Set HTTP response");
|
|
3274
|
+
try {
|
|
3275
|
+
return await response.json();
|
|
3276
|
+
} catch {
|
|
3277
|
+
throw new JOSEError("Failed to parse the JSON Web Key Set HTTP response as JSON");
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
var jwksCache = Symbol();
|
|
3281
|
+
function isFreshJwksCache(input, cacheMaxAge) {
|
|
3282
|
+
if (typeof input !== "object" || input === null) return false;
|
|
3283
|
+
if (!("uat" in input) || typeof input.uat !== "number" || Date.now() - input.uat >= cacheMaxAge) return false;
|
|
3284
|
+
if (!("jwks" in input) || !isObject(input.jwks) || !Array.isArray(input.jwks.keys) || !Array.prototype.every.call(input.jwks.keys, isObject)) return false;
|
|
3285
|
+
return true;
|
|
3286
|
+
}
|
|
3287
|
+
var RemoteJWKSet = class {
|
|
3288
|
+
#url;
|
|
3289
|
+
#timeoutDuration;
|
|
3290
|
+
#cooldownDuration;
|
|
3291
|
+
#cacheMaxAge;
|
|
3292
|
+
#jwksTimestamp;
|
|
3293
|
+
#pendingFetch;
|
|
3294
|
+
#headers;
|
|
3295
|
+
#customFetch;
|
|
3296
|
+
#local;
|
|
3297
|
+
#cache;
|
|
3298
|
+
constructor(url, options) {
|
|
3299
|
+
if (!(url instanceof URL)) throw new TypeError("url must be an instance of URL");
|
|
3300
|
+
this.#url = new URL(url.href);
|
|
3301
|
+
this.#timeoutDuration = typeof options?.timeoutDuration === "number" ? options?.timeoutDuration : 5e3;
|
|
3302
|
+
this.#cooldownDuration = typeof options?.cooldownDuration === "number" ? options?.cooldownDuration : 3e4;
|
|
3303
|
+
this.#cacheMaxAge = typeof options?.cacheMaxAge === "number" ? options?.cacheMaxAge : 6e5;
|
|
3304
|
+
this.#headers = new Headers(options?.headers);
|
|
3305
|
+
if (USER_AGENT && !this.#headers.has("User-Agent")) this.#headers.set("User-Agent", USER_AGENT);
|
|
3306
|
+
if (!this.#headers.has("accept")) {
|
|
3307
|
+
this.#headers.set("accept", "application/json");
|
|
3308
|
+
this.#headers.append("accept", "application/jwk-set+json");
|
|
3309
|
+
}
|
|
3310
|
+
this.#customFetch = options?.[customFetch];
|
|
3311
|
+
if (options?.[jwksCache] !== void 0) {
|
|
3312
|
+
this.#cache = options?.[jwksCache];
|
|
3313
|
+
if (isFreshJwksCache(options?.[jwksCache], this.#cacheMaxAge)) {
|
|
3314
|
+
this.#jwksTimestamp = this.#cache.uat;
|
|
3315
|
+
this.#local = createLocalJWKSet(this.#cache.jwks);
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
pendingFetch() {
|
|
3320
|
+
return !!this.#pendingFetch;
|
|
3321
|
+
}
|
|
3322
|
+
coolingDown() {
|
|
3323
|
+
return typeof this.#jwksTimestamp === "number" ? Date.now() < this.#jwksTimestamp + this.#cooldownDuration : false;
|
|
3324
|
+
}
|
|
3325
|
+
fresh() {
|
|
3326
|
+
return typeof this.#jwksTimestamp === "number" ? Date.now() < this.#jwksTimestamp + this.#cacheMaxAge : false;
|
|
3327
|
+
}
|
|
3328
|
+
jwks() {
|
|
3329
|
+
return this.#local?.jwks();
|
|
3330
|
+
}
|
|
3331
|
+
async getKey(protectedHeader, token) {
|
|
3332
|
+
if (!this.#local || !this.fresh()) await this.reload();
|
|
3333
|
+
try {
|
|
3334
|
+
return await this.#local(protectedHeader, token);
|
|
3335
|
+
} catch (err) {
|
|
3336
|
+
if (err instanceof JWKSNoMatchingKey) {
|
|
3337
|
+
if (this.coolingDown() === false) {
|
|
3338
|
+
await this.reload();
|
|
3339
|
+
return this.#local(protectedHeader, token);
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
throw err;
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
async reload() {
|
|
3346
|
+
if (this.#pendingFetch && isCloudflareWorkers()) this.#pendingFetch = void 0;
|
|
3347
|
+
this.#pendingFetch ||= fetchJwks(this.#url.href, this.#headers, AbortSignal.timeout(this.#timeoutDuration), this.#customFetch).then((json) => {
|
|
3348
|
+
this.#local = createLocalJWKSet(json);
|
|
3349
|
+
if (this.#cache) {
|
|
3350
|
+
this.#cache.uat = Date.now();
|
|
3351
|
+
this.#cache.jwks = json;
|
|
3352
|
+
}
|
|
3353
|
+
this.#jwksTimestamp = Date.now();
|
|
3354
|
+
this.#pendingFetch = void 0;
|
|
3355
|
+
}).catch((err) => {
|
|
3356
|
+
this.#pendingFetch = void 0;
|
|
3357
|
+
throw err;
|
|
3358
|
+
});
|
|
3359
|
+
await this.#pendingFetch;
|
|
3360
|
+
}
|
|
3361
|
+
};
|
|
3362
|
+
function createRemoteJWKSet(url, options) {
|
|
3363
|
+
const set = new RemoteJWKSet(url, options);
|
|
3364
|
+
const remoteJWKSet = async (protectedHeader, token) => set.getKey(protectedHeader, token);
|
|
3365
|
+
Object.defineProperties(remoteJWKSet, {
|
|
3366
|
+
coolingDown: {
|
|
3367
|
+
get: () => set.coolingDown(),
|
|
3368
|
+
enumerable: true,
|
|
3369
|
+
configurable: false
|
|
3370
|
+
},
|
|
3371
|
+
fresh: {
|
|
3372
|
+
get: () => set.fresh(),
|
|
3373
|
+
enumerable: true,
|
|
3374
|
+
configurable: false
|
|
3375
|
+
},
|
|
3376
|
+
reload: {
|
|
3377
|
+
value: () => set.reload(),
|
|
3378
|
+
enumerable: true,
|
|
3379
|
+
configurable: false,
|
|
3380
|
+
writable: false
|
|
3381
|
+
},
|
|
3382
|
+
reloading: {
|
|
3383
|
+
get: () => set.pendingFetch(),
|
|
3384
|
+
enumerable: true,
|
|
3385
|
+
configurable: false
|
|
3386
|
+
},
|
|
3387
|
+
jwks: {
|
|
3388
|
+
value: () => set.jwks(),
|
|
3389
|
+
enumerable: true,
|
|
3390
|
+
configurable: false,
|
|
3391
|
+
writable: false
|
|
3392
|
+
}
|
|
3393
|
+
});
|
|
3394
|
+
return remoteJWKSet;
|
|
3395
|
+
}
|
|
3396
|
+
//#endregion
|
|
3397
|
+
//#region ../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/index.js
|
|
3398
|
+
var webapi_exports = /* @__PURE__ */ __exportAll({
|
|
3399
|
+
SignJWT: () => SignJWT,
|
|
3400
|
+
cryptoRuntime: () => cryptoRuntime,
|
|
3401
|
+
errors: () => errors_exports,
|
|
3402
|
+
jwtVerify: () => jwtVerify
|
|
3403
|
+
});
|
|
3404
|
+
//#endregion
|
|
3405
|
+
//#region src/services/OidcLoginService.ts
|
|
3406
|
+
var DEFAULT_SCOPES = [
|
|
3407
|
+
"openid",
|
|
3408
|
+
"profile",
|
|
3409
|
+
"email"
|
|
3410
|
+
];
|
|
3411
|
+
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 15;
|
|
3412
|
+
var DEFAULT_METADATA_CACHE_TTL_MS = 300 * 1e3;
|
|
3413
|
+
var WELL_KNOWN_PATH = "/.well-known/openid-configuration";
|
|
3414
|
+
var OidcLoginError = class extends Error {
|
|
3415
|
+
constructor(message) {
|
|
3416
|
+
super(message);
|
|
3417
|
+
this.name = "OidcLoginError";
|
|
3418
|
+
}
|
|
3419
|
+
};
|
|
3420
|
+
function getUsersOidcConfig() {
|
|
3421
|
+
return getPackageConfig("users", {}).auth?.oidc ?? {};
|
|
3422
|
+
}
|
|
3423
|
+
function normalizeIssuer(issuer) {
|
|
3424
|
+
return issuer.replace(/\/+$/u, "");
|
|
3425
|
+
}
|
|
3426
|
+
function resolveDiscoveryUrl(provider) {
|
|
3427
|
+
if (provider.discoveryUrl) return provider.discoveryUrl;
|
|
3428
|
+
return `${normalizeIssuer(provider.issuer)}${WELL_KNOWN_PATH}`;
|
|
3429
|
+
}
|
|
3430
|
+
function bytesToBase64Url(bytes) {
|
|
3431
|
+
let binary = "";
|
|
3432
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
3433
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
|
|
3434
|
+
}
|
|
3435
|
+
function base64UrlToBytes(value) {
|
|
3436
|
+
const normalized = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
3437
|
+
const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, "=");
|
|
3438
|
+
const binary = atob(padded);
|
|
3439
|
+
const bytes = new Uint8Array(binary.length);
|
|
3440
|
+
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
|
|
3441
|
+
return bytes;
|
|
3442
|
+
}
|
|
3443
|
+
function randomBase64Url(byteLength = 32) {
|
|
3444
|
+
const bytes = new Uint8Array(byteLength);
|
|
3445
|
+
crypto.getRandomValues(bytes);
|
|
3446
|
+
return bytesToBase64Url(bytes);
|
|
3447
|
+
}
|
|
3448
|
+
async function pkceChallenge(codeVerifier) {
|
|
3449
|
+
const bytes = new TextEncoder().encode(codeVerifier);
|
|
3450
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
3451
|
+
return bytesToBase64Url(new Uint8Array(digest));
|
|
3452
|
+
}
|
|
3453
|
+
function bytesToBase64(bytes) {
|
|
3454
|
+
let binary = "";
|
|
3455
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
3456
|
+
return btoa(binary);
|
|
3457
|
+
}
|
|
3458
|
+
function formUrlEncode(value) {
|
|
3459
|
+
const params = new URLSearchParams();
|
|
3460
|
+
params.set("value", value);
|
|
3461
|
+
return params.toString().slice(6);
|
|
3462
|
+
}
|
|
3463
|
+
function encodeBasicAuth(clientId, clientSecret) {
|
|
3464
|
+
const credentials = `${formUrlEncode(clientId)}:${formUrlEncode(clientSecret)}`;
|
|
3465
|
+
return bytesToBase64(new TextEncoder().encode(credentials));
|
|
3466
|
+
}
|
|
3467
|
+
function asString(value) {
|
|
3468
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
3469
|
+
}
|
|
3470
|
+
function asBoolean(value) {
|
|
3471
|
+
if (typeof value === "boolean") return value;
|
|
3472
|
+
if (value === "true") return true;
|
|
3473
|
+
if (value === "false") return false;
|
|
3474
|
+
}
|
|
3475
|
+
function ensureFetch(fetchOverride) {
|
|
3476
|
+
const runtimeFetch = fetchOverride ?? globalThis.fetch;
|
|
3477
|
+
if (!runtimeFetch) throw new OidcLoginError("OIDC login requires a fetch implementation.");
|
|
3478
|
+
return runtimeFetch;
|
|
3479
|
+
}
|
|
3480
|
+
function requireString(value, field) {
|
|
3481
|
+
if (typeof value !== "string" || value.length === 0) throw new OidcLoginError(`OIDC provider metadata missing "${field}".`);
|
|
3482
|
+
return value;
|
|
3483
|
+
}
|
|
3484
|
+
async function readJsonResponse(response, context) {
|
|
3485
|
+
try {
|
|
3486
|
+
return await response.json();
|
|
3487
|
+
} catch {
|
|
3488
|
+
throw new OidcLoginError(`${context} returned invalid JSON.`);
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
function normalizeClaims(payload, issuer, sourceName = "OIDC claims") {
|
|
3492
|
+
const sub = asString(payload.sub);
|
|
3493
|
+
if (!sub) throw new OidcLoginError(`${sourceName} missing required "sub" claim.`);
|
|
3494
|
+
return {
|
|
3495
|
+
sub,
|
|
3496
|
+
iss: asString(payload.iss) ?? issuer,
|
|
3497
|
+
email: asString(payload.email),
|
|
3498
|
+
email_verified: asBoolean(payload.email_verified),
|
|
3499
|
+
name: asString(payload.name),
|
|
3500
|
+
preferred_username: asString(payload.preferred_username)
|
|
3501
|
+
};
|
|
3502
|
+
}
|
|
3503
|
+
function mergeClaims(idTokenClaims, userInfoClaims) {
|
|
3504
|
+
if (userInfoClaims.sub !== idTokenClaims.sub) throw new OidcLoginError("OIDC userinfo subject does not match ID token.");
|
|
3505
|
+
return {
|
|
3506
|
+
...idTokenClaims,
|
|
3507
|
+
email: idTokenClaims.email ?? userInfoClaims.email,
|
|
3508
|
+
email_verified: idTokenClaims.email_verified ?? userInfoClaims.email_verified,
|
|
3509
|
+
name: idTokenClaims.name ?? userInfoClaims.name,
|
|
3510
|
+
preferred_username: idTokenClaims.preferred_username ?? userInfoClaims.preferred_username
|
|
3511
|
+
};
|
|
3512
|
+
}
|
|
3513
|
+
function validateAuthorizedParty(payload, clientId) {
|
|
3514
|
+
const authorizedParty = asString(payload.azp);
|
|
3515
|
+
if (authorizedParty && authorizedParty !== clientId) throw new OidcLoginError("OIDC authorized party validation failed.");
|
|
3516
|
+
if (Array.isArray(payload.aud) && payload.aud.length > 1 && authorizedParty !== clientId) throw new OidcLoginError("OIDC authorized party validation failed.");
|
|
3517
|
+
}
|
|
3518
|
+
function getTokenEndpointAuthMethod(provider) {
|
|
3519
|
+
return provider.tokenEndpointAuthMethod ?? (provider.clientSecret ? "client_secret_basic" : "none");
|
|
3520
|
+
}
|
|
3521
|
+
function getScopes(provider) {
|
|
3522
|
+
return provider.scopes?.length ? provider.scopes : [...DEFAULT_SCOPES];
|
|
3523
|
+
}
|
|
3524
|
+
function parseTokenResponse(json) {
|
|
3525
|
+
if (typeof json.id_token !== "string") throw new OidcLoginError("OIDC token response missing \"id_token\".");
|
|
3526
|
+
return {
|
|
3527
|
+
accessToken: asString(json.access_token),
|
|
3528
|
+
expiresAt: typeof json.expires_in === "number" ? new Date(Date.now() + json.expires_in * 1e3) : void 0,
|
|
3529
|
+
idToken: json.id_token,
|
|
3530
|
+
refreshToken: asString(json.refresh_token),
|
|
3531
|
+
scope: asString(json.scope),
|
|
3532
|
+
tokenType: asString(json.token_type)
|
|
3533
|
+
};
|
|
3534
|
+
}
|
|
3535
|
+
function parseTransaction(value) {
|
|
3536
|
+
if (!value || typeof value !== "object") throw new OidcLoginError("Invalid OIDC login transaction.");
|
|
3537
|
+
const record = value;
|
|
3538
|
+
if (!record.provider || !record.state || !record.nonce || !record.codeVerifier || typeof record.createdAt !== "number") throw new OidcLoginError("Invalid OIDC login transaction.");
|
|
3539
|
+
return {
|
|
3540
|
+
codeVerifier: record.codeVerifier,
|
|
3541
|
+
createdAt: record.createdAt,
|
|
3542
|
+
nonce: record.nonce,
|
|
3543
|
+
provider: record.provider,
|
|
3544
|
+
returnTo: asString(record.returnTo),
|
|
3545
|
+
state: record.state
|
|
3546
|
+
};
|
|
3547
|
+
}
|
|
3548
|
+
function encodeOidcTransaction(transaction) {
|
|
3549
|
+
const json = JSON.stringify(transaction);
|
|
3550
|
+
return bytesToBase64Url(new TextEncoder().encode(json));
|
|
3551
|
+
}
|
|
3552
|
+
function decodeOidcTransaction(value) {
|
|
3553
|
+
try {
|
|
3554
|
+
const json = new TextDecoder().decode(base64UrlToBytes(value));
|
|
3555
|
+
return parseTransaction(JSON.parse(json));
|
|
3556
|
+
} catch (error) {
|
|
3557
|
+
if (error instanceof OidcLoginError) throw error;
|
|
3558
|
+
throw new OidcLoginError("Invalid OIDC login transaction.");
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
function resolveOidcProviderConfig(providerName, options = {}) {
|
|
3562
|
+
const oidcConfig = getUsersOidcConfig();
|
|
3563
|
+
const providers = {
|
|
3564
|
+
...oidcConfig.providers ?? {},
|
|
3565
|
+
...options.providers ?? {}
|
|
3566
|
+
};
|
|
3567
|
+
const resolvedProviderName = providerName ?? options.defaultProvider ?? oidcConfig.defaultProvider;
|
|
3568
|
+
if (!resolvedProviderName) throw new OidcLoginError("No OIDC provider specified. Pass a provider name or configure packages.users.auth.oidc.defaultProvider.");
|
|
3569
|
+
const provider = providers[resolvedProviderName];
|
|
3570
|
+
if (!provider) throw new OidcLoginError(`Unknown OIDC provider "${resolvedProviderName}". Configure it under packages.users.auth.oidc.providers.`);
|
|
3571
|
+
if (!provider.issuer || !provider.clientId) throw new OidcLoginError(`OIDC provider "${resolvedProviderName}" requires issuer and clientId.`);
|
|
3572
|
+
return {
|
|
3573
|
+
provider,
|
|
3574
|
+
providerName: resolvedProviderName
|
|
3575
|
+
};
|
|
3576
|
+
}
|
|
3577
|
+
var OidcLoginService = class {
|
|
3578
|
+
classOptions;
|
|
3579
|
+
clockTolerance;
|
|
3580
|
+
fetchImpl;
|
|
3581
|
+
metadataCacheTtlMs;
|
|
3582
|
+
metadataFetchedAt = 0;
|
|
3583
|
+
metadataPromise;
|
|
3584
|
+
remoteJwks;
|
|
3585
|
+
provider;
|
|
3586
|
+
providerName;
|
|
3587
|
+
constructor(options) {
|
|
3588
|
+
const { clockTolerance, fetch: fetchOverride, metadataCacheTtlMs, provider, providerName, ...classOptions } = options;
|
|
3589
|
+
if (!provider.redirectUri) throw new OidcLoginError(`OIDC provider "${providerName}" requires a redirectUri.`);
|
|
3590
|
+
this.classOptions = classOptions;
|
|
3591
|
+
this.clockTolerance = clockTolerance ?? DEFAULT_CLOCK_TOLERANCE_SECONDS;
|
|
3592
|
+
this.fetchImpl = ensureFetch(fetchOverride);
|
|
3593
|
+
this.metadataCacheTtlMs = metadataCacheTtlMs ?? DEFAULT_METADATA_CACHE_TTL_MS;
|
|
3594
|
+
this.provider = {
|
|
3595
|
+
...provider,
|
|
3596
|
+
issuer: normalizeIssuer(provider.issuer)
|
|
3597
|
+
};
|
|
3598
|
+
this.providerName = providerName;
|
|
3599
|
+
}
|
|
3600
|
+
createTransaction(returnTo) {
|
|
3601
|
+
return {
|
|
3602
|
+
codeVerifier: randomBase64Url(32),
|
|
3603
|
+
createdAt: Date.now(),
|
|
3604
|
+
nonce: randomBase64Url(32),
|
|
3605
|
+
provider: this.providerName,
|
|
3606
|
+
returnTo,
|
|
3607
|
+
state: randomBase64Url(32)
|
|
3608
|
+
};
|
|
3609
|
+
}
|
|
3610
|
+
async getMetadata() {
|
|
3611
|
+
const cacheExpired = this.metadataFetchedAt > 0 && Date.now() - this.metadataFetchedAt > this.metadataCacheTtlMs;
|
|
3612
|
+
if (!this.metadataPromise || cacheExpired) this.metadataPromise = this.fetchMetadata().then((metadata) => {
|
|
3613
|
+
this.metadataFetchedAt = Date.now();
|
|
3614
|
+
this.remoteJwks = void 0;
|
|
3615
|
+
return metadata;
|
|
3616
|
+
}).catch((error) => {
|
|
3617
|
+
this.metadataPromise = void 0;
|
|
3618
|
+
throw error;
|
|
3619
|
+
});
|
|
3620
|
+
return this.metadataPromise;
|
|
3621
|
+
}
|
|
3622
|
+
async fetchMetadata() {
|
|
3623
|
+
const discoveryUrl = resolveDiscoveryUrl(this.provider);
|
|
3624
|
+
const response = await this.fetchImpl(discoveryUrl, { headers: { accept: "application/json" } });
|
|
3625
|
+
if (!response.ok) throw new OidcLoginError(`OIDC discovery failed for "${this.providerName}" (${response.status}).`);
|
|
3626
|
+
const metadata = await readJsonResponse(response, "OIDC discovery");
|
|
3627
|
+
const issuer = requireString(metadata.issuer ?? this.provider.issuer, "issuer");
|
|
3628
|
+
if (normalizeIssuer(issuer) !== this.provider.issuer) throw new OidcLoginError(`OIDC discovery issuer mismatch for "${this.providerName}".`);
|
|
3629
|
+
return {
|
|
3630
|
+
...metadata,
|
|
3631
|
+
authorization_endpoint: requireString(metadata.authorization_endpoint, "authorization_endpoint"),
|
|
3632
|
+
issuer,
|
|
3633
|
+
jwks_uri: requireString(metadata.jwks_uri, "jwks_uri"),
|
|
3634
|
+
token_endpoint: requireString(metadata.token_endpoint, "token_endpoint")
|
|
3635
|
+
};
|
|
3636
|
+
}
|
|
3637
|
+
async createAuthorizationUrl(options = {}) {
|
|
3638
|
+
const metadata = await this.getMetadata();
|
|
3639
|
+
const transaction = options.transaction ?? this.createTransaction();
|
|
3640
|
+
const codeChallenge = await pkceChallenge(transaction.codeVerifier);
|
|
3641
|
+
const url = new URL(metadata.authorization_endpoint);
|
|
3642
|
+
url.searchParams.set("response_type", "code");
|
|
3643
|
+
url.searchParams.set("client_id", this.provider.clientId);
|
|
3644
|
+
url.searchParams.set("redirect_uri", this.provider.redirectUri);
|
|
3645
|
+
url.searchParams.set("scope", getScopes(this.provider).join(" "));
|
|
3646
|
+
url.searchParams.set("state", transaction.state);
|
|
3647
|
+
url.searchParams.set("nonce", transaction.nonce);
|
|
3648
|
+
url.searchParams.set("code_challenge", codeChallenge);
|
|
3649
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
3650
|
+
for (const [key, value] of Object.entries({
|
|
3651
|
+
...this.provider.authorizationParams,
|
|
3652
|
+
...options.authorizationParams
|
|
3653
|
+
})) if (value !== void 0) url.searchParams.set(key, String(value));
|
|
3654
|
+
return {
|
|
3655
|
+
transaction,
|
|
3656
|
+
url
|
|
3657
|
+
};
|
|
3658
|
+
}
|
|
3659
|
+
async exchangeCallback(callbackUrl, transaction) {
|
|
3660
|
+
const code = this.validateCallback(callbackUrl, transaction);
|
|
3661
|
+
const tokens = await this.exchangeCode(code, transaction);
|
|
3662
|
+
const idTokenClaims = await this.verifyIdToken(tokens.idToken, transaction.nonce);
|
|
3663
|
+
return {
|
|
3664
|
+
claims: await this.enrichClaimsFromUserInfo(idTokenClaims, tokens),
|
|
3665
|
+
tokens
|
|
3666
|
+
};
|
|
3667
|
+
}
|
|
3668
|
+
async completeLogin(callbackUrl, transaction) {
|
|
3669
|
+
const { claims, tokens } = await this.exchangeCallback(callbackUrl, transaction);
|
|
3670
|
+
return {
|
|
3671
|
+
...await (await UserCollection.create(this.classOptions)).getOrCreateFromOidc(claims, this.providerName),
|
|
3672
|
+
claims,
|
|
3673
|
+
tokens
|
|
3674
|
+
};
|
|
3675
|
+
}
|
|
3676
|
+
validateCallback(callbackUrl, transaction) {
|
|
3677
|
+
if (transaction.provider !== this.providerName) throw new OidcLoginError("OIDC login transaction provider mismatch.");
|
|
3678
|
+
const url = callbackUrl instanceof URL ? callbackUrl : new URL(callbackUrl);
|
|
3679
|
+
const error = url.searchParams.get("error");
|
|
3680
|
+
if (error) {
|
|
3681
|
+
const description = url.searchParams.get("error_description");
|
|
3682
|
+
throw new OidcLoginError(description ? `OIDC provider returned "${error}": ${description}` : `OIDC provider returned "${error}".`);
|
|
3683
|
+
}
|
|
3684
|
+
const state = url.searchParams.get("state");
|
|
3685
|
+
if (!state || state !== transaction.state) throw new OidcLoginError("OIDC state validation failed.");
|
|
3686
|
+
const responseIssuer = url.searchParams.get("iss");
|
|
3687
|
+
if (responseIssuer && normalizeIssuer(responseIssuer) !== this.provider.issuer) throw new OidcLoginError("OIDC response issuer validation failed.");
|
|
3688
|
+
const code = url.searchParams.get("code");
|
|
3689
|
+
if (!code) throw new OidcLoginError("OIDC callback missing authorization code.");
|
|
3690
|
+
return code;
|
|
3691
|
+
}
|
|
3692
|
+
async exchangeCode(code, transaction) {
|
|
3693
|
+
const metadata = await this.getMetadata();
|
|
3694
|
+
const body = new URLSearchParams({
|
|
3695
|
+
code,
|
|
3696
|
+
code_verifier: transaction.codeVerifier,
|
|
3697
|
+
grant_type: "authorization_code",
|
|
3698
|
+
redirect_uri: this.provider.redirectUri
|
|
3699
|
+
});
|
|
3700
|
+
const headers = new Headers({
|
|
3701
|
+
accept: "application/json",
|
|
3702
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
3703
|
+
});
|
|
3704
|
+
const authMethod = getTokenEndpointAuthMethod(this.provider);
|
|
3705
|
+
if (authMethod === "client_secret_basic") {
|
|
3706
|
+
if (!this.provider.clientSecret) throw new OidcLoginError(`OIDC provider "${this.providerName}" is missing clientSecret.`);
|
|
3707
|
+
headers.set("authorization", `Basic ${encodeBasicAuth(this.provider.clientId, this.provider.clientSecret)}`);
|
|
3708
|
+
} else {
|
|
3709
|
+
body.set("client_id", this.provider.clientId);
|
|
3710
|
+
if (authMethod === "client_secret_post") {
|
|
3711
|
+
if (!this.provider.clientSecret) throw new OidcLoginError(`OIDC provider "${this.providerName}" is missing clientSecret.`);
|
|
3712
|
+
body.set("client_secret", this.provider.clientSecret);
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
const response = await this.fetchImpl(metadata.token_endpoint, {
|
|
3716
|
+
body,
|
|
3717
|
+
headers,
|
|
3718
|
+
method: "POST"
|
|
3719
|
+
});
|
|
3720
|
+
const json = await readJsonResponse(response, "OIDC token endpoint");
|
|
3721
|
+
if (!response.ok) {
|
|
3722
|
+
const providerError = asString(json.error);
|
|
3723
|
+
const description = asString(json.error_description);
|
|
3724
|
+
throw new OidcLoginError(description ? `OIDC token exchange failed (${providerError ?? response.status}): ${description}` : `OIDC token exchange failed (${providerError ?? response.status}).`);
|
|
3725
|
+
}
|
|
3726
|
+
return parseTokenResponse(json);
|
|
3727
|
+
}
|
|
3728
|
+
async enrichClaimsFromUserInfo(claims, tokens) {
|
|
3729
|
+
if (claims.email || !tokens.accessToken) return claims;
|
|
3730
|
+
const metadata = await this.getMetadata();
|
|
3731
|
+
if (!metadata.userinfo_endpoint) return claims;
|
|
3732
|
+
const tokenType = tokens.tokenType ?? "Bearer";
|
|
3733
|
+
const response = await this.fetchImpl(metadata.userinfo_endpoint, { headers: {
|
|
3734
|
+
accept: "application/json",
|
|
3735
|
+
authorization: `${tokenType} ${tokens.accessToken}`
|
|
3736
|
+
} });
|
|
3737
|
+
if (!response.ok) throw new OidcLoginError(`OIDC userinfo request failed (${response.status}).`);
|
|
3738
|
+
return mergeClaims(claims, normalizeClaims(await readJsonResponse(response, "OIDC userinfo endpoint"), metadata.issuer, "OIDC userinfo"));
|
|
3739
|
+
}
|
|
3740
|
+
async verifyIdToken(idToken, expectedNonce) {
|
|
3741
|
+
const metadata = await this.getMetadata();
|
|
3742
|
+
this.remoteJwks ??= createRemoteJWKSet(new URL(metadata.jwks_uri), { [customFetch]: this.fetchImpl });
|
|
3743
|
+
const { payload } = await jwtVerify(idToken, this.remoteJwks, {
|
|
3744
|
+
audience: this.provider.clientId,
|
|
3745
|
+
clockTolerance: this.clockTolerance,
|
|
3746
|
+
issuer: metadata.issuer
|
|
3747
|
+
});
|
|
3748
|
+
validateAuthorizedParty(payload, this.provider.clientId);
|
|
3749
|
+
if (payload.nonce !== expectedNonce) throw new OidcLoginError("OIDC nonce validation failed.");
|
|
3750
|
+
return normalizeClaims(payload, metadata.issuer, "OIDC ID token");
|
|
3751
|
+
}
|
|
3752
|
+
};
|
|
3753
|
+
//#endregion
|
|
3754
|
+
//#region src/services/PermissionResolver.ts
|
|
3755
|
+
var PermissionResolver = class PermissionResolver {
|
|
3756
|
+
options;
|
|
3757
|
+
membershipCollection;
|
|
3758
|
+
rolePermissionCollection;
|
|
3759
|
+
membershipOverrideCollection;
|
|
3760
|
+
groupMemberCollection;
|
|
3761
|
+
groupRoleCollection;
|
|
3762
|
+
permissionCollection;
|
|
3763
|
+
tenantCollection;
|
|
3764
|
+
tenantPermissionOverrideCollection;
|
|
3765
|
+
constructor(options) {
|
|
3766
|
+
this.options = options;
|
|
3767
|
+
}
|
|
3768
|
+
/**
|
|
3769
|
+
* Initialize collections
|
|
3770
|
+
*
|
|
3771
|
+
* Each collection is created via the inherited static `SmrtCollection.create()`
|
|
3772
|
+
* factory, which is generically typed to return the concrete subclass instance.
|
|
3773
|
+
*/
|
|
3774
|
+
async initialize() {
|
|
3775
|
+
this.membershipCollection = await MembershipCollection.create(this.options);
|
|
3776
|
+
this.rolePermissionCollection = await RolePermissionCollection.create(this.options);
|
|
3777
|
+
this.membershipOverrideCollection = await MembershipOverrideCollection.create(this.options);
|
|
3778
|
+
this.groupMemberCollection = await GroupMemberCollection.create(this.options);
|
|
3779
|
+
this.groupRoleCollection = await GroupRoleCollection.create(this.options);
|
|
3780
|
+
this.permissionCollection = await PermissionCollection.create(this.options);
|
|
3781
|
+
this.tenantCollection = await TenantCollection.create(this.options);
|
|
3782
|
+
this.tenantPermissionOverrideCollection = await TenantPermissionOverrideCollection.create(this.options);
|
|
3783
|
+
}
|
|
3784
|
+
/**
|
|
3785
|
+
* Resolve effective permissions for a tenant, considering hierarchy inheritance.
|
|
3786
|
+
*
|
|
3787
|
+
* Algorithm:
|
|
3788
|
+
* 1. Get the tenant and its ancestors (from root to immediate parent)
|
|
3789
|
+
* 2. Batch fetch all permission overrides for the entire chain (single query)
|
|
3790
|
+
* 3. Walk down the chain, building up permissions:
|
|
3791
|
+
* - Start with root tenant's permissions
|
|
3792
|
+
* - For each child: if parent.cascadePermissions && child.inheritPermissions:
|
|
3793
|
+
* - Merge parent's permissions
|
|
3794
|
+
* - Apply child's overrides (GRANT adds, DENY removes)
|
|
3795
|
+
* 4. Return the final effective permission set
|
|
3796
|
+
*/
|
|
3797
|
+
async resolveTenantPermissions(tenantId) {
|
|
3798
|
+
const result = {
|
|
3799
|
+
permissions: /* @__PURE__ */ new Set(),
|
|
3800
|
+
contributingTenantIds: [],
|
|
3801
|
+
inheritanceActive: false,
|
|
3802
|
+
deniedPermissions: /* @__PURE__ */ new Set()
|
|
3803
|
+
};
|
|
3804
|
+
const tenant = await this.tenantCollection.get({ id: tenantId });
|
|
3805
|
+
if (!tenant) return result;
|
|
3806
|
+
const chain = [...await this.tenantCollection.getAncestorsFromRoot(tenantId), tenant];
|
|
3807
|
+
const chainTenantIds = chain.map((t) => t.id);
|
|
3808
|
+
const allOverridesMap = await this.tenantPermissionOverrideCollection.getOverridesByEffectBatch(chainTenantIds);
|
|
3809
|
+
const allPermissionIds = /* @__PURE__ */ new Set();
|
|
3810
|
+
const deniedPermissionIds = /* @__PURE__ */ new Set();
|
|
3811
|
+
for (const overrides of allOverridesMap.values()) {
|
|
3812
|
+
for (const id of overrides.grantedPermissionIds) allPermissionIds.add(id);
|
|
3813
|
+
for (const id of overrides.deniedPermissionIds) {
|
|
3814
|
+
allPermissionIds.add(id);
|
|
3815
|
+
deniedPermissionIds.add(id);
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3818
|
+
let inheritedPermissions = /* @__PURE__ */ new Set();
|
|
3819
|
+
for (let i = 0; i < chain.length; i++) {
|
|
3820
|
+
const current = chain[i];
|
|
3821
|
+
const isFirst = i === 0;
|
|
3822
|
+
const previous = isFirst ? null : chain[i - 1];
|
|
3823
|
+
const shouldInherit = !isFirst && previous?.cascadePermissions && current.inheritPermissions;
|
|
3824
|
+
if (shouldInherit) result.inheritanceActive = true;
|
|
3825
|
+
const overrides = allOverridesMap.get(current.id) ?? {
|
|
3826
|
+
grantedPermissionIds: [],
|
|
3827
|
+
deniedPermissionIds: [],
|
|
3828
|
+
inheritedPermissionIds: []
|
|
3829
|
+
};
|
|
3830
|
+
const currentPermissions = /* @__PURE__ */ new Set();
|
|
3831
|
+
let contributed = false;
|
|
3832
|
+
if (shouldInherit && inheritedPermissions.size > 0) {
|
|
3833
|
+
for (const permId of inheritedPermissions) currentPermissions.add(permId);
|
|
3834
|
+
contributed = true;
|
|
3835
|
+
}
|
|
3836
|
+
for (const permId of overrides.grantedPermissionIds) {
|
|
3837
|
+
currentPermissions.add(permId);
|
|
3838
|
+
contributed = true;
|
|
3839
|
+
}
|
|
3840
|
+
for (const permId of overrides.deniedPermissionIds) if (currentPermissions.has(permId)) {
|
|
3841
|
+
currentPermissions.delete(permId);
|
|
3842
|
+
contributed = true;
|
|
3843
|
+
}
|
|
3844
|
+
if (contributed && !result.contributingTenantIds.includes(current.id)) result.contributingTenantIds.push(current.id);
|
|
3845
|
+
inheritedPermissions = currentPermissions;
|
|
3846
|
+
}
|
|
3847
|
+
if (allPermissionIds.size > 0) {
|
|
3848
|
+
const permissionsMap = await this.permissionCollection.findByIds(Array.from(allPermissionIds));
|
|
3849
|
+
for (const permId of inheritedPermissions) {
|
|
3850
|
+
const perm = permissionsMap.get(permId);
|
|
3851
|
+
if (perm?.slug) result.permissions.add(perm.slug);
|
|
3852
|
+
}
|
|
3853
|
+
for (const permId of deniedPermissionIds) {
|
|
3854
|
+
if (inheritedPermissions.has(permId)) continue;
|
|
3855
|
+
const perm = permissionsMap.get(permId);
|
|
3856
|
+
if (perm?.slug) result.deniedPermissions.add(perm.slug);
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
return result;
|
|
3860
|
+
}
|
|
3861
|
+
/**
|
|
3862
|
+
* Get the inheritance chain for a tenant (for debugging/display purposes)
|
|
3863
|
+
*/
|
|
3864
|
+
async getTenantInheritanceChain(tenantId) {
|
|
3865
|
+
const tenant = await this.tenantCollection.get({ id: tenantId });
|
|
3866
|
+
if (!tenant) return [];
|
|
3867
|
+
const ancestors = await this.tenantCollection.getAncestorsFromRoot(tenantId);
|
|
3868
|
+
const chain = [];
|
|
3869
|
+
for (let i = 0; i < ancestors.length; i++) {
|
|
3870
|
+
const current = ancestors[i];
|
|
3871
|
+
const next = i + 1 < ancestors.length ? ancestors[i + 1] : tenant;
|
|
3872
|
+
chain.push({
|
|
3873
|
+
tenant: current,
|
|
3874
|
+
inherits: false,
|
|
3875
|
+
cascades: current.cascadePermissions && next.inheritPermissions
|
|
3876
|
+
});
|
|
3877
|
+
}
|
|
3878
|
+
chain.push({
|
|
3879
|
+
tenant,
|
|
3880
|
+
inherits: tenant.inheritPermissions && ancestors.length > 0,
|
|
3881
|
+
cascades: tenant.cascadePermissions
|
|
3882
|
+
});
|
|
3883
|
+
return chain;
|
|
3884
|
+
}
|
|
3885
|
+
/**
|
|
3886
|
+
* Resolve all effective permissions for a user in a tenant.
|
|
3887
|
+
*
|
|
3888
|
+
* Precedence (broad -> specific, most-specific wins):
|
|
3889
|
+
* tenant-inherited (cascade)
|
|
3890
|
+
* -> role
|
|
3891
|
+
* -> group roles
|
|
3892
|
+
* -> tenant-DENY (removes; overrides role/group grants, tenant-wide)
|
|
3893
|
+
* -> membership GRANT (re-adds; most specific, can win over a tenant-DENY)
|
|
3894
|
+
* -> membership DENY (absolute; always wins)
|
|
3895
|
+
*
|
|
3896
|
+
* Algorithm:
|
|
3897
|
+
* 1. Get membership and collect all permission IDs from all sources
|
|
3898
|
+
* 2. Batch fetch all permissions in a single query
|
|
3899
|
+
* 3. Apply permissions from role, then groups
|
|
3900
|
+
* 4. Subtract tenant-level DENY'd slugs (hard tenant-wide block)
|
|
3901
|
+
* 5. Apply membership GRANT overrides (can re-add a tenant-DENY'd slug)
|
|
3902
|
+
* 6. Subtract membership DENY overrides (absolute precedence)
|
|
3903
|
+
*/
|
|
3904
|
+
async resolvePermissions(userId, tenantId, options = {}) {
|
|
3905
|
+
const result = {
|
|
3906
|
+
permissions: /* @__PURE__ */ new Set(),
|
|
3907
|
+
membershipId: null,
|
|
3908
|
+
roleId: null,
|
|
3909
|
+
groupIds: [],
|
|
3910
|
+
deniedPermissionIds: []
|
|
3911
|
+
};
|
|
3912
|
+
const membership = options.membership === void 0 ? await this.membershipCollection.findByUserAndTenant(userId, tenantId) : options.membership;
|
|
3913
|
+
if (!membership || !membership.isActive()) return result;
|
|
3914
|
+
if (membership.userId !== userId || membership.tenantId !== tenantId) return result;
|
|
3915
|
+
result.membershipId = membership.id ?? null;
|
|
3916
|
+
result.roleId = membership.roleId ?? null;
|
|
3917
|
+
const tenantPermissions = await this.resolveTenantPermissions(tenantId);
|
|
3918
|
+
for (const slug of tenantPermissions.permissions) result.permissions.add(slug);
|
|
3919
|
+
if (!membership.roleId) return result;
|
|
3920
|
+
const allPermissionIds = /* @__PURE__ */ new Set();
|
|
3921
|
+
const rolePermissionIds = [];
|
|
3922
|
+
const groupRolePermissionIds = /* @__PURE__ */ new Map();
|
|
3923
|
+
const baseRolePermIds = await this.rolePermissionCollection.getPermissionIds(membership.roleId);
|
|
3924
|
+
for (const id of baseRolePermIds) {
|
|
3925
|
+
allPermissionIds.add(id);
|
|
3926
|
+
rolePermissionIds.push(id);
|
|
3927
|
+
}
|
|
3928
|
+
const groupIds = await this.groupMemberCollection.getGroupIdsForTenant(userId, tenantId);
|
|
3929
|
+
result.groupIds = groupIds;
|
|
3930
|
+
for (const groupId of groupIds) {
|
|
3931
|
+
const groupRoleIds = await this.groupRoleCollection.getRoleIds(groupId);
|
|
3932
|
+
for (const roleId of groupRoleIds) {
|
|
3933
|
+
const permIds = await this.rolePermissionCollection.getPermissionIds(roleId);
|
|
3934
|
+
for (const id of permIds) allPermissionIds.add(id);
|
|
3935
|
+
groupRolePermissionIds.set(roleId, permIds);
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
if (!membership.id) return result;
|
|
3939
|
+
const membershipId = membership.id;
|
|
3940
|
+
const grantedPermissionIds = await this.membershipOverrideCollection.getGrantedPermissionIds(membershipId);
|
|
3941
|
+
const deniedPermissionIds = await this.membershipOverrideCollection.getDeniedPermissionIds(membershipId);
|
|
3942
|
+
result.deniedPermissionIds = deniedPermissionIds;
|
|
3943
|
+
for (const id of grantedPermissionIds) allPermissionIds.add(id);
|
|
3944
|
+
for (const id of deniedPermissionIds) allPermissionIds.add(id);
|
|
3945
|
+
const permissionsMap = await this.permissionCollection.findByIds(Array.from(allPermissionIds));
|
|
3946
|
+
const permissionIdToSlug = /* @__PURE__ */ new Map();
|
|
3947
|
+
for (const [id, perm] of permissionsMap) if (perm.slug) permissionIdToSlug.set(id, perm.slug);
|
|
3948
|
+
for (const permId of rolePermissionIds) {
|
|
3949
|
+
const slug = permissionIdToSlug.get(permId);
|
|
3950
|
+
if (slug) result.permissions.add(slug);
|
|
3951
|
+
}
|
|
3952
|
+
for (const permIds of groupRolePermissionIds.values()) for (const permId of permIds) {
|
|
3953
|
+
const slug = permissionIdToSlug.get(permId);
|
|
3954
|
+
if (slug) result.permissions.add(slug);
|
|
3955
|
+
}
|
|
3956
|
+
for (const slug of tenantPermissions.deniedPermissions) result.permissions.delete(slug);
|
|
3957
|
+
for (const permId of grantedPermissionIds) {
|
|
3958
|
+
const slug = permissionIdToSlug.get(permId);
|
|
3959
|
+
if (slug) result.permissions.add(slug);
|
|
3960
|
+
}
|
|
3961
|
+
for (const permId of deniedPermissionIds) {
|
|
3962
|
+
const slug = permissionIdToSlug.get(permId);
|
|
3963
|
+
if (slug) result.permissions.delete(slug);
|
|
3964
|
+
}
|
|
3965
|
+
return result;
|
|
3966
|
+
}
|
|
3967
|
+
/**
|
|
3968
|
+
* Check if a user has a specific permission in a tenant
|
|
3969
|
+
*/
|
|
3970
|
+
async hasPermission(userId, tenantId, permissionSlug, options = {}) {
|
|
3971
|
+
return (await this.resolvePermissions(userId, tenantId, options)).permissions.has(permissionSlug);
|
|
3972
|
+
}
|
|
3973
|
+
/**
|
|
3974
|
+
* Check if a user has all of the specified permissions
|
|
3975
|
+
*/
|
|
3976
|
+
async hasAllPermissions(userId, tenantId, permissionSlugs, options = {}) {
|
|
3977
|
+
const result = await this.resolvePermissions(userId, tenantId, options);
|
|
3978
|
+
return permissionSlugs.every((slug) => result.permissions.has(slug));
|
|
3979
|
+
}
|
|
3980
|
+
/**
|
|
3981
|
+
* Check if a user has any of the specified permissions
|
|
3982
|
+
*/
|
|
3983
|
+
async hasAnyPermission(userId, tenantId, permissionSlugs, options = {}) {
|
|
3984
|
+
const result = await this.resolvePermissions(userId, tenantId, options);
|
|
3985
|
+
return permissionSlugs.some((slug) => result.permissions.has(slug));
|
|
3986
|
+
}
|
|
3987
|
+
/**
|
|
3988
|
+
* Static factory method
|
|
3989
|
+
*/
|
|
3990
|
+
static async create(options) {
|
|
3991
|
+
const resolver = new PermissionResolver(options);
|
|
3992
|
+
await resolver.initialize();
|
|
3993
|
+
return resolver;
|
|
3994
|
+
}
|
|
3995
|
+
};
|
|
3996
|
+
//#endregion
|
|
3997
|
+
//#region src/services/SessionService.ts
|
|
3998
|
+
var SessionService = class SessionService {
|
|
3999
|
+
options;
|
|
4000
|
+
sessionCollection;
|
|
4001
|
+
userCollection;
|
|
4002
|
+
membershipCollection;
|
|
4003
|
+
permissionResolver;
|
|
4004
|
+
defaultTTL;
|
|
4005
|
+
autoExtend;
|
|
4006
|
+
constructor(options) {
|
|
4007
|
+
this.options = options;
|
|
4008
|
+
this.defaultTTL = options.defaultTTL ?? 604800;
|
|
4009
|
+
this.autoExtend = options.autoExtend ?? false;
|
|
4010
|
+
}
|
|
4011
|
+
/**
|
|
4012
|
+
* Initialize collections
|
|
4013
|
+
*/
|
|
4014
|
+
async initialize() {
|
|
4015
|
+
this.sessionCollection = await SessionCollection.create(this.options);
|
|
4016
|
+
this.userCollection = await UserCollection.create(this.options);
|
|
4017
|
+
this.membershipCollection = await MembershipCollection.create(this.options);
|
|
4018
|
+
this.permissionResolver = await PermissionResolver.create(this.options);
|
|
4019
|
+
}
|
|
4020
|
+
/**
|
|
4021
|
+
* Create a new session for a user
|
|
4022
|
+
*
|
|
4023
|
+
* @param userId - The user ID
|
|
4024
|
+
* @param tenantId - Optional tenant context
|
|
4025
|
+
* @param options - Additional session options
|
|
4026
|
+
* @returns The session ID
|
|
4027
|
+
*/
|
|
4028
|
+
async createSession(userId, tenantId, options) {
|
|
4029
|
+
return (await this.sessionCollection.createSession({
|
|
4030
|
+
userId,
|
|
4031
|
+
tenantId,
|
|
4032
|
+
ttl: options?.ttl ?? this.defaultTTL,
|
|
4033
|
+
userAgent: options?.userAgent,
|
|
4034
|
+
ipAddress: options?.ipAddress,
|
|
4035
|
+
data: options?.data
|
|
4036
|
+
})).id;
|
|
4037
|
+
}
|
|
4038
|
+
/**
|
|
4039
|
+
* Load full session context (user + permissions)
|
|
4040
|
+
*
|
|
4041
|
+
* Returns null if session is invalid or user doesn't exist
|
|
4042
|
+
*/
|
|
4043
|
+
async loadSessionContext(sessionId) {
|
|
4044
|
+
const session = await this.sessionCollection.findValidSession(sessionId);
|
|
4045
|
+
if (!session) return null;
|
|
4046
|
+
const user = await this.userCollection.get(session.userId);
|
|
4047
|
+
if (!user || !user.isActive()) return null;
|
|
4048
|
+
let permissions = [];
|
|
4049
|
+
let membership = null;
|
|
4050
|
+
if (session.tenantId) {
|
|
4051
|
+
const resolvedMembership = await this.membershipCollection.findByUserAndTenant(session.userId, session.tenantId);
|
|
4052
|
+
membership = resolvedMembership?.isActive() ? resolvedMembership : null;
|
|
4053
|
+
const result = await this.permissionResolver.resolvePermissions(session.userId, session.tenantId, { membership });
|
|
4054
|
+
permissions = Array.from(result.permissions);
|
|
4055
|
+
}
|
|
4056
|
+
if (this.autoExtend) session.extend(this.defaultTTL);
|
|
4057
|
+
else session.touch();
|
|
4058
|
+
await session.save();
|
|
4059
|
+
return {
|
|
4060
|
+
user,
|
|
4061
|
+
membership,
|
|
4062
|
+
permissions,
|
|
4063
|
+
tenantId: session.tenantId,
|
|
4064
|
+
sessionId: session.id
|
|
4065
|
+
};
|
|
4066
|
+
}
|
|
4067
|
+
/**
|
|
4068
|
+
* Get the initialized database connection backing this session service.
|
|
4069
|
+
*/
|
|
4070
|
+
getDatabase() {
|
|
4071
|
+
return this.sessionCollection.db;
|
|
4072
|
+
}
|
|
4073
|
+
/**
|
|
4074
|
+
* Refresh session (extend expiry, update lastAccessed)
|
|
4075
|
+
*/
|
|
4076
|
+
async refreshSession(sessionId) {
|
|
4077
|
+
return this.sessionCollection.touch(sessionId, true, this.defaultTTL);
|
|
4078
|
+
}
|
|
4079
|
+
/**
|
|
4080
|
+
* Destroy a session (revoke it)
|
|
4081
|
+
*/
|
|
4082
|
+
async destroySession(sessionId) {
|
|
4083
|
+
return this.sessionCollection.revokeSession(sessionId);
|
|
4084
|
+
}
|
|
4085
|
+
/**
|
|
4086
|
+
* Destroy all sessions for a user (logout from all devices)
|
|
4087
|
+
*/
|
|
4088
|
+
async destroyAllUserSessions(userId) {
|
|
4089
|
+
return this.sessionCollection.revokeUserSessions(userId);
|
|
4090
|
+
}
|
|
4091
|
+
/**
|
|
4092
|
+
* Switch tenant context for a session.
|
|
4093
|
+
*
|
|
4094
|
+
* A session's `tenantId` is the tenant-isolation key for every `@TenantScoped`
|
|
4095
|
+
* query, so it must never be set to a tenant the session's user is not an
|
|
4096
|
+
* active member of — otherwise a caller could read/write another tenant's data
|
|
4097
|
+
* by feeding an arbitrary id here (e.g. straight from untrusted form data).
|
|
4098
|
+
*
|
|
4099
|
+
* Fail-closed (#1400): the user's ACTIVE membership in the target tenant is
|
|
4100
|
+
* verified BEFORE any write. A non-member switch returns
|
|
4101
|
+
* `{ switched: false, ... }` and mutates nothing.
|
|
4102
|
+
*
|
|
4103
|
+
* Session-id ROTATION (#1354 follow-up): a successful switch into a non-null
|
|
4104
|
+
* tenant mints a BRAND-NEW session (fresh secure id, fresh TTL) for the same
|
|
4105
|
+
* user with the new tenant, then REVOKES the old session — so any captured
|
|
4106
|
+
* pre-switch session id immediately stops validating, shrinking the blast
|
|
4107
|
+
* radius of a leaked id across a privilege/tenant boundary. The device context
|
|
4108
|
+
* (user agent, IP, custom data) carries over to the new session. Callers MUST
|
|
4109
|
+
* persist the returned `sessionId` (e.g. re-set the cookie).
|
|
4110
|
+
*
|
|
4111
|
+
* Passing `null` clears the tenant context, is always allowed, and stays
|
|
4112
|
+
* in-place (no rotation — there is no privilege boundary being crossed).
|
|
4113
|
+
*
|
|
4114
|
+
* @returns A {@link SwitchTenantResult}; check `switched` for success.
|
|
4115
|
+
*/
|
|
4116
|
+
async switchTenant(sessionId, tenantId) {
|
|
4117
|
+
const failClosed = {
|
|
4118
|
+
switched: false,
|
|
4119
|
+
sessionId: null,
|
|
4120
|
+
session: null,
|
|
4121
|
+
rotated: false
|
|
4122
|
+
};
|
|
4123
|
+
const session = await this.sessionCollection.findValidSession(sessionId);
|
|
4124
|
+
if (!session) return failClosed;
|
|
4125
|
+
if (tenantId === null) {
|
|
4126
|
+
if (!await this.sessionCollection.setSessionTenant(sessionId, null)) return failClosed;
|
|
4127
|
+
return {
|
|
4128
|
+
switched: true,
|
|
4129
|
+
sessionId,
|
|
4130
|
+
session: await this.sessionCollection.findValidSession(sessionId),
|
|
4131
|
+
rotated: false
|
|
4132
|
+
};
|
|
4133
|
+
}
|
|
4134
|
+
const membership = await this.membershipCollection.findByUserAndTenant(session.userId, tenantId);
|
|
4135
|
+
if (!membership || !membership.isActive()) return failClosed;
|
|
4136
|
+
await this.sessionCollection.revokeSession(sessionId);
|
|
4137
|
+
const rotated = await this.sessionCollection.createSession({
|
|
4138
|
+
userId: session.userId,
|
|
4139
|
+
tenantId,
|
|
4140
|
+
ttl: this.defaultTTL,
|
|
4141
|
+
userAgent: session.userAgent,
|
|
4142
|
+
ipAddress: session.ipAddress,
|
|
4143
|
+
data: session.data
|
|
4144
|
+
});
|
|
4145
|
+
return {
|
|
4146
|
+
switched: true,
|
|
4147
|
+
sessionId: rotated.id,
|
|
4148
|
+
session: rotated,
|
|
4149
|
+
rotated: true
|
|
4150
|
+
};
|
|
4151
|
+
}
|
|
4152
|
+
/**
|
|
4153
|
+
* Get all active sessions for a user (for "manage sessions" UI)
|
|
4154
|
+
*/
|
|
4155
|
+
async getUserSessions(userId) {
|
|
4156
|
+
return this.sessionCollection.findByUser(userId);
|
|
4157
|
+
}
|
|
4158
|
+
/**
|
|
4159
|
+
* Clean up expired sessions (run periodically)
|
|
4160
|
+
*/
|
|
4161
|
+
async cleanupExpiredSessions() {
|
|
4162
|
+
return this.sessionCollection.deleteExpired();
|
|
4163
|
+
}
|
|
4164
|
+
/**
|
|
4165
|
+
* Check if a permission is granted for the session
|
|
4166
|
+
*/
|
|
4167
|
+
async hasPermission(sessionId, permission) {
|
|
4168
|
+
const context = await this.loadSessionContext(sessionId);
|
|
4169
|
+
if (!context) return false;
|
|
4170
|
+
return context.permissions.includes(permission);
|
|
4171
|
+
}
|
|
4172
|
+
/**
|
|
4173
|
+
* Get session data
|
|
4174
|
+
*/
|
|
4175
|
+
async getSessionData(sessionId, key) {
|
|
4176
|
+
return this.sessionCollection.getSessionData(sessionId, key);
|
|
4177
|
+
}
|
|
4178
|
+
/**
|
|
4179
|
+
* Set session data
|
|
4180
|
+
*/
|
|
4181
|
+
async setSessionData(sessionId, key, value) {
|
|
4182
|
+
return this.sessionCollection.setSessionData(sessionId, key, value);
|
|
4183
|
+
}
|
|
4184
|
+
/**
|
|
4185
|
+
* Static factory method
|
|
4186
|
+
*/
|
|
4187
|
+
static async create(options) {
|
|
4188
|
+
const service = new SessionService(options);
|
|
4189
|
+
await service.initialize();
|
|
4190
|
+
return service;
|
|
4191
|
+
}
|
|
4192
|
+
};
|
|
4193
|
+
//#endregion
|
|
4194
|
+
//#region src/services/SessionPermissionContext.ts
|
|
4195
|
+
var requestPermissionContextStorage = new AsyncLocalStorage();
|
|
4196
|
+
function getCurrentSessionPermissionContext() {
|
|
4197
|
+
return requestPermissionContextStorage.getStore();
|
|
4198
|
+
}
|
|
4199
|
+
function getRequestScopedDatabase() {
|
|
4200
|
+
return getCurrentSessionPermissionContext()?.database;
|
|
4201
|
+
}
|
|
4202
|
+
globalThis.__smrtGetRequestPermissionContext ??= getCurrentSessionPermissionContext;
|
|
4203
|
+
globalThis.__smrtGetRequestScopedDatabase ??= getRequestScopedDatabase;
|
|
4204
|
+
function isProbablyPostgres(configDb, database) {
|
|
4205
|
+
if (configDb && typeof configDb === "object" && !("query" in configDb) && "type" in configDb && configDb.type === "postgres") return true;
|
|
4206
|
+
if (typeof database.url === "string" && database.url.startsWith("postgres")) return true;
|
|
4207
|
+
return (database.constructor?.name || "").toLowerCase().includes("postgres");
|
|
4208
|
+
}
|
|
4209
|
+
async function setPostgresSessionVariables(database, context) {
|
|
4210
|
+
await database.query("SELECT set_config('smrt.tenant_id', $1, true)", context.tenantId ?? "");
|
|
4211
|
+
await database.query("SELECT set_config('smrt.user_id', $1, true)", context.userId ?? "");
|
|
4212
|
+
await database.query("SELECT set_config('smrt.session_id', $1, true)", context.sessionId ?? "");
|
|
4213
|
+
await database.query("SELECT set_config('smrt.permissions', $1, true)", JSON.stringify(context.permissions));
|
|
4214
|
+
await database.query("SELECT set_config('smrt.super_admin_bypass', $1, true)", context.superAdminBypass ? "true" : "false");
|
|
4215
|
+
await database.query("SELECT set_config('smrt.system_context', $1, true)", context.systemContext ? "true" : "false");
|
|
4216
|
+
}
|
|
4217
|
+
function isModuleNotFoundError(error) {
|
|
4218
|
+
return error instanceof Error && (error.code === "ERR_MODULE_NOT_FOUND" || /Cannot find package '@happyvertical\/smrt-tenancy'/.test(error.message));
|
|
4219
|
+
}
|
|
4220
|
+
async function runWithOptionalTenantContext(options, context, fn) {
|
|
4221
|
+
if (options.systemContext) try {
|
|
4222
|
+
return await (await import("@happyvertical/smrt-tenancy")).withSystemContext(fn);
|
|
4223
|
+
} catch (error) {
|
|
4224
|
+
if (isModuleNotFoundError(error)) return await fn();
|
|
4225
|
+
throw error;
|
|
4226
|
+
}
|
|
4227
|
+
if (!options.enterTenantContext || !context.tenantId) return await fn();
|
|
4228
|
+
try {
|
|
4229
|
+
return await (await import("@happyvertical/smrt-tenancy")).withTenant({
|
|
4230
|
+
database: context.database,
|
|
4231
|
+
permissions: context.permissionSet,
|
|
4232
|
+
superAdminBypass: context.superAdminBypass,
|
|
4233
|
+
tenantId: context.tenantId,
|
|
4234
|
+
user: context.user ?? void 0,
|
|
4235
|
+
userId: context.userId ?? void 0
|
|
4236
|
+
}, fn);
|
|
4237
|
+
} catch (error) {
|
|
4238
|
+
if (isModuleNotFoundError(error)) return await fn();
|
|
4239
|
+
throw error;
|
|
4240
|
+
}
|
|
4241
|
+
}
|
|
4242
|
+
async function withSessionPermissionContext(options, fn) {
|
|
4243
|
+
const { enterTenantContext: _enterTenantContext, postgresRls: _postgresRls, sessionId: _sessionId, sessionService: _sessionService, superAdminBypass: _superAdminBypass, systemContext: _systemContext, ...sessionServiceOptions } = options;
|
|
4244
|
+
const sessionService = options.sessionService ?? await SessionService.create({ ...sessionServiceOptions });
|
|
4245
|
+
const configuredDb = sessionServiceOptions.db ?? sessionServiceOptions.persistence;
|
|
4246
|
+
const session = options.sessionId === void 0 || options.sessionId === null ? null : await sessionService.loadSessionContext(options.sessionId);
|
|
4247
|
+
const permissionSet = new Set(session?.permissions ?? []);
|
|
4248
|
+
const baseDatabase = sessionService.getDatabase();
|
|
4249
|
+
const config = getPackageConfig("users", {});
|
|
4250
|
+
const usePostgresRls = (options.postgresRls ?? config.permissions?.postgres?.enabled ?? false) && isProbablyPostgres(configuredDb, baseDatabase);
|
|
4251
|
+
let transaction;
|
|
4252
|
+
let rolledBack = false;
|
|
4253
|
+
if (usePostgresRls) {
|
|
4254
|
+
if (!baseDatabase.beginTransaction) throw new Error("Postgres RLS requires a database adapter that supports beginTransaction().");
|
|
4255
|
+
transaction = await baseDatabase.beginTransaction();
|
|
4256
|
+
}
|
|
4257
|
+
const runtimeContext = {
|
|
4258
|
+
database: transaction ?? baseDatabase,
|
|
4259
|
+
permissions: session?.permissions ?? [],
|
|
4260
|
+
permissionSet,
|
|
4261
|
+
membership: session?.membership ?? null,
|
|
4262
|
+
postgresRls: usePostgresRls,
|
|
4263
|
+
session,
|
|
4264
|
+
sessionId: session?.sessionId ?? null,
|
|
4265
|
+
superAdminBypass: options.superAdminBypass ?? false,
|
|
4266
|
+
systemContext: options.systemContext ?? false,
|
|
4267
|
+
tenantId: session?.tenantId ?? null,
|
|
4268
|
+
user: session?.user ?? null,
|
|
4269
|
+
userId: session?.user.id ?? null
|
|
4270
|
+
};
|
|
4271
|
+
try {
|
|
4272
|
+
if (transaction) await setPostgresSessionVariables(transaction, runtimeContext);
|
|
4273
|
+
return await requestPermissionContextStorage.run(runtimeContext, async () => runWithOptionalTenantContext(options, runtimeContext, () => fn(runtimeContext)));
|
|
4274
|
+
} catch (error) {
|
|
4275
|
+
if (transaction) {
|
|
4276
|
+
if (transaction.isActive ? transaction.isActive() : true) {
|
|
4277
|
+
rolledBack = true;
|
|
4278
|
+
await transaction.rollback();
|
|
4279
|
+
}
|
|
4280
|
+
}
|
|
4281
|
+
throw error;
|
|
4282
|
+
} finally {
|
|
4283
|
+
if (transaction && !rolledBack) {
|
|
4284
|
+
if (transaction.isActive ? transaction.isActive() : true) await transaction.commit();
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
4287
|
+
}
|
|
4288
|
+
//#endregion
|
|
4289
|
+
//#region src/services/TerminalAuthService.ts
|
|
4290
|
+
var DEFAULT_CLI_AUTH_POLL_INTERVAL_SECONDS = 2;
|
|
4291
|
+
var DEFAULT_CLI_AUTH_REQUEST_TTL_SECONDS = 600;
|
|
4292
|
+
var DEFAULT_CLI_SESSION_TTL_SECONDS = 720 * 60 * 60;
|
|
4293
|
+
function base64url(bytes) {
|
|
4294
|
+
return bytes.toString("base64url");
|
|
4295
|
+
}
|
|
4296
|
+
function hashDeviceCode(deviceCode) {
|
|
4297
|
+
return createHash("sha256").update(deviceCode).digest("hex");
|
|
4298
|
+
}
|
|
4299
|
+
function isExpired(request) {
|
|
4300
|
+
return new Date(request.expiresAt).getTime() <= Date.now();
|
|
4301
|
+
}
|
|
4302
|
+
var TerminalAuthService = class TerminalAuthService {
|
|
4303
|
+
options;
|
|
4304
|
+
userCodePrefix;
|
|
4305
|
+
requestTtlSeconds;
|
|
4306
|
+
sessionTtlSeconds;
|
|
4307
|
+
pollIntervalSeconds;
|
|
4308
|
+
verificationPath;
|
|
4309
|
+
maxApproveAttempts;
|
|
4310
|
+
approveAttemptWindowMs;
|
|
4311
|
+
failedApprovesByUser = /* @__PURE__ */ new Map();
|
|
4312
|
+
requestCollection;
|
|
4313
|
+
sessionService;
|
|
4314
|
+
constructor(options) {
|
|
4315
|
+
this.options = options;
|
|
4316
|
+
this.userCodePrefix = (options.userCodePrefix ?? "").trim().toUpperCase();
|
|
4317
|
+
this.requestTtlSeconds = options.requestTtlSeconds ?? 600;
|
|
4318
|
+
this.sessionTtlSeconds = options.sessionTtlSeconds ?? 2592e3;
|
|
4319
|
+
this.pollIntervalSeconds = options.pollIntervalSeconds ?? 2;
|
|
4320
|
+
this.maxApproveAttempts = options.maxApproveAttempts ?? 5;
|
|
4321
|
+
this.approveAttemptWindowMs = (options.approveAttemptWindowSeconds ?? 300) * 1e3;
|
|
4322
|
+
const verificationPath = options.verificationPath ?? "/terminal-login";
|
|
4323
|
+
this.verificationPath = verificationPath.startsWith("/") ? verificationPath : `/${verificationPath}`;
|
|
4324
|
+
}
|
|
4325
|
+
async initialize() {
|
|
4326
|
+
this.requestCollection = await UsersCliAuthRequestCollection.create(this.options);
|
|
4327
|
+
this.sessionService = await SessionService.create({
|
|
4328
|
+
...this.options,
|
|
4329
|
+
autoExtend: this.options.sessionAutoExtend ?? true,
|
|
4330
|
+
cookieName: this.options.sessionCookieName,
|
|
4331
|
+
defaultTTL: this.sessionTtlSeconds
|
|
4332
|
+
});
|
|
4333
|
+
}
|
|
4334
|
+
static async create(options) {
|
|
4335
|
+
const service = new TerminalAuthService(options);
|
|
4336
|
+
await service.initialize();
|
|
4337
|
+
return service;
|
|
4338
|
+
}
|
|
4339
|
+
makeUserCode() {
|
|
4340
|
+
const raw = randomBytes(6).toString("hex").toUpperCase().slice(0, 8);
|
|
4341
|
+
return this.userCodePrefix ? `${this.userCodePrefix}-${raw}` : raw;
|
|
4342
|
+
}
|
|
4343
|
+
/**
|
|
4344
|
+
* Generate a user code that is not currently in use by another *active*
|
|
4345
|
+
* (pending or approved-but-unclaimed) request. Collisions are vanishingly
|
|
4346
|
+
* rare (1 in 2^32 per attempt) but happen at scale; retrying keeps the
|
|
4347
|
+
* affected CLI from being locked out.
|
|
4348
|
+
*/
|
|
4349
|
+
async makeUniqueUserCode(maxAttempts = 5) {
|
|
4350
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
4351
|
+
const candidate = this.makeUserCode();
|
|
4352
|
+
const existing = await this.requestCollection.findByUserCode(candidate);
|
|
4353
|
+
if (!existing || existing.status === "expired") return candidate;
|
|
4354
|
+
}
|
|
4355
|
+
throw new TerminalAuthError("Unable to generate a unique terminal user code; try again.");
|
|
4356
|
+
}
|
|
4357
|
+
/**
|
|
4358
|
+
* Start a new request. Returns the device code the CLI keeps secret, the
|
|
4359
|
+
* user code the human types into the browser, and the verification URL to
|
|
4360
|
+
* open. The device code is stored only as a hash.
|
|
4361
|
+
*/
|
|
4362
|
+
async createRequest(origin) {
|
|
4363
|
+
const trimmedOrigin = origin.replace(/\/+$/u, "");
|
|
4364
|
+
const deviceCode = base64url(randomBytes(32));
|
|
4365
|
+
const userCode = await this.makeUniqueUserCode();
|
|
4366
|
+
const expiresAt = new Date(Date.now() + this.requestTtlSeconds * 1e3);
|
|
4367
|
+
await (await this.requestCollection.create({
|
|
4368
|
+
deviceCodeHash: hashDeviceCode(deviceCode),
|
|
4369
|
+
expiresAt,
|
|
4370
|
+
status: "pending",
|
|
4371
|
+
userCode
|
|
4372
|
+
})).save();
|
|
4373
|
+
return {
|
|
4374
|
+
deviceCode,
|
|
4375
|
+
expiresAt: expiresAt.toISOString(),
|
|
4376
|
+
interval: this.pollIntervalSeconds,
|
|
4377
|
+
userCode,
|
|
4378
|
+
verificationUrl: `${trimmedOrigin}${this.verificationPath}?code=${encodeURIComponent(userCode)}`
|
|
4379
|
+
};
|
|
4380
|
+
}
|
|
4381
|
+
/**
|
|
4382
|
+
* Look up a request by user code. Performs lazy expiry: pending requests
|
|
4383
|
+
* past their TTL are flipped to `expired` and persisted.
|
|
4384
|
+
*/
|
|
4385
|
+
async getRequestForUserCode(userCode) {
|
|
4386
|
+
const request = await this.requestCollection.findByUserCode(userCode);
|
|
4387
|
+
if (!request) return null;
|
|
4388
|
+
if (request.status === "pending" && isExpired(request)) {
|
|
4389
|
+
request.status = "expired";
|
|
4390
|
+
await request.save();
|
|
4391
|
+
}
|
|
4392
|
+
return request;
|
|
4393
|
+
}
|
|
4394
|
+
/**
|
|
4395
|
+
* Mark a pending request as approved and mint a bearer session bound to the
|
|
4396
|
+
* approving user. Idempotent: re-approving an already-approved request is a
|
|
4397
|
+
* no-op. Throws if the user/tenant are missing, the request is unknown, or
|
|
4398
|
+
* the request has expired.
|
|
4399
|
+
*/
|
|
4400
|
+
async approveRequest(input) {
|
|
4401
|
+
if (!input.user.id || !input.tenantId) throw new TerminalAuthError("An authenticated tenant session is required.");
|
|
4402
|
+
this.assertApproveRateLimit(input.user.id);
|
|
4403
|
+
const request = await this.getRequestForUserCode(input.userCode);
|
|
4404
|
+
if (!request) {
|
|
4405
|
+
this.recordFailedApprove(input.user.id);
|
|
4406
|
+
throw new TerminalAuthError("Terminal login request not found.");
|
|
4407
|
+
}
|
|
4408
|
+
if (request.status === "approved") return request;
|
|
4409
|
+
if (request.status !== "pending" || isExpired(request)) {
|
|
4410
|
+
request.status = "expired";
|
|
4411
|
+
await request.save();
|
|
4412
|
+
this.recordFailedApprove(input.user.id);
|
|
4413
|
+
throw new TerminalAuthError("Terminal login request has expired.");
|
|
4414
|
+
}
|
|
4415
|
+
const sessionId = await this.sessionService.createSession(input.user.id, input.tenantId, {
|
|
4416
|
+
data: {
|
|
4417
|
+
approvedBy: input.user.email ?? input.user.id,
|
|
4418
|
+
kind: "terminal"
|
|
4419
|
+
},
|
|
4420
|
+
ipAddress: input.ipAddress,
|
|
4421
|
+
ttl: this.sessionTtlSeconds,
|
|
4422
|
+
userAgent: input.userAgent
|
|
4423
|
+
});
|
|
4424
|
+
request.approvedAt = /* @__PURE__ */ new Date();
|
|
4425
|
+
request.sessionId = sessionId;
|
|
4426
|
+
request.status = "approved";
|
|
4427
|
+
request.tenantId = input.tenantId;
|
|
4428
|
+
request.userId = input.user.id;
|
|
4429
|
+
await request.save();
|
|
4430
|
+
this.failedApprovesByUser.delete(input.user.id);
|
|
4431
|
+
return request;
|
|
4432
|
+
}
|
|
4433
|
+
/**
|
|
4434
|
+
* Throw {@link TerminalAuthRateLimitError} if the user has exceeded the
|
|
4435
|
+
* configured failed-approve budget inside the current sliding window.
|
|
4436
|
+
*/
|
|
4437
|
+
assertApproveRateLimit(userId) {
|
|
4438
|
+
const tracking = this.failedApprovesByUser.get(userId);
|
|
4439
|
+
if (!tracking) return;
|
|
4440
|
+
const elapsedMs = Date.now() - tracking.firstAttemptAt;
|
|
4441
|
+
if (elapsedMs >= this.approveAttemptWindowMs) {
|
|
4442
|
+
this.failedApprovesByUser.delete(userId);
|
|
4443
|
+
return;
|
|
4444
|
+
}
|
|
4445
|
+
if (tracking.count >= this.maxApproveAttempts) throw new TerminalAuthRateLimitError("Too many failed terminal-login attempts. Try again later.", Math.max(1, Math.ceil((this.approveAttemptWindowMs - elapsedMs) / 1e3)));
|
|
4446
|
+
}
|
|
4447
|
+
/** Increment the failed-approve counter for `userId`, starting the window if needed. */
|
|
4448
|
+
recordFailedApprove(userId) {
|
|
4449
|
+
const tracking = this.failedApprovesByUser.get(userId);
|
|
4450
|
+
if (!tracking) {
|
|
4451
|
+
this.failedApprovesByUser.set(userId, {
|
|
4452
|
+
count: 1,
|
|
4453
|
+
firstAttemptAt: Date.now()
|
|
4454
|
+
});
|
|
4455
|
+
return;
|
|
4456
|
+
}
|
|
4457
|
+
tracking.count += 1;
|
|
4458
|
+
}
|
|
4459
|
+
/**
|
|
4460
|
+
* Exchange a device code (the secret the CLI keeps) for an access token
|
|
4461
|
+
* once the request has been approved. Returns `{ status: 'pending' }` while
|
|
4462
|
+
* the CLI should keep polling, and `{ status: 'expired' }` if the request
|
|
4463
|
+
* was never approved within its TTL.
|
|
4464
|
+
*/
|
|
4465
|
+
async exchangeDeviceCode(deviceCode) {
|
|
4466
|
+
const request = await this.requestCollection.findByDeviceCodeHash(hashDeviceCode(deviceCode));
|
|
4467
|
+
if (!request) return { status: "expired" };
|
|
4468
|
+
if (request.status === "approved" && request.sessionId) return {
|
|
4469
|
+
accessToken: request.sessionId,
|
|
4470
|
+
expiresIn: this.sessionTtlSeconds,
|
|
4471
|
+
status: "approved",
|
|
4472
|
+
tokenType: "Bearer"
|
|
4473
|
+
};
|
|
4474
|
+
if (isExpired(request)) {
|
|
4475
|
+
if (request.status === "pending") {
|
|
4476
|
+
request.status = "expired";
|
|
4477
|
+
await request.save();
|
|
4478
|
+
}
|
|
4479
|
+
return { status: "expired" };
|
|
4480
|
+
}
|
|
4481
|
+
if (request.status === "pending") return {
|
|
4482
|
+
expiresAt: new Date(request.expiresAt).toISOString(),
|
|
4483
|
+
interval: this.pollIntervalSeconds,
|
|
4484
|
+
status: "pending"
|
|
4485
|
+
};
|
|
4486
|
+
return { status: "expired" };
|
|
4487
|
+
}
|
|
4488
|
+
/**
|
|
4489
|
+
* Resolve a bearer token issued by this service into a full session
|
|
4490
|
+
* context — load the user, permissions, and tenant just like the cookie
|
|
4491
|
+
* path does. Returns `null` if the token is unknown, expired, or revoked.
|
|
4492
|
+
*/
|
|
4493
|
+
async loadBearerSession(token) {
|
|
4494
|
+
return this.sessionService.loadSessionContext(token);
|
|
4495
|
+
}
|
|
4496
|
+
/**
|
|
4497
|
+
* Revoke a bearer token (CLI logout). Returns true if a session was
|
|
4498
|
+
* actually revoked.
|
|
4499
|
+
*/
|
|
4500
|
+
async destroyBearerSession(token) {
|
|
4501
|
+
return this.sessionService.destroySession(token);
|
|
4502
|
+
}
|
|
4503
|
+
/**
|
|
4504
|
+
* Delete expired pending requests (cleanup job).
|
|
4505
|
+
*/
|
|
4506
|
+
async cleanupExpiredRequests() {
|
|
4507
|
+
return this.requestCollection.deleteExpired();
|
|
4508
|
+
}
|
|
4509
|
+
/** The default polling interval clients should use. Exposed for tests. */
|
|
4510
|
+
get pollInterval() {
|
|
4511
|
+
return this.pollIntervalSeconds;
|
|
4512
|
+
}
|
|
4513
|
+
};
|
|
4514
|
+
var TerminalAuthError = class extends Error {
|
|
4515
|
+
constructor(message) {
|
|
4516
|
+
super(message);
|
|
4517
|
+
this.name = "TerminalAuthError";
|
|
4518
|
+
}
|
|
4519
|
+
};
|
|
4520
|
+
var TerminalAuthRateLimitError = class extends TerminalAuthError {
|
|
4521
|
+
retryAfterSeconds;
|
|
4522
|
+
constructor(message, retryAfterSeconds) {
|
|
4523
|
+
super(message);
|
|
4524
|
+
this.name = "TerminalAuthRateLimitError";
|
|
4525
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
4526
|
+
}
|
|
4527
|
+
};
|
|
4528
|
+
//#endregion
|
|
4529
|
+
export { DEFAULT_ROLE_SLUGS as $, RolePermissionCollection as A, GroupRoleCollection as B, TenantHierarchyError as C, DEFAULT_SESSION_TTL as D, SessionCollection as E, parsePermissionSlug as F, Group as G, GroupMemberCollection as H, MembershipOverrideCollection as I, User as J, UsersCliAuthRequestCollection as K, MembershipOverride as L, PermissionCollection as M, Permission as N, Session as O, isValidPermissionSlug as P, DEFAULT_ROLES as Q, MembershipCollection as R, TenantCollection as S, Tenant as T, GroupMember as U, GroupRole as V, GroupCollection as W, normalizeEmail as X, isValidEmail as Y, AccessRequestStatus as Z, resolveOidcProviderConfig as _, TerminalAuthRateLimitError as a, TenantStatus as at, TenantPermissionOverrideCollection as b, getRequestScopedDatabase as c, PermissionResolver as d, DEFAULT_TENANT_POLICY as et, OidcLoginError as f, getUsersOidcConfig as g, encodeOidcTransaction as h, TerminalAuthError as i, TenantPermissionEffect as it, RolePermission as j, generateSessionId as k, withSessionPermissionContext as l, decodeOidcTransaction as m, DEFAULT_CLI_AUTH_REQUEST_TTL_SECONDS as n, OverrideEffect as nt, TerminalAuthService as o, UserStatus as ot, OidcLoginService as p, UsersCliAuthRequest as q, DEFAULT_CLI_SESSION_TTL_SECONDS as r, SessionStatus as rt, getCurrentSessionPermissionContext as s, DEFAULT_CLI_AUTH_POLL_INTERVAL_SECONDS as t, MembershipStatus as tt, SessionService as u, webapi_exports as v, MAX_TENANT_HIERARCHY_DEPTH as w, TenantPermissionOverride as x, UserCollection as y, Membership as z };
|
|
4530
|
+
|
|
4531
|
+
//# sourceMappingURL=TerminalAuthService-BXAAuaXf.js.map
|