@ibgib/node-gib 0.0.18 → 0.0.20

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.
Files changed (48) hide show
  1. package/dist/serve-gib/handlers/api/identity/keystone-custodian-add-master-password.handler.d.mts +39 -0
  2. package/dist/serve-gib/handlers/api/identity/keystone-custodian-add-master-password.handler.d.mts.map +1 -0
  3. package/dist/serve-gib/handlers/api/identity/keystone-custodian-add-master-password.handler.mjs +197 -0
  4. package/dist/serve-gib/handlers/api/identity/keystone-custodian-add-master-password.handler.mjs.map +1 -0
  5. package/dist/serve-gib/handlers/api/identity/keystone-custodian-genesis.handler.d.mts.map +1 -1
  6. package/dist/serve-gib/handlers/api/identity/keystone-custodian-genesis.handler.mjs +6 -3
  7. package/dist/serve-gib/handlers/api/identity/keystone-custodian-genesis.handler.mjs.map +1 -1
  8. package/dist/serve-gib/handlers/api/identity/keystone-custodian-register-delegate.handler.d.mts.map +1 -1
  9. package/dist/serve-gib/handlers/api/identity/keystone-custodian-register-delegate.handler.mjs +5 -3
  10. package/dist/serve-gib/handlers/api/identity/keystone-custodian-register-delegate.handler.mjs.map +1 -1
  11. package/dist/serve-gib/handlers/api/keystone/sso-login.handler.d.mts.map +1 -1
  12. package/dist/serve-gib/handlers/api/keystone/sso-login.handler.mjs +11 -5
  13. package/dist/serve-gib/handlers/api/keystone/sso-login.handler.mjs.map +1 -1
  14. package/dist/serve-gib/identity-index/identity-index-service.d.mts +5 -1
  15. package/dist/serve-gib/identity-index/identity-index-service.d.mts.map +1 -1
  16. package/dist/serve-gib/identity-index/identity-index-service.mjs +10 -1
  17. package/dist/serve-gib/identity-index/identity-index-service.mjs.map +1 -1
  18. package/dist/serve-gib/index.d.mts +1 -0
  19. package/dist/serve-gib/index.d.mts.map +1 -1
  20. package/dist/serve-gib/index.mjs +3 -0
  21. package/dist/serve-gib/index.mjs.map +1 -1
  22. package/dist/serve-gib/path-constants.d.mts +4 -0
  23. package/dist/serve-gib/path-constants.d.mts.map +1 -1
  24. package/dist/serve-gib/path-constants.mjs +4 -0
  25. package/dist/serve-gib/path-constants.mjs.map +1 -1
  26. package/e2e/identity-custodian-lifecycle.e2e.md +37 -0
  27. package/e2e/identity-sso-lifecycle.e2e.md +9 -7
  28. package/package.json +3 -3
  29. package/src/serve-gib/handlers/api/identity/keystone-custodian-add-master-password.handler.mts +245 -0
  30. package/src/serve-gib/handlers/api/identity/keystone-custodian-add-master-password.respec.mts +167 -0
  31. package/src/serve-gib/handlers/api/identity/keystone-custodian-genesis.handler.mts +6 -3
  32. package/src/serve-gib/handlers/api/identity/keystone-custodian-register-delegate.handler.mts +5 -3
  33. package/src/serve-gib/handlers/api/identity/keystone-custodian-register-delegate.respec.mts +3 -3
  34. package/src/serve-gib/handlers/api/keystone/sso-login.handler.mts +12 -6
  35. package/src/serve-gib/identity-index/identity-index-service.mts +11 -1
  36. package/src/serve-gib/index.mts +3 -0
  37. package/src/serve-gib/path-constants.mts +5 -0
  38. package/data/identity_index/email/hashes/32/13/49/e64f32730f01a61ada211f29eaf0a210fc2386b668ad04d7e8a9b16766.json +0 -4
  39. package/dist/serve-gib/handlers/api/ibgib/ibgib-handler-types.d.mts +0 -41
  40. package/dist/serve-gib/handlers/api/ibgib/ibgib-handler-types.d.mts.map +0 -1
  41. package/dist/serve-gib/handlers/api/ibgib/ibgib-handler-types.mjs +0 -34
  42. package/dist/serve-gib/handlers/api/ibgib/ibgib-handler-types.mjs.map +0 -1
  43. package/dist/serve-gib/handlers/api/ibgib/ibgib.handler.d.mts +0 -26
  44. package/dist/serve-gib/handlers/api/ibgib/ibgib.handler.d.mts.map +0 -1
  45. package/dist/serve-gib/handlers/api/ibgib/ibgib.handler.mjs +0 -179
  46. package/dist/serve-gib/handlers/api/ibgib/ibgib.handler.mjs.map +0 -1
  47. package/src/serve-gib/handlers/api/ibgib/ibgib-handler-types.mts +0 -49
  48. package/src/serve-gib/handlers/api/ibgib/ibgib.handler.mts +0 -176
@@ -1 +1 @@
1
- {"version":3,"file":"path-constants.d.mts","sourceRoot":"","sources":["../../src/serve-gib/path-constants.mts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,eAAO,MAAM,eAAe,OAAO,CAAC;AAEpC;;;GAGG;AACH,eAAO,MAAM,kBAAkB,UAK9B,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB;IACzB;;OAEG;IACH,MAAM;IAEN;;OAEG;IACH,WAAW;IAEX;;OAEG;IACH,UAAU;IAEV;;OAEG;IACH,QAAQ;IAER;;OAEG;IACH,aAAa;IAEb;;OAEG;IACH,UAAU;IAEV;;OAEG;IACH,eAAe;IAEf;;;;;;;;OAQG;IACH,gBAAgB;IAEhB;;OAEG;IACH,eAAe;IAEf;;OAEG;IACH,oBAAoB;IAEpB;;;;;;;;;;;OAWG;IACH,aAAa;IACb;;OAEG;IACH,OAAO;IACP;;OAEG;IACH,iBAAiB;IACjB;;OAEG;IACH,kBAAkB;IAClB;;OAEG;IACH,mBAAmB;IACnB;;OAEG;IACH,aAAa;IACb;;OAEG;IACH,UAAU;IACV;;OAEG;IACH,uBAAuB;IAEvB;;OAEG;IACH,YAAY;IACZ,iBAAiB;IAEjB;;OAEG;IACH,0BAA0B;IAE1B;;OAEG;IACH,oCAAoC;CACvC,CAAC;AAEF,eAAO,MAAM,uBAAuB,QAA4B,CAAC"}
1
+ {"version":3,"file":"path-constants.d.mts","sourceRoot":"","sources":["../../src/serve-gib/path-constants.mts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,eAAO,MAAM,eAAe,OAAO,CAAC;AAEpC;;;GAGG;AACH,eAAO,MAAM,kBAAkB,UAK9B,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB;IACzB;;OAEG;IACH,MAAM;IAEN;;OAEG;IACH,WAAW;IAEX;;OAEG;IACH,UAAU;IAEV;;OAEG;IACH,QAAQ;IAER;;OAEG;IACH,aAAa;IAEb;;OAEG;IACH,UAAU;IAEV;;OAEG;IACH,eAAe;IAEf;;;;;;;;OAQG;IACH,gBAAgB;IAEhB;;OAEG;IACH,eAAe;IAEf;;OAEG;IACH,oBAAoB;IAEpB;;;;;;;;;;;OAWG;IACH,aAAa;IACb;;OAEG;IACH,OAAO;IACP;;OAEG;IACH,iBAAiB;IACjB;;OAEG;IACH,kBAAkB;IAClB;;OAEG;IACH,mBAAmB;IACnB;;OAEG;IACH,aAAa;IACb;;OAEG;IACH,UAAU;IACV;;OAEG;IACH,uBAAuB;IAEvB;;OAEG;IACH,YAAY;IACZ,iBAAiB;IAEjB;;OAEG;IACH,0BAA0B;IAE1B;;OAEG;IACH,oCAAoC;IAEpC;;OAEG;IACH,sCAAsC;CACzC,CAAC;AAEF,eAAO,MAAM,uBAAuB,QAA4B,CAAC"}
@@ -119,6 +119,10 @@ export const API_PATH_REGEXES = {
119
119
  * /api/identity/custodian/register-delegate
120
120
  */
121
121
  KEYSTONE_CUSTODIAN_REGISTER_DELEGATE: /^\/api\/identity\/custodian\/register-delegate\/?$/i,
122
+ /**
123
+ * /api/identity/custodian/add-master-password
124
+ */
125
+ KEYSTONE_CUSTODIAN_ADD_MASTER_PASSWORD: /^\/api\/identity\/custodian\/add-master-password\/?$/i,
122
126
  };
123
127
  export const VALID_STATIC_PATH_REGEX = /^\/[a-zA-Z0-9\-_\.\/]*$/;
124
128
  //# sourceMappingURL=path-constants.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"path-constants.mjs","sourceRoot":"","sources":["../../src/serve-gib/path-constants.mts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,CAAC;AAEpC;;;GAGG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAC9B,MAAM,EAAO,yBAAyB;IACtC,MAAM,EAAO,uBAAuB;IACpC,IAAI,EAAS,gBAAgB;IAC7B,UAAU,EAAG,wCAAwC;CACxD,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC5B;;OAEG;IACH,MAAM,EAAE,oBAAoB;IAE5B;;OAEG;IACH,WAAW,EAAE,oFAAoF;IAEjG;;OAEG;IACH,UAAU,EAAE,6EAA6E;IAEzF;;OAEG;IACH,QAAQ,EAAE,sBAAsB;IAEhC;;OAEG;IACH,aAAa,EAAE,mDAAmD;IAElE;;OAEG;IACH,UAAU,EAAE,mBAAmB;IAE/B;;OAEG;IACH,eAAe,EAAE,2DAA2D;IAE5E;;;;;;;;OAQG;IACH,gBAAgB,EAAE,+BAA+B;IAEjD;;OAEG;IACH,eAAe,EAAE,8BAA8B;IAE/C;;OAEG;IACH,oBAAoB,EAAE,mCAAmC;IAEzD;;;;;;;;;;;OAWG;IACH,aAAa,EAAE,4BAA4B;IAC3C;;OAEG;IACH,OAAO,EAAE,mDAAmD;IAC5D;;OAEG;IACH,iBAAiB,EAAE,iCAAiC;IACpD;;OAEG;IACH,kBAAkB,EAAE,kCAAkC;IACtD;;OAEG;IACH,mBAAmB,EAAE,mCAAmC;IACxD;;OAEG;IACH,aAAa,EAAE,4BAA4B;IAC3C;;OAEG;IACH,UAAU,EAAE,yBAAyB;IACrC;;OAEG;IACH,uBAAuB,EAAE,sCAAsC;IAE/D;;OAEG;IACH,YAAY,EAAE,2BAA2B;IACzC,iBAAiB,EAAE,gCAAgC;IAEnD;;OAEG;IACH,0BAA0B,EAAE,2CAA2C;IAEvE;;OAEG;IACH,oCAAoC,EAAE,qDAAqD;CAC9F,CAAC;AAEF,MAAM,CAAC,MAAM,uBAAuB,GAAG,yBAAyB,CAAC"}
1
+ {"version":3,"file":"path-constants.mjs","sourceRoot":"","sources":["../../src/serve-gib/path-constants.mts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,CAAC;AAEpC;;;GAGG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAC9B,MAAM,EAAO,yBAAyB;IACtC,MAAM,EAAO,uBAAuB;IACpC,IAAI,EAAS,gBAAgB;IAC7B,UAAU,EAAG,wCAAwC;CACxD,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC5B;;OAEG;IACH,MAAM,EAAE,oBAAoB;IAE5B;;OAEG;IACH,WAAW,EAAE,oFAAoF;IAEjG;;OAEG;IACH,UAAU,EAAE,6EAA6E;IAEzF;;OAEG;IACH,QAAQ,EAAE,sBAAsB;IAEhC;;OAEG;IACH,aAAa,EAAE,mDAAmD;IAElE;;OAEG;IACH,UAAU,EAAE,mBAAmB;IAE/B;;OAEG;IACH,eAAe,EAAE,2DAA2D;IAE5E;;;;;;;;OAQG;IACH,gBAAgB,EAAE,+BAA+B;IAEjD;;OAEG;IACH,eAAe,EAAE,8BAA8B;IAE/C;;OAEG;IACH,oBAAoB,EAAE,mCAAmC;IAEzD;;;;;;;;;;;OAWG;IACH,aAAa,EAAE,4BAA4B;IAC3C;;OAEG;IACH,OAAO,EAAE,mDAAmD;IAC5D;;OAEG;IACH,iBAAiB,EAAE,iCAAiC;IACpD;;OAEG;IACH,kBAAkB,EAAE,kCAAkC;IACtD;;OAEG;IACH,mBAAmB,EAAE,mCAAmC;IACxD;;OAEG;IACH,aAAa,EAAE,4BAA4B;IAC3C;;OAEG;IACH,UAAU,EAAE,yBAAyB;IACrC;;OAEG;IACH,uBAAuB,EAAE,sCAAsC;IAE/D;;OAEG;IACH,YAAY,EAAE,2BAA2B;IACzC,iBAAiB,EAAE,gCAAgC;IAEnD;;OAEG;IACH,0BAA0B,EAAE,2CAA2C;IAEvE;;OAEG;IACH,oCAAoC,EAAE,qDAAqD;IAE3F;;OAEG;IACH,sCAAsC,EAAE,uDAAuD;CAClG,CAAC;AAEF,MAAM,CAAC,MAAM,uBAAuB,GAAG,yBAAyB,CAAC"}
@@ -91,3 +91,40 @@ Attempt to re-submit a previously consumed `custodianReqs` payload to `POST /api
91
91
  3. Verify rejection outcome:
92
92
  - Server rejects the request with **HTTP 409 Conflict**:
93
93
  `"Custodian requirements payload has already been used. Nonce single-turn reservation violation."`
94
+
95
+
96
+ ### Phase 4: Keystone Details Inspection & Sovereign Elevation
97
+ Inspect the identity in `<ibgib-keystone-details>` to verify that capabilities, device badges, and password options adapt dynamically to the custodial state, and add a sovereign master password.
98
+
99
+ #### Steps
100
+ 1. In Browser 1, open `<ibgib-identity-header>` and click the active identity to open `<ibgib-keystone-details>`.
101
+ 2. Verify dynamic UI elements:
102
+ - **Password Card**: Displays **"Add Master Password"** (with New Password and Confirm Password inputs; Current Password is hidden).
103
+ - **SSO Status Card**: Displays `"Google (Primary Account) ✅"` or provider primary indicator.
104
+ - **Registered Delegates Table**:
105
+ - Browser 1's delegate displays the green **"This Device"** badge.
106
+ - Browser 2's delegate (from Phase 2) displays the blue **"Remote Device"** badge.
107
+ 3. Click **View Details** on Browser 2's remote delegate:
108
+ - Verify modal opens showing remote device metadata without throwing or failing.
109
+ 4. In the **Add Master Password** card, enter `<E2E_TEST_NEW_PASSWORD>` in New Password and Confirm Password inputs, and click **Add Master Password**.
110
+ 5. Verify outcomes:
111
+ - Client adds a sovereign `manage` pool via `keystoneService.addPools()`, evolves the primary identity, and syncs to server.
112
+ - The password card updates dynamically to **"Change Password"** (now displaying Current Password input).
113
+ - Identity capability transitions from Custodial to **Hybrid**.
114
+
115
+
116
+ ### Phase 5: Silent Background Sync & Expired Session Renewal ("Sign Back In")
117
+ Verify that daily background sync operations execute silently via stored IndexedDB delegate tokens ($S_D$), and verify that expired sync delegates cleanly pause sync and prompt the user to "Sign Back In".
118
+
119
+ #### Steps
120
+ 1. In Browser 1, click **Check for Updates** and **Update Identity**:
121
+ - Verify sync succeeds silently in the background without prompting for master passwords.
122
+ 2. Simulate delegate expiration / session expiry:
123
+ - Option A: Clear the stored device delegate secret from IndexedDB (`space_gib_db` -> `space_gib_store` -> delete `ibgib_device_delegate_secret_<primaryTjpAddr>`).
124
+ - Option B: Test with a short TTL development profile (`sync-profile.dev.json` with `defaultTtlSeconds: 10`) and wait for expiration.
125
+ 3. Trigger a sync check or page refresh:
126
+ - Verify `KeystoneSyncService` detects the expired / missing delegate token and fires `EVENT_IBGIB_IDENTITY_RENEWAL_REQUIRED`.
127
+ - Verify the UI prompts the user to **"Sign Back In"**.
128
+ 4. Re-authenticate via Email OTP (or Google SSO / Master Password):
129
+ - Verify single-turn re-authentication generates and registers a fresh outward sync delegate ($D_{\text{device}}$).
130
+ - Verify the new secret ($S_D$) is stored in IndexedDB and silent background sync resumes seamlessly.
@@ -99,18 +99,20 @@ Open Identity Manager on `https://local.ibgib.space`, trigger Google OAuth2 link
99
99
 
100
100
 
101
101
  ### Phase 4: New-Device Google SSO Sign-In (Incognito Mode)
102
- Open a clean Incognito browser window to `https://local.ibgib.space`, complete Google SSO sign-in, verify that `IdentityIndexService` resolves the primary identity, verifies the `custodian-manage-google` pool, and pairs the new device.
102
+ Open a clean Incognito browser window to `https://local.ibgib.space`, navigate to Sign In, click **Continue with Google**, complete Google SSO authentication, and verify that the client automatically creates the device delegate and signs in without prompting for a password.
103
103
 
104
104
  #### Steps
105
105
  1. Open a fresh **Incognito Window** (ctrl+shift+N) to `https://local.ibgib.space`.
106
- 2. In `<ibgib-identity-header>`, click **Sign In with Google**.
107
- 3. Complete Google OAuth2 popup authentication.
108
- 4. Verify server log processing:
106
+ 2. In `<ibgib-identity-header>`, click **Sign In** (which renders the streamlined `<ibgib-add-device>` component).
107
+ 3. Click **Continue with Google** (or enter `<E2E_TEST_GMAIL_ADDRESS_ALICE>` and complete 2FA).
108
+ 4. Complete Google OAuth2 popup authentication with `<E2E_TEST_GMAIL_ADDRESS_ALICE>`.
109
+ 5. Verify server log processing:
109
110
  - `[IdentityIndexService]` resolves `<E2E_TEST_GMAIL_ADDRESS_ALICE>` -> `primaryTjpAddr`.
110
- - Server loads domain graph, verifies `custodian-manage-google` challenge pool, and issues delegate challenge.
111
- 5. Verify client UI result:
112
- - Browser receives delegate response and pairs device seamlessly.
111
+ - `[SsoLoginHandler]` loads domain graph, verifies `custodian-manage-google` challenge pool, and returns `custodianReqs`.
112
+ 6. Verify client UI result:
113
+ - Browser creates local sync delegate, registers it with the server, and pairs the device seamlessly.
113
114
  - Active identity header updates to `alice` with active status badge `🟢`.
115
+ - Zero password prompts are shown.
114
116
 
115
117
 
116
118
  ### Phase 5: Delegate Device Tagging & Revocation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ibgib/node-gib",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
4
4
  "description": "Node and server-specific ibGib code, i.e., things that don't run in the browser.",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -25,8 +25,8 @@
25
25
  "license": "ISC",
26
26
  "dependencies": {
27
27
  "@ibgib/helper-gib": "^0.0.37",
28
- "@ibgib/ts-gib": "^0.5.33",
29
- "@ibgib/core-gib": "^0.1.73",
28
+ "@ibgib/ts-gib": "^0.5.34",
29
+ "@ibgib/core-gib": "^0.1.75",
30
30
  "@ibgib/encrypt-gib": "^0.3.1"
31
31
  },
32
32
  "devDependencies": {
@@ -0,0 +1,245 @@
1
+ /**
2
+ * @module serve-gib/handlers/api/identity/keystone-custodian-add-master-password.handler
3
+ *
4
+ * POST /api/identity/custodian/add-master-password
5
+ *
6
+ * Single-turn sovereignty elevation endpoint for purely custodial identities.
7
+ * Authenticates the user via fresh OAuth code or 6-digit email OTP, validates that
8
+ * the provided `newManagePool` config matches the existing custodian pool config,
9
+ * evolves the custodian manage pool into an auxiliary inward custodian pool (e.g. `custodian-manage-google`),
10
+ * installs the user's new sovereign pool as `manage`, and signs the evolution using the server's custodian secret.
11
+ */
12
+
13
+ import { extractErrorMsg, hash } from '@ibgib/helper-gib/dist/helpers/utils-helper.mjs';
14
+ import { getIbGibAddr } from '@ibgib/ts-gib/dist/helper.mjs';
15
+ import { getTjpAddr } from '@ibgib/core-gib/dist/common/other/ibgib-helper.mjs';
16
+ import { KeystoneService_V1 } from '@ibgib/core-gib/dist/keystone/keystone-service-v1.mjs';
17
+ import { KeystoneChallengePool } from '@ibgib/core-gib/dist/keystone/keystone-types.mjs';
18
+ import { KEYSTONE_VERB_MANAGE, POOL_ID_MANAGE } from '@ibgib/core-gib/dist/keystone/keystone-constants.mjs';
19
+ import { CUSTODIAN_POOL_ID_PREFIX } from '@ibgib/core-gib/dist/keystone/delegate/delegate-constants.mjs';
20
+ import { KeystoneClaim } from '@ibgib/core-gib/dist/keystone/keystone-types.mjs';
21
+ import { validateEmail } from '@ibgib/core-gib/dist/keystone/keystone-helpers.mjs';
22
+
23
+ import { GLOBAL_LOG_A_LOT } from '../../../constants.mjs';
24
+ import { ServeGibHandlerBase } from '../../handler-base.mjs';
25
+ import { RequestContext, ResponseResult, ServeGibHttpMethod } from '../../../types.mjs';
26
+ import { API_PATH_REGEXES } from '../../../path-constants.mjs';
27
+ import { bootstrapDomainMetaspace } from '../../../bootstrap-helper.mjs';
28
+ import { getIdentityIndexService } from '../../../identity-index/identity-index-service.mjs';
29
+ import { normalizeEmail } from '../../../identity-index/identity-index-helpers.mjs';
30
+ import { loadSsoServerConfig } from '../../../../identity/sso/sso-config-helper.mjs';
31
+ import { SsoCustodianService } from '../../../../identity/sso/sso-custodian-service.mjs';
32
+ import { SsoProviderId } from '../../../../identity/sso/sso-types.mjs';
33
+ import { getPendingStagingDir, getPendingTjpHash, verifyPendingCode } from '../../../../email/verify/email-verify-helpers.mjs';
34
+
35
+ const logalot = GLOBAL_LOG_A_LOT || true;
36
+
37
+ let custodian: SsoCustodianService | undefined;
38
+ function getCustodian(): SsoCustodianService {
39
+ if (!custodian) {
40
+ const config = loadSsoServerConfig();
41
+ custodian = new SsoCustodianService(config);
42
+ }
43
+ return custodian;
44
+ }
45
+
46
+ export interface CustodianAddMasterPasswordRequestBody {
47
+ primaryTjpAddr: string;
48
+ newManagePool: KeystoneChallengePool;
49
+ providerId?: SsoProviderId;
50
+ code?: string;
51
+ redirectUri?: string;
52
+ email?: string;
53
+ }
54
+
55
+ export interface CustodianAddMasterPasswordResponseBody {
56
+ success: boolean;
57
+ primaryTjpAddr: string;
58
+ primaryKeystone: any;
59
+ message?: string;
60
+ }
61
+
62
+ /**
63
+ * POST /api/identity/custodian/add-master-password
64
+ */
65
+ export class KeystoneCustodianAddMasterPasswordHandler extends ServeGibHandlerBase<undefined, undefined> {
66
+ protected override lc: string = `[${KeystoneCustodianAddMasterPasswordHandler.name}]`;
67
+ protected override method: ServeGibHttpMethod = 'POST';
68
+ protected override regex = API_PATH_REGEXES.KEYSTONE_CUSTODIAN_ADD_MASTER_PASSWORD;
69
+
70
+ protected async handleRouteImpl(
71
+ reqCtx: RequestContext<undefined, undefined>
72
+ ): Promise<ResponseResult | undefined> {
73
+ const lc = `${this.lc}[${this.handleRouteImpl.name}]`;
74
+ try {
75
+ if (logalot) { console.log(`${lc} starting... (I: 8a1b2c3d4e5f67890a1b2c3d4e5f6790)`); }
76
+
77
+ if (reqCtx.method !== 'POST') {
78
+ return this.error(405, 'Method Not Allowed');
79
+ }
80
+
81
+ if (!reqCtx.body) {
82
+ return this.error(400, 'Request body is empty (E: 8a1b2c3d4e5f67890a1b2c3d4e5f6791)');
83
+ }
84
+
85
+ let body: CustodianAddMasterPasswordRequestBody;
86
+ try {
87
+ body = JSON.parse(reqCtx.body);
88
+ } catch {
89
+ return this.error(400, 'Request body must be valid JSON (E: 8a1b2c3d4e5f67890a1b2c3d4e5f6792)');
90
+ }
91
+
92
+ const { primaryTjpAddr, newManagePool, providerId, code, redirectUri, email } = body;
93
+
94
+ if (!primaryTjpAddr) {
95
+ return this.error(400, 'primaryTjpAddr is required (E: 8a1b2c3d4e5f67890a1b2c3d4e5f6793)');
96
+ }
97
+
98
+ if (!newManagePool || typeof newManagePool !== 'object' || newManagePool.id !== POOL_ID_MANAGE) {
99
+ return this.error(400, 'newManagePool is invalid. Must be an object with id "manage" (E: 8a1b2c3d4e5f67890a1b2c3d4e5f6794)');
100
+ }
101
+
102
+ if (!newManagePool.challenges || Object.keys(newManagePool.challenges).length === 0) {
103
+ return this.error(400, 'newManagePool must contain pre-generated public challenges (E: 8a1b2c3d4e5f67890a1b2c3d4e5f6795)');
104
+ }
105
+
106
+ // 1. Authenticate user credentials via OAuth code or Email OTP
107
+ let verifiedEmail: string;
108
+ let effectiveProvider: string = providerId || 'email';
109
+
110
+ if (providerId && code && redirectUri) {
111
+ const service = getCustodian();
112
+ const userInfo = await service.getOAuthUserInfo(providerId, code, redirectUri);
113
+ if (!userInfo || !userInfo.email) {
114
+ return this.error(401, `Failed to authenticate with SSO provider ${providerId} (E: 8a1b2c3d4e5f67890a1b2c3d4e5f6796)`);
115
+ }
116
+ verifiedEmail = normalizeEmail(userInfo.email);
117
+ } else if (email && code) {
118
+ if (!validateEmail(email)) {
119
+ return this.error(400, 'Invalid email address format (E: 8a1b2c3d4e5f67890a1b2c3d4e5f6797)');
120
+ }
121
+ verifiedEmail = normalizeEmail(email);
122
+
123
+ const tjpHash = await getPendingTjpHash(primaryTjpAddr);
124
+ const pendingInfo = reqCtx.dataDir && tjpHash ? await getPendingStagingDir({ dataDir: reqCtx.dataDir, tjpHash }) : undefined;
125
+
126
+ if (!pendingInfo || pendingInfo.isExpired) {
127
+ return this.error(400, 'Verification record missing or expired. Please request a new verification code. (E: 8a1b2c3d4e5f67890a1b2c3d4e5f6798)');
128
+ }
129
+
130
+ const codeVerifyResult = await verifyPendingCode({
131
+ pendingDir: pendingInfo.pendingDir,
132
+ code,
133
+ });
134
+
135
+ if (!codeVerifyResult.valid) {
136
+ return this.error(401, codeVerifyResult.error || 'Invalid verification code (E: 8a1b2c3d4e5f67890a1b2c3d4e5f6799)');
137
+ }
138
+ } else {
139
+ return this.error(400, 'Authentication credentials required (providerId+code+redirectUri or email+code) (E: 8a1b2c3d4e5f67890a1b2c3d4e5f679a)');
140
+ }
141
+
142
+ // 2. Lookup primary identity and confirm email matches
143
+ const indexService = getIdentityIndexService();
144
+ const identityRecord = await indexService.lookupIdentityByEmail({ email: verifiedEmail });
145
+ if (!identityRecord || identityRecord.primaryTjpAddr !== primaryTjpAddr) {
146
+ return this.error(403, `Authenticated email ${verifiedEmail} does not match primary identity ${primaryTjpAddr} (E: 8a1b2c3d4e5f67890a1b2c3d4e5f679b)`);
147
+ }
148
+
149
+ // 3. Bootstrap domain metaspace and load latest keystone
150
+ const metaspace = await bootstrapDomainMetaspace(primaryTjpAddr, reqCtx.dataDir);
151
+ const space = await metaspace.getLocalUserSpace({ lock: false });
152
+ if (!space) {
153
+ return this.error(500, `Local domain space not found for identity ${primaryTjpAddr}`);
154
+ }
155
+
156
+ const keystoneService = new KeystoneService_V1();
157
+ const latestKeystone = await keystoneService.getLatestKeystone({
158
+ addr: primaryTjpAddr,
159
+ metaspace,
160
+ space
161
+ });
162
+
163
+ if (!latestKeystone || !latestKeystone.data) {
164
+ return this.error(404, `Primary identity keystone not found for address ${primaryTjpAddr}`);
165
+ }
166
+
167
+ // 4. Verify existing custodial manage pool
168
+ const prevPools = latestKeystone.data.challengePools || [];
169
+ const existingManagePool = prevPools.find(p => p.id === POOL_ID_MANAGE);
170
+ if (!existingManagePool) {
171
+ return this.error(400, 'Primary keystone missing active "manage" pool (E: 8a1b2c3d4e5f67890a1b2c3d4e5f679c)');
172
+ }
173
+
174
+ // Validate new pool config matches existing manage pool config structure
175
+ if (existingManagePool.config?.type && newManagePool.config?.type !== existingManagePool.config.type) {
176
+ return this.error(400, `newManagePool type "${newManagePool.config?.type}" does not match required type "${existingManagePool.config.type}" (E: 8a1b2c3d4e5f67890a1b2c3d4e5f679d)`);
177
+ }
178
+
179
+ // 5. Build next pool set:
180
+ // Convert existing custodian manage pool to auxiliary custodian pool (e.g. custodian-manage-google)
181
+ // and install user's newManagePool as 'manage'.
182
+ const custodianPoolId = `${CUSTODIAN_POOL_ID_PREFIX}${effectiveProvider}`;
183
+ const auxiliaryCustodianPool: KeystoneChallengePool = {
184
+ ...existingManagePool,
185
+ id: custodianPoolId,
186
+ };
187
+
188
+ const nextPools: KeystoneChallengePool[] = [];
189
+ for (const pool of prevPools) {
190
+ if (pool.id === POOL_ID_MANAGE) {
191
+ nextPools.push(newManagePool);
192
+ nextPools.push(auxiliaryCustodianPool);
193
+ } else if (pool.id === custodianPoolId) {
194
+ // Skip if already present to prevent duplicate
195
+ } else {
196
+ nextPools.push(pool);
197
+ }
198
+ }
199
+
200
+ // 6. Sign evolution turn using server's custodian secret
201
+ const ssoConfig = loadSsoServerConfig();
202
+ const serverSecret = ssoConfig.ssoServerKdfSecret;
203
+ const primarySecret = await hash({ s: `${serverSecret}_custodian_${verifiedEmail}` });
204
+
205
+ const claim: Partial<KeystoneClaim> = {
206
+ verb: KEYSTONE_VERB_MANAGE,
207
+ target: getIbGibAddr({ ibGib: latestKeystone })
208
+ };
209
+
210
+ const evolvedKeystone = await keystoneService.sign({
211
+ latestKeystone,
212
+ masterSecret: primarySecret,
213
+ claim,
214
+ poolId: POOL_ID_MANAGE,
215
+ metaspace,
216
+ space,
217
+ dataToPatch: {
218
+ challengePools: nextPools,
219
+ frameDetails: {
220
+ action: 'add_master_password',
221
+ custodyMode: 'hybrid',
222
+ elevatedAt: new Date().toISOString(),
223
+ }
224
+ }
225
+ });
226
+
227
+ // 7. Persist evolved keystone in domain space
228
+ await metaspace.put({ ibGibs: [evolvedKeystone], space });
229
+ await metaspace.registerNewIbGib({ ibGib: evolvedKeystone, space });
230
+
231
+ const response: CustodianAddMasterPasswordResponseBody = {
232
+ success: true,
233
+ primaryTjpAddr,
234
+ primaryKeystone: evolvedKeystone,
235
+ message: 'Successfully elevated identity to sovereign master password'
236
+ };
237
+
238
+ return this.ok(response);
239
+
240
+ } catch (error) {
241
+ console.error(`${lc} ${extractErrorMsg(error)}`);
242
+ return this.error(500, `Internal error adding master password: ${extractErrorMsg(error)}`);
243
+ }
244
+ }
245
+ }
@@ -0,0 +1,167 @@
1
+ import { existsSync, rmSync } from 'node:fs';
2
+
3
+ import {
4
+ respecfully,
5
+ iReckon,
6
+ ifWe,
7
+ ifWeMight
8
+ } from '@ibgib/helper-gib/dist/respec-gib/respec-gib.mjs';
9
+
10
+ import { KeystoneCustodianGenesisHandler } from './keystone-custodian-genesis.handler.mjs';
11
+ import { KeystoneCustodianAddMasterPasswordHandler } from './keystone-custodian-add-master-password.handler.mjs';
12
+ import { KeystoneService_V1 } from '@ibgib/core-gib/dist/keystone/keystone-service-v1.mjs';
13
+ import { POOL_ID_MANAGE } from '@ibgib/core-gib/dist/keystone/keystone-constants.mjs';
14
+ import { CUSTODIAN_POOL_ID_PREFIX } from '@ibgib/core-gib/dist/keystone/delegate/delegate-constants.mjs';
15
+ import { generatePoolChallenges } from '@ibgib/core-gib/dist/keystone/keystone-helpers.mjs';
16
+ import { getPendingStagingDir, getPendingTjpHash, savePendingCodeRecord, savePendingStagingKeystone } from '../../../../email/verify/email-verify-helpers.mjs';
17
+ import { resetIdentityIndexService } from '../../../identity-index/identity-index-service.mjs';
18
+
19
+ const sir = `[${import.meta.url}]`;
20
+
21
+ await respecfully(sir, 'KeystoneCustodianAddMasterPasswordHandler', async () => {
22
+ const testDataDir = './test-data-custodian-add-password';
23
+
24
+ if (existsSync(testDataDir)) {
25
+ rmSync(testDataDir, { recursive: true, force: true });
26
+ }
27
+
28
+ const origDataDir = process.env.DATA_DIR;
29
+ process.env.DATA_DIR = testDataDir;
30
+ process.env.KEYSTONE_PROFILE_CUSTODIAN_PRIMARY = 'primary-profile.dev';
31
+ process.env.SSO_SERVER_KDF_SECRET = 'test-sso-server-kdf-secret-12345';
32
+ process.env.SESSION_SECRET = 'test-session-secret-12345';
33
+ resetIdentityIndexService();
34
+
35
+ const genesisHandler = new KeystoneCustodianGenesisHandler();
36
+ const addPasswordHandler = new KeystoneCustodianAddMasterPasswordHandler();
37
+
38
+ await ifWe(sir, 'reject request with 400 if required fields are missing', async () => {
39
+ const reqCtx: any = {
40
+ method: 'POST',
41
+ pathname: '/api/identity/custodian/add-master-password',
42
+ url: new URL('http://localhost/api/identity/custodian/add-master-password'),
43
+ body: JSON.stringify({}),
44
+ dataDir: testDataDir
45
+ };
46
+
47
+ const res = await addPasswordHandler.handleRoute(reqCtx);
48
+ iReckon(sir, res?.status).isGonnaBe(400);
49
+ });
50
+
51
+ await ifWe(sir, 'successfully elevate custodial identity to sovereign master password', async () => {
52
+ try {
53
+ const email = 'alice.elevate@example.com';
54
+ const genesisCode = '123456';
55
+
56
+ // 1. Create custodial identity via genesis handler with staged code
57
+ const genesisStaging = await savePendingStagingKeystone({
58
+ primaryTjpAddr: `pending:${email}`,
59
+ emails: [email],
60
+ dataDir: testDataDir,
61
+ });
62
+ await savePendingCodeRecord({
63
+ pendingDir: genesisStaging.pendingDir,
64
+ code: genesisCode,
65
+ });
66
+
67
+ const genesisReqCtx: any = {
68
+ method: 'POST',
69
+ pathname: '/api/identity/custodian/genesis',
70
+ url: new URL('http://localhost/api/identity/custodian/genesis'),
71
+ body: JSON.stringify({ email, username: 'alice', code: genesisCode }),
72
+ dataDir: testDataDir
73
+ };
74
+
75
+ const genesisRes = await genesisHandler.handleRoute(genesisReqCtx);
76
+ if (genesisRes?.status !== 201) {
77
+ console.error(`[DEBUG] genesisRes failed: ${genesisRes?.status}, body:`, JSON.stringify(genesisRes?.body));
78
+ }
79
+ iReckon(sir, genesisRes?.status).isGonnaBe(201);
80
+
81
+ const genesisBody = typeof genesisRes?.body === 'string' ? JSON.parse(genesisRes?.body) : genesisRes?.body;
82
+ const primaryTjpAddr = genesisBody.primaryTjpAddr;
83
+ const initialKeystone = genesisBody.primaryKeystone;
84
+
85
+ iReckon(sir, primaryTjpAddr).not.isGonnaBeUndefined();
86
+ iReckon(sir, initialKeystone).not.isGonnaBeUndefined();
87
+
88
+ const initialManagePool = initialKeystone.data.challengePools.find((p: any) => p.id === POOL_ID_MANAGE);
89
+ iReckon(sir, initialManagePool).not.isGonnaBeUndefined();
90
+
91
+ // 2. Generate new sovereign manage challenge pool locally with user's new password
92
+ const userNewPassword = 'AliceSuperSecretPassphrase2026!';
93
+ const newChallenges = await generatePoolChallenges({
94
+ config: initialManagePool.config,
95
+ masterSecret: userNewPassword,
96
+ });
97
+ const newManagePool = {
98
+ id: POOL_ID_MANAGE,
99
+ config: initialManagePool.config,
100
+ challenges: newChallenges,
101
+ };
102
+
103
+ // Stage verification code for the elevation turn
104
+ const elevateStaging = await savePendingStagingKeystone({
105
+ primaryTjpAddr,
106
+ emails: [email],
107
+ dataDir: testDataDir,
108
+ });
109
+ const elevateCode = '654321';
110
+ await savePendingCodeRecord({
111
+ pendingDir: elevateStaging.pendingDir,
112
+ code: elevateCode,
113
+ });
114
+
115
+ // 3. Call add-master-password endpoint
116
+ const addPasswordReqCtx: any = {
117
+ method: 'POST',
118
+ pathname: '/api/identity/custodian/add-master-password',
119
+ url: new URL('http://localhost/api/identity/custodian/add-master-password'),
120
+ body: JSON.stringify({
121
+ primaryTjpAddr,
122
+ email,
123
+ code: elevateCode,
124
+ newManagePool,
125
+ }),
126
+ dataDir: testDataDir
127
+ };
128
+
129
+ const addPasswordRes = await addPasswordHandler.handleRoute(addPasswordReqCtx);
130
+ if (addPasswordRes?.status !== 200) {
131
+ console.error(`[DEBUG] addPasswordRes failed with status: ${addPasswordRes?.status}, body:`, addPasswordRes?.body);
132
+ }
133
+ iReckon(sir, addPasswordRes?.status).isGonnaBe(200);
134
+
135
+ const addPasswordBody = typeof addPasswordRes?.body === 'string' ? JSON.parse(addPasswordRes?.body) : addPasswordRes?.body;
136
+ iReckon(sir, addPasswordBody.success).isGonnaBeTrue();
137
+ iReckon(sir, addPasswordBody.primaryTjpAddr).isGonnaBe(primaryTjpAddr);
138
+
139
+ const evolvedKeystone = addPasswordBody.primaryKeystone;
140
+ iReckon(sir, evolvedKeystone).not.isGonnaBeUndefined();
141
+ iReckon(sir, evolvedKeystone.data.n).isGonnaBe(1);
142
+ iReckon(sir, evolvedKeystone.data.frameDetails.custodyMode).isGonnaBe('hybrid');
143
+
144
+ // Check pools: should contain both 'manage' and 'custodian-manage-email'
145
+ const poolIds = evolvedKeystone.data.challengePools.map((p: any) => p.id);
146
+ iReckon(sir, poolIds.includes('manage')).isGonnaBeTrue();
147
+ iReckon(sir, poolIds.includes(`${CUSTODIAN_POOL_ID_PREFIX}email`)).isGonnaBeTrue();
148
+
149
+ // 4. Verify user password solves the evolved manage pool
150
+ const keystoneService = new KeystoneService_V1();
151
+ const isUserPasswordValid = await keystoneService.verifySigningSecret({
152
+ keystoneIbGib: evolvedKeystone,
153
+ signingSecret: userNewPassword,
154
+ poolId: POOL_ID_MANAGE,
155
+ });
156
+ iReckon(sir, isUserPasswordValid).isGonnaBeTrue();
157
+ } catch (error) {
158
+ console.error(`[DEBUG TEST 2 EXCEPTION]`, error);
159
+ throw error;
160
+ }
161
+ });
162
+
163
+ if (existsSync(testDataDir)) {
164
+ rmSync(testDataDir, { recursive: true, force: true });
165
+ }
166
+ process.env.DATA_DIR = origDataDir;
167
+ });
@@ -126,9 +126,12 @@ export class KeystoneCustodianGenesisHandler extends ServeGibHandlerBase<undefin
126
126
  const metaspace = await bootstrapDomainMetaspace(primaryTjpAddr, reqCtx.dataDir);
127
127
  const space = await metaspace.getLocalUserSpace({ lock: false });
128
128
  if (space) {
129
- const latestAddr = await metaspace.getLatestAddr({ addr: primaryTjpAddr, space }) ?? primaryTjpAddr;
130
- const getRes = await metaspace.get({ addrs: [latestAddr], space });
131
- primaryKeystone = getRes.ibGibs?.[0];
129
+ const keystoneService = new KeystoneService_V1();
130
+ primaryKeystone = await keystoneService.getLatestKeystone({
131
+ addr: primaryTjpAddr,
132
+ metaspace,
133
+ space
134
+ });
132
135
  }
133
136
  } else {
134
137
  // New custodian identity creation
@@ -140,9 +140,11 @@ export class KeystoneCustodianRegisterDelegateHandler extends ServeGibHandlerBas
140
140
  return this.error(500, `Could not load domain space for primary identity ${custodianReqs.primaryTjpAddr}`);
141
141
  }
142
142
 
143
- const latestPrimaryAddr = await metaspace.getLatestAddr({ addr: custodianReqs.primaryTjpAddr, space }) ?? custodianReqs.primaryTjpAddr;
144
- const getRes = await metaspace.get({ addrs: [latestPrimaryAddr], space });
145
- const parentKeystone = getRes.ibGibs?.[0] as KeystoneIbGib_V1 | undefined;
143
+ const parentKeystone = await keystoneService.getLatestKeystone({
144
+ addr: custodianReqs.primaryTjpAddr,
145
+ metaspace,
146
+ space
147
+ });
146
148
 
147
149
  if (!parentKeystone) {
148
150
  return this.error(404, `Primary identity keystone not found for address ${custodianReqs.primaryTjpAddr}`);
@@ -10,7 +10,7 @@ import { KeystoneCustodianGenesisHandler } from './keystone-custodian-genesis.ha
10
10
  import { KeystoneCustodianRegisterDelegateHandler } from './keystone-custodian-register-delegate.handler.mjs';
11
11
  import { KeystoneProfileBuilder } from '@ibgib/core-gib/dist/keystone/policy/keystone-profile-builder.mjs';
12
12
  import { KeystoneService_V1 } from '@ibgib/core-gib/dist/keystone/keystone-service-v1.mjs';
13
- import { deriveLocalCustodianDelegateSecret } from '@ibgib/web-gib/dist/identity/custodian-delegate-helper.mjs';
13
+ import { deriveDeviceDelegateSecret } from '@ibgib/core-gib/dist/keystone/delegate/delegate-helpers.mjs';
14
14
  import { bootstrapDomainMetaspace } from '../../../bootstrap-helper.mjs';
15
15
 
16
16
  const sir = `[${import.meta.url}]`;
@@ -58,8 +58,8 @@ await respecfully(sir, 'KeystoneCustodianRegisterDelegateHandler', async () => {
58
58
  iReckon(sir, custodianReqs).not.isGonnaBeUndefined();
59
59
 
60
60
  // Step 2: Build client delegate keystone containing custodianReqs
61
- const delegateSecret = await deriveLocalCustodianDelegateSecret({ primaryTjpAddr });
62
- const delegateBuilder = KeystoneProfileBuilder.buildKeystone('sync')
61
+ const delegateSecret = await deriveDeviceDelegateSecret({ primaryTjpAddr, deviceTag: 'phone' });
62
+ const delegateBuilder = KeystoneProfileBuilder.buildKeystone('sync-profile.dev')
63
63
  .withUsername('bob-sync-phone')
64
64
  .withDescription('Sync Delegate for bob on phone')
65
65
  .withDetails({
@@ -8,7 +8,7 @@
8
8
  * generates a fresh single-turn `CustodianRequirements` payload, and returns it to the client.
9
9
  */
10
10
 
11
- import { extractErrorMsg, hash, getUUID } from '@ibgib/helper-gib/dist/helpers/utils-helper.mjs';
11
+ import { extractErrorMsg, hash, getUUID, getSaferSubstring } from '@ibgib/helper-gib/dist/helpers/utils-helper.mjs';
12
12
  import { getTjpAddr } from '@ibgib/core-gib/dist/common/other/ibgib-helper.mjs';
13
13
  import { KeystoneProfileBuilder } from '@ibgib/core-gib/dist/keystone/policy/keystone-profile-builder.mjs';
14
14
  import { KeystoneService_V1 } from '@ibgib/core-gib/dist/keystone/keystone-service-v1.mjs';
@@ -103,7 +103,9 @@ export class SsoLoginHandler extends ServeGibHandlerBase<undefined, undefined> {
103
103
  }
104
104
 
105
105
  const email = userInfo.email.trim().toLowerCase();
106
- const username = userInfo.name || email.split('@')[0];
106
+ const rawSSOName = userInfo.name || '';
107
+ const username = getSaferSubstring({ text: rawSSOName || email.split('@')[0], length: 62, keepLiterals: ['_', '-'] })
108
+ ;
107
109
 
108
110
  // 2. Check if primary identity exists for this email
109
111
  const ssoConfig = loadSsoServerConfig();
@@ -122,10 +124,12 @@ export class SsoLoginHandler extends ServeGibHandlerBase<undefined, undefined> {
122
124
  primaryTjpAddr = existingLookup.primaryTjpAddr;
123
125
  const domainMetaspace = await bootstrapDomainMetaspace(primaryTjpAddr, reqCtx.dataDir);
124
126
  const space = await domainMetaspace.getLocalUserSpace({ lock: false });
125
-
126
- const latestPrimaryAddr = await domainMetaspace.getLatestAddr({ addr: primaryTjpAddr, space: space! }) ?? primaryTjpAddr;
127
- const getRes = await domainMetaspace.get({ addrs: [latestPrimaryAddr], space: space! });
128
- primaryKeystone = getRes.ibGibs?.[0];
127
+ const keystoneService = new KeystoneService_V1();
128
+ primaryKeystone = await keystoneService.getLatestKeystone({
129
+ addr: primaryTjpAddr,
130
+ metaspace: domainMetaspace,
131
+ space: space!
132
+ });
129
133
  } else {
130
134
  // First-time custodian identity creation
131
135
  isNewIdentity = true;
@@ -145,6 +149,8 @@ export class SsoLoginHandler extends ServeGibHandlerBase<undefined, undefined> {
145
149
  client: 'space-gib-server',
146
150
  role: 'custodian-primary',
147
151
  custodianProvider: providerId,
152
+ custodyMode: 'full',
153
+ [`rawSSOName-${providerId}`]: rawSSOName,
148
154
  });
149
155
 
150
156
  const configs = await primaryBuilder.compileConfigs();