@happyvertical/smrt-users 0.57.1 → 0.57.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.
@@ -3,12 +3,12 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-users",
6
- "packageVersion": "0.57.1",
6
+ "packageVersion": "0.57.3",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "cd5cf425052f3c1a47b9732fa8240fd12c2be4291a922e4412714b0513bbca2e",
11
- "packageJson": "871a2cad86fa16748e09f43dc8edb5f9c6326c4dbcd3ef669b7d0327aa116441",
10
+ "manifest": "aecb25488c6141cf87d3e8ba61bb54fa3b94ea97712f7d9f9f77cc7b74bda80c",
11
+ "packageJson": "e2574af9f3e6c489a9c13789cd1166b98a0f84de857279a27427804d3b14758e",
12
12
  "agents": "c4ab6a1aae24c0fb687d4d02ef25dcb5e6942ad7e8318e9d0dd8d03d5750b788",
13
13
  "moduleDoc:agents/permissions.md": "02b2f03f0e057721369d9c693a624a6fdb3df4935401531a4373ff18229a64be",
14
14
  "moduleDoc:agents/oidc-provisioning.md": "b73f3fdaf61136720132a614c7e14237b7bd35f7bf5d43eabb29f565485803b9",
@@ -74,7 +74,7 @@
74
74
  "users"
75
75
  ],
76
76
  "risks": [
77
- "cross-package-refs:1",
77
+ "cross-package-refs:2",
78
78
  "sensitive-fields-excluded",
79
79
  "sti-inheritance"
80
80
  ],
@@ -3194,6 +3194,687 @@
3194
3194
  "tags": [],
3195
3195
  "risks": []
3196
3196
  },
3197
+ {
3198
+ "name": "MemberClass",
3199
+ "qualifiedName": "@happyvertical/smrt-users:MemberClass",
3200
+ "displayLabelField": "name",
3201
+ "collection": "memberclasses",
3202
+ "description": "A tenant-configured legal class; it has no authority or entitlement effect.",
3203
+ "tableName": "member_classes",
3204
+ "packageName": "@happyvertical/smrt-users",
3205
+ "extends": "SmrtObject",
3206
+ "fields": [
3207
+ {
3208
+ "name": "tenantId",
3209
+ "type": "text",
3210
+ "required": true,
3211
+ "columnType": "UUID"
3212
+ },
3213
+ {
3214
+ "name": "key",
3215
+ "type": "text",
3216
+ "required": false,
3217
+ "columnType": "TEXT"
3218
+ },
3219
+ {
3220
+ "name": "name",
3221
+ "type": "text",
3222
+ "required": false,
3223
+ "columnType": "TEXT"
3224
+ },
3225
+ {
3226
+ "name": "description",
3227
+ "type": "text",
3228
+ "required": false,
3229
+ "columnType": "TEXT"
3230
+ },
3231
+ {
3232
+ "name": "policyMetadata",
3233
+ "type": "json",
3234
+ "required": false,
3235
+ "columnType": "JSON"
3236
+ }
3237
+ ],
3238
+ "relationships": [],
3239
+ "methods": [],
3240
+ "tenant": {
3241
+ "scoped": true,
3242
+ "mode": "required",
3243
+ "field": "tenantId"
3244
+ },
3245
+ "conflictColumns": [
3246
+ "tenant_id",
3247
+ "slug",
3248
+ "context"
3249
+ ],
3250
+ "surfaces": [],
3251
+ "relationshipFeatures": [
3252
+ "uuidColumns"
3253
+ ],
3254
+ "tags": [],
3255
+ "risks": []
3256
+ },
3257
+ {
3258
+ "name": "LegalMember",
3259
+ "qualifiedName": "@happyvertical/smrt-users:LegalMember",
3260
+ "collection": "legalmembers",
3261
+ "description": "The legal register entry, deliberately separate from RBAC Membership.",
3262
+ "tableName": "legal_members",
3263
+ "packageName": "@happyvertical/smrt-users",
3264
+ "extends": "SmrtObject",
3265
+ "fields": [
3266
+ {
3267
+ "name": "tenantId",
3268
+ "type": "text",
3269
+ "required": true,
3270
+ "columnType": "UUID"
3271
+ },
3272
+ {
3273
+ "name": "memberClassId",
3274
+ "type": "foreignKey",
3275
+ "required": false,
3276
+ "related": "MemberClass",
3277
+ "columnType": "UUID"
3278
+ },
3279
+ {
3280
+ "name": "kind",
3281
+ "type": "text",
3282
+ "required": false,
3283
+ "columnType": "TEXT"
3284
+ },
3285
+ {
3286
+ "name": "displayName",
3287
+ "type": "text",
3288
+ "required": false,
3289
+ "columnType": "TEXT"
3290
+ },
3291
+ {
3292
+ "name": "userId",
3293
+ "type": "foreignKey",
3294
+ "required": false,
3295
+ "related": "User",
3296
+ "columnType": "UUID"
3297
+ },
3298
+ {
3299
+ "name": "profileId",
3300
+ "type": "crossPackageRef",
3301
+ "required": false,
3302
+ "related": "@happyvertical/smrt-profiles:Profile",
3303
+ "columnType": "UUID"
3304
+ },
3305
+ {
3306
+ "name": "externalPartyRef",
3307
+ "type": "text",
3308
+ "required": false,
3309
+ "columnType": "TEXT"
3310
+ },
3311
+ {
3312
+ "name": "joinedOn",
3313
+ "type": "text",
3314
+ "required": false,
3315
+ "columnType": "TEXT"
3316
+ },
3317
+ {
3318
+ "name": "leftOn",
3319
+ "type": "text",
3320
+ "required": false,
3321
+ "columnType": "TEXT"
3322
+ },
3323
+ {
3324
+ "name": "status",
3325
+ "type": "text",
3326
+ "required": false,
3327
+ "columnType": "TEXT"
3328
+ }
3329
+ ],
3330
+ "relationships": [
3331
+ {
3332
+ "name": "memberClassId",
3333
+ "type": "foreignKey",
3334
+ "required": false,
3335
+ "related": "MemberClass",
3336
+ "columnType": "UUID"
3337
+ },
3338
+ {
3339
+ "name": "userId",
3340
+ "type": "foreignKey",
3341
+ "required": false,
3342
+ "related": "User",
3343
+ "columnType": "UUID"
3344
+ },
3345
+ {
3346
+ "name": "profileId",
3347
+ "type": "crossPackageRef",
3348
+ "required": false,
3349
+ "related": "@happyvertical/smrt-profiles:Profile",
3350
+ "columnType": "UUID"
3351
+ }
3352
+ ],
3353
+ "methods": [],
3354
+ "tenant": {
3355
+ "scoped": true,
3356
+ "mode": "required",
3357
+ "field": "tenantId"
3358
+ },
3359
+ "conflictColumns": [
3360
+ "tenant_id",
3361
+ "slug",
3362
+ "context"
3363
+ ],
3364
+ "surfaces": [],
3365
+ "relationshipFeatures": [
3366
+ "crossPackageRef",
3367
+ "foreignKey",
3368
+ "uuidColumns"
3369
+ ],
3370
+ "tags": [],
3371
+ "risks": []
3372
+ },
3373
+ {
3374
+ "name": "LegalMemberStatusHistory",
3375
+ "qualifiedName": "@happyvertical/smrt-users:LegalMemberStatusHistory",
3376
+ "collection": "legalmemberstatushistories",
3377
+ "description": "Append-only effective history; current status never erases it.",
3378
+ "tableName": "legal_member_status_history",
3379
+ "packageName": "@happyvertical/smrt-users",
3380
+ "extends": "SmrtObject",
3381
+ "fields": [
3382
+ {
3383
+ "name": "tenantId",
3384
+ "type": "text",
3385
+ "required": true,
3386
+ "columnType": "UUID"
3387
+ },
3388
+ {
3389
+ "name": "memberId",
3390
+ "type": "foreignKey",
3391
+ "required": false,
3392
+ "related": "LegalMember",
3393
+ "columnType": "UUID"
3394
+ },
3395
+ {
3396
+ "name": "status",
3397
+ "type": "text",
3398
+ "required": false,
3399
+ "columnType": "TEXT"
3400
+ },
3401
+ {
3402
+ "name": "effectiveOn",
3403
+ "type": "text",
3404
+ "required": false,
3405
+ "columnType": "TEXT"
3406
+ },
3407
+ {
3408
+ "name": "sequence",
3409
+ "type": "integer",
3410
+ "required": false,
3411
+ "columnType": "INTEGER"
3412
+ },
3413
+ {
3414
+ "name": "reason",
3415
+ "type": "text",
3416
+ "required": false,
3417
+ "columnType": "TEXT"
3418
+ },
3419
+ {
3420
+ "name": "actorRef",
3421
+ "type": "text",
3422
+ "required": false,
3423
+ "columnType": "TEXT"
3424
+ }
3425
+ ],
3426
+ "relationships": [
3427
+ {
3428
+ "name": "memberId",
3429
+ "type": "foreignKey",
3430
+ "required": false,
3431
+ "related": "LegalMember",
3432
+ "columnType": "UUID"
3433
+ }
3434
+ ],
3435
+ "methods": [
3436
+ "delete",
3437
+ "save"
3438
+ ],
3439
+ "methodSignatures": [
3440
+ {
3441
+ "name": "delete",
3442
+ "async": true,
3443
+ "returns": "Promise<void>"
3444
+ },
3445
+ {
3446
+ "name": "save",
3447
+ "async": true,
3448
+ "returns": "Promise<this>"
3449
+ }
3450
+ ],
3451
+ "tenant": {
3452
+ "scoped": true,
3453
+ "mode": "required",
3454
+ "field": "tenantId"
3455
+ },
3456
+ "conflictColumns": [
3457
+ "tenant_id",
3458
+ "slug",
3459
+ "context"
3460
+ ],
3461
+ "surfaces": [],
3462
+ "withheldSurfaces": [
3463
+ {
3464
+ "kind": "api",
3465
+ "operation": "save",
3466
+ "code": "lifecycle-method",
3467
+ "reason": "`save` is a framework lifecycle method, not a distinct operation",
3468
+ "objectName": "@happyvertical/smrt-users:LegalMemberStatusHistory"
3469
+ }
3470
+ ],
3471
+ "relationshipFeatures": [
3472
+ "foreignKey",
3473
+ "uuidColumns"
3474
+ ],
3475
+ "tags": [],
3476
+ "risks": []
3477
+ },
3478
+ {
3479
+ "name": "LegalMemberReference",
3480
+ "qualifiedName": "@happyvertical/smrt-users:LegalMemberReference",
3481
+ "collection": "legalmemberreferences",
3482
+ "description": "A link to authoritative share/dues/subscription/agreement/ledger evidence.",
3483
+ "tableName": "legal_member_references",
3484
+ "packageName": "@happyvertical/smrt-users",
3485
+ "extends": "SmrtObject",
3486
+ "fields": [
3487
+ {
3488
+ "name": "tenantId",
3489
+ "type": "text",
3490
+ "required": true,
3491
+ "columnType": "UUID"
3492
+ },
3493
+ {
3494
+ "name": "memberId",
3495
+ "type": "foreignKey",
3496
+ "required": false,
3497
+ "related": "LegalMember",
3498
+ "columnType": "UUID"
3499
+ },
3500
+ {
3501
+ "name": "kind",
3502
+ "type": "text",
3503
+ "required": false,
3504
+ "columnType": "TEXT"
3505
+ },
3506
+ {
3507
+ "name": "sourceType",
3508
+ "type": "text",
3509
+ "required": false,
3510
+ "columnType": "TEXT"
3511
+ },
3512
+ {
3513
+ "name": "sourceId",
3514
+ "type": "text",
3515
+ "required": false,
3516
+ "columnType": "TEXT"
3517
+ },
3518
+ {
3519
+ "name": "authoritative",
3520
+ "type": "boolean",
3521
+ "required": false,
3522
+ "columnType": "BOOLEAN"
3523
+ },
3524
+ {
3525
+ "name": "effectiveFrom",
3526
+ "type": "text",
3527
+ "required": false,
3528
+ "columnType": "TEXT"
3529
+ },
3530
+ {
3531
+ "name": "effectiveTo",
3532
+ "type": "text",
3533
+ "required": false,
3534
+ "columnType": "TEXT"
3535
+ },
3536
+ {
3537
+ "name": "revokesId",
3538
+ "type": "foreignKey",
3539
+ "required": false,
3540
+ "related": "LegalMemberReference",
3541
+ "columnType": "UUID"
3542
+ }
3543
+ ],
3544
+ "relationships": [
3545
+ {
3546
+ "name": "memberId",
3547
+ "type": "foreignKey",
3548
+ "required": false,
3549
+ "related": "LegalMember",
3550
+ "columnType": "UUID"
3551
+ },
3552
+ {
3553
+ "name": "revokesId",
3554
+ "type": "foreignKey",
3555
+ "required": false,
3556
+ "related": "LegalMemberReference",
3557
+ "columnType": "UUID"
3558
+ }
3559
+ ],
3560
+ "methods": [
3561
+ "delete",
3562
+ "save"
3563
+ ],
3564
+ "methodSignatures": [
3565
+ {
3566
+ "name": "delete",
3567
+ "async": true,
3568
+ "returns": "Promise<void>"
3569
+ },
3570
+ {
3571
+ "name": "save",
3572
+ "async": true,
3573
+ "returns": "Promise<this>"
3574
+ }
3575
+ ],
3576
+ "tenant": {
3577
+ "scoped": true,
3578
+ "mode": "required",
3579
+ "field": "tenantId"
3580
+ },
3581
+ "conflictColumns": [
3582
+ "tenant_id",
3583
+ "slug",
3584
+ "context"
3585
+ ],
3586
+ "surfaces": [],
3587
+ "withheldSurfaces": [
3588
+ {
3589
+ "kind": "api",
3590
+ "operation": "save",
3591
+ "code": "lifecycle-method",
3592
+ "reason": "`save` is a framework lifecycle method, not a distinct operation",
3593
+ "objectName": "@happyvertical/smrt-users:LegalMemberReference"
3594
+ }
3595
+ ],
3596
+ "relationshipFeatures": [
3597
+ "foreignKey",
3598
+ "uuidColumns"
3599
+ ],
3600
+ "tags": [],
3601
+ "risks": []
3602
+ },
3603
+ {
3604
+ "name": "MemberOfficerAppointment",
3605
+ "qualifiedName": "@happyvertical/smrt-users:MemberOfficerAppointment",
3606
+ "collection": "memberofficerappointments",
3607
+ "description": "A dated register of board/officer appointments; it does not implement elections.",
3608
+ "tableName": "member_officer_appointments",
3609
+ "packageName": "@happyvertical/smrt-users",
3610
+ "extends": "SmrtObject",
3611
+ "fields": [
3612
+ {
3613
+ "name": "tenantId",
3614
+ "type": "text",
3615
+ "required": true,
3616
+ "columnType": "UUID"
3617
+ },
3618
+ {
3619
+ "name": "memberId",
3620
+ "type": "foreignKey",
3621
+ "required": false,
3622
+ "related": "LegalMember",
3623
+ "columnType": "UUID"
3624
+ },
3625
+ {
3626
+ "name": "role",
3627
+ "type": "text",
3628
+ "required": false,
3629
+ "columnType": "TEXT"
3630
+ },
3631
+ {
3632
+ "name": "effectiveFrom",
3633
+ "type": "text",
3634
+ "required": false,
3635
+ "columnType": "TEXT"
3636
+ },
3637
+ {
3638
+ "name": "effectiveTo",
3639
+ "type": "text",
3640
+ "required": false,
3641
+ "columnType": "TEXT"
3642
+ },
3643
+ {
3644
+ "name": "authority",
3645
+ "type": "json",
3646
+ "required": false,
3647
+ "columnType": "JSON"
3648
+ }
3649
+ ],
3650
+ "relationships": [
3651
+ {
3652
+ "name": "memberId",
3653
+ "type": "foreignKey",
3654
+ "required": false,
3655
+ "related": "LegalMember",
3656
+ "columnType": "UUID"
3657
+ }
3658
+ ],
3659
+ "methods": [],
3660
+ "tenant": {
3661
+ "scoped": true,
3662
+ "mode": "required",
3663
+ "field": "tenantId"
3664
+ },
3665
+ "conflictColumns": [
3666
+ "tenant_id",
3667
+ "slug",
3668
+ "context"
3669
+ ],
3670
+ "surfaces": [],
3671
+ "relationshipFeatures": [
3672
+ "foreignKey",
3673
+ "uuidColumns"
3674
+ ],
3675
+ "tags": [],
3676
+ "risks": []
3677
+ },
3678
+ {
3679
+ "name": "MemberEligibilitySnapshot",
3680
+ "qualifiedName": "@happyvertical/smrt-users:MemberEligibilitySnapshot",
3681
+ "collection": "membereligibilitysnapshots",
3682
+ "tableName": "member_eligibility_snapshots",
3683
+ "packageName": "@happyvertical/smrt-users",
3684
+ "extends": "SmrtObject",
3685
+ "fields": [
3686
+ {
3687
+ "name": "tenantId",
3688
+ "type": "text",
3689
+ "required": true,
3690
+ "columnType": "UUID"
3691
+ },
3692
+ {
3693
+ "name": "memberId",
3694
+ "type": "foreignKey",
3695
+ "required": false,
3696
+ "related": "LegalMember",
3697
+ "columnType": "UUID"
3698
+ },
3699
+ {
3700
+ "name": "recordDate",
3701
+ "type": "text",
3702
+ "required": false,
3703
+ "columnType": "TEXT"
3704
+ },
3705
+ {
3706
+ "name": "eligible",
3707
+ "type": "boolean",
3708
+ "required": false,
3709
+ "columnType": "BOOLEAN"
3710
+ },
3711
+ {
3712
+ "name": "reason",
3713
+ "type": "text",
3714
+ "required": false,
3715
+ "columnType": "TEXT"
3716
+ },
3717
+ {
3718
+ "name": "evidence",
3719
+ "type": "json",
3720
+ "required": false,
3721
+ "columnType": "JSON"
3722
+ }
3723
+ ],
3724
+ "relationships": [
3725
+ {
3726
+ "name": "memberId",
3727
+ "type": "foreignKey",
3728
+ "required": false,
3729
+ "related": "LegalMember",
3730
+ "columnType": "UUID"
3731
+ }
3732
+ ],
3733
+ "methods": [
3734
+ "delete",
3735
+ "save"
3736
+ ],
3737
+ "methodSignatures": [
3738
+ {
3739
+ "name": "delete",
3740
+ "async": true,
3741
+ "returns": "Promise<void>"
3742
+ },
3743
+ {
3744
+ "name": "save",
3745
+ "async": true,
3746
+ "returns": "Promise<this>"
3747
+ }
3748
+ ],
3749
+ "tenant": {
3750
+ "scoped": true,
3751
+ "mode": "required",
3752
+ "field": "tenantId"
3753
+ },
3754
+ "conflictColumns": [
3755
+ "tenant_id",
3756
+ "slug",
3757
+ "context"
3758
+ ],
3759
+ "surfaces": [],
3760
+ "withheldSurfaces": [
3761
+ {
3762
+ "kind": "api",
3763
+ "operation": "save",
3764
+ "code": "lifecycle-method",
3765
+ "reason": "`save` is a framework lifecycle method, not a distinct operation",
3766
+ "objectName": "@happyvertical/smrt-users:MemberEligibilitySnapshot"
3767
+ }
3768
+ ],
3769
+ "relationshipFeatures": [
3770
+ "foreignKey",
3771
+ "uuidColumns"
3772
+ ],
3773
+ "tags": [],
3774
+ "risks": []
3775
+ },
3776
+ {
3777
+ "name": "MemberClassCollection",
3778
+ "qualifiedName": "@happyvertical/smrt-users:MemberClassCollection",
3779
+ "collection": "memberclasses",
3780
+ "tableName": "member_classes",
3781
+ "packageName": "@happyvertical/smrt-users",
3782
+ "extends": "SmrtCollection",
3783
+ "fields": [],
3784
+ "relationships": [],
3785
+ "methods": [],
3786
+ "surfaces": [],
3787
+ "relationshipFeatures": [
3788
+ "uuidColumns"
3789
+ ],
3790
+ "tags": [],
3791
+ "risks": []
3792
+ },
3793
+ {
3794
+ "name": "LegalMemberCollection",
3795
+ "qualifiedName": "@happyvertical/smrt-users:LegalMemberCollection",
3796
+ "collection": "legalmembers",
3797
+ "tableName": "legal_members",
3798
+ "packageName": "@happyvertical/smrt-users",
3799
+ "extends": "SmrtCollection",
3800
+ "fields": [],
3801
+ "relationships": [],
3802
+ "methods": [],
3803
+ "surfaces": [],
3804
+ "relationshipFeatures": [
3805
+ "uuidColumns"
3806
+ ],
3807
+ "tags": [],
3808
+ "risks": []
3809
+ },
3810
+ {
3811
+ "name": "LegalMemberStatusHistoryCollection",
3812
+ "qualifiedName": "@happyvertical/smrt-users:LegalMemberStatusHistoryCollection",
3813
+ "collection": "legalmemberstatushistories",
3814
+ "tableName": "legal_member_status_history",
3815
+ "packageName": "@happyvertical/smrt-users",
3816
+ "extends": "SmrtCollection",
3817
+ "fields": [],
3818
+ "relationships": [],
3819
+ "methods": [],
3820
+ "surfaces": [],
3821
+ "relationshipFeatures": [
3822
+ "uuidColumns"
3823
+ ],
3824
+ "tags": [],
3825
+ "risks": []
3826
+ },
3827
+ {
3828
+ "name": "LegalMemberReferenceCollection",
3829
+ "qualifiedName": "@happyvertical/smrt-users:LegalMemberReferenceCollection",
3830
+ "collection": "legalmemberreferences",
3831
+ "tableName": "legal_member_references",
3832
+ "packageName": "@happyvertical/smrt-users",
3833
+ "extends": "SmrtCollection",
3834
+ "fields": [],
3835
+ "relationships": [],
3836
+ "methods": [],
3837
+ "surfaces": [],
3838
+ "relationshipFeatures": [
3839
+ "uuidColumns"
3840
+ ],
3841
+ "tags": [],
3842
+ "risks": []
3843
+ },
3844
+ {
3845
+ "name": "MemberOfficerAppointmentCollection",
3846
+ "qualifiedName": "@happyvertical/smrt-users:MemberOfficerAppointmentCollection",
3847
+ "collection": "memberofficerappointments",
3848
+ "tableName": "member_officer_appointments",
3849
+ "packageName": "@happyvertical/smrt-users",
3850
+ "extends": "SmrtCollection",
3851
+ "fields": [],
3852
+ "relationships": [],
3853
+ "methods": [],
3854
+ "surfaces": [],
3855
+ "relationshipFeatures": [
3856
+ "uuidColumns"
3857
+ ],
3858
+ "tags": [],
3859
+ "risks": []
3860
+ },
3861
+ {
3862
+ "name": "MemberEligibilitySnapshotCollection",
3863
+ "qualifiedName": "@happyvertical/smrt-users:MemberEligibilitySnapshotCollection",
3864
+ "collection": "membereligibilitysnapshots",
3865
+ "tableName": "member_eligibility_snapshots",
3866
+ "packageName": "@happyvertical/smrt-users",
3867
+ "extends": "SmrtCollection",
3868
+ "fields": [],
3869
+ "relationships": [],
3870
+ "methods": [],
3871
+ "surfaces": [],
3872
+ "relationshipFeatures": [
3873
+ "uuidColumns"
3874
+ ],
3875
+ "tags": [],
3876
+ "risks": []
3877
+ },
3197
3878
  {
3198
3879
  "name": "AccessRequest",
3199
3880
  "qualifiedName": "@happyvertical/smrt-users:AccessRequest",
@@ -7077,12 +7758,12 @@
7077
7758
  ],
7078
7759
  "prompts": [],
7079
7760
  "relationshipsV2": {
7080
- "foreignKeyFields": 27,
7081
- "crossPackageRefFields": 1,
7761
+ "foreignKeyFields": 34,
7762
+ "crossPackageRefFields": 2,
7082
7763
  "junctionCollections": 0,
7083
7764
  "hierarchicalObjects": 0,
7084
7765
  "polymorphicAssociations": 0,
7085
- "uuidColumns": 116
7766
+ "uuidColumns": 156
7086
7767
  },
7087
7768
  "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/services/PasswordCredentialService.ts`, `src/sveltekit/password-handlers.ts` | email + password sign-in and password lifecycle (#3274); shared scrypt in `credential-hash.ts` | this file, \"Security boundaries\"; README \"Password sign-in\" |\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. Generated CLI is list/get or closed (`cli: false` for\n credential/grant models) because agents shell out to it; a `cli` without\n `include` is full CRUD (#3742). Their catalog slugs stay full via\n `USERS_AUTHORITY_CATALOG_MODELS`. Preserve the registry assertions in\n `security-audit-1400.test.ts` and `issue-3742-cli-surface-lockdown.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 Subject and source budgets are sized independently (#3531); `source: false`\n ignores the source key, so a reservation then needs a subject (fail closed).\n The `source-locked` audit entry never carries a subject or caller metadata.\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- `UsersPasswordCredential` is one row per person, sensitive, with no\n generated surface; `PasswordCredentialService` and its handlers are its only\n read/write paths. Sign-in normalizes the email like `emailKey`, reserves\n before work, always runs one scrypt verify (dummy hash when absent), never\n creates a user, and maps every non-rate-limit refusal to one\n `PasswordCredentialError`. It re-checks the credential after the mint (a\n rehash that keeps row and version is confirmed by re-verifying); the\n parameter-upgrade write is a guarded UPDATE that must never replace a\n rotated password, and verification against a weaker stored hash is padded\n to the policy's scrypt cost. Self-service writes are guarded on the proven\n credential (first set insert-only); session sweeps run before and after\n each write. Self-service needs the current password except for a first\n set; `users.password.manage` follows the PIN's single-tenant target\n rule and never resets the actor's own. Layered sessions never manage\n passwords; a `mustChange` sign-in mints an empty ceiling.\n `replaceForgottenPassword` (#3905, opt-in `recovery`) is the only\n self-service write without the current password: it decides on the stored\n session row (owner, first-class, no ceiling, listed non-password/pin\n `authMethod`, fresh `outOfBandProofAt` stamp) and ends every session,\n its own included. Never set that stamp from client input.\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",
7088
7769
  "moduleDocs": [