@happyvertical/smrt-users 0.31.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +40 -15
- package/dist/chunks/{TerminalAuthService-DoAMQ_yn.js → TerminalAuthService-D5VVPG9e.js} +247 -89
- package/dist/chunks/TerminalAuthService-D5VVPG9e.js.map +1 -0
- package/dist/chunks/{index-DkoYIvIu.js → index-CitgZk-4.js} +10 -10
- package/dist/chunks/{index-DkoYIvIu.js.map → index-CitgZk-4.js.map} +1 -1
- package/dist/collections/GroupMemberCollection.d.ts +9 -0
- package/dist/collections/GroupMemberCollection.d.ts.map +1 -1
- package/dist/collections/SessionCollection.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +38 -100
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +2 -2
- package/dist/services/PermissionResolver.d.ts +24 -3
- package/dist/services/PermissionResolver.d.ts.map +1 -1
- package/dist/services/SessionService.d.ts +42 -5
- package/dist/services/SessionService.d.ts.map +1 -1
- package/dist/services/index.d.ts +1 -1
- package/dist/services/index.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +6 -6
- package/dist/svelte/components/InviteUserModal.svelte +72 -169
- package/dist/svelte/components/InviteUserModal.svelte.d.ts.map +1 -1
- package/dist/svelte/components/UserCard.svelte +2 -1
- package/dist/svelte/components/UserCard.svelte.d.ts.map +1 -1
- package/dist/svelte/components/UserForm.svelte +11 -4
- package/dist/svelte/components/UserForm.svelte.d.ts.map +1 -1
- package/dist/svelte/components/UserMenu.svelte +100 -25
- package/dist/svelte/components/UserMenu.svelte.d.ts +5 -4
- package/dist/svelte/components/UserMenu.svelte.d.ts.map +1 -1
- package/dist/svelte/components/__tests__/InviteUserModal.test.js +11 -0
- package/dist/svelte/components/__tests__/UserMenu.test.js +45 -0
- package/dist/svelte/components/__tests__/UserStatus.test.js +36 -0
- package/dist/sveltekit/index.d.ts +17 -1
- package/dist/sveltekit/index.d.ts.map +1 -1
- package/dist/sveltekit.js +37 -9
- package/dist/sveltekit.js.map +1 -1
- package/package.json +8 -8
- package/dist/chunks/TerminalAuthService-DoAMQ_yn.js.map +0 -1
package/AGENTS.md
CHANGED
|
@@ -18,14 +18,30 @@ Multi-tenant user management with RBAC, hierarchical tenants, session handling,
|
|
|
18
18
|
| MembershipOverride | Per-user permission grant/deny. **DENY always wins.** |
|
|
19
19
|
| TenantPermissionOverride | Tenant-level cascade overrides. Effect: INHERIT/GRANT/DENY. |
|
|
20
20
|
|
|
21
|
-
## Permission Resolution —
|
|
22
|
-
|
|
23
|
-
PermissionResolver
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
## Permission Resolution — Precedence (broad → specific, most-specific wins)
|
|
22
|
+
|
|
23
|
+
`PermissionResolver.resolvePermissions` builds the effective set in this order;
|
|
24
|
+
each later layer overrides earlier ones:
|
|
25
|
+
|
|
26
|
+
1. **Tenant-inherited** — walk ancestors, apply each `TenantPermissionOverride`
|
|
27
|
+
down the cascade (GRANT adds, DENY removes within the hierarchy)
|
|
28
|
+
2. **Membership role** — base permissions from the user's role in the tenant
|
|
29
|
+
3. **Group roles** — permissions from all groups the user belongs to **in that tenant**
|
|
30
|
+
4. **Tenant-level DENY** *(removes; overrides role/group grants, tenant-wide)* — a
|
|
31
|
+
`TenantPermissionOverride` with effect `DENY` is a HARD, tenant-wide block: it
|
|
32
|
+
subtracts the DENY'd slug even if a role or group granted it (steps 2–3). It
|
|
33
|
+
sits just **above** the per-user membership overrides and **below** role/group.
|
|
34
|
+
5. **Membership GRANT override** *(re-adds; most specific)* — a per-user GRANT can
|
|
35
|
+
re-add a slug a tenant DENY'd in step 4, because it is more specific.
|
|
36
|
+
6. **Membership DENY override** *(absolute; always wins)* — a per-user DENY removes
|
|
37
|
+
the slug last and is never overridden.
|
|
38
|
+
|
|
39
|
+
So a permission a role grants but the tenant DENYs is **removed**, unless that
|
|
40
|
+
exact user also has a membership-GRANT override for it. A membership-DENY always
|
|
41
|
+
wins. Tenant-DENY of an inherited/cascade grant still blocks it (unchanged).
|
|
42
|
+
The hard block reflects the tenant cascade's **net** resolution, not an
|
|
43
|
+
unconditional union of every DENY in the chain — so a more-specific tenant GRANT
|
|
44
|
+
(e.g. a child sub-tenant re-granting a permission its parent DENYs) still wins.
|
|
29
45
|
|
|
30
46
|
**Critical**: `getGroupIdsForTenant(userId, tenantId)` (joins with groups table to scope by tenant). Never use `getGroupIds()` — it's cross-tenant.
|
|
31
47
|
|
|
@@ -64,13 +80,22 @@ await switchSessionTenant(event, tenantId, { db });
|
|
|
64
80
|
structural regression test (`security-audit-1400.test.ts`) enumerates the
|
|
65
81
|
registry to assert no authority model exposes a mutating op. (`cli` stays
|
|
66
82
|
enabled — local-operator surface, outside the network/agent threat model.)
|
|
67
|
-
- **`switchTenant` is fail-closed
|
|
68
|
-
`switchSessionTenant` verify the session's user
|
|
69
|
-
target tenant before
|
|
70
|
-
every `@TenantScoped` query). A non-member
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
83
|
+
- **`switchTenant` is fail-closed AND rotates the session id.**
|
|
84
|
+
`SessionService.switchTenant` / `switchSessionTenant` verify the session's user
|
|
85
|
+
has an ACTIVE membership in the target tenant before any write (the tenant id
|
|
86
|
+
is the isolation key for every `@TenantScoped` query). A non-member/unknown-
|
|
87
|
+
session switch returns `{ switched: false, sessionId: null, ... }` and mutates
|
|
88
|
+
nothing. On a successful switch into a NON-null tenant the session id is
|
|
89
|
+
ROTATED: a fresh `Session` (new secure id, fresh TTL, same user, new tenant,
|
|
90
|
+
device context carried over) is minted and the old session is REVOKED — so a
|
|
91
|
+
captured pre-switch id immediately stops validating, shrinking the blast radius
|
|
92
|
+
of a leaked id across a tenant boundary. `switchTenant` returns a
|
|
93
|
+
`SwitchTenantResult` (`{ switched, sessionId, session, rotated }`); callers MUST
|
|
94
|
+
persist the returned `sessionId`. `switchSessionTenant` does this for you by
|
|
95
|
+
re-setting the session cookie (preserving httpOnly/secure/sameSite) to the new
|
|
96
|
+
id. A `null` clear stays in place (no rotation, no cookie change). The
|
|
97
|
+
low-level `SessionCollection.setSessionTenant` is the UNGUARDED primitive (used
|
|
98
|
+
for the null-clear path) — never call it with an untrusted tenant id.
|
|
74
99
|
- **OIDC `email_verified` is enforced.** `UserCollection.getOrCreateFromOidc`
|
|
75
100
|
refuses to provision a user when the IdP explicitly returns
|
|
76
101
|
`email_verified: false` (opt out with `{ allowUnverifiedEmail: true }`). An
|
|
@@ -6,14 +6,14 @@ import { MembershipStatus, OverrideEffect, SessionStatus, TenantStatus, TenantPe
|
|
|
6
6
|
ObjectRegistry.registerPackageManifest(
|
|
7
7
|
new URL("./manifest.json", import.meta.url)
|
|
8
8
|
);
|
|
9
|
-
var __defProp$
|
|
10
|
-
var __getOwnPropDesc$
|
|
11
|
-
var __decorateClass$
|
|
12
|
-
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$
|
|
9
|
+
var __defProp$a = Object.defineProperty;
|
|
10
|
+
var __getOwnPropDesc$b = Object.getOwnPropertyDescriptor;
|
|
11
|
+
var __decorateClass$b = (decorators, target, key, kind) => {
|
|
12
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$b(target, key) : target;
|
|
13
13
|
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
14
14
|
if (decorator = decorators[i])
|
|
15
15
|
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
16
|
-
if (kind && result) __defProp$
|
|
16
|
+
if (kind && result) __defProp$a(target, key, result);
|
|
17
17
|
return result;
|
|
18
18
|
};
|
|
19
19
|
let UsersCliAuthRequest = class extends SmrtObject {
|
|
@@ -26,31 +26,31 @@ let UsersCliAuthRequest = class extends SmrtObject {
|
|
|
26
26
|
expiresAt = /* @__PURE__ */ new Date();
|
|
27
27
|
approvedAt = null;
|
|
28
28
|
};
|
|
29
|
-
__decorateClass$
|
|
29
|
+
__decorateClass$b([
|
|
30
30
|
field({ type: "text" })
|
|
31
31
|
], UsersCliAuthRequest.prototype, "userCode", 2);
|
|
32
|
-
__decorateClass$
|
|
32
|
+
__decorateClass$b([
|
|
33
33
|
field({ type: "text" })
|
|
34
34
|
], UsersCliAuthRequest.prototype, "deviceCodeHash", 2);
|
|
35
|
-
__decorateClass$
|
|
35
|
+
__decorateClass$b([
|
|
36
36
|
field({ type: "text" })
|
|
37
37
|
], UsersCliAuthRequest.prototype, "status", 2);
|
|
38
|
-
__decorateClass$
|
|
38
|
+
__decorateClass$b([
|
|
39
39
|
field({ type: "text" })
|
|
40
40
|
], UsersCliAuthRequest.prototype, "userId", 2);
|
|
41
|
-
__decorateClass$
|
|
41
|
+
__decorateClass$b([
|
|
42
42
|
field({ type: "text" })
|
|
43
43
|
], UsersCliAuthRequest.prototype, "tenantId", 2);
|
|
44
|
-
__decorateClass$
|
|
44
|
+
__decorateClass$b([
|
|
45
45
|
field({ type: "text" })
|
|
46
46
|
], UsersCliAuthRequest.prototype, "sessionId", 2);
|
|
47
|
-
__decorateClass$
|
|
47
|
+
__decorateClass$b([
|
|
48
48
|
field({ type: "datetime" })
|
|
49
49
|
], UsersCliAuthRequest.prototype, "expiresAt", 2);
|
|
50
|
-
__decorateClass$
|
|
50
|
+
__decorateClass$b([
|
|
51
51
|
field({ type: "datetime", nullable: true })
|
|
52
52
|
], UsersCliAuthRequest.prototype, "approvedAt", 2);
|
|
53
|
-
UsersCliAuthRequest = __decorateClass$
|
|
53
|
+
UsersCliAuthRequest = __decorateClass$b([
|
|
54
54
|
smrt({
|
|
55
55
|
tableName: "users_cli_auth_requests",
|
|
56
56
|
api: { include: [] },
|
|
@@ -98,6 +98,68 @@ class UsersCliAuthRequestCollection extends SmrtCollection {
|
|
|
98
98
|
return count;
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
+
var __defProp$9 = Object.defineProperty;
|
|
102
|
+
var __getOwnPropDesc$a = Object.getOwnPropertyDescriptor;
|
|
103
|
+
var __decorateClass$a = (decorators, target, key, kind) => {
|
|
104
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$a(target, key) : target;
|
|
105
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
106
|
+
if (decorator = decorators[i])
|
|
107
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
108
|
+
if (kind && result) __defProp$9(target, key, result);
|
|
109
|
+
return result;
|
|
110
|
+
};
|
|
111
|
+
let Group = class extends SmrtObject {
|
|
112
|
+
tenantId;
|
|
113
|
+
/**
|
|
114
|
+
* Display name for the group
|
|
115
|
+
*/
|
|
116
|
+
name = "";
|
|
117
|
+
/**
|
|
118
|
+
* Description of the group
|
|
119
|
+
*/
|
|
120
|
+
description = "";
|
|
121
|
+
constructor(options = {}) {
|
|
122
|
+
super(options);
|
|
123
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
124
|
+
if (options.name !== void 0) this.name = options.name;
|
|
125
|
+
if (options.description !== void 0)
|
|
126
|
+
this.description = options.description;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
__decorateClass$a([
|
|
130
|
+
foreignKey("Tenant", { required: true })
|
|
131
|
+
], Group.prototype, "tenantId", 2);
|
|
132
|
+
Group = __decorateClass$a([
|
|
133
|
+
smrt({
|
|
134
|
+
// #1400: read-only generated surface — RBAC/identity writes go through
|
|
135
|
+
// permission-gated services, not auth-only generated CRUD.
|
|
136
|
+
api: { include: ["list", "get"] },
|
|
137
|
+
mcp: { include: ["list", "get"] },
|
|
138
|
+
cli: true
|
|
139
|
+
})
|
|
140
|
+
], Group);
|
|
141
|
+
class GroupCollection extends SmrtCollection {
|
|
142
|
+
static _itemClass = Group;
|
|
143
|
+
/**
|
|
144
|
+
* Find all groups in a tenant
|
|
145
|
+
*/
|
|
146
|
+
async findByTenant(tenantId) {
|
|
147
|
+
return await this.list({
|
|
148
|
+
where: { tenantId },
|
|
149
|
+
orderBy: "name ASC"
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Find group by slug within a tenant
|
|
154
|
+
*/
|
|
155
|
+
async findBySlug(slug, tenantId) {
|
|
156
|
+
const results = await this.list({
|
|
157
|
+
where: { slug, tenantId },
|
|
158
|
+
limit: 1
|
|
159
|
+
});
|
|
160
|
+
return results.length > 0 ? results[0] : null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
101
163
|
var __defProp$8 = Object.defineProperty;
|
|
102
164
|
var __getOwnPropDesc$9 = Object.getOwnPropertyDescriptor;
|
|
103
165
|
var __decorateClass$9 = (decorators, target, key, kind) => {
|
|
@@ -135,6 +197,22 @@ GroupMember = __decorateClass$9([
|
|
|
135
197
|
], GroupMember);
|
|
136
198
|
class GroupMemberCollection extends SmrtCollection {
|
|
137
199
|
static _itemClass = GroupMember;
|
|
200
|
+
/** Memoized Group collection, used to resolve the Group table name. */
|
|
201
|
+
groupCollection;
|
|
202
|
+
/**
|
|
203
|
+
* Resolve the database table name for the `Group` model from the registry
|
|
204
|
+
* (via a shared-connection GroupCollection) rather than hardcoding `groups`.
|
|
205
|
+
* A `@smrt({ tableName })` override or table prefix on Group would otherwise
|
|
206
|
+
* make raw joins reference a non-existent or foreign table.
|
|
207
|
+
*/
|
|
208
|
+
async getGroupTableName() {
|
|
209
|
+
if (!this.groupCollection) {
|
|
210
|
+
this.groupCollection = await GroupCollection.create({
|
|
211
|
+
db: this.options.db
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return this.groupCollection.tableName;
|
|
215
|
+
}
|
|
138
216
|
/**
|
|
139
217
|
* Find all members of a group
|
|
140
218
|
*/
|
|
@@ -203,10 +281,11 @@ class GroupMemberCollection extends SmrtCollection {
|
|
|
203
281
|
* This prevents cross-tenant permission leakage by filtering groups by tenant
|
|
204
282
|
*/
|
|
205
283
|
async getGroupIdsForTenant(userId, tenantId) {
|
|
284
|
+
const groupTable = await this.getGroupTableName();
|
|
206
285
|
const sql = `
|
|
207
286
|
SELECT gm.group_id
|
|
208
287
|
FROM ${this.tableName} gm
|
|
209
|
-
INNER JOIN
|
|
288
|
+
INNER JOIN ${groupTable} g ON g.id = gm.group_id
|
|
210
289
|
WHERE gm.user_id = ? AND g.tenant_id = ?
|
|
211
290
|
`;
|
|
212
291
|
const result = await this.db.query(sql, userId, tenantId);
|
|
@@ -1047,8 +1126,17 @@ class SessionCollection extends SmrtCollection {
|
|
|
1047
1126
|
if (!session) return null;
|
|
1048
1127
|
if (!session.isValid()) {
|
|
1049
1128
|
if (session.isExpired() && session.status === SessionStatus.ACTIVE) {
|
|
1050
|
-
|
|
1051
|
-
await
|
|
1129
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1130
|
+
await this.db.query(
|
|
1131
|
+
`UPDATE ${this.tableName}
|
|
1132
|
+
SET status = ?, updated_at = ?
|
|
1133
|
+
WHERE id = ? AND status = ? AND expires_at < ?`,
|
|
1134
|
+
SessionStatus.EXPIRED,
|
|
1135
|
+
now,
|
|
1136
|
+
sessionId,
|
|
1137
|
+
SessionStatus.ACTIVE,
|
|
1138
|
+
now
|
|
1139
|
+
);
|
|
1052
1140
|
}
|
|
1053
1141
|
return null;
|
|
1054
1142
|
}
|
|
@@ -4207,7 +4295,8 @@ class PermissionResolver {
|
|
|
4207
4295
|
const result = {
|
|
4208
4296
|
permissions: /* @__PURE__ */ new Set(),
|
|
4209
4297
|
contributingTenantIds: [],
|
|
4210
|
-
inheritanceActive: false
|
|
4298
|
+
inheritanceActive: false,
|
|
4299
|
+
deniedPermissions: /* @__PURE__ */ new Set()
|
|
4211
4300
|
};
|
|
4212
4301
|
const tenant = await this.tenantCollection.get({ id: tenantId });
|
|
4213
4302
|
if (!tenant) {
|
|
@@ -4220,9 +4309,13 @@ class PermissionResolver {
|
|
|
4220
4309
|
chainTenantIds
|
|
4221
4310
|
);
|
|
4222
4311
|
const allPermissionIds = /* @__PURE__ */ new Set();
|
|
4312
|
+
const deniedPermissionIds = /* @__PURE__ */ new Set();
|
|
4223
4313
|
for (const overrides of allOverridesMap.values()) {
|
|
4224
4314
|
for (const id of overrides.grantedPermissionIds) allPermissionIds.add(id);
|
|
4225
|
-
for (const id of overrides.deniedPermissionIds)
|
|
4315
|
+
for (const id of overrides.deniedPermissionIds) {
|
|
4316
|
+
allPermissionIds.add(id);
|
|
4317
|
+
deniedPermissionIds.add(id);
|
|
4318
|
+
}
|
|
4226
4319
|
}
|
|
4227
4320
|
let inheritedPermissions = /* @__PURE__ */ new Set();
|
|
4228
4321
|
for (let i = 0; i < chain.length; i++) {
|
|
@@ -4270,6 +4363,13 @@ class PermissionResolver {
|
|
|
4270
4363
|
result.permissions.add(perm.slug);
|
|
4271
4364
|
}
|
|
4272
4365
|
}
|
|
4366
|
+
for (const permId of deniedPermissionIds) {
|
|
4367
|
+
if (inheritedPermissions.has(permId)) continue;
|
|
4368
|
+
const perm = permissionsMap.get(permId);
|
|
4369
|
+
if (perm?.slug) {
|
|
4370
|
+
result.deniedPermissions.add(perm.slug);
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4273
4373
|
}
|
|
4274
4374
|
return result;
|
|
4275
4375
|
}
|
|
@@ -4301,13 +4401,23 @@ class PermissionResolver {
|
|
|
4301
4401
|
return chain;
|
|
4302
4402
|
}
|
|
4303
4403
|
/**
|
|
4304
|
-
* Resolve all effective permissions for a user in a tenant
|
|
4404
|
+
* Resolve all effective permissions for a user in a tenant.
|
|
4405
|
+
*
|
|
4406
|
+
* Precedence (broad -> specific, most-specific wins):
|
|
4407
|
+
* tenant-inherited (cascade)
|
|
4408
|
+
* -> role
|
|
4409
|
+
* -> group roles
|
|
4410
|
+
* -> tenant-DENY (removes; overrides role/group grants, tenant-wide)
|
|
4411
|
+
* -> membership GRANT (re-adds; most specific, can win over a tenant-DENY)
|
|
4412
|
+
* -> membership DENY (absolute; always wins)
|
|
4305
4413
|
*
|
|
4306
4414
|
* Algorithm:
|
|
4307
4415
|
* 1. Get membership and collect all permission IDs from all sources
|
|
4308
4416
|
* 2. Batch fetch all permissions in a single query
|
|
4309
|
-
* 3. Apply permissions from role, groups
|
|
4310
|
-
* 4. DENY
|
|
4417
|
+
* 3. Apply permissions from role, then groups
|
|
4418
|
+
* 4. Subtract tenant-level DENY'd slugs (hard tenant-wide block)
|
|
4419
|
+
* 5. Apply membership GRANT overrides (can re-add a tenant-DENY'd slug)
|
|
4420
|
+
* 6. Subtract membership DENY overrides (absolute precedence)
|
|
4311
4421
|
*/
|
|
4312
4422
|
async resolvePermissions(userId, tenantId, options = {}) {
|
|
4313
4423
|
const result = {
|
|
@@ -4396,6 +4506,9 @@ class PermissionResolver {
|
|
|
4396
4506
|
}
|
|
4397
4507
|
}
|
|
4398
4508
|
}
|
|
4509
|
+
for (const slug of tenantPermissions.deniedPermissions) {
|
|
4510
|
+
result.permissions.delete(slug);
|
|
4511
|
+
}
|
|
4399
4512
|
for (const permId of grantedPermissionIds) {
|
|
4400
4513
|
const slug = permissionIdToSlug.get(permId);
|
|
4401
4514
|
if (slug) {
|
|
@@ -4555,23 +4668,66 @@ class SessionService {
|
|
|
4555
4668
|
* query, so it must never be set to a tenant the session's user is not an
|
|
4556
4669
|
* active member of — otherwise a caller could read/write another tenant's data
|
|
4557
4670
|
* by feeding an arbitrary id here (e.g. straight from untrusted form data).
|
|
4558
|
-
*
|
|
4559
|
-
*
|
|
4560
|
-
*
|
|
4671
|
+
*
|
|
4672
|
+
* Fail-closed (#1400): the user's ACTIVE membership in the target tenant is
|
|
4673
|
+
* verified BEFORE any write. A non-member switch returns
|
|
4674
|
+
* `{ switched: false, ... }` and mutates nothing.
|
|
4675
|
+
*
|
|
4676
|
+
* Session-id ROTATION (#1354 follow-up): a successful switch into a non-null
|
|
4677
|
+
* tenant mints a BRAND-NEW session (fresh secure id, fresh TTL) for the same
|
|
4678
|
+
* user with the new tenant, then REVOKES the old session — so any captured
|
|
4679
|
+
* pre-switch session id immediately stops validating, shrinking the blast
|
|
4680
|
+
* radius of a leaked id across a privilege/tenant boundary. The device context
|
|
4681
|
+
* (user agent, IP, custom data) carries over to the new session. Callers MUST
|
|
4682
|
+
* persist the returned `sessionId` (e.g. re-set the cookie).
|
|
4683
|
+
*
|
|
4684
|
+
* Passing `null` clears the tenant context, is always allowed, and stays
|
|
4685
|
+
* in-place (no rotation — there is no privilege boundary being crossed).
|
|
4686
|
+
*
|
|
4687
|
+
* @returns A {@link SwitchTenantResult}; check `switched` for success.
|
|
4561
4688
|
*/
|
|
4562
4689
|
async switchTenant(sessionId, tenantId) {
|
|
4690
|
+
const failClosed = {
|
|
4691
|
+
switched: false,
|
|
4692
|
+
sessionId: null,
|
|
4693
|
+
session: null,
|
|
4694
|
+
rotated: false
|
|
4695
|
+
};
|
|
4563
4696
|
const session = await this.sessionCollection.findValidSession(sessionId);
|
|
4564
|
-
if (!session) return
|
|
4565
|
-
if (tenantId
|
|
4566
|
-
const
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4697
|
+
if (!session) return failClosed;
|
|
4698
|
+
if (tenantId === null) {
|
|
4699
|
+
const ok = await this.sessionCollection.setSessionTenant(sessionId, null);
|
|
4700
|
+
if (!ok) return failClosed;
|
|
4701
|
+
const updated = await this.sessionCollection.findValidSession(sessionId);
|
|
4702
|
+
return {
|
|
4703
|
+
switched: true,
|
|
4704
|
+
sessionId,
|
|
4705
|
+
session: updated,
|
|
4706
|
+
rotated: false
|
|
4707
|
+
};
|
|
4573
4708
|
}
|
|
4574
|
-
|
|
4709
|
+
const membership = await this.membershipCollection.findByUserAndTenant(
|
|
4710
|
+
session.userId,
|
|
4711
|
+
tenantId
|
|
4712
|
+
);
|
|
4713
|
+
if (!membership || !membership.isActive()) {
|
|
4714
|
+
return failClosed;
|
|
4715
|
+
}
|
|
4716
|
+
await this.sessionCollection.revokeSession(sessionId);
|
|
4717
|
+
const rotated = await this.sessionCollection.createSession({
|
|
4718
|
+
userId: session.userId,
|
|
4719
|
+
tenantId,
|
|
4720
|
+
ttl: this.defaultTTL,
|
|
4721
|
+
userAgent: session.userAgent,
|
|
4722
|
+
ipAddress: session.ipAddress,
|
|
4723
|
+
data: session.data
|
|
4724
|
+
});
|
|
4725
|
+
return {
|
|
4726
|
+
switched: true,
|
|
4727
|
+
sessionId: rotated.id,
|
|
4728
|
+
session: rotated,
|
|
4729
|
+
rotated: true
|
|
4730
|
+
};
|
|
4575
4731
|
}
|
|
4576
4732
|
/**
|
|
4577
4733
|
* Get all active sessions for a user (for "manage sessions" UI)
|
|
@@ -5042,53 +5198,55 @@ class TerminalAuthRateLimitError extends TerminalAuthError {
|
|
|
5042
5198
|
}
|
|
5043
5199
|
}
|
|
5044
5200
|
export {
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5201
|
+
checkKeyLength as $,
|
|
5202
|
+
Tenant as A,
|
|
5203
|
+
TenantHierarchyError as B,
|
|
5204
|
+
TenantPermissionOverride as C,
|
|
5049
5205
|
DEFAULT_ROLES as D,
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5206
|
+
TenantPermissionOverrideCollection as E,
|
|
5207
|
+
TerminalAuthError as F,
|
|
5208
|
+
Group as G,
|
|
5209
|
+
TerminalAuthRateLimitError as H,
|
|
5210
|
+
TerminalAuthService as I,
|
|
5211
|
+
User as J,
|
|
5212
|
+
UserCollection as K,
|
|
5213
|
+
decodeOidcTransaction as L,
|
|
5058
5214
|
MembershipCollection as M,
|
|
5059
|
-
|
|
5215
|
+
encodeOidcTransaction as N,
|
|
5060
5216
|
OidcLoginError as O,
|
|
5061
5217
|
PermissionCollection as P,
|
|
5062
|
-
|
|
5218
|
+
generateSessionId as Q,
|
|
5063
5219
|
RolePermission as R,
|
|
5064
5220
|
Session as S,
|
|
5065
5221
|
TenantCollection as T,
|
|
5066
5222
|
UsersCliAuthRequest as U,
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5223
|
+
getCurrentSessionPermissionContext as V,
|
|
5224
|
+
getRequestScopedDatabase as W,
|
|
5225
|
+
getUsersOidcConfig as X,
|
|
5226
|
+
resolveOidcProviderConfig as Y,
|
|
5227
|
+
withSessionPermissionContext as Z,
|
|
5228
|
+
getSigKey as _,
|
|
5073
5229
|
isValidPermissionSlug as a,
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5230
|
+
subtleAlgorithm as a0,
|
|
5231
|
+
JWSInvalid as a1,
|
|
5232
|
+
isDisjoint as a2,
|
|
5233
|
+
validateCrit as a3,
|
|
5234
|
+
checkKeyType as a4,
|
|
5235
|
+
encode as a5,
|
|
5236
|
+
encode$1 as a6,
|
|
5237
|
+
concat as a7,
|
|
5238
|
+
normalizeKey as a8,
|
|
5239
|
+
JWTClaimsBuilder as a9,
|
|
5240
|
+
JWTInvalid as aa,
|
|
5241
|
+
errors as ab,
|
|
5242
|
+
jwtVerify as ac,
|
|
5243
|
+
compactVerify as ad,
|
|
5244
|
+
createLocalJWKSet as ae,
|
|
5245
|
+
createRemoteJWKSet as af,
|
|
5246
|
+
customFetch as ag,
|
|
5247
|
+
flattenedVerify as ah,
|
|
5248
|
+
importJWK as ai,
|
|
5249
|
+
jwksCache as aj,
|
|
5092
5250
|
DEFAULT_TENANT_POLICY as b,
|
|
5093
5251
|
DEFAULT_ROLE_SLUGS as c,
|
|
5094
5252
|
UsersCliAuthRequestCollection as d,
|
|
@@ -5097,22 +5255,22 @@ export {
|
|
|
5097
5255
|
DEFAULT_CLI_SESSION_TTL_SECONDS as g,
|
|
5098
5256
|
DEFAULT_SESSION_TTL as h,
|
|
5099
5257
|
isValidEmail as i,
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5258
|
+
GroupCollection as j,
|
|
5259
|
+
GroupMember as k,
|
|
5260
|
+
GroupMemberCollection as l,
|
|
5261
|
+
GroupRole as m,
|
|
5104
5262
|
normalizeEmail as n,
|
|
5105
|
-
|
|
5263
|
+
GroupRoleCollection as o,
|
|
5106
5264
|
parsePermissionSlug as p,
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5265
|
+
MAX_TENANT_HIERARCHY_DEPTH as q,
|
|
5266
|
+
Membership as r,
|
|
5267
|
+
MembershipOverride as s,
|
|
5268
|
+
MembershipOverrideCollection as t,
|
|
5269
|
+
OidcLoginService as u,
|
|
5270
|
+
Permission as v,
|
|
5271
|
+
PermissionResolver as w,
|
|
5272
|
+
RolePermissionCollection as x,
|
|
5273
|
+
SessionCollection as y,
|
|
5274
|
+
SessionService as z
|
|
5117
5275
|
};
|
|
5118
|
-
//# sourceMappingURL=TerminalAuthService-
|
|
5276
|
+
//# sourceMappingURL=TerminalAuthService-D5VVPG9e.js.map
|