@happyvertical/smrt-users 0.52.0 → 0.53.5
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 +43 -2
- package/README.md +227 -2
- package/agents/permissions.md +12 -1
- package/agents/retention.md +14 -7
- package/dist/chunks/{TerminalAuthService-Br64vHtd.js → TerminalAuthService-BdB_0vDA.js} +3492 -2172
- package/dist/chunks/TerminalAuthService-BdB_0vDA.js.map +1 -0
- package/dist/collections/CliAuthApproveLimitCollection.d.ts +4 -0
- package/dist/collections/CliAuthApproveLimitCollection.d.ts.map +1 -1
- package/dist/collections/CliAuthRequestCollection.d.ts.map +1 -1
- package/dist/collections/LoginAttemptCollection.d.ts +105 -0
- package/dist/collections/LoginAttemptCollection.d.ts.map +1 -0
- package/dist/collections/LoginAuditEventCollection.d.ts +19 -0
- package/dist/collections/LoginAuditEventCollection.d.ts.map +1 -0
- package/dist/collections/PinCredentialCollection.d.ts +26 -0
- package/dist/collections/PinCredentialCollection.d.ts.map +1 -0
- package/dist/collections/RoleCollection.d.ts.map +1 -1
- package/dist/collections/RolePermissionCollection.d.ts +4 -0
- package/dist/collections/RolePermissionCollection.d.ts.map +1 -1
- package/dist/collections/SessionCollection.d.ts +33 -1
- package/dist/collections/SessionCollection.d.ts.map +1 -1
- package/dist/collections/index.d.ts +3 -0
- package/dist/collections/index.d.ts.map +1 -1
- package/dist/collections/seeding-transaction.d.ts +43 -0
- package/dist/collections/seeding-transaction.d.ts.map +1 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +180 -32
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +3281 -2018
- package/dist/migrations/deduplicateRolePermissions.d.ts +23 -0
- package/dist/migrations/deduplicateRolePermissions.d.ts.map +1 -0
- package/dist/models/CliAuthApproveLimit.d.ts +8 -1
- package/dist/models/CliAuthApproveLimit.d.ts.map +1 -1
- package/dist/models/LoginAttempt.d.ts +59 -0
- package/dist/models/LoginAttempt.d.ts.map +1 -0
- package/dist/models/LoginAuditEvent.d.ts +49 -0
- package/dist/models/LoginAuditEvent.d.ts.map +1 -0
- package/dist/models/PinCredential.d.ts +31 -0
- package/dist/models/PinCredential.d.ts.map +1 -0
- package/dist/models/RolePermission.d.ts.map +1 -1
- package/dist/models/Session.d.ts +69 -1
- package/dist/models/Session.d.ts.map +1 -1
- package/dist/models/index.d.ts +4 -1
- package/dist/models/index.d.ts.map +1 -1
- package/dist/retention.d.ts +7 -1
- package/dist/retention.d.ts.map +1 -1
- package/dist/services/DeviceCredentialService.d.ts +348 -0
- package/dist/services/DeviceCredentialService.d.ts.map +1 -0
- package/dist/services/LoginAttemptLimiter.d.ts +227 -0
- package/dist/services/LoginAttemptLimiter.d.ts.map +1 -0
- package/dist/services/MobileAuthService.d.ts.map +1 -1
- package/dist/services/OperationPermissionService.d.ts.map +1 -1
- package/dist/services/PermissionCatalogService.d.ts.map +1 -1
- package/dist/services/SessionPermissionContext.d.ts.map +1 -1
- package/dist/services/SessionService.d.ts +56 -1
- package/dist/services/SessionService.d.ts.map +1 -1
- package/dist/services/TerminalAuthService.d.ts +18 -3
- package/dist/services/TerminalAuthService.d.ts.map +1 -1
- package/dist/services/index.d.ts +4 -2
- package/dist/services/index.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +685 -11
- package/dist/sveltekit/device-credential-handlers.d.ts +60 -0
- package/dist/sveltekit/device-credential-handlers.d.ts.map +1 -0
- package/dist/sveltekit/index.d.ts +10 -0
- package/dist/sveltekit/index.d.ts.map +1 -1
- package/dist/sveltekit/types.d.ts +6 -0
- package/dist/sveltekit/types.d.ts.map +1 -1
- package/dist/sveltekit.js +184 -4
- package/dist/sveltekit.js.map +1 -1
- package/package.json +13 -13
- package/dist/__tests__/helpers/oidc-test-server.d.ts +0 -45
- package/dist/__tests__/helpers/oidc-test-server.d.ts.map +0 -1
- package/dist/__tests__/helpers/postgres-rls-reset.d.ts +0 -15
- package/dist/__tests__/helpers/postgres-rls-reset.d.ts.map +0 -1
- package/dist/chunks/TerminalAuthService-Br64vHtd.js.map +0 -1
- package/dist/svelte/components/__tests__/InviteUserModal.test.js +0 -65
- package/dist/svelte/components/__tests__/UserAvatar.test.js +0 -31
- package/dist/svelte/components/__tests__/UserCard.test.js +0 -39
- package/dist/svelte/components/__tests__/UserForm.test.js +0 -50
- package/dist/svelte/components/__tests__/UserList.test.js +0 -48
- package/dist/svelte/components/__tests__/UserMenu.test.js +0 -83
- package/dist/svelte/components/__tests__/UserStatus.test.js +0 -36
package/dist/smrt-knowledge.json
CHANGED
|
@@ -3,18 +3,18 @@
|
|
|
3
3
|
"sensitiveFieldsExcluded": true,
|
|
4
4
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
5
|
"packageName": "@happyvertical/smrt-users",
|
|
6
|
-
"packageVersion": "0.
|
|
6
|
+
"packageVersion": "0.53.5",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
12
|
-
"agents": "
|
|
13
|
-
"moduleDoc:agents/permissions.md": "
|
|
10
|
+
"manifest": "272762d139d7b14daf501ff723c9e5587f0acdb46f60f405f1bf65d7f04dc9c6",
|
|
11
|
+
"packageJson": "fc0002c8def67fa279b4b897489ea8cb0ff3e2986122d8b67f4995fae1a82a4f",
|
|
12
|
+
"agents": "a0c6228ba98d46fc4bb2d39d6cc857c0a8a15571661169d2f8ffdbd9c1b0b966",
|
|
13
|
+
"moduleDoc:agents/permissions.md": "5f6c0afab7e1fd0887b0d29b5a5b5ab2635694207bade70145d3cbfdb62ba78c",
|
|
14
14
|
"moduleDoc:agents/oidc-provisioning.md": "5efa607ff23b9a693391e0c02670324a8cea4114d4968c9718f10a763da919c6",
|
|
15
15
|
"moduleDoc:agents/mobile-auth.md": "25e85e83f43e3a46383a92fa809f3a003b4545c3026e78dbe1bccbbc35161e18",
|
|
16
16
|
"moduleDoc:agents/terminal-auth-testing.md": "8a84c16a19e381427aed81d3b1882344c427fe22a70e87f5ac2db836b1a6822d",
|
|
17
|
-
"moduleDoc:agents/retention.md": "
|
|
17
|
+
"moduleDoc:agents/retention.md": "073874bf16ce3fbbc503157679dc7fe69800fb39a59e40299bcc713f099bf3d8"
|
|
18
18
|
},
|
|
19
19
|
"exports": [
|
|
20
20
|
".",
|
|
@@ -968,6 +968,133 @@
|
|
|
968
968
|
"tags": [],
|
|
969
969
|
"risks": []
|
|
970
970
|
},
|
|
971
|
+
{
|
|
972
|
+
"name": "UsersLoginAttemptCollection",
|
|
973
|
+
"qualifiedName": "@happyvertical/smrt-users:UsersLoginAttemptCollection",
|
|
974
|
+
"collection": "usersloginattempts",
|
|
975
|
+
"tableName": "users_login_attempts",
|
|
976
|
+
"packageName": "@happyvertical/smrt-users",
|
|
977
|
+
"extends": "SmrtCollection",
|
|
978
|
+
"fields": [],
|
|
979
|
+
"relationships": [],
|
|
980
|
+
"methods": [
|
|
981
|
+
"deleteIdle",
|
|
982
|
+
"recordFailure",
|
|
983
|
+
"releaseAttempt",
|
|
984
|
+
"reserveAttempt"
|
|
985
|
+
],
|
|
986
|
+
"methodSignatures": [
|
|
987
|
+
{
|
|
988
|
+
"name": "deleteIdle",
|
|
989
|
+
"async": true,
|
|
990
|
+
"params": [
|
|
991
|
+
"options?: object"
|
|
992
|
+
],
|
|
993
|
+
"returns": "Promise<number>"
|
|
994
|
+
},
|
|
995
|
+
{
|
|
996
|
+
"name": "recordFailure",
|
|
997
|
+
"async": true,
|
|
998
|
+
"params": [
|
|
999
|
+
"input: RecordLoginFailureInput"
|
|
1000
|
+
],
|
|
1001
|
+
"returns": "Promise<RecordedLoginFailure>"
|
|
1002
|
+
},
|
|
1003
|
+
{
|
|
1004
|
+
"name": "releaseAttempt",
|
|
1005
|
+
"async": true,
|
|
1006
|
+
"params": [
|
|
1007
|
+
"limiterKey: string",
|
|
1008
|
+
"windowStartedAt: string",
|
|
1009
|
+
"options?: object"
|
|
1010
|
+
],
|
|
1011
|
+
"returns": "Promise<void>"
|
|
1012
|
+
},
|
|
1013
|
+
{
|
|
1014
|
+
"name": "reserveAttempt",
|
|
1015
|
+
"async": true,
|
|
1016
|
+
"params": [
|
|
1017
|
+
"input: ReserveLoginAttemptInput"
|
|
1018
|
+
],
|
|
1019
|
+
"returns": "Promise<LoginAttemptReservation>"
|
|
1020
|
+
}
|
|
1021
|
+
],
|
|
1022
|
+
"surfaces": [],
|
|
1023
|
+
"withheldSurfaces": [
|
|
1024
|
+
{
|
|
1025
|
+
"kind": "api",
|
|
1026
|
+
"operation": "deleteIdle",
|
|
1027
|
+
"code": "api-disabled",
|
|
1028
|
+
"reason": "api is disabled",
|
|
1029
|
+
"objectName": "@happyvertical/smrt-users:UsersLoginAttemptCollection"
|
|
1030
|
+
},
|
|
1031
|
+
{
|
|
1032
|
+
"kind": "api",
|
|
1033
|
+
"operation": "recordFailure",
|
|
1034
|
+
"code": "api-disabled",
|
|
1035
|
+
"reason": "api is disabled",
|
|
1036
|
+
"objectName": "@happyvertical/smrt-users:UsersLoginAttemptCollection"
|
|
1037
|
+
},
|
|
1038
|
+
{
|
|
1039
|
+
"kind": "api",
|
|
1040
|
+
"operation": "releaseAttempt",
|
|
1041
|
+
"code": "api-disabled",
|
|
1042
|
+
"reason": "api is disabled",
|
|
1043
|
+
"objectName": "@happyvertical/smrt-users:UsersLoginAttemptCollection"
|
|
1044
|
+
},
|
|
1045
|
+
{
|
|
1046
|
+
"kind": "api",
|
|
1047
|
+
"operation": "reserveAttempt",
|
|
1048
|
+
"code": "api-disabled",
|
|
1049
|
+
"reason": "api is disabled",
|
|
1050
|
+
"objectName": "@happyvertical/smrt-users:UsersLoginAttemptCollection"
|
|
1051
|
+
}
|
|
1052
|
+
],
|
|
1053
|
+
"relationshipFeatures": [
|
|
1054
|
+
"uuidColumns"
|
|
1055
|
+
],
|
|
1056
|
+
"tags": [],
|
|
1057
|
+
"risks": []
|
|
1058
|
+
},
|
|
1059
|
+
{
|
|
1060
|
+
"name": "UsersLoginAuditEventCollection",
|
|
1061
|
+
"qualifiedName": "@happyvertical/smrt-users:UsersLoginAuditEventCollection",
|
|
1062
|
+
"collection": "usersloginauditevents",
|
|
1063
|
+
"tableName": "users_login_audit_events",
|
|
1064
|
+
"packageName": "@happyvertical/smrt-users",
|
|
1065
|
+
"extends": "SmrtCollection",
|
|
1066
|
+
"fields": [],
|
|
1067
|
+
"relationships": [],
|
|
1068
|
+
"methods": [
|
|
1069
|
+
"deleteOlderThan"
|
|
1070
|
+
],
|
|
1071
|
+
"methodSignatures": [
|
|
1072
|
+
{
|
|
1073
|
+
"name": "deleteOlderThan",
|
|
1074
|
+
"async": true,
|
|
1075
|
+
"params": [
|
|
1076
|
+
"maxAgeMs: number",
|
|
1077
|
+
"options?: object"
|
|
1078
|
+
],
|
|
1079
|
+
"returns": "Promise<number>"
|
|
1080
|
+
}
|
|
1081
|
+
],
|
|
1082
|
+
"surfaces": [],
|
|
1083
|
+
"withheldSurfaces": [
|
|
1084
|
+
{
|
|
1085
|
+
"kind": "api",
|
|
1086
|
+
"operation": "deleteOlderThan",
|
|
1087
|
+
"code": "api-disabled",
|
|
1088
|
+
"reason": "api is disabled",
|
|
1089
|
+
"objectName": "@happyvertical/smrt-users:UsersLoginAuditEventCollection"
|
|
1090
|
+
}
|
|
1091
|
+
],
|
|
1092
|
+
"relationshipFeatures": [
|
|
1093
|
+
"uuidColumns"
|
|
1094
|
+
],
|
|
1095
|
+
"tags": [],
|
|
1096
|
+
"risks": []
|
|
1097
|
+
},
|
|
971
1098
|
{
|
|
972
1099
|
"name": "UsersMagicLinkTokenCollection",
|
|
973
1100
|
"qualifiedName": "@happyvertical/smrt-users:UsersMagicLinkTokenCollection",
|
|
@@ -1752,6 +1879,77 @@
|
|
|
1752
1879
|
"tags": [],
|
|
1753
1880
|
"risks": []
|
|
1754
1881
|
},
|
|
1882
|
+
{
|
|
1883
|
+
"name": "UsersPinCredentialCollection",
|
|
1884
|
+
"qualifiedName": "@happyvertical/smrt-users:UsersPinCredentialCollection",
|
|
1885
|
+
"collection": "userspincredentials",
|
|
1886
|
+
"tableName": "users_pin_credentials",
|
|
1887
|
+
"packageName": "@happyvertical/smrt-users",
|
|
1888
|
+
"extends": "SmrtCollection",
|
|
1889
|
+
"fields": [],
|
|
1890
|
+
"relationships": [],
|
|
1891
|
+
"methods": [
|
|
1892
|
+
"deleteByUserId",
|
|
1893
|
+
"findByUserId",
|
|
1894
|
+
"upsertForUser"
|
|
1895
|
+
],
|
|
1896
|
+
"methodSignatures": [
|
|
1897
|
+
{
|
|
1898
|
+
"name": "deleteByUserId",
|
|
1899
|
+
"async": true,
|
|
1900
|
+
"params": [
|
|
1901
|
+
"userId: string"
|
|
1902
|
+
],
|
|
1903
|
+
"returns": "Promise<boolean>"
|
|
1904
|
+
},
|
|
1905
|
+
{
|
|
1906
|
+
"name": "findByUserId",
|
|
1907
|
+
"async": true,
|
|
1908
|
+
"params": [
|
|
1909
|
+
"userId: string"
|
|
1910
|
+
],
|
|
1911
|
+
"returns": "Promise<UsersPinCredential | null>"
|
|
1912
|
+
},
|
|
1913
|
+
{
|
|
1914
|
+
"name": "upsertForUser",
|
|
1915
|
+
"async": true,
|
|
1916
|
+
"params": [
|
|
1917
|
+
"userId: string",
|
|
1918
|
+
"write: PinCredentialWrite"
|
|
1919
|
+
],
|
|
1920
|
+
"returns": "Promise<UsersPinCredential>"
|
|
1921
|
+
}
|
|
1922
|
+
],
|
|
1923
|
+
"surfaces": [],
|
|
1924
|
+
"withheldSurfaces": [
|
|
1925
|
+
{
|
|
1926
|
+
"kind": "api",
|
|
1927
|
+
"operation": "deleteByUserId",
|
|
1928
|
+
"code": "api-disabled",
|
|
1929
|
+
"reason": "api is disabled",
|
|
1930
|
+
"objectName": "@happyvertical/smrt-users:UsersPinCredentialCollection"
|
|
1931
|
+
},
|
|
1932
|
+
{
|
|
1933
|
+
"kind": "api",
|
|
1934
|
+
"operation": "findByUserId",
|
|
1935
|
+
"code": "api-disabled",
|
|
1936
|
+
"reason": "api is disabled",
|
|
1937
|
+
"objectName": "@happyvertical/smrt-users:UsersPinCredentialCollection"
|
|
1938
|
+
},
|
|
1939
|
+
{
|
|
1940
|
+
"kind": "api",
|
|
1941
|
+
"operation": "upsertForUser",
|
|
1942
|
+
"code": "api-disabled",
|
|
1943
|
+
"reason": "api is disabled",
|
|
1944
|
+
"objectName": "@happyvertical/smrt-users:UsersPinCredentialCollection"
|
|
1945
|
+
}
|
|
1946
|
+
],
|
|
1947
|
+
"relationshipFeatures": [
|
|
1948
|
+
"uuidColumns"
|
|
1949
|
+
],
|
|
1950
|
+
"tags": [],
|
|
1951
|
+
"risks": []
|
|
1952
|
+
},
|
|
1755
1953
|
{
|
|
1756
1954
|
"name": "ResourceGrantCollection",
|
|
1757
1955
|
"qualifiedName": "@happyvertical/smrt-users:ResourceGrantCollection",
|
|
@@ -2300,10 +2498,13 @@
|
|
|
2300
2498
|
"deleteExpired",
|
|
2301
2499
|
"deleteUserSessions",
|
|
2302
2500
|
"findByUser",
|
|
2501
|
+
"findChildren",
|
|
2303
2502
|
"findValidSession",
|
|
2304
2503
|
"getSessionData",
|
|
2504
|
+
"revokeChildren",
|
|
2305
2505
|
"revokeSession",
|
|
2306
2506
|
"revokeUserSessions",
|
|
2507
|
+
"revokeUserSessionsByAuthMethod",
|
|
2307
2508
|
"setSessionData",
|
|
2308
2509
|
"setSessionTenant",
|
|
2309
2510
|
"touch"
|
|
@@ -2349,6 +2550,14 @@
|
|
|
2349
2550
|
],
|
|
2350
2551
|
"returns": "Promise<Session[]>"
|
|
2351
2552
|
},
|
|
2553
|
+
{
|
|
2554
|
+
"name": "findChildren",
|
|
2555
|
+
"async": true,
|
|
2556
|
+
"params": [
|
|
2557
|
+
"parentSessionId: string"
|
|
2558
|
+
],
|
|
2559
|
+
"returns": "Promise<Session[]>"
|
|
2560
|
+
},
|
|
2352
2561
|
{
|
|
2353
2562
|
"name": "findValidSession",
|
|
2354
2563
|
"async": true,
|
|
@@ -2366,6 +2575,15 @@
|
|
|
2366
2575
|
],
|
|
2367
2576
|
"returns": "Promise<T | undefined>"
|
|
2368
2577
|
},
|
|
2578
|
+
{
|
|
2579
|
+
"name": "revokeChildren",
|
|
2580
|
+
"async": true,
|
|
2581
|
+
"params": [
|
|
2582
|
+
"parentSessionId: string",
|
|
2583
|
+
"options?: object"
|
|
2584
|
+
],
|
|
2585
|
+
"returns": "Promise<number>"
|
|
2586
|
+
},
|
|
2369
2587
|
{
|
|
2370
2588
|
"name": "revokeSession",
|
|
2371
2589
|
"async": true,
|
|
@@ -2382,6 +2600,16 @@
|
|
|
2382
2600
|
],
|
|
2383
2601
|
"returns": "Promise<number>"
|
|
2384
2602
|
},
|
|
2603
|
+
{
|
|
2604
|
+
"name": "revokeUserSessionsByAuthMethod",
|
|
2605
|
+
"async": true,
|
|
2606
|
+
"params": [
|
|
2607
|
+
"userId: string",
|
|
2608
|
+
"authMethod: SessionAuthMethod",
|
|
2609
|
+
"options?: object"
|
|
2610
|
+
],
|
|
2611
|
+
"returns": "Promise<number>"
|
|
2612
|
+
},
|
|
2385
2613
|
{
|
|
2386
2614
|
"name": "setSessionData",
|
|
2387
2615
|
"async": true,
|
|
@@ -2421,6 +2649,30 @@
|
|
|
2421
2649
|
"path": "/sessions/createSession",
|
|
2422
2650
|
"method": "POST"
|
|
2423
2651
|
},
|
|
2652
|
+
{
|
|
2653
|
+
"kind": "api",
|
|
2654
|
+
"name": "sessions.findChildren",
|
|
2655
|
+
"operation": "findChildren",
|
|
2656
|
+
"objectName": "@happyvertical/smrt-users:SessionCollection",
|
|
2657
|
+
"path": "/sessions/findChildren",
|
|
2658
|
+
"method": "POST"
|
|
2659
|
+
},
|
|
2660
|
+
{
|
|
2661
|
+
"kind": "api",
|
|
2662
|
+
"name": "sessions.revokeChildren",
|
|
2663
|
+
"operation": "revokeChildren",
|
|
2664
|
+
"objectName": "@happyvertical/smrt-users:SessionCollection",
|
|
2665
|
+
"path": "/sessions/revokeChildren",
|
|
2666
|
+
"method": "POST"
|
|
2667
|
+
},
|
|
2668
|
+
{
|
|
2669
|
+
"kind": "api",
|
|
2670
|
+
"name": "sessions.revokeUserSessionsByAuthMethod",
|
|
2671
|
+
"operation": "revokeUserSessionsByAuthMethod",
|
|
2672
|
+
"objectName": "@happyvertical/smrt-users:SessionCollection",
|
|
2673
|
+
"path": "/sessions/revokeUserSessionsByAuthMethod",
|
|
2674
|
+
"method": "POST"
|
|
2675
|
+
},
|
|
2424
2676
|
{
|
|
2425
2677
|
"kind": "api",
|
|
2426
2678
|
"name": "sessions.findValidSession",
|
|
@@ -2515,6 +2767,24 @@
|
|
|
2515
2767
|
"operation": "createSession",
|
|
2516
2768
|
"objectName": "@happyvertical/smrt-users:SessionCollection"
|
|
2517
2769
|
},
|
|
2770
|
+
{
|
|
2771
|
+
"kind": "cli",
|
|
2772
|
+
"name": "sessioncollection_findChildren",
|
|
2773
|
+
"operation": "findChildren",
|
|
2774
|
+
"objectName": "@happyvertical/smrt-users:SessionCollection"
|
|
2775
|
+
},
|
|
2776
|
+
{
|
|
2777
|
+
"kind": "cli",
|
|
2778
|
+
"name": "sessioncollection_revokeChildren",
|
|
2779
|
+
"operation": "revokeChildren",
|
|
2780
|
+
"objectName": "@happyvertical/smrt-users:SessionCollection"
|
|
2781
|
+
},
|
|
2782
|
+
{
|
|
2783
|
+
"kind": "cli",
|
|
2784
|
+
"name": "sessioncollection_revokeUserSessionsByAuthMethod",
|
|
2785
|
+
"operation": "revokeUserSessionsByAuthMethod",
|
|
2786
|
+
"objectName": "@happyvertical/smrt-users:SessionCollection"
|
|
2787
|
+
},
|
|
2518
2788
|
{
|
|
2519
2789
|
"kind": "cli",
|
|
2520
2790
|
"name": "sessioncollection_findValidSession",
|
|
@@ -2587,6 +2857,24 @@
|
|
|
2587
2857
|
"operation": "createSession",
|
|
2588
2858
|
"objectName": "@happyvertical/smrt-users:SessionCollection"
|
|
2589
2859
|
},
|
|
2860
|
+
{
|
|
2861
|
+
"kind": "mcp",
|
|
2862
|
+
"name": "sessioncollection_findchildren",
|
|
2863
|
+
"operation": "findChildren",
|
|
2864
|
+
"objectName": "@happyvertical/smrt-users:SessionCollection"
|
|
2865
|
+
},
|
|
2866
|
+
{
|
|
2867
|
+
"kind": "mcp",
|
|
2868
|
+
"name": "sessioncollection_revokechildren",
|
|
2869
|
+
"operation": "revokeChildren",
|
|
2870
|
+
"objectName": "@happyvertical/smrt-users:SessionCollection"
|
|
2871
|
+
},
|
|
2872
|
+
{
|
|
2873
|
+
"kind": "mcp",
|
|
2874
|
+
"name": "sessioncollection_revokeusersessionsbyauthmethod",
|
|
2875
|
+
"operation": "revokeUserSessionsByAuthMethod",
|
|
2876
|
+
"objectName": "@happyvertical/smrt-users:SessionCollection"
|
|
2877
|
+
},
|
|
2590
2878
|
{
|
|
2591
2879
|
"kind": "mcp",
|
|
2592
2880
|
"name": "sessioncollection_findvalidsession",
|
|
@@ -4652,6 +4940,166 @@
|
|
|
4652
4940
|
"tags": [],
|
|
4653
4941
|
"risks": []
|
|
4654
4942
|
},
|
|
4943
|
+
{
|
|
4944
|
+
"name": "UsersLoginAttempt",
|
|
4945
|
+
"qualifiedName": "@happyvertical/smrt-users:UsersLoginAttempt",
|
|
4946
|
+
"collection": "usersloginattempts",
|
|
4947
|
+
"tableName": "users_login_attempts",
|
|
4948
|
+
"packageName": "@happyvertical/smrt-users",
|
|
4949
|
+
"extends": "SmrtObject",
|
|
4950
|
+
"fields": [
|
|
4951
|
+
{
|
|
4952
|
+
"name": "limiterKey",
|
|
4953
|
+
"type": "text",
|
|
4954
|
+
"required": true,
|
|
4955
|
+
"columnType": "TEXT"
|
|
4956
|
+
},
|
|
4957
|
+
{
|
|
4958
|
+
"name": "scope",
|
|
4959
|
+
"type": "text",
|
|
4960
|
+
"required": true,
|
|
4961
|
+
"columnType": "TEXT"
|
|
4962
|
+
},
|
|
4963
|
+
{
|
|
4964
|
+
"name": "attemptCount",
|
|
4965
|
+
"type": "integer",
|
|
4966
|
+
"required": true,
|
|
4967
|
+
"columnType": "INTEGER",
|
|
4968
|
+
"default": 0
|
|
4969
|
+
},
|
|
4970
|
+
{
|
|
4971
|
+
"name": "windowStartedAt",
|
|
4972
|
+
"type": "datetime",
|
|
4973
|
+
"required": true,
|
|
4974
|
+
"columnType": "TIMESTAMP"
|
|
4975
|
+
},
|
|
4976
|
+
{
|
|
4977
|
+
"name": "failureStreak",
|
|
4978
|
+
"type": "integer",
|
|
4979
|
+
"required": true,
|
|
4980
|
+
"columnType": "INTEGER",
|
|
4981
|
+
"default": 0
|
|
4982
|
+
},
|
|
4983
|
+
{
|
|
4984
|
+
"name": "lockedUntil",
|
|
4985
|
+
"type": "datetime",
|
|
4986
|
+
"required": false,
|
|
4987
|
+
"columnType": "TIMESTAMP"
|
|
4988
|
+
},
|
|
4989
|
+
{
|
|
4990
|
+
"name": "lastFailedAt",
|
|
4991
|
+
"type": "datetime",
|
|
4992
|
+
"required": false,
|
|
4993
|
+
"columnType": "TIMESTAMP"
|
|
4994
|
+
},
|
|
4995
|
+
{
|
|
4996
|
+
"name": "retainUntil",
|
|
4997
|
+
"type": "datetime",
|
|
4998
|
+
"required": true,
|
|
4999
|
+
"columnType": "TIMESTAMP"
|
|
5000
|
+
}
|
|
5001
|
+
],
|
|
5002
|
+
"relationships": [],
|
|
5003
|
+
"methods": [],
|
|
5004
|
+
"surfaces": [],
|
|
5005
|
+
"relationshipFeatures": [
|
|
5006
|
+
"uuidColumns"
|
|
5007
|
+
],
|
|
5008
|
+
"tags": [],
|
|
5009
|
+
"risks": []
|
|
5010
|
+
},
|
|
5011
|
+
{
|
|
5012
|
+
"name": "UsersLoginAuditEvent",
|
|
5013
|
+
"qualifiedName": "@happyvertical/smrt-users:UsersLoginAuditEvent",
|
|
5014
|
+
"collection": "usersloginauditevents",
|
|
5015
|
+
"tableName": "users_login_audit_events",
|
|
5016
|
+
"packageName": "@happyvertical/smrt-users",
|
|
5017
|
+
"extends": "SmrtObject",
|
|
5018
|
+
"fields": [
|
|
5019
|
+
{
|
|
5020
|
+
"name": "kind",
|
|
5021
|
+
"type": "text",
|
|
5022
|
+
"required": true,
|
|
5023
|
+
"columnType": "TEXT"
|
|
5024
|
+
},
|
|
5025
|
+
{
|
|
5026
|
+
"name": "outcome",
|
|
5027
|
+
"type": "text",
|
|
5028
|
+
"required": true,
|
|
5029
|
+
"columnType": "TEXT"
|
|
5030
|
+
},
|
|
5031
|
+
{
|
|
5032
|
+
"name": "subjectKeyHash",
|
|
5033
|
+
"type": "text",
|
|
5034
|
+
"required": false,
|
|
5035
|
+
"columnType": "TEXT"
|
|
5036
|
+
},
|
|
5037
|
+
{
|
|
5038
|
+
"name": "sourceKeyHash",
|
|
5039
|
+
"type": "text",
|
|
5040
|
+
"required": false,
|
|
5041
|
+
"columnType": "TEXT"
|
|
5042
|
+
},
|
|
5043
|
+
{
|
|
5044
|
+
"name": "retryAfterSeconds",
|
|
5045
|
+
"type": "integer",
|
|
5046
|
+
"required": false,
|
|
5047
|
+
"columnType": "INTEGER"
|
|
5048
|
+
},
|
|
5049
|
+
{
|
|
5050
|
+
"name": "occurredAt",
|
|
5051
|
+
"type": "datetime",
|
|
5052
|
+
"required": true,
|
|
5053
|
+
"columnType": "TIMESTAMP"
|
|
5054
|
+
},
|
|
5055
|
+
{
|
|
5056
|
+
"name": "metadata",
|
|
5057
|
+
"type": "text",
|
|
5058
|
+
"required": false,
|
|
5059
|
+
"columnType": "TEXT"
|
|
5060
|
+
}
|
|
5061
|
+
],
|
|
5062
|
+
"relationships": [],
|
|
5063
|
+
"methods": [
|
|
5064
|
+
"getMetadata",
|
|
5065
|
+
"setMetadata"
|
|
5066
|
+
],
|
|
5067
|
+
"methodSignatures": [
|
|
5068
|
+
{
|
|
5069
|
+
"name": "getMetadata",
|
|
5070
|
+
"returns": "Record<string, unknown>"
|
|
5071
|
+
},
|
|
5072
|
+
{
|
|
5073
|
+
"name": "setMetadata",
|
|
5074
|
+
"params": [
|
|
5075
|
+
"value: Record<string, unknown>"
|
|
5076
|
+
],
|
|
5077
|
+
"returns": "void"
|
|
5078
|
+
}
|
|
5079
|
+
],
|
|
5080
|
+
"surfaces": [],
|
|
5081
|
+
"withheldSurfaces": [
|
|
5082
|
+
{
|
|
5083
|
+
"kind": "api",
|
|
5084
|
+
"operation": "getMetadata",
|
|
5085
|
+
"code": "api-disabled",
|
|
5086
|
+
"reason": "api is disabled",
|
|
5087
|
+
"objectName": "@happyvertical/smrt-users:UsersLoginAuditEvent"
|
|
5088
|
+
},
|
|
5089
|
+
{
|
|
5090
|
+
"kind": "api",
|
|
5091
|
+
"operation": "setMetadata",
|
|
5092
|
+
"code": "api-disabled",
|
|
5093
|
+
"reason": "api is disabled",
|
|
5094
|
+
"objectName": "@happyvertical/smrt-users:UsersLoginAuditEvent"
|
|
5095
|
+
}
|
|
5096
|
+
],
|
|
5097
|
+
"relationshipFeatures": [
|
|
5098
|
+
"uuidColumns"
|
|
5099
|
+
],
|
|
5100
|
+
"tags": [],
|
|
5101
|
+
"risks": []
|
|
5102
|
+
},
|
|
4655
5103
|
{
|
|
4656
5104
|
"name": "UsersMagicLinkToken",
|
|
4657
5105
|
"qualifiedName": "@happyvertical/smrt-users:UsersMagicLinkToken",
|
|
@@ -5271,6 +5719,66 @@
|
|
|
5271
5719
|
"tags": [],
|
|
5272
5720
|
"risks": []
|
|
5273
5721
|
},
|
|
5722
|
+
{
|
|
5723
|
+
"name": "UsersPinCredential",
|
|
5724
|
+
"qualifiedName": "@happyvertical/smrt-users:UsersPinCredential",
|
|
5725
|
+
"collection": "userspincredentials",
|
|
5726
|
+
"tableName": "users_pin_credentials",
|
|
5727
|
+
"packageName": "@happyvertical/smrt-users",
|
|
5728
|
+
"extends": "SmrtObject",
|
|
5729
|
+
"fields": [
|
|
5730
|
+
{
|
|
5731
|
+
"name": "userId",
|
|
5732
|
+
"type": "foreignKey",
|
|
5733
|
+
"required": true,
|
|
5734
|
+
"related": "User",
|
|
5735
|
+
"columnType": "UUID"
|
|
5736
|
+
},
|
|
5737
|
+
{
|
|
5738
|
+
"name": "version",
|
|
5739
|
+
"type": "integer",
|
|
5740
|
+
"required": true,
|
|
5741
|
+
"columnType": "INTEGER",
|
|
5742
|
+
"default": 1
|
|
5743
|
+
},
|
|
5744
|
+
{
|
|
5745
|
+
"name": "mustReset",
|
|
5746
|
+
"type": "boolean",
|
|
5747
|
+
"required": true,
|
|
5748
|
+
"columnType": "BOOLEAN",
|
|
5749
|
+
"default": false
|
|
5750
|
+
},
|
|
5751
|
+
{
|
|
5752
|
+
"name": "rotatedAt",
|
|
5753
|
+
"type": "datetime",
|
|
5754
|
+
"required": false,
|
|
5755
|
+
"columnType": "TIMESTAMP"
|
|
5756
|
+
},
|
|
5757
|
+
{
|
|
5758
|
+
"name": "rotatedBy",
|
|
5759
|
+
"type": "text",
|
|
5760
|
+
"required": false,
|
|
5761
|
+
"columnType": "TEXT"
|
|
5762
|
+
}
|
|
5763
|
+
],
|
|
5764
|
+
"relationships": [
|
|
5765
|
+
{
|
|
5766
|
+
"name": "userId",
|
|
5767
|
+
"type": "foreignKey",
|
|
5768
|
+
"required": true,
|
|
5769
|
+
"related": "User",
|
|
5770
|
+
"columnType": "UUID"
|
|
5771
|
+
}
|
|
5772
|
+
],
|
|
5773
|
+
"methods": [],
|
|
5774
|
+
"surfaces": [],
|
|
5775
|
+
"relationshipFeatures": [
|
|
5776
|
+
"foreignKey",
|
|
5777
|
+
"uuidColumns"
|
|
5778
|
+
],
|
|
5779
|
+
"tags": [],
|
|
5780
|
+
"risks": []
|
|
5781
|
+
},
|
|
5274
5782
|
{
|
|
5275
5783
|
"name": "ResourceGrant",
|
|
5276
5784
|
"qualifiedName": "@happyvertical/smrt-users:ResourceGrant",
|
|
@@ -5665,6 +6173,10 @@
|
|
|
5665
6173
|
}
|
|
5666
6174
|
],
|
|
5667
6175
|
"methods": [],
|
|
6176
|
+
"conflictColumns": [
|
|
6177
|
+
"role_id",
|
|
6178
|
+
"permission_id"
|
|
6179
|
+
],
|
|
5668
6180
|
"surfaces": [
|
|
5669
6181
|
{
|
|
5670
6182
|
"kind": "api",
|
|
@@ -5792,6 +6304,12 @@
|
|
|
5792
6304
|
"required": false,
|
|
5793
6305
|
"columnType": "JSON",
|
|
5794
6306
|
"default": {}
|
|
6307
|
+
},
|
|
6308
|
+
{
|
|
6309
|
+
"name": "authMethod",
|
|
6310
|
+
"type": "text",
|
|
6311
|
+
"required": false,
|
|
6312
|
+
"columnType": "TEXT"
|
|
5795
6313
|
}
|
|
5796
6314
|
],
|
|
5797
6315
|
"relationships": [
|
|
@@ -5812,8 +6330,12 @@
|
|
|
5812
6330
|
],
|
|
5813
6331
|
"methods": [
|
|
5814
6332
|
"extend",
|
|
6333
|
+
"getAbsoluteExpiry",
|
|
5815
6334
|
"getData",
|
|
6335
|
+
"getIdleSeconds",
|
|
6336
|
+
"getPermissionCeiling",
|
|
5816
6337
|
"isExpired",
|
|
6338
|
+
"isLayered",
|
|
5817
6339
|
"isRevoked",
|
|
5818
6340
|
"isValid",
|
|
5819
6341
|
"recordActivity",
|
|
@@ -5831,6 +6353,10 @@
|
|
|
5831
6353
|
],
|
|
5832
6354
|
"returns": "void"
|
|
5833
6355
|
},
|
|
6356
|
+
{
|
|
6357
|
+
"name": "getAbsoluteExpiry",
|
|
6358
|
+
"returns": "Date | null"
|
|
6359
|
+
},
|
|
5834
6360
|
{
|
|
5835
6361
|
"name": "getData",
|
|
5836
6362
|
"params": [
|
|
@@ -5838,10 +6364,22 @@
|
|
|
5838
6364
|
],
|
|
5839
6365
|
"returns": "T | undefined"
|
|
5840
6366
|
},
|
|
6367
|
+
{
|
|
6368
|
+
"name": "getIdleSeconds",
|
|
6369
|
+
"returns": "number | null"
|
|
6370
|
+
},
|
|
6371
|
+
{
|
|
6372
|
+
"name": "getPermissionCeiling",
|
|
6373
|
+
"returns": "string[] | null"
|
|
6374
|
+
},
|
|
5841
6375
|
{
|
|
5842
6376
|
"name": "isExpired",
|
|
5843
6377
|
"returns": "boolean"
|
|
5844
6378
|
},
|
|
6379
|
+
{
|
|
6380
|
+
"name": "isLayered",
|
|
6381
|
+
"returns": "boolean"
|
|
6382
|
+
},
|
|
5845
6383
|
{
|
|
5846
6384
|
"name": "isRevoked",
|
|
5847
6385
|
"returns": "boolean"
|
|
@@ -5942,6 +6480,30 @@
|
|
|
5942
6480
|
"operation": "delete",
|
|
5943
6481
|
"objectName": "@happyvertical/smrt-users:Session"
|
|
5944
6482
|
},
|
|
6483
|
+
{
|
|
6484
|
+
"kind": "cli",
|
|
6485
|
+
"name": "session_isLayered",
|
|
6486
|
+
"operation": "isLayered",
|
|
6487
|
+
"objectName": "@happyvertical/smrt-users:Session"
|
|
6488
|
+
},
|
|
6489
|
+
{
|
|
6490
|
+
"kind": "cli",
|
|
6491
|
+
"name": "session_getAbsoluteExpiry",
|
|
6492
|
+
"operation": "getAbsoluteExpiry",
|
|
6493
|
+
"objectName": "@happyvertical/smrt-users:Session"
|
|
6494
|
+
},
|
|
6495
|
+
{
|
|
6496
|
+
"kind": "cli",
|
|
6497
|
+
"name": "session_getIdleSeconds",
|
|
6498
|
+
"operation": "getIdleSeconds",
|
|
6499
|
+
"objectName": "@happyvertical/smrt-users:Session"
|
|
6500
|
+
},
|
|
6501
|
+
{
|
|
6502
|
+
"kind": "cli",
|
|
6503
|
+
"name": "session_getPermissionCeiling",
|
|
6504
|
+
"operation": "getPermissionCeiling",
|
|
6505
|
+
"objectName": "@happyvertical/smrt-users:Session"
|
|
6506
|
+
},
|
|
5945
6507
|
{
|
|
5946
6508
|
"kind": "cli",
|
|
5947
6509
|
"name": "session_isValid",
|
|
@@ -6017,6 +6579,13 @@
|
|
|
6017
6579
|
"reason": "not listed in api.include",
|
|
6018
6580
|
"objectName": "@happyvertical/smrt-users:Session"
|
|
6019
6581
|
},
|
|
6582
|
+
{
|
|
6583
|
+
"kind": "api",
|
|
6584
|
+
"operation": "getAbsoluteExpiry",
|
|
6585
|
+
"code": "not-included",
|
|
6586
|
+
"reason": "not listed in api.include",
|
|
6587
|
+
"objectName": "@happyvertical/smrt-users:Session"
|
|
6588
|
+
},
|
|
6020
6589
|
{
|
|
6021
6590
|
"kind": "api",
|
|
6022
6591
|
"operation": "getData",
|
|
@@ -6024,6 +6593,20 @@
|
|
|
6024
6593
|
"reason": "not listed in api.include",
|
|
6025
6594
|
"objectName": "@happyvertical/smrt-users:Session"
|
|
6026
6595
|
},
|
|
6596
|
+
{
|
|
6597
|
+
"kind": "api",
|
|
6598
|
+
"operation": "getIdleSeconds",
|
|
6599
|
+
"code": "not-included",
|
|
6600
|
+
"reason": "not listed in api.include",
|
|
6601
|
+
"objectName": "@happyvertical/smrt-users:Session"
|
|
6602
|
+
},
|
|
6603
|
+
{
|
|
6604
|
+
"kind": "api",
|
|
6605
|
+
"operation": "getPermissionCeiling",
|
|
6606
|
+
"code": "not-included",
|
|
6607
|
+
"reason": "not listed in api.include",
|
|
6608
|
+
"objectName": "@happyvertical/smrt-users:Session"
|
|
6609
|
+
},
|
|
6027
6610
|
{
|
|
6028
6611
|
"kind": "api",
|
|
6029
6612
|
"operation": "isExpired",
|
|
@@ -6031,6 +6614,13 @@
|
|
|
6031
6614
|
"reason": "not listed in api.include",
|
|
6032
6615
|
"objectName": "@happyvertical/smrt-users:Session"
|
|
6033
6616
|
},
|
|
6617
|
+
{
|
|
6618
|
+
"kind": "api",
|
|
6619
|
+
"operation": "isLayered",
|
|
6620
|
+
"code": "not-included",
|
|
6621
|
+
"reason": "not listed in api.include",
|
|
6622
|
+
"objectName": "@happyvertical/smrt-users:Session"
|
|
6623
|
+
},
|
|
6034
6624
|
{
|
|
6035
6625
|
"kind": "api",
|
|
6036
6626
|
"operation": "isRevoked",
|
|
@@ -8400,6 +8990,30 @@
|
|
|
8400
8990
|
"path": "/sessions/createSession",
|
|
8401
8991
|
"method": "POST"
|
|
8402
8992
|
},
|
|
8993
|
+
{
|
|
8994
|
+
"kind": "api",
|
|
8995
|
+
"name": "sessions.findChildren",
|
|
8996
|
+
"operation": "findChildren",
|
|
8997
|
+
"objectName": "@happyvertical/smrt-users:SessionCollection",
|
|
8998
|
+
"path": "/sessions/findChildren",
|
|
8999
|
+
"method": "POST"
|
|
9000
|
+
},
|
|
9001
|
+
{
|
|
9002
|
+
"kind": "api",
|
|
9003
|
+
"name": "sessions.revokeChildren",
|
|
9004
|
+
"operation": "revokeChildren",
|
|
9005
|
+
"objectName": "@happyvertical/smrt-users:SessionCollection",
|
|
9006
|
+
"path": "/sessions/revokeChildren",
|
|
9007
|
+
"method": "POST"
|
|
9008
|
+
},
|
|
9009
|
+
{
|
|
9010
|
+
"kind": "api",
|
|
9011
|
+
"name": "sessions.revokeUserSessionsByAuthMethod",
|
|
9012
|
+
"operation": "revokeUserSessionsByAuthMethod",
|
|
9013
|
+
"objectName": "@happyvertical/smrt-users:SessionCollection",
|
|
9014
|
+
"path": "/sessions/revokeUserSessionsByAuthMethod",
|
|
9015
|
+
"method": "POST"
|
|
9016
|
+
},
|
|
8403
9017
|
{
|
|
8404
9018
|
"kind": "api",
|
|
8405
9019
|
"name": "sessions.findValidSession",
|
|
@@ -8494,6 +9108,24 @@
|
|
|
8494
9108
|
"operation": "createSession",
|
|
8495
9109
|
"objectName": "@happyvertical/smrt-users:SessionCollection"
|
|
8496
9110
|
},
|
|
9111
|
+
{
|
|
9112
|
+
"kind": "cli",
|
|
9113
|
+
"name": "sessioncollection_findChildren",
|
|
9114
|
+
"operation": "findChildren",
|
|
9115
|
+
"objectName": "@happyvertical/smrt-users:SessionCollection"
|
|
9116
|
+
},
|
|
9117
|
+
{
|
|
9118
|
+
"kind": "cli",
|
|
9119
|
+
"name": "sessioncollection_revokeChildren",
|
|
9120
|
+
"operation": "revokeChildren",
|
|
9121
|
+
"objectName": "@happyvertical/smrt-users:SessionCollection"
|
|
9122
|
+
},
|
|
9123
|
+
{
|
|
9124
|
+
"kind": "cli",
|
|
9125
|
+
"name": "sessioncollection_revokeUserSessionsByAuthMethod",
|
|
9126
|
+
"operation": "revokeUserSessionsByAuthMethod",
|
|
9127
|
+
"objectName": "@happyvertical/smrt-users:SessionCollection"
|
|
9128
|
+
},
|
|
8497
9129
|
{
|
|
8498
9130
|
"kind": "cli",
|
|
8499
9131
|
"name": "sessioncollection_findValidSession",
|
|
@@ -8566,6 +9198,24 @@
|
|
|
8566
9198
|
"operation": "createSession",
|
|
8567
9199
|
"objectName": "@happyvertical/smrt-users:SessionCollection"
|
|
8568
9200
|
},
|
|
9201
|
+
{
|
|
9202
|
+
"kind": "mcp",
|
|
9203
|
+
"name": "sessioncollection_findchildren",
|
|
9204
|
+
"operation": "findChildren",
|
|
9205
|
+
"objectName": "@happyvertical/smrt-users:SessionCollection"
|
|
9206
|
+
},
|
|
9207
|
+
{
|
|
9208
|
+
"kind": "mcp",
|
|
9209
|
+
"name": "sessioncollection_revokechildren",
|
|
9210
|
+
"operation": "revokeChildren",
|
|
9211
|
+
"objectName": "@happyvertical/smrt-users:SessionCollection"
|
|
9212
|
+
},
|
|
9213
|
+
{
|
|
9214
|
+
"kind": "mcp",
|
|
9215
|
+
"name": "sessioncollection_revokeusersessionsbyauthmethod",
|
|
9216
|
+
"operation": "revokeUserSessionsByAuthMethod",
|
|
9217
|
+
"objectName": "@happyvertical/smrt-users:SessionCollection"
|
|
9218
|
+
},
|
|
8569
9219
|
{
|
|
8570
9220
|
"kind": "mcp",
|
|
8571
9221
|
"name": "sessioncollection_findvalidsession",
|
|
@@ -10146,6 +10796,30 @@
|
|
|
10146
10796
|
"operation": "delete",
|
|
10147
10797
|
"objectName": "@happyvertical/smrt-users:Session"
|
|
10148
10798
|
},
|
|
10799
|
+
{
|
|
10800
|
+
"kind": "cli",
|
|
10801
|
+
"name": "session_isLayered",
|
|
10802
|
+
"operation": "isLayered",
|
|
10803
|
+
"objectName": "@happyvertical/smrt-users:Session"
|
|
10804
|
+
},
|
|
10805
|
+
{
|
|
10806
|
+
"kind": "cli",
|
|
10807
|
+
"name": "session_getAbsoluteExpiry",
|
|
10808
|
+
"operation": "getAbsoluteExpiry",
|
|
10809
|
+
"objectName": "@happyvertical/smrt-users:Session"
|
|
10810
|
+
},
|
|
10811
|
+
{
|
|
10812
|
+
"kind": "cli",
|
|
10813
|
+
"name": "session_getIdleSeconds",
|
|
10814
|
+
"operation": "getIdleSeconds",
|
|
10815
|
+
"objectName": "@happyvertical/smrt-users:Session"
|
|
10816
|
+
},
|
|
10817
|
+
{
|
|
10818
|
+
"kind": "cli",
|
|
10819
|
+
"name": "session_getPermissionCeiling",
|
|
10820
|
+
"operation": "getPermissionCeiling",
|
|
10821
|
+
"objectName": "@happyvertical/smrt-users:Session"
|
|
10822
|
+
},
|
|
10149
10823
|
{
|
|
10150
10824
|
"kind": "cli",
|
|
10151
10825
|
"name": "session_isValid",
|
|
@@ -10579,19 +11253,19 @@
|
|
|
10579
11253
|
],
|
|
10580
11254
|
"prompts": [],
|
|
10581
11255
|
"relationshipsV2": {
|
|
10582
|
-
"foreignKeyFields":
|
|
11256
|
+
"foreignKeyFields": 26,
|
|
10583
11257
|
"crossPackageRefFields": 1,
|
|
10584
11258
|
"junctionCollections": 0,
|
|
10585
11259
|
"hierarchicalObjects": 0,
|
|
10586
11260
|
"polymorphicAssociations": 0,
|
|
10587
|
-
"uuidColumns":
|
|
11261
|
+
"uuidColumns": 100
|
|
10588
11262
|
},
|
|
10589
|
-
"agentDoc": "# @happyvertical/smrt-users\n\nMulti-tenant identity, RBAC, hierarchical tenants, sessions, and SvelteKit auth.\n\n## Modules\n\nRead only the module relevant to the change; protocol and provisioning details\nare not prerequisites for unrelated user-package work.\n\n| Module | Scope | Module doc |\n|---|---|---|\n| `src/services/PermissionResolver.ts` | permission precedence, inherited memberships, guards, RLS, role seeding | [agents/permissions.md](agents/permissions.md) |\n| `src/services/OidcLoginService.ts`, collections and OIDC handlers | identity reconciliation, transaction boundaries, migration readiness | [agents/oidc-provisioning.md](agents/oidc-provisioning.md) |\n| `src/services/MobileAuthService.ts` | mobile handshake, bearer sessions, bootstrap extension boundary | [agents/mobile-auth.md](agents/mobile-auth.md) |\n| `src/services/TerminalAuthService.ts`, `src/collections/
|
|
11263
|
+
"agentDoc": "# @happyvertical/smrt-users\n\nMulti-tenant identity, RBAC, hierarchical tenants, sessions, and SvelteKit auth.\n\n## Modules\n\nRead only the module relevant to the change; protocol and provisioning details\nare not prerequisites for unrelated user-package work.\n\n| Module | Scope | Module doc |\n|---|---|---|\n| `src/services/PermissionResolver.ts` | permission precedence, inherited memberships, guards, RLS, role seeding | [agents/permissions.md](agents/permissions.md) |\n| `src/services/OidcLoginService.ts`, collections and OIDC handlers | identity reconciliation, transaction boundaries, migration readiness | [agents/oidc-provisioning.md](agents/oidc-provisioning.md) |\n| `src/services/MobileAuthService.ts` | mobile handshake, bearer sessions, bootstrap extension boundary | [agents/mobile-auth.md](agents/mobile-auth.md) |\n| `src/services/TerminalAuthService.ts`, `src/services/LoginAttemptLimiter.ts`, `src/collections/LoginAttemptCollection.ts` | terminal approval concurrency, shared login budget and lockout (#3273), retry-safe database fixtures | [agents/terminal-auth-testing.md](agents/terminal-auth-testing.md) |\n| `src/services/DeviceCredentialService.ts` | per-person PIN layered on a device session (#3276), verifier seam for other credential kinds | this file, \"Security boundaries\" |\n| `src/retention.ts` | expired session/token/CLI-auth reaping and retention sweep wiring | [agents/retention.md](agents/retention.md) |\n\n## Models and authority\n\n- Users are global; tenant access comes through Membership (unique user/tenant).\n User email is normalized and globally unique through readonly nullable\n `emailKey`. `profileId` is a unique cross-package reference: at most one User\n owns each non-null Profile.\n- Tenant uses STI and a materialized hierarchy, maximum depth 10.\n `hierarchyPath`/`hierarchyLevel` are derived from `parentTenantId` by\n `Tenant.save()` on every save (any caller-supplied value is recomputed) and\n descendants are re-materialized when they change; never hand-maintain them.\n Legacy rows are backfilled with `smrt db:materialize-tenant-hierarchy`\n (`materializeTenantHierarchy()`). Tests that need a corrupt path must write it\n with raw SQL (#3036).\n- Role with `tenantId = null` is available to all tenants; `isSystem` prevents\n deletion. `inheritsToDescendants` is opt-in. Seed owner/admin/member/viewer\n through `RoleCollection.seedSystemRoles()` at application initialization.\n- Group roles apply only in their own tenant. Use `getGroupIdsForTenant()`,\n never cross-tenant `getGroupIds()`, for authorization.\n- Upward visibility is opt-in and read-only: `permissions.ancestorRead`\n ({ roles, collections, maxDepth? }) lets a DESCENDANT membership contribute\n declared `<collection>.read` at an ancestor, only when no membership resolved\n there, intersected with BOTH the role's own catalog grants and the\n principal's effective permissions in that descendant, and only for SYSTEM\n roles (a tenant-scoped role sharing a declared slug is ignored). Off by default, never\n write, never lateral, and never row visibility — sibling rows stay scoped by\n tenancy/RLS. Read the permissions module before changing it.\n- Membership DENY always wins. Direct inactive membership blocks inherited\n authority; a direct active membership pins resolution instead of unioning it\n with ancestors. Read the permissions module before changing these rules.\n- AccessRequest has no generated API/MCP/CLI operations; access it through\n `AccessRequestService`. Its JSON field is `requestContext`, not reserved\n slug-scoping `context`.\n\n## Security boundaries\n\n- Generated REST/MCP operations on identity/RBAC models are list/get only.\n Route authentication does not authorize authority mutations, and these models\n are not tenant-scoped. Use permission-gated services or explicitly checked\n consumer handlers. CLI remains a local-operator surface. Preserve the\n registry assertions in `security-audit-1400.test.ts`.\n- Resource guards take the resource tenant, not the session tenant. Omit a\n session membership when targeting a different tenant; mismatches fail closed.\n `loadSessionContext().tenantAuthorization` must be checked by required-tenant\n consumers: null membership can represent inherited authority.\n- Sessions use secure UUIDs; TTL is seconds (default seven days). Access marks\n expired sessions EXPIRED. Magic-link tokens are single use.\n- `Session.authMethod` is server-set at mint time and never read from the\n client; every mint site in this package sets it. A session with\n `parentSessionId` is valid only while its parent validates\n (`loadSessionContext` enforces this; there is no FK cascade by design), may\n not switch to a tenant other than the parent's, and is revoked with its\n parent by `destroySession`.\n- Credential sign-ins reserve through `LoginAttemptLimiter` BEFORE credential\n work and key the subject on the submitted identifier, never a resolved user,\n so unknown and wrong take the same path. Reservations are fail-closed: keep\n them on failure, release on success or abort, never skip the reserve.\n Limiter writes are raw SQL and bypass the change feed; the audit sink is the\n observability seam, and it must never throw into the sign-in path.\n- `DeviceCredentialService` accepts a person credential only from a\n first-class session whose `authMethod` is a device-enrollment method\n (`terminal` by default) and that the host's `assertEnrolledDevice` confirms.\n A layered or PIN session can never administer PINs regardless of\n permissions. PIN hashes carry a per-row salt and the configured pepper;\n treat an unset pepper as a production misconfiguration.\n- A person session carries the PERSON's authority (resolved from their own\n membership on every load), never the device account's. `Session.data` keys\n in `SESSION_DATA_KEYS` are server-set controls enforced by the model:\n `permissionCeiling` only ever intersects (malformed fails closed to none)\n and suppresses `superAdminBypass`/`systemContext`; `absoluteExpiresAt`\n bounds `extend()` and `isValid()`; `idleSeconds` overrides the resolving\n service's TTL policy. Never write them from client input. The ceiling is a\n sign-in snapshot; `singleOccupant` revokes other children only AFTER a\n successful mint. A `mustReset` sign-in mints with an empty ceiling and\n `setPin` ends that session; do not grant it authority another way.\n- A layered session whose tenant differs from its parent's never loads (null\n is allowed: narrower). Limiter rows carry `retain_until`; retention must\n not fall back to a process-wide horizon.\n- `Session.parentSessionId` is the parent's bearer: sensitive, never a\n `list()` filter (raw-SQL id reads, which also keep revocation sweeps free of\n caller list bounds), never serialized to clients. `users.pin.manage` reaches\n only people whose every active membership is in the actor's tenant (the PIN\n is global per person). A sign-in re-checks the verified\n credential generation AFTER the mint (`stillValid`); keep that ordering.\n Operation guards deny outside a session's ceiling; `loadPersonSession`\n re-checks device enrollment.\n- Tenant switching verifies active membership before writing and rotates the\n session ID for non-null targets, revoking the old session. Persist the returned\n `SwitchTenantResult.sessionId`; `switchSessionTenant()` updates the cookie and\n preserves its security settings. Failed switches mutate nothing; null clears\n do not rotate, and layered sessions never rotate (a replacement would escape\n the revocation sweeps they depend on). Layers do not nest. `SessionCollection.setSessionTenant()` is unguarded and must\n never receive an untrusted tenant ID.\n- OIDC provisioning is atomic and fail-closed. Preserve exact issuer/subject\n identifiers, claim-source email verification, unique global Person ownership,\n and transaction-bound reconciliation. Read the OIDC module and canonical\n scenario matrix before changing any provisioning path.\n\n## Entry points and validation\n\n`src/sveltekit/` owns `createSessionHandler`, `createSessionCookie`,\n`destroySessionCookie`, and `switchSessionTenant`. Use README integration examples\nrather than copying application setup into these instructions.\n\nFrom the repository root:\n\n```bash\npnpm --filter @happyvertical/smrt-users test\npnpm --filter @happyvertical/smrt-users typecheck\npnpm --filter @happyvertical/smrt-users test:postgres\n```\n\nStart with the relevant test file via `test -- src/__tests__/<file>.test.ts`.\nRun `test:postgres` for RLS, principal context, OIDC, or terminal-auth database\nchanges. `typecheck` includes Svelte accessibility checks; plain `tsc` is\ninsufficient. Follow root knowledge freshness checks before shipping.\n",
|
|
10590
11264
|
"moduleDocs": [
|
|
10591
11265
|
{
|
|
10592
11266
|
"path": "agents/permissions.md",
|
|
10593
11267
|
"module": "permissions",
|
|
10594
|
-
"content": "# Permissions and tenant inheritance\n\nRead for `src/services/PermissionResolver.ts`, operation guards, role seeding,\nRLS, and tenant-hierarchy changes. Integration examples live in README's\nmanifest-derived permission catalog and PostgreSQL RLS sections.\n\n## Membership selection\n\n1. A direct target-tenant membership pins resolution: active resolves normally;\n pending/suspended returns empty. Never union direct and ancestor roles.\n2. Otherwise walk hierarchyPath nearest-first for the nearest active ancestor\n membership whose role has inheritsToDescendants. Skip inactive/unflagged\n ancestors; they neither grant nor block. No qualifying ancestor means empty.\n3. Run later layers against the target tenant. Only target-tenant groups apply;\n membership overrides travel with the chosen ancestor membership.\n inheritedFromTenantId reports that ancestor, null for direct resolution.\n\nVerify hierarchyPath link-by-link against loaded parentTenantId rows before\ntrusting it: excess depth, self-reference, duplicates or inconsistent paths\nfail closed. Bound traversal by MAX_TENANT_HIERARCHY_DEPTH. Tenant status is not\nconsulted, matching direct resolution. Long-lived user/tenant caches must\ninvalidate ancestor membership changes and inheritsToDescendants flips;\nrequest-scoped caches do not survive those requests.\n\n`loadSessionContext().tenantAuthorization` is authoritative for required-tenant\nconsumers; membership null can mean inherited authority.\n\n4. If no membership resolved at all, apply the opt-in ancestor-read policy\n (smrt#2939): the user's ACTIVE memberships on VERIFIED DESCENDANTS of the\n target, whose role slug is declared in `permissions.ancestorRead.roles` and\n within `maxDepth` hops, contribute `<collection>.read` for declared\n collections, intersected with BOTH that role's own catalog grants (so a\n descendant-side tenant GRANT, group role, or membership GRANT cannot widen\n it) AND the principal's effective permissions in that descendant (so its\n DENY cascade and membership DENY apply). Only a SYSTEM\n role (`tenantId` null, `isSystem: true`) matches a declared slug: slugs are\n not unique across a hierarchy, so a tenant-scoped custom role of the same\n name must not opt its tenant in. Off by default;\n read-only; never lateral; never reached when a direct or inherited\n membership already decided. The target tenant's DENY still subtracts.\n `ancestorReadFromTenantIds` reports the contributing descendants. The grant\n carries no membership, so `membershipId`/`tenantAuthorization.membershipId`\n stay null and a consumer requiring a non-empty membership still refuses.\n\n## Permission precedence\n\nApply these layers in order, later layers overriding earlier ones:\n\n1. Ancestor TenantPermissionOverride cascade (GRANT adds, DENY removes).\n2. Selected membership role permissions.\n3. Target-tenant group-role permissions.\n4. Tenant cascade's net DENY removes role/group grants.\n5. Membership GRANT may re-add a tenant-denied slug.\n6. Membership DENY removes last and always wins.\n\nThe tenant block is the cascade's net result, not the union of historical DENYs:\na child's more-specific GRANT can override a parent DENY. Use a direct child\nmembership or tenant DENY to attenuate inherited role authority.\n`getGroupIdsForTenant(userId, tenantId)` is required; getGroupIds is cross-tenant.\n\n## Hierarchies and seeding\n\n- `Tenant.save()` derives hierarchyPath/hierarchyLevel from the REAL\n parentTenantId chain on every save (never from an ancestor's stored path),\n refuses missing parents, cycles and over-depth before writing, and\n re-materializes descendants by raw column update (so STI subclass columns\n are never clobbered). createChild/moveToParent/makeRoot are thin wrappers.\n `materializeTenantHierarchy(db, { dryRun })` / `smrt\n db:materialize-tenant-hierarchy` backfills legacy rows: idempotent, one\n transaction, refuses a broken chain without writing (#3036). A NEW or\n CHANGED parent link is authority-bearing, so `Tenant.save()` first loads the\n parent through TenantCollection (the caller's tenancy scope) and refuses an\n invisible one on every path — helpers, `create({ parentTenantId })`, or a\n plain assignment + save; system context bypasses as usual, and an unchanged\n link is never re-checked. getTree(rootId?)\n returns UI structure. Maximum depth is 10.\n- Tenant override cascade requires parent cascadePermissions and child\n inheritPermissions. These flags do not gate the independent, per-role\n inheritsToDescendants membership flow.\n- `permissions.ancestorRead` in the `users` package config declares upward,\n read-only visibility: `{ roles, collections, maxDepth? }`. Both lists are\n required and non-empty and `maxDepth >= 1`, else the policy is OFF — there is\n no partially valid declaration. `isAncestorReadableSlug()` is the single place\n the read-only bound is enforced; only a two-segment `<collection>.<action>`\n slug whose action normalizes to `read` on a declared collection passes.\n `PermissionResolver.create(options, { ancestorReadPolicy })` binds one\n resolver; `null` forces it off. It grants the OPERATION at the ancestor, never\n row visibility — sibling rows stay scoped by the tenancy interceptor and RLS.\n- seedSystemRoles({ inheritsToDescendants: ['owner', 'admin'] }) flags listed\n slugs additively, never unflags omitted ones, and rejects unknown slugs.\n Default seeds are exact-tenant.\n- RolePermissionCollection.seedRolePermissions() or\n seedSystemRoles({ seedPermissions: true }) maps owner/admin to all catalog\n permissions, member to ordinary-resource read/create, viewer to read. Member\n create excludes identity/RBAC/security resources and their joins/overrides.\n Seeding is additive/idempotent; removal requires prune: true.\n- When a package adds built-in self-personalization permissions after role\n creation, explicitly call seedDefaultRolePersonalizationPermissions(). It\n upgrades owner/admin/member/viewer idempotently and never grants custom roles.\n\n## Guards and RLS\n\nPermissionCatalogService derives collection.action slugs from manifests,\nincluding custom actions; list/get map to read. Guards derive a class's slug\nthrough the same resolution (explicit `collection`, manifest entry, then its\nSTI base's), so an STI subtype resolves its STI base's collection\n(`Network extends Tenant` -> `tenants.update`) in every build, including a\nbundle decorated before its manifest loads (#3125). Hand-written mutations in form\nhandlers, endpoints, CLI and jobs use assertOperationPermission(). It requires\ncatalog presence then resolves permissions, throwing fail-closed by default.\nUse onDeny: 'return', checkOperationPermission or hasOperationPermission only\nwhen the caller handles a structured/boolean denial.\n\nGenerated SvelteKit write routes (POST/PUT/PATCH/DELETE CRUD and mutating\ncustom actions) enforce the same `collection.action` slug (custom actions use\n`collection.<method>`; actions hosted on a collection class use the item collection, which the catalog now emits) after authentication: the slug must be in the request's\nsession permission snapshot (`locals.permissions`/`permissionSet`/\n`smrtPermissions` or `locals.tenantContext.permissions`), otherwise 403. Only\n`locals.tenantContext.superAdminBypass` skips it; `api.public: true` stays open.\n\nResource-anchored guards receive the resource tenant ID. When it differs from\nthe session tenant, omit the session membership so the resolver selects the\nappropriate authority; mismatched supplied membership/tenant fails closed.\nInherited root-admin authority can then authorize descendant resources without\napplication-side membership fan-out.\n\nResolution cost must not scale with catalog size in objects (#3047): role →\npermission ids and ids → slugs are `list({ select })` projection reads (the\ncollections' beforeList interceptors still run; no RolePermission/Permission\nis hydrated), so an owner mapped to the whole catalog costs two queries and\nno per-permission objects. Keep new resolver reads projections when only ids\nor slugs are needed, and do not add public collection methods for them — each\nbecomes a custom-action slug in the manifest-derived catalog. The resolver's\ncollections never inherit `defaultListLimit`/`maxListLimit` from the caller's\noptions, and id-list reads chunk at `IN_LIST_CHUNK_SIZE` with an explicit\nlimit: a truncated authorization read would narrow the result silently.\n`issue-3047-permission-resolution-cost-postgres.test.ts` pins the hydration\nbudget and a golden equivalence matrix recorded on the hydrating resolver.\n\nThe same rule holds for every framework-internal authorization read (#3048):\ncollections that back a decision — the resolver's, the ResourceGrant exact-tuple\nread in `checkResourceOperationPermission()` (DENY wins only if every row is\nread), and TenantService's owned-tenant quota count — are created through\n`withoutListBounds()` (`src/services/authorization-read-options.ts`), which\nstrips only the caller's list bounds. Use it for any new decision read;\n`issue-3048-authorization-list-bounds-postgres.test.ts` pins it.\n\nPermissionResolver runs every resolution inside smrt-tenancy's system context\n(#3036). Its reads are deliberately cross-tenant and framework-owned — the\nancestor TenantPermissionOverride batch, ancestor memberships for\ninheritsToDescendants, descendant memberships for ancestor-read — and are keyed\nby the explicit (userId, tenantId); with those classes registered tenant-scoped\n(`autoFilter`) the consumer's filter otherwise threw TenantIsolationError or\nsilently narrowed them. It returns slugs and ids only, never rows, and the\ncaller's own reads stay filtered. Do not move a resolver read back under the\nambient tenant; the Postgres regression suite\n`issue-3036-hierarchy-resolution-postgres.test.ts` pins this. Because nothing\nbackstops those reads any more, the tenant-override cascade verifies its chain\ntoo: it uses hierarchyPath only when it agrees link-by-link with\nparentTenantId, otherwise walks the real parent links (so a forged path cannot\ncascade an unrelated tenant in, and a never-materialized row still gets its real\nancestors' DENYs — and GRANTs), and throws TenantHierarchyError on a broken\nreal chain: a per-request failure for that tenant's whole subtree, which\n`smrt db:materialize-tenant-hierarchy --dry-run` reports ahead of time (see\nREADME \"Upgrading an existing fleet\").\n`getTenantInheritanceChain()` returns Tenant rows, so it is NOT run in system\ncontext; it reports the same verified chain the cascade applies when the\ncaller can read every ancestor. Otherwise it never reports PARENT_NOT_FOUND\nfor a row the caller simply cannot see: an interceptor refusal surfaces as\nTenantIsolationError, and a silently filtered row (e.g. RLS) ends the chain at\nthe nearest readable ancestor. Descendant path rewrites are raw column updates recorded through\n`bumpChangeFeed`; `updated_at` is intentionally not bumped (derived columns;\nevery save recomputes them). They are not atomic with the moving row's save —\nan interrupted subtree fails closed until re-saved or backfilled.\n\nSystem context and super-admin bypass are honored. Pass\nallowSuperAdminBypass: false for money or separation-of-duties operations that\nrequire an explicit grant.\n\ngeneratePostgresPermissionSql/applyPostgresPermissionPolicies enforce generated\nRLS using smrt.permissions and smrt.tenant_id, installed with set_config by\nwithSessionPermissionContext. This bounds REST, MCP and in-process database\naccess once principal context is set. Child-tenant sessions receive resolved\ninherited permissions automatically, but RLS row filtering remains bound to the\nsession tenant: a root session's app-level guard authorization does not itself\npermit child rows through RLS. Preserve this distinction.\n"
|
|
11268
|
+
"content": "# Permissions and tenant inheritance\n\nRead for `src/services/PermissionResolver.ts`, operation guards, role seeding,\nRLS, and tenant-hierarchy changes. Integration examples live in README's\nmanifest-derived permission catalog and PostgreSQL RLS sections.\n\n## Membership selection\n\n1. A direct target-tenant membership pins resolution: active resolves normally;\n pending/suspended returns empty. Never union direct and ancestor roles.\n2. Otherwise walk hierarchyPath nearest-first for the nearest active ancestor\n membership whose role has inheritsToDescendants. Skip inactive/unflagged\n ancestors; they neither grant nor block. No qualifying ancestor means empty.\n3. Run later layers against the target tenant. Only target-tenant groups apply;\n membership overrides travel with the chosen ancestor membership.\n inheritedFromTenantId reports that ancestor, null for direct resolution.\n\nVerify hierarchyPath link-by-link against loaded parentTenantId rows before\ntrusting it: excess depth, self-reference, duplicates or inconsistent paths\nfail closed. Bound traversal by MAX_TENANT_HIERARCHY_DEPTH. Tenant status is not\nconsulted, matching direct resolution. Long-lived user/tenant caches must\ninvalidate ancestor membership changes and inheritsToDescendants flips;\nrequest-scoped caches do not survive those requests.\n\n`loadSessionContext().tenantAuthorization` is authoritative for required-tenant\nconsumers; membership null can mean inherited authority.\n\n4. If no membership resolved at all, apply the opt-in ancestor-read policy\n (smrt#2939): the user's ACTIVE memberships on VERIFIED DESCENDANTS of the\n target, whose role slug is declared in `permissions.ancestorRead.roles` and\n within `maxDepth` hops, contribute `<collection>.read` for declared\n collections, intersected with BOTH that role's own catalog grants (so a\n descendant-side tenant GRANT, group role, or membership GRANT cannot widen\n it) AND the principal's effective permissions in that descendant (so its\n DENY cascade and membership DENY apply). Only a SYSTEM\n role (`tenantId` null, `isSystem: true`) matches a declared slug: slugs are\n not unique across a hierarchy, so a tenant-scoped custom role of the same\n name must not opt its tenant in. Off by default;\n read-only; never lateral; never reached when a direct or inherited\n membership already decided. The target tenant's DENY still subtracts.\n `ancestorReadFromTenantIds` reports the contributing descendants. The grant\n carries no membership, so `membershipId`/`tenantAuthorization.membershipId`\n stay null and a consumer requiring a non-empty membership still refuses.\n\n## Permission precedence\n\nApply these layers in order, later layers overriding earlier ones:\n\n1. Ancestor TenantPermissionOverride cascade (GRANT adds, DENY removes).\n2. Selected membership role permissions.\n3. Target-tenant group-role permissions.\n4. Tenant cascade's net DENY removes role/group grants.\n5. Membership GRANT may re-add a tenant-denied slug.\n6. Membership DENY removes last and always wins.\n\nThe tenant block is the cascade's net result, not the union of historical DENYs:\na child's more-specific GRANT can override a parent DENY. Use a direct child\nmembership or tenant DENY to attenuate inherited role authority.\n`getGroupIdsForTenant(userId, tenantId)` is required; getGroupIds is cross-tenant.\n\n## Hierarchies and seeding\n\n- `Tenant.save()` derives hierarchyPath/hierarchyLevel from the REAL\n parentTenantId chain on every save (never from an ancestor's stored path),\n refuses missing parents, cycles and over-depth before writing, and\n re-materializes descendants by raw column update (so STI subclass columns\n are never clobbered). createChild/moveToParent/makeRoot are thin wrappers.\n `materializeTenantHierarchy(db, { dryRun })` / `smrt\n db:materialize-tenant-hierarchy` backfills legacy rows: idempotent, one\n transaction, refuses a broken chain without writing (#3036). A NEW or\n CHANGED parent link is authority-bearing, so `Tenant.save()` first loads the\n parent through TenantCollection (the caller's tenancy scope) and refuses an\n invisible one on every path — helpers, `create({ parentTenantId })`, or a\n plain assignment + save; system context bypasses as usual, and an unchanged\n link is never re-checked. getTree(rootId?)\n returns UI structure. Maximum depth is 10.\n- Tenant override cascade requires parent cascadePermissions and child\n inheritPermissions. These flags do not gate the independent, per-role\n inheritsToDescendants membership flow.\n- `permissions.ancestorRead` in the `users` package config declares upward,\n read-only visibility: `{ roles, collections, maxDepth? }`. Both lists are\n required and non-empty and `maxDepth >= 1`, else the policy is OFF — there is\n no partially valid declaration. `isAncestorReadableSlug()` is the single place\n the read-only bound is enforced; only a two-segment `<collection>.<action>`\n slug whose action normalizes to `read` on a declared collection passes.\n `PermissionResolver.create(options, { ancestorReadPolicy })` binds one\n resolver; `null` forces it off. It grants the OPERATION at the ancestor, never\n row visibility — sibling rows stay scoped by the tenancy interceptor and RLS.\n- seedSystemRoles({ inheritsToDescendants: ['owner', 'admin'] }) flags listed\n slugs additively, never unflags omitted ones, and rejects unknown slugs.\n Default seeds are exact-tenant.\n- RolePermissionCollection.seedRolePermissions() or\n seedSystemRoles({ seedPermissions: true }) maps owner/admin to all catalog\n permissions, member to ordinary-resource read/create, viewer to read. Member\n create excludes identity/RBAC/security resources and their joins/overrides.\n Seeding is additive/idempotent; removal requires prune: true. Catalog sync\n and grants plan from one read, then commit in `SEEDING_BATCH_SIZE` batches\n (#3323): never one durable commit per row (an fsync each on local SQLite),\n and never one catalog-wide transaction (`@happyvertical/sql` transaction\n scopes cost superlinearly per statement). Pass a caller transaction's handle.\n- When a package adds built-in self-personalization permissions after role\n creation, explicitly call seedDefaultRolePersonalizationPermissions(). It\n upgrades owner/admin/member/viewer idempotently and never grants custom roles.\n\n## Guards and RLS\n\nPermissionCatalogService derives collection.action slugs from manifests,\nincluding custom actions; list/get map to read. Guards derive a class's slug\nthrough the same resolution (explicit `collection`, manifest entry, then its\nSTI base's), so an STI subtype resolves its STI base's collection\n(`Network extends Tenant` -> `tenants.update`) in every build, including a\nbundle decorated before its manifest loads (#3125). Hand-written mutations in form\nhandlers, endpoints, CLI and jobs use assertOperationPermission(). It requires\ncatalog presence then resolves permissions, throwing fail-closed by default.\nUse onDeny: 'return', checkOperationPermission or hasOperationPermission only\nwhen the caller handles a structured/boolean denial.\n\nGenerated SvelteKit write routes (POST/PUT/PATCH/DELETE CRUD and mutating\ncustom actions) enforce the same `collection.action` slug (custom actions use\n`collection.<method>`; actions hosted on a collection class use the item collection, which the catalog now emits) after authentication: the slug must be in the request's\nsession permission snapshot (`locals.permissions`/`permissionSet`/\n`smrtPermissions` or `locals.tenantContext.permissions`), otherwise 403. Only\n`locals.tenantContext.superAdminBypass` skips it; `api.public: true` stays open.\n\nResource-anchored guards receive the resource tenant ID. When it differs from\nthe session tenant, omit the session membership so the resolver selects the\nappropriate authority; mismatched supplied membership/tenant fails closed.\nInherited root-admin authority can then authorize descendant resources without\napplication-side membership fan-out.\n\nResolution cost must not scale with catalog size in objects (#3047): role →\npermission ids and ids → slugs are `list({ select })` projection reads (the\ncollections' beforeList interceptors still run; no RolePermission/Permission\nis hydrated), so an owner mapped to the whole catalog costs two queries and\nno per-permission objects. Keep new resolver reads projections when only ids\nor slugs are needed, and do not add public collection methods for them — each\nbecomes a custom-action slug in the manifest-derived catalog. The resolver's\ncollections never inherit `defaultListLimit`/`maxListLimit` from the caller's\noptions, and id-list reads chunk at `IN_LIST_CHUNK_SIZE` with an explicit\nlimit: a truncated authorization read would narrow the result silently.\n`issue-3047-permission-resolution-cost-postgres.test.ts` pins the hydration\nbudget and a golden equivalence matrix recorded on the hydrating resolver.\n\nThe same rule holds for every framework-internal authorization read (#3048):\ncollections that back a decision — the resolver's, the ResourceGrant exact-tuple\nread in `checkResourceOperationPermission()` (DENY wins only if every row is\nread), and TenantService's owned-tenant quota count — are created through\n`withoutListBounds()` (`src/services/authorization-read-options.ts`), which\nstrips only the caller's list bounds. Use it for any new decision read;\n`issue-3048-authorization-list-bounds-postgres.test.ts` pins it.\n\nPermissionResolver runs every resolution inside smrt-tenancy's system context\n(#3036). Its reads are deliberately cross-tenant and framework-owned — the\nancestor TenantPermissionOverride batch, ancestor memberships for\ninheritsToDescendants, descendant memberships for ancestor-read — and are keyed\nby the explicit (userId, tenantId); with those classes registered tenant-scoped\n(`autoFilter`) the consumer's filter otherwise threw TenantIsolationError or\nsilently narrowed them. It returns slugs and ids only, never rows, and the\ncaller's own reads stay filtered. Do not move a resolver read back under the\nambient tenant; the Postgres regression suite\n`issue-3036-hierarchy-resolution-postgres.test.ts` pins this. Because nothing\nbackstops those reads any more, the tenant-override cascade verifies its chain\ntoo: it uses hierarchyPath only when it agrees link-by-link with\nparentTenantId, otherwise walks the real parent links (so a forged path cannot\ncascade an unrelated tenant in, and a never-materialized row still gets its real\nancestors' DENYs — and GRANTs), and throws TenantHierarchyError on a broken\nreal chain: a per-request failure for that tenant's whole subtree, which\n`smrt db:materialize-tenant-hierarchy --dry-run` reports ahead of time (see\nREADME \"Upgrading an existing fleet\").\n`getTenantInheritanceChain()` returns Tenant rows, so it is NOT run in system\ncontext; it reports the same verified chain the cascade applies when the\ncaller can read every ancestor. Otherwise it never reports PARENT_NOT_FOUND\nfor a row the caller simply cannot see: an interceptor refusal surfaces as\nTenantIsolationError, and a silently filtered row (e.g. RLS) ends the chain at\nthe nearest readable ancestor. Descendant path rewrites are raw column updates recorded through\n`bumpChangeFeed`; `updated_at` is intentionally not bumped (derived columns;\nevery save recomputes them). They are not atomic with the moving row's save —\nan interrupted subtree fails closed until re-saved or backfilled.\n\nSystem context and super-admin bypass are honored. Pass\nallowSuperAdminBypass: false for money or separation-of-duties operations that\nrequire an explicit grant.\n\ngeneratePostgresPermissionSql/applyPostgresPermissionPolicies enforce generated\nRLS using smrt.permissions and smrt.tenant_id, installed with set_config by\nwithSessionPermissionContext. This bounds REST, MCP and in-process database\naccess once principal context is set. Child-tenant sessions receive resolved\ninherited permissions automatically, but RLS row filtering remains bound to the\nsession tenant: a root session's app-level guard authorization does not itself\npermit child rows through RLS. Preserve this distinction.\n\nRolePermission's natural key is `(role_id, permission_id)` (#3329); repeated or\nconcurrent inserts converge without changing the grant ID. Existing deployments\nmust run `deduplicateRolePermissions()` with stopped writers before ordinary\nschema migration; see README's \"Upgrade duplicate role grants\". It keeps the\noldest row and installs uniqueness atomically. Runtime seeding never performs\nthis schema repair.\n"
|
|
10595
11269
|
},
|
|
10596
11270
|
{
|
|
10597
11271
|
"path": "agents/oidc-provisioning.md",
|
|
@@ -10611,7 +11285,7 @@
|
|
|
10611
11285
|
{
|
|
10612
11286
|
"path": "agents/retention.md",
|
|
10613
11287
|
"module": "retention",
|
|
10614
|
-
"content": "# smrt-users/credential retention\n\nModule semantics for `src/retention.ts` and the three `deleteExpired()` paths\nit drives. Package orientation, the permission model, and the security rules\nthat apply before editing anything live in [../AGENTS.md](../AGENTS.md) — read\nthat first.\n\n## Credential retention (#2375)\n\n`SessionCollection.deleteExpired()`,\n`UsersMagicLinkTokenCollection.deleteExpired()` and\n`UsersCliAuthRequestCollection.deleteExpired()` all existed, and all waited for\nan application to remember to call them. Expired credential rows are the worst\nkind of unbounded growth: worthless the moment they expire, and exactly the\nrows an attacker would like to still find in the table.\n\n`src/retention.ts` contributes the three to the framework retention sweep in\n`@happyvertical/smrt-core`, so `smrt db:prune` and a running jobs `TaskRunner`\nreap them.\n\n- **The entry point registers on import.** `src/index.ts` calls\n `registerUserRetentionTasks()`, so any process that loaded this package\n contributes the tasks — including `smrt db:prune`, which imports the package\n optionally for exactly that reason. Registering is not scheduling: nothing is\n deleted until something runs a sweep.\n- **Task names** are `users-sessions`, `users-magic-link-tokens`,\n `users-cli-auth-requests` — prefixed with the package's short name because
|
|
11288
|
+
"content": "# smrt-users/credential retention\n\nModule semantics for `src/retention.ts` and the three `deleteExpired()` paths\nit drives. Package orientation, the permission model, and the security rules\nthat apply before editing anything live in [../AGENTS.md](../AGENTS.md) — read\nthat first.\n\n## Credential retention (#2375)\n\n`SessionCollection.deleteExpired()`,\n`UsersMagicLinkTokenCollection.deleteExpired()` and\n`UsersCliAuthRequestCollection.deleteExpired()` all existed, and all waited for\nan application to remember to call them. Expired credential rows are the worst\nkind of unbounded growth: worthless the moment they expire, and exactly the\nrows an attacker would like to still find in the table.\n\n`src/retention.ts` contributes the three to the framework retention sweep in\n`@happyvertical/smrt-core`, so `smrt db:prune` and a running jobs `TaskRunner`\nreap them.\n\n- **The entry point registers on import.** `src/index.ts` calls\n `registerUserRetentionTasks()`, so any process that loaded this package\n contributes the tasks — including `smrt db:prune`, which imports the package\n optionally for exactly that reason. Registering is not scheduling: nothing is\n deleted until something runs a sweep.\n- **Task names** are `users-sessions`, `users-magic-link-tokens`,\n `users-cli-auth-requests`, `users-login-attempts`, `users-login-audit-events`\n — prefixed with the package's short name because the retention registry is\n one process-global namespace. Opt one out with\n `runRetentionSweep(db, { tasks: { 'users-sessions': false } })` or\n `smrt db:prune --skip users-sessions`; `unregisterUserRetentionTasks()`\n removes all of them.\n- **The credential tasks have no retention window to configure.** An expired\n credential has nothing worth retaining, so each deletes only already-expired\n rows. An application that keeps expired sessions for audit should opt the\n task out and archive them itself. The two #3273 tasks differ: limiter rows\n are pruned once past their own `retain_until` — written by the limiter from\n its configured window and streak-forgiveness horizon, so a custom-configured\n limiter is never swept early — and never while a lockout is live; audit\n events are pruned after\n `DEFAULT_LOGIN_AUDIT_RETENTION_DAYS` (90). A host that needs longer audit\n retention opts `users-login-audit-events` out and archives.\n- **All three are a single counted DELETE**, not a hydrate-and-delete loop.\n They now run unattended on a timer, and a per-row delete that throws part-way\n leaves the rest of the expired rows un-reaped — the #1400 reasoning that\n already applied to sessions, now applied to the other two. Counting first\n also gives a usable figure where `rowCount` is not populated, and is what\n makes `{ dryRun }` preview the same predicate rather than an estimate. The\n count and the delete are not one transaction, so the figure is approximate\n under concurrent writers.\n- **`expiresAt` is `@field({ indexed: true })`** on `Session`,\n `UsersMagicLinkToken` and `UsersCliAuthRequest`: the prune predicate scans\n that column on every pass.\n- **CLI bearer handoff is single-use.** An approved request becomes `consumed`\n and clears its `sessionId` when one poller wins the exchange. The CLI\n retention task deletes pending requests past their TTL, requests already\n marked `expired` by lazy expiry, and consumed history; it retains approved\n requests until exchange so a near-expiry approval cannot orphan its bearer\n session.\n"
|
|
10615
11289
|
}
|
|
10616
11290
|
]
|
|
10617
11291
|
}
|