@happyvertical/smrt-users 0.37.1 → 0.37.3
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 +2 -1
- package/README.md +63 -0
- package/dist/chunks/TerminalAuthService-BXAAuaXf.js +4531 -0
- package/dist/chunks/TerminalAuthService-BXAAuaXf.js.map +1 -0
- package/dist/collections/AccessRequestCollection.d.ts +28 -0
- package/dist/collections/AccessRequestCollection.d.ts.map +1 -0
- package/dist/collections/index.d.ts +1 -0
- package/dist/collections/index.d.ts.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1571 -1322
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +421 -2
- package/dist/models/AccessRequest.d.ts +131 -0
- package/dist/models/AccessRequest.d.ts.map +1 -0
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.d.ts.map +1 -1
- package/dist/playground.js +114 -136
- package/dist/playground.js.map +1 -1
- package/dist/services/AccessRequestService.d.ts +326 -0
- package/dist/services/AccessRequestService.d.ts.map +1 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +129 -15
- package/dist/sveltekit.js +685 -881
- package/dist/sveltekit.js.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.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-bY1oWeAh.js +0 -5261
- package/dist/chunks/TerminalAuthService-bY1oWeAh.js.map +0 -1
- package/dist/chunks/index-DBpq-WMK.js +0 -169
- package/dist/chunks/index-DBpq-WMK.js.map +0 -1
package/dist/smrt-knowledge.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-
|
|
3
|
+
"generatedAt": "2026-07-01T20:04:31.726Z",
|
|
4
4
|
"packageName": "@happyvertical/smrt-users",
|
|
5
|
-
"packageVersion": "0.37.
|
|
5
|
+
"packageVersion": "0.37.3",
|
|
6
6
|
"sourceManifestPath": "dist/manifest.json",
|
|
7
7
|
"agentDocPath": "AGENTS.md",
|
|
8
8
|
"sourceHashes": {
|
|
9
|
-
"manifest": "
|
|
10
|
-
"packageJson": "
|
|
11
|
-
"agents": "
|
|
9
|
+
"manifest": "151bae33398ad16290ce7cee2f2f77770df2ac065eb6f4221f549042702237ec",
|
|
10
|
+
"packageJson": "f9351c7506a3421f47aacc48422de62da791beb9ea3a980b61923d18ee4886dd",
|
|
11
|
+
"agents": "7244609f8590febc0f58df6f139e22317a1489541071ac249a5c07557b95dee3"
|
|
12
12
|
},
|
|
13
13
|
"exports": [
|
|
14
14
|
".",
|
|
@@ -26,18 +26,18 @@
|
|
|
26
26
|
"@happyvertical/smrt-tenancy": "workspace:*",
|
|
27
27
|
"@happyvertical/smrt-types": "workspace:*",
|
|
28
28
|
"@happyvertical/smrt-ui": "workspace:*",
|
|
29
|
-
"jose": "^6.
|
|
29
|
+
"jose": "^6.2.3",
|
|
30
30
|
"@happyvertical/smrt-vitest": "workspace:*",
|
|
31
31
|
"@happyvertical/sql": "catalog:",
|
|
32
|
-
"@sveltejs/package": "^2.5.
|
|
33
|
-
"@sveltejs/vite-plugin-svelte": "^
|
|
34
|
-
"@types/node": "
|
|
32
|
+
"@sveltejs/package": "^2.5.8",
|
|
33
|
+
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
|
34
|
+
"@types/node": "24.13.2",
|
|
35
35
|
"fast-glob": "^3.3.3",
|
|
36
|
-
"svelte": "^5.
|
|
37
|
-
"svelte-check": "^4.
|
|
36
|
+
"svelte": "^5.56.4",
|
|
37
|
+
"svelte-check": "^4.7.1",
|
|
38
38
|
"typescript": "^5.9.3",
|
|
39
|
-
"vite": "^
|
|
40
|
-
"vitest": "^4.
|
|
39
|
+
"vite": "^8.1.2",
|
|
40
|
+
"vitest": "^4.1.9"
|
|
41
41
|
},
|
|
42
42
|
"smrtDependencies": [
|
|
43
43
|
"@happyvertical/smrt-config",
|
|
@@ -55,6 +55,28 @@
|
|
|
55
55
|
"tags": [],
|
|
56
56
|
"risks": [],
|
|
57
57
|
"objects": [
|
|
58
|
+
{
|
|
59
|
+
"name": "AccessRequestCollection",
|
|
60
|
+
"qualifiedName": "@happyvertical/smrt-users:AccessRequestCollection",
|
|
61
|
+
"collection": "accessrequests",
|
|
62
|
+
"tableName": "access_requests",
|
|
63
|
+
"packageName": "@happyvertical/smrt-users",
|
|
64
|
+
"extends": "SmrtCollection",
|
|
65
|
+
"fields": [],
|
|
66
|
+
"relationships": [],
|
|
67
|
+
"methods": [
|
|
68
|
+
"findByEmail",
|
|
69
|
+
"findByStatus",
|
|
70
|
+
"findOpen",
|
|
71
|
+
"findOpenByEmail"
|
|
72
|
+
],
|
|
73
|
+
"surfaces": [],
|
|
74
|
+
"relationshipFeatures": [
|
|
75
|
+
"uuidColumns"
|
|
76
|
+
],
|
|
77
|
+
"tags": [],
|
|
78
|
+
"risks": []
|
|
79
|
+
},
|
|
58
80
|
{
|
|
59
81
|
"name": "UsersCliAuthRequestCollection",
|
|
60
82
|
"qualifiedName": "@happyvertical/smrt-users:UsersCliAuthRequestCollection",
|
|
@@ -414,6 +436,98 @@
|
|
|
414
436
|
"tags": [],
|
|
415
437
|
"risks": []
|
|
416
438
|
},
|
|
439
|
+
{
|
|
440
|
+
"name": "AccessRequest",
|
|
441
|
+
"qualifiedName": "@happyvertical/smrt-users:AccessRequest",
|
|
442
|
+
"collection": "accessrequests",
|
|
443
|
+
"tableName": "access_requests",
|
|
444
|
+
"packageName": "@happyvertical/smrt-users",
|
|
445
|
+
"extends": "SmrtObject",
|
|
446
|
+
"fields": [
|
|
447
|
+
{
|
|
448
|
+
"name": "email",
|
|
449
|
+
"type": "text",
|
|
450
|
+
"required": true,
|
|
451
|
+
"columnType": "TEXT"
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
"name": "name",
|
|
455
|
+
"type": "text",
|
|
456
|
+
"required": false,
|
|
457
|
+
"columnType": "TEXT"
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
"name": "status",
|
|
461
|
+
"type": "text",
|
|
462
|
+
"required": false,
|
|
463
|
+
"columnType": "TEXT"
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
"name": "source",
|
|
467
|
+
"type": "text",
|
|
468
|
+
"required": false,
|
|
469
|
+
"columnType": "TEXT"
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
"name": "requestContext",
|
|
473
|
+
"type": "text",
|
|
474
|
+
"required": false,
|
|
475
|
+
"columnType": "TEXT"
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
"name": "note",
|
|
479
|
+
"type": "text",
|
|
480
|
+
"required": false,
|
|
481
|
+
"columnType": "TEXT"
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
"name": "requestedAt",
|
|
485
|
+
"type": "datetime",
|
|
486
|
+
"required": false,
|
|
487
|
+
"columnType": "TIMESTAMP"
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
"name": "decidedAt",
|
|
491
|
+
"type": "datetime",
|
|
492
|
+
"required": false,
|
|
493
|
+
"columnType": "TIMESTAMP"
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
"name": "decidedBy",
|
|
497
|
+
"type": "text",
|
|
498
|
+
"required": false,
|
|
499
|
+
"columnType": "TEXT"
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
"name": "resultingUserId",
|
|
503
|
+
"type": "text",
|
|
504
|
+
"required": false,
|
|
505
|
+
"columnType": "TEXT"
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
"name": "tenantHint",
|
|
509
|
+
"type": "text",
|
|
510
|
+
"required": false,
|
|
511
|
+
"columnType": "TEXT"
|
|
512
|
+
}
|
|
513
|
+
],
|
|
514
|
+
"relationships": [],
|
|
515
|
+
"methods": [
|
|
516
|
+
"getRequestContext",
|
|
517
|
+
"getTenantHint",
|
|
518
|
+
"isApproved",
|
|
519
|
+
"isOpen",
|
|
520
|
+
"isTerminal",
|
|
521
|
+
"setRequestContext",
|
|
522
|
+
"setTenantHint"
|
|
523
|
+
],
|
|
524
|
+
"surfaces": [],
|
|
525
|
+
"relationshipFeatures": [
|
|
526
|
+
"uuidColumns"
|
|
527
|
+
],
|
|
528
|
+
"tags": [],
|
|
529
|
+
"risks": []
|
|
530
|
+
},
|
|
417
531
|
{
|
|
418
532
|
"name": "UsersCliAuthRequest",
|
|
419
533
|
"qualifiedName": "@happyvertical/smrt-users:UsersCliAuthRequest",
|
|
@@ -2739,7 +2853,7 @@
|
|
|
2739
2853
|
"junctionCollections": 0,
|
|
2740
2854
|
"hierarchicalObjects": 0,
|
|
2741
2855
|
"polymorphicAssociations": 0,
|
|
2742
|
-
"uuidColumns":
|
|
2856
|
+
"uuidColumns": 49
|
|
2743
2857
|
},
|
|
2744
|
-
"agentDoc": "# @happyvertical/smrt-users\n\nMulti-tenant user management with RBAC, hierarchical tenants, session handling, and SvelteKit integration.\n\n## Models (
|
|
2858
|
+
"agentDoc": "# @happyvertical/smrt-users\n\nMulti-tenant user management with RBAC, hierarchical tenants, session handling, and SvelteKit integration.\n\n## Models (14)\n\n| Model | Key Pattern |\n|-------|-------------|\n| User | Auth identity. `profileId` is plain string (not FK) to smrt-profiles. Email auto-lowercased. |\n| AccessRequest | \"Request access / waitlist\" record captured before a `User` exists. CLOSED generated surface (`api`/`mcp`/`cli` = `[]`) — all access via `AccessRequestService`. Email normalized + indexed; JSON `requestContext` (NOT `context` — reserved for slug scoping). |\n| Tenant | **STI** + hierarchical parent-child. `hierarchyPath` (materialized path), `hierarchyLevel`. Max depth 10. |\n| Session | Server-side. Secure UUID. TTL in **seconds** (not ms). Status auto-updates to EXPIRED on access. |\n| MagicLinkToken | Single-use email login token. Backed by `MagicLinkService`. |\n| Role | `tenantId = null` → system role (available to all tenants). `isSystem: true` blocks deletion. |\n| Permission | Slug format: `resource.action`. Parsed by PermissionResolver. |\n| Membership | User + Tenant + Role junction. UNIQUE(userId, tenantId). |\n| Group | Team within a tenant. Multiple roles via GroupRole. |\n| GroupMember, GroupRole, RolePermission | Join tables. |\n| MembershipOverride | Per-user permission grant/deny. **DENY always wins.** |\n| TenantPermissionOverride | Tenant-level cascade overrides. Effect: INHERIT/GRANT/DENY. |\n\n## Permission Resolution — Precedence (broad → specific, most-specific wins)\n\n`PermissionResolver.resolvePermissions` builds the effective set in this order;\neach later layer overrides earlier ones:\n\n1. **Tenant-inherited** — walk ancestors, apply each `TenantPermissionOverride`\n down the cascade (GRANT adds, DENY removes within the hierarchy)\n2. **Membership role** — base permissions from the user's role in the tenant\n3. **Group roles** — permissions from all groups the user belongs to **in that tenant**\n4. **Tenant-level DENY** *(removes; overrides role/group grants, tenant-wide)* — a\n `TenantPermissionOverride` with effect `DENY` is a HARD, tenant-wide block: it\n subtracts the DENY'd slug even if a role or group granted it (steps 2–3). It\n sits just **above** the per-user membership overrides and **below** role/group.\n5. **Membership GRANT override** *(re-adds; most specific)* — a per-user GRANT can\n re-add a slug a tenant DENY'd in step 4, because it is more specific.\n6. **Membership DENY override** *(absolute; always wins)* — a per-user DENY removes\n the slug last and is never overridden.\n\nSo a permission a role grants but the tenant DENYs is **removed**, unless that\nexact user also has a membership-GRANT override for it. A membership-DENY always\nwins. Tenant-DENY of an inherited/cascade grant still blocks it (unchanged).\nThe hard block reflects the tenant cascade's **net** resolution, not an\nunconditional union of every DENY in the chain — so a more-specific tenant GRANT\n(e.g. a child sub-tenant re-granting a permission its parent DENYs) still wins.\n\n**Critical**: `getGroupIdsForTenant(userId, tenantId)` (joins with groups table to scope by tenant). Never use `getGroupIds()` — it's cross-tenant.\n\n## Hierarchical Tenants\n\n- `TenantCollection.createChild()` auto-calculates hierarchy fields, enforces depth limit\n- `moveToParent()` updates tenant + ALL descendants' paths/levels\n- `cascadePermissions` (parent pushes down) + `inheritPermissions` (child accepts) — both must be true\n- `getTree(rootId?)` returns nested structure for UI\n\n## SvelteKit Integration\n\n```typescript\n// hooks.server.ts\nexport const handle = createSessionHandler({ db, ttl: 604800, skipPaths: ['/api/public'] });\n// Populates event.locals: { user, membership, permissions: string[], tenantId, sessionId }\n\n// +page.server.ts\nawait createSessionCookie(event, userId, tenantId, { db });\nawait destroySessionCookie(event, { db });\nawait switchSessionTenant(event, tenantId, { db });\n```\n\n## Security (S5 #1400)\n\n- **Generated REST/MCP surface is READ-ONLY for every RBAC/identity model.**\n User, Tenant, Group, Membership, MembershipOverride, Role, Permission,\n RolePermission, GroupRole, GroupMember, and TenantPermissionOverride generate\n `list`/`get` only — `create`/`update`/`delete` are intentionally NOT\n generated. The merged `requireRouteAuth` gate (#1540) enforces *authentication*,\n not *authorization*, and these models are not `@TenantScoped`, so an\n auto-generated mutating route would let any authenticated user self-grant a\n role/permission, flip a tenant's cascade flags, or change another user's auth\n identity. Mutate them through the permission-gated services (`TenantService`,\n collection helpers) or consumer-owned, permission-checked handlers. A\n structural regression test (`security-audit-1400.test.ts`) enumerates the\n registry to assert no authority model exposes a mutating op. (`cli` stays\n enabled — local-operator surface, outside the network/agent threat model.)\n- **`switchTenant` is fail-closed AND rotates the session id.**\n `SessionService.switchTenant` / `switchSessionTenant` verify the session's user\n has an ACTIVE membership in the target tenant before any write (the tenant id\n is the isolation key for every `@TenantScoped` query). A non-member/unknown-\n session switch returns `{ switched: false, sessionId: null, ... }` and mutates\n nothing. On a successful switch into a NON-null tenant the session id is\n ROTATED: a fresh `Session` (new secure id, fresh TTL, same user, new tenant,\n device context carried over) is minted and the old session is REVOKED — so a\n captured pre-switch id immediately stops validating, shrinking the blast radius\n of a leaked id across a tenant boundary. `switchTenant` returns a\n `SwitchTenantResult` (`{ switched, sessionId, session, rotated }`); callers MUST\n persist the returned `sessionId`. `switchSessionTenant` does this for you by\n re-setting the session cookie (preserving httpOnly/secure/sameSite) to the new\n id. A `null` clear stays in place (no rotation, no cookie change). The\n low-level `SessionCollection.setSessionTenant` is the UNGUARDED primitive (used\n for the null-clear path) — never call it with an untrusted tenant id.\n- **OIDC `email_verified` is enforced.** `UserCollection.getOrCreateFromOidc`\n refuses to provision a user when the IdP explicitly returns\n `email_verified: false` (opt out with `{ allowUnverifiedEmail: true }`). An\n absent claim makes no assertion and is not enforced.\n\n## Gotchas\n\n- **seedSystemRoles() required**: call `RoleCollection.seedSystemRoles()` at app init (creates owner/admin/member/viewer)\n- **PermissionResolver casts `as any`**: collections have protected constructors — known framework limitation\n- **Session TTL in seconds**: `DEFAULT_SESSION_TTL = 7 * 24 * 60 * 60` (not milliseconds)\n- **Users are cross-tenant**: one user, many tenants via Membership. Email globally unique.\n- **Batch permission queries**: resolver fetches all permission IDs in one query, then maps to slugs (avoids N+1)\n"
|
|
2745
2859
|
}
|