@ibgib/node-gib 0.0.19 → 0.0.21

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 (58) hide show
  1. package/dist/email/verify/email-verify-helpers.d.mts +6 -1
  2. package/dist/email/verify/email-verify-helpers.d.mts.map +1 -1
  3. package/dist/email/verify/email-verify-helpers.mjs +6 -2
  4. package/dist/email/verify/email-verify-helpers.mjs.map +1 -1
  5. package/dist/identity/sso/sso-custodian-service.d.mts +8 -0
  6. package/dist/identity/sso/sso-custodian-service.d.mts.map +1 -1
  7. package/dist/identity/sso/sso-custodian-service.mjs +126 -77
  8. package/dist/identity/sso/sso-custodian-service.mjs.map +1 -1
  9. package/dist/serve-gib/handlers/api/email/email-verification-code.handler.d.mts.map +1 -1
  10. package/dist/serve-gib/handlers/api/email/email-verification-code.handler.mjs +50 -4
  11. package/dist/serve-gib/handlers/api/email/email-verification-code.handler.mjs.map +1 -1
  12. package/dist/serve-gib/handlers/api/identity/keystone-custodian-add-master-password.handler.d.mts +42 -0
  13. package/dist/serve-gib/handlers/api/identity/keystone-custodian-add-master-password.handler.d.mts.map +1 -0
  14. package/dist/serve-gib/handlers/api/identity/keystone-custodian-add-master-password.handler.mjs +201 -0
  15. package/dist/serve-gib/handlers/api/identity/keystone-custodian-add-master-password.handler.mjs.map +1 -0
  16. package/dist/serve-gib/handlers/api/identity/keystone-custodian-register-delegate.handler.d.mts.map +1 -1
  17. package/dist/serve-gib/handlers/api/identity/keystone-custodian-register-delegate.handler.mjs +5 -1
  18. package/dist/serve-gib/handlers/api/identity/keystone-custodian-register-delegate.handler.mjs.map +1 -1
  19. package/dist/serve-gib/handlers/api/keystone/keystone-evolve.handler.d.mts.map +1 -1
  20. package/dist/serve-gib/handlers/api/keystone/keystone-evolve.handler.mjs +6 -1
  21. package/dist/serve-gib/handlers/api/keystone/keystone-evolve.handler.mjs.map +1 -1
  22. package/dist/serve-gib/handlers/api/keystone/sso-config.handler.d.mts.map +1 -1
  23. package/dist/serve-gib/handlers/api/keystone/sso-config.handler.mjs +2 -11
  24. package/dist/serve-gib/handlers/api/keystone/sso-config.handler.mjs.map +1 -1
  25. package/dist/serve-gib/handlers/api/keystone/sso-link.handler.d.mts.map +1 -1
  26. package/dist/serve-gib/handlers/api/keystone/sso-link.handler.mjs +66 -26
  27. package/dist/serve-gib/handlers/api/keystone/sso-link.handler.mjs.map +1 -1
  28. package/dist/serve-gib/handlers/api/keystone/sso-login.handler.d.mts.map +1 -1
  29. package/dist/serve-gib/handlers/api/keystone/sso-login.handler.mjs +3 -10
  30. package/dist/serve-gib/handlers/api/keystone/sso-login.handler.mjs.map +1 -1
  31. package/dist/serve-gib/identity-index/identity-index-service.d.mts +5 -1
  32. package/dist/serve-gib/identity-index/identity-index-service.d.mts.map +1 -1
  33. package/dist/serve-gib/identity-index/identity-index-service.mjs +44 -20
  34. package/dist/serve-gib/identity-index/identity-index-service.mjs.map +1 -1
  35. package/dist/serve-gib/index.d.mts +1 -0
  36. package/dist/serve-gib/index.d.mts.map +1 -1
  37. package/dist/serve-gib/index.mjs +3 -0
  38. package/dist/serve-gib/index.mjs.map +1 -1
  39. package/dist/serve-gib/path-constants.d.mts +4 -0
  40. package/dist/serve-gib/path-constants.d.mts.map +1 -1
  41. package/dist/serve-gib/path-constants.mjs +4 -0
  42. package/dist/serve-gib/path-constants.mjs.map +1 -1
  43. package/e2e/identity-custodian-lifecycle.e2e.md +37 -0
  44. package/package.json +2 -2
  45. package/src/email/verify/email-verify-helpers.mts +13 -2
  46. package/src/identity/sso/sso-custodian-service.mts +129 -84
  47. package/src/serve-gib/handlers/api/email/email-verification-code.handler.mts +52 -5
  48. package/src/serve-gib/handlers/api/identity/keystone-custodian-add-master-password.handler.mts +252 -0
  49. package/src/serve-gib/handlers/api/identity/keystone-custodian-add-master-password.respec.mts +167 -0
  50. package/src/serve-gib/handlers/api/identity/keystone-custodian-register-delegate.handler.mts +6 -1
  51. package/src/serve-gib/handlers/api/identity/keystone-custodian-register-delegate.respec.mts +2 -2
  52. package/src/serve-gib/handlers/api/keystone/keystone-evolve.handler.mts +6 -1
  53. package/src/serve-gib/handlers/api/keystone/sso-config.handler.mts +2 -12
  54. package/src/serve-gib/handlers/api/keystone/sso-link.handler.mts +61 -26
  55. package/src/serve-gib/handlers/api/keystone/sso-login.handler.mts +3 -11
  56. package/src/serve-gib/identity-index/identity-index-service.mts +39 -19
  57. package/src/serve-gib/index.mts +3 -0
  58. package/src/serve-gib/path-constants.mts +5 -0
@@ -4,7 +4,10 @@
4
4
  * Handler for POST /api/email/verification-code
5
5
  */
6
6
 
7
- import { extractErrorMsg, getUUID } from '@ibgib/helper-gib/dist/helpers/utils-helper.mjs';
7
+ import * as path from 'node:path';
8
+ import * as fs from 'node:fs/promises';
9
+ import { existsSync } from 'node:fs';
10
+ import { extractErrorMsg, getUUID, delay } from '@ibgib/helper-gib/dist/helpers/utils-helper.mjs';
8
11
  import { EmailSenderAws } from '../../../../email/aws/email-sender-aws.mjs';
9
12
  // import { generateVerificationCode, getPendingTjpHash, savePendingCodeRecord, getPendingStagingDir, validateEmail } from '../../../../email/index.mjs';
10
13
  import { EmailSender, OutboundEmail } from '../../../../email/email-types.mjs';
@@ -13,7 +16,7 @@ import { API_PATH_REGEXES } from '../../../path-constants.mjs';
13
16
  import { RequestContext, ResponseResult, ServeGibHttpMethod } from '../../../types.mjs';
14
17
  import { ServeGibHandlerBase } from '../../handler-base.mjs';
15
18
  import { GLOBAL_LOG_A_LOT } from '../../../constants.mjs';
16
- import { createDefaultEmailSender, generateVerificationCode, getPendingStagingDir, getPendingTjpHash, savePendingCodeRecord, savePendingStagingKeystone } from '../../../../email/verify/email-verify-helpers.mjs';
19
+ import { createDefaultEmailSender, generateVerificationCode, getPendingStagingDir, getPendingTjpHash, savePendingCodeRecord, savePendingStagingKeystone, PendingCodeRecord } from '../../../../email/verify/email-verify-helpers.mjs';
17
20
  import { validateEmail } from '../../../../email/email-helpers.mjs';
18
21
  import { normalizeEmail } from '../../../identity-index/identity-index-helpers.mjs';
19
22
 
@@ -75,11 +78,14 @@ export class EmailVerificationCodeHandler extends ServeGibHandlerBase {
75
78
  return this.error(400, 'Invalid email address format (E: e3f4a5b67890123456789abcdef012)');
76
79
  }
77
80
 
81
+ // Artificial delay to mitigate brute force & timing attacks (min 1s)
82
+ await delay(1000 + Math.random() * 500);
83
+
78
84
  const normEmail = normalizeEmail(email);
79
85
  const effectivePrimaryTjpAddr = primaryTjpAddr || `pending:${normEmail}`;
80
86
  const tjpHash = await getPendingTjpHash(effectivePrimaryTjpAddr);
81
87
 
82
- const code = generateVerificationCode();
88
+ let code: string = generateVerificationCode();
83
89
  if (reqCtx.dataDir && tjpHash) {
84
90
  let info = await getPendingStagingDir({ dataDir: reqCtx.dataDir, tjpHash });
85
91
  if (!info) {
@@ -93,8 +99,49 @@ export class EmailVerificationCodeHandler extends ServeGibHandlerBase {
93
99
  }
94
100
 
95
101
  if (info) {
96
- await savePendingCodeRecord({ pendingDir: info.pendingDir, code });
97
- if (logalot) { console.timeLog(timestampUUID, 'code record saved to pending staging'); }
102
+ const codeFilePath = path.join(info.pendingDir, 'code.json');
103
+ let existingRecord: PendingCodeRecord | undefined;
104
+ if (existsSync(codeFilePath)) {
105
+ try {
106
+ existingRecord = JSON.parse(await fs.readFile(codeFilePath, 'utf-8'));
107
+ } catch { /* ignore corrupted record */ }
108
+ }
109
+
110
+ const now = Date.now();
111
+ if (existingRecord) {
112
+ // Check 30-second cooldown
113
+ const elapsedMs = now - (existingRecord.dispatchedAt || 0);
114
+ if (elapsedMs < 30_000) {
115
+ const remainingSecs = Math.ceil((30_000 - elapsedMs) / 1000);
116
+ return this.error(429, `Please wait ${remainingSecs}s before requesting another verification code.`);
117
+ }
118
+
119
+ // Check Max Resend limit (5 resends per 15-minute window)
120
+ if (existingRecord.resendCount >= 5 && now < existingRecord.expiresAt) {
121
+ return this.error(429, 'Too many verification code requests. Please wait 15 minutes before trying again.');
122
+ }
123
+
124
+ // Reuse existing valid code if not expired and attempts < maxAttempts
125
+ if (existingRecord.rawCode && now < existingRecord.expiresAt && existingRecord.attempts < existingRecord.maxAttempts) {
126
+ code = existingRecord.rawCode;
127
+ existingRecord.dispatchedAt = now;
128
+ existingRecord.resendCount = (existingRecord.resendCount || 0) + 1;
129
+ await fs.writeFile(codeFilePath, JSON.stringify(existingRecord, null, 2), 'utf-8');
130
+ } else {
131
+ // Generate fresh code and increment resendCount
132
+ code = generateVerificationCode();
133
+ await savePendingCodeRecord({
134
+ pendingDir: info.pendingDir,
135
+ code,
136
+ resendCount: (existingRecord.resendCount || 0) + 1,
137
+ attempts: 0,
138
+ });
139
+ }
140
+ } else {
141
+ code = generateVerificationCode();
142
+ await savePendingCodeRecord({ pendingDir: info.pendingDir, code, resendCount: 0, attempts: 0 });
143
+ }
144
+ if (logalot) { console.timeLog(timestampUUID, 'code record processed in pending staging'); }
98
145
  } else {
99
146
  console.warn(`${lc} Failed to create or access pending staging directory for tjpHash ${tjpHash}`);
100
147
  }
@@ -0,0 +1,252 @@
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 { getDependencyGraph } from '@ibgib/core-gib/dist/common/other/graph-helper.mjs';
17
+ import { FlatIbGibGraph } from '@ibgib/core-gib/dist/common/other/graph-types.mjs';
18
+ import { KeystoneService_V1 } from '@ibgib/core-gib/dist/keystone/keystone-service-v1.mjs';
19
+ import { KeystoneChallengePool } from '@ibgib/core-gib/dist/keystone/keystone-types.mjs';
20
+ import { KEYSTONE_VERB_MANAGE, POOL_ID_MANAGE } from '@ibgib/core-gib/dist/keystone/keystone-constants.mjs';
21
+ import { CUSTODIAN_POOL_ID_PREFIX } from '@ibgib/core-gib/dist/keystone/delegate/delegate-constants.mjs';
22
+ import { KeystoneClaim } from '@ibgib/core-gib/dist/keystone/keystone-types.mjs';
23
+ import { validateEmail } from '@ibgib/core-gib/dist/keystone/keystone-helpers.mjs';
24
+
25
+ import { GLOBAL_LOG_A_LOT } from '../../../constants.mjs';
26
+ import { ServeGibHandlerBase } from '../../handler-base.mjs';
27
+ import { RequestContext, ResponseResult, ServeGibHttpMethod } from '../../../types.mjs';
28
+ import { API_PATH_REGEXES } from '../../../path-constants.mjs';
29
+ import { bootstrapDomainMetaspace } from '../../../bootstrap-helper.mjs';
30
+ import { getIdentityIndexService } from '../../../identity-index/identity-index-service.mjs';
31
+ import { normalizeEmail } from '../../../identity-index/identity-index-helpers.mjs';
32
+ import { loadSsoServerConfig } from '../../../../identity/sso/sso-config-helper.mjs';
33
+ import { getSsoCustodianService } from '../../../../identity/sso/sso-custodian-service.mjs';
34
+ import { SsoProviderId } from '../../../../identity/sso/sso-types.mjs';
35
+ import { getPendingStagingDir, getPendingTjpHash, verifyPendingCode } from '../../../../email/verify/email-verify-helpers.mjs';
36
+
37
+ const logalot = GLOBAL_LOG_A_LOT || true;
38
+
39
+ export interface CustodianAddMasterPasswordRequestBody {
40
+ code: string;
41
+ providerId?: SsoProviderId;
42
+ redirectUri?: string;
43
+ email?: string;
44
+ primaryTjpAddr: string;
45
+ newManagePool: KeystoneChallengePool;
46
+ }
47
+
48
+ export interface CustodianAddMasterPasswordResponseBody {
49
+ success: boolean;
50
+ primaryTjpAddr?: string;
51
+ primaryKeystone?: any;
52
+ primaryGraph?: FlatIbGibGraph;
53
+ message?: string;
54
+ error?: string;
55
+ }
56
+
57
+ /**
58
+ * POST /api/identity/custodian/add-master-password
59
+ */
60
+ export class KeystoneCustodianAddMasterPasswordHandler extends ServeGibHandlerBase<undefined, undefined> {
61
+ protected override lc: string = `[${KeystoneCustodianAddMasterPasswordHandler.name}]`;
62
+ protected override method: ServeGibHttpMethod = 'POST';
63
+ protected override regex = API_PATH_REGEXES.KEYSTONE_CUSTODIAN_ADD_MASTER_PASSWORD;
64
+
65
+ protected async handleRouteImpl(
66
+ reqCtx: RequestContext<undefined, undefined>
67
+ ): Promise<ResponseResult | undefined> {
68
+ const lc = `${this.lc}[${this.handleRouteImpl.name}]`;
69
+ try {
70
+ if (logalot) { console.log(`${lc} starting... (I: 8a1b2c3d4e5f67890a1b2c3d4e5f6790)`); }
71
+
72
+ if (reqCtx.method !== 'POST') {
73
+ return this.error(405, 'Method Not Allowed');
74
+ }
75
+
76
+ if (!reqCtx.body) {
77
+ return this.error(400, 'Request body is empty (E: 8a1b2c3d4e5f67890a1b2c3d4e5f6791)');
78
+ }
79
+
80
+ let body: CustodianAddMasterPasswordRequestBody;
81
+ try {
82
+ body = JSON.parse(reqCtx.body);
83
+ } catch {
84
+ return this.error(400, 'Request body must be valid JSON (E: 8a1b2c3d4e5f67890a1b2c3d4e5f6792)');
85
+ }
86
+
87
+ const { primaryTjpAddr, newManagePool, providerId, code, redirectUri, email } = body;
88
+
89
+ if (!primaryTjpAddr) {
90
+ return this.error(400, 'primaryTjpAddr is required (E: 8a1b2c3d4e5f67890a1b2c3d4e5f6793)');
91
+ }
92
+
93
+ if (!newManagePool || typeof newManagePool !== 'object' || newManagePool.id !== POOL_ID_MANAGE) {
94
+ return this.error(400, 'newManagePool is invalid. Must be an object with id "manage" (E: 8a1b2c3d4e5f67890a1b2c3d4e5f6794)');
95
+ }
96
+
97
+ if (!newManagePool.challenges || Object.keys(newManagePool.challenges).length === 0) {
98
+ return this.error(400, 'newManagePool must contain pre-generated public challenges (E: 8a1b2c3d4e5f67890a1b2c3d4e5f6795)');
99
+ }
100
+
101
+ // 1. Authenticate user credentials via OAuth code or Email OTP
102
+ let verifiedEmail: string;
103
+ let effectiveProvider: string = providerId || 'email';
104
+
105
+ if (providerId && code && redirectUri) {
106
+ const service = getSsoCustodianService();
107
+ const userInfo = await service.getOAuthUserInfo(providerId, code, redirectUri);
108
+ if (!userInfo || !userInfo.email) {
109
+ return this.error(401, `Failed to authenticate with SSO provider ${providerId} (E: 8a1b2c3d4e5f67890a1b2c3d4e5f6796)`);
110
+ }
111
+ verifiedEmail = normalizeEmail(userInfo.email);
112
+ } else if (email && code) {
113
+ if (!validateEmail(email)) {
114
+ return this.error(400, 'Invalid email address format (E: 8a1b2c3d4e5f67890a1b2c3d4e5f6797)');
115
+ }
116
+ verifiedEmail = normalizeEmail(email);
117
+
118
+ const tjpHash = await getPendingTjpHash(primaryTjpAddr);
119
+ let pendingInfo = reqCtx.dataDir && tjpHash ? await getPendingStagingDir({ dataDir: reqCtx.dataDir, tjpHash }) : undefined;
120
+ if (!pendingInfo) {
121
+ const emailTjpHash = await getPendingTjpHash(`pending:${verifiedEmail}`);
122
+ pendingInfo = reqCtx.dataDir && emailTjpHash ? await getPendingStagingDir({ dataDir: reqCtx.dataDir, tjpHash: emailTjpHash }) : undefined;
123
+ }
124
+
125
+ if (!pendingInfo || pendingInfo.isExpired) {
126
+ return this.error(400, 'Verification record missing or expired. Please request a new verification code. (E: 8a1b2c3d4e5f67890a1b2c3d4e5f6798)');
127
+ }
128
+
129
+ const codeVerifyResult = await verifyPendingCode({
130
+ pendingDir: pendingInfo.pendingDir,
131
+ code,
132
+ });
133
+
134
+ if (!codeVerifyResult.valid) {
135
+ return this.error(401, codeVerifyResult.error || 'Invalid verification code (E: 8a1b2c3d4e5f67890a1b2c3d4e5f6799)');
136
+ }
137
+ } else {
138
+ return this.error(400, 'Authentication credentials required (providerId+code+redirectUri or email+code) (E: 8a1b2c3d4e5f67890a1b2c3d4e5f679a)');
139
+ }
140
+
141
+ // 2. Lookup primary identity and confirm email matches
142
+ const indexService = getIdentityIndexService();
143
+ const identityRecord = await indexService.lookupIdentityByEmail({ email: verifiedEmail });
144
+ if (!identityRecord || identityRecord.primaryTjpAddr !== primaryTjpAddr) {
145
+ return this.error(403, `Authenticated email ${verifiedEmail} does not match primary identity ${primaryTjpAddr} (E: 8a1b2c3d4e5f67890a1b2c3d4e5f679b)`);
146
+ }
147
+
148
+ // 3. Bootstrap domain metaspace and load latest keystone
149
+ const metaspace = await bootstrapDomainMetaspace(primaryTjpAddr, reqCtx.dataDir);
150
+ const space = await metaspace.getLocalUserSpace({ lock: false });
151
+ if (!space) {
152
+ return this.error(500, `Local domain space not found for identity ${primaryTjpAddr}`);
153
+ }
154
+
155
+ const keystoneService = new KeystoneService_V1();
156
+ const latestKeystone = await keystoneService.getLatestKeystone({
157
+ addr: primaryTjpAddr,
158
+ metaspace,
159
+ space
160
+ });
161
+
162
+ if (!latestKeystone || !latestKeystone.data) {
163
+ return this.error(404, `Primary identity keystone not found for address ${primaryTjpAddr}`);
164
+ }
165
+
166
+ // 4. Verify existing custodial manage pool
167
+ const prevPools = latestKeystone.data.challengePools || [];
168
+ const existingManagePool = prevPools.find(p => p.id === POOL_ID_MANAGE);
169
+ if (!existingManagePool) {
170
+ return this.error(400, 'Primary keystone missing active "manage" pool (E: 8a1b2c3d4e5f67890a1b2c3d4e5f679c)');
171
+ }
172
+
173
+ // Validate new pool config matches existing manage pool config structure
174
+ if (existingManagePool.config?.type && newManagePool.config?.type !== existingManagePool.config.type) {
175
+ return this.error(400, `newManagePool type "${newManagePool.config?.type}" does not match required type "${existingManagePool.config.type}" (E: 8a1b2c3d4e5f67890a1b2c3d4e5f679d)`);
176
+ }
177
+
178
+ // 5. Build next pool set:
179
+ // Convert existing custodian manage pool to auxiliary custodian pool (e.g. custodian-manage-google)
180
+ // and install user's newManagePool as 'manage'.
181
+ const custodianPoolId = `${CUSTODIAN_POOL_ID_PREFIX}${effectiveProvider}`;
182
+ const auxiliaryCustodianPool: KeystoneChallengePool = {
183
+ ...existingManagePool,
184
+ id: custodianPoolId,
185
+ };
186
+
187
+ const nextPools: KeystoneChallengePool[] = [];
188
+ for (const pool of prevPools) {
189
+ if (pool.id === POOL_ID_MANAGE) {
190
+ nextPools.push(newManagePool);
191
+ nextPools.push(auxiliaryCustodianPool);
192
+ } else if (pool.id === custodianPoolId) {
193
+ // Skip if already present to prevent duplicate
194
+ } else {
195
+ nextPools.push(pool);
196
+ }
197
+ }
198
+
199
+ // 6. Sign evolution turn using server's custodian secret
200
+ const ssoConfig = loadSsoServerConfig();
201
+ const serverSecret = ssoConfig.ssoServerKdfSecret;
202
+ const primarySecret = await hash({ s: `${serverSecret}_custodian_${verifiedEmail}` });
203
+
204
+ const claim: Partial<KeystoneClaim> = {
205
+ verb: KEYSTONE_VERB_MANAGE,
206
+ target: getIbGibAddr({ ibGib: latestKeystone })
207
+ };
208
+
209
+ const evolvedKeystone = await keystoneService.sign({
210
+ latestKeystone,
211
+ masterSecret: primarySecret,
212
+ claim,
213
+ poolId: POOL_ID_MANAGE,
214
+ metaspace,
215
+ space,
216
+ dataToPatch: {
217
+ challengePools: nextPools,
218
+ frameDetails: {
219
+ action: 'add_master_password',
220
+ custodyMode: 'hybrid',
221
+ elevatedAt: new Date().toISOString(),
222
+ }
223
+ }
224
+ });
225
+
226
+ // 7. Persist evolved keystone in domain space
227
+ await metaspace.put({ ibGibs: [evolvedKeystone], space });
228
+ await metaspace.registerNewIbGib({ ibGib: evolvedKeystone, space });
229
+
230
+ // Extract complete primary keystone dependency graph across all frames
231
+ const primaryGraph = await getDependencyGraph({
232
+ ibGib: evolvedKeystone,
233
+ space,
234
+ live: false
235
+ });
236
+
237
+ const response: CustodianAddMasterPasswordResponseBody = {
238
+ success: true,
239
+ primaryTjpAddr,
240
+ primaryKeystone: evolvedKeystone,
241
+ primaryGraph,
242
+ message: 'Successfully elevated identity to sovereign master password'
243
+ };
244
+
245
+ return this.ok(response);
246
+
247
+ } catch (error) {
248
+ console.error(`${lc} ${extractErrorMsg(error)}`);
249
+ return this.error(500, `Internal error adding master password: ${extractErrorMsg(error)}`);
250
+ }
251
+ }
252
+ }
@@ -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
+ });
@@ -162,15 +162,20 @@ export class KeystoneCustodianRegisterDelegateHandler extends ServeGibHandlerBas
162
162
 
163
163
  const primarySecret = await hash({ s: `${serverSecret}_custodian_${primaryEmail}` });
164
164
 
165
+ const delegateDeviceName = delegateKeystone.data?.frameDetails?.deviceTag
166
+ || frameDetails?.deviceTag
167
+ || 'client-delegate';
168
+
165
169
  // Execute primary keystone evolution linking the new delegate
166
170
  const evolvedPrimaryKeystone = await keystoneService.registerDelegate({
167
171
  parentKeystone,
168
172
  delegateKeystone,
169
173
  masterSecret: primarySecret,
174
+ signingPoolId: 'manage',
170
175
  metaspace,
171
176
  space,
172
177
  allowedVerbs: custodianReqs.allowedVerbs || [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
173
- deviceName: frameDetails?.deviceTag || 'client-delegate'
178
+ deviceName: delegateDeviceName
174
179
  });
175
180
 
176
181
  // Persist delegate keystone in domain space
@@ -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,7 +58,7 @@ 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 });
61
+ const delegateSecret = await deriveDeviceDelegateSecret({ primaryTjpAddr, deviceTag: 'phone' });
62
62
  const delegateBuilder = KeystoneProfileBuilder.buildKeystone('sync-profile.dev')
63
63
  .withUsername('bob-sync-phone')
64
64
  .withDescription('Sync Delegate for bob on phone')
@@ -12,6 +12,7 @@
12
12
 
13
13
  import { extractErrorMsg } from '@ibgib/helper-gib/dist/helpers/utils-helper.mjs';
14
14
  import { getIbGibAddr } from '@ibgib/ts-gib/dist/helper.mjs';
15
+ import { getGibInfo } from '@ibgib/ts-gib/dist/V1/transforms/transform-helper.mjs';
15
16
  import { IbGib_V1 } from '@ibgib/ts-gib/dist/V1/types.mjs';
16
17
  import { validateAndRegisterEvolveKeystone } from '@ibgib/core-gib/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-receiver/sync-websocket-peer-helpers.mjs';
17
18
  import { KeystoneIbGib_V1 } from '@ibgib/core-gib/dist/keystone/keystone-types.mjs';
@@ -56,7 +57,11 @@ export class KeystoneEvolveHandler extends ServeGibHandlerWithMetaspaceBase<Evol
56
57
  if (match && match.length >= 3) {
57
58
  const domainIb = decodeURIComponent(match[1]);
58
59
  const domainGib = decodeURIComponent(match[2]);
59
- const domainAddr = getIbGibAddr({ ib: domainIb, gib: domainGib });
60
+ let domainAddr = getIbGibAddr({ ib: domainIb, gib: domainGib });
61
+ const { tjpGib } = getGibInfo({ ibGibAddr: domainAddr });
62
+ if (tjpGib && domainIb) {
63
+ domainAddr = `${domainIb}^${tjpGib}`;
64
+ }
60
65
  return {
61
66
  domainAddr,
62
67
  domainInfo: this.getDomainInfo({ domainAddr })
@@ -1,6 +1,5 @@
1
1
  import { extractErrorMsg } from '@ibgib/helper-gib/dist/helpers/utils-helper.mjs';
2
- import { loadSsoServerConfig } from '../../../../identity/sso/sso-config-helper.mjs';
3
- import { SsoCustodianService } from '../../../../identity/sso/sso-custodian-service.mjs';
2
+ import { getSsoCustodianService } from '../../../../identity/sso/sso-custodian-service.mjs';
4
3
 
5
4
  import { GLOBAL_LOG_A_LOT } from '../../../constants.mjs';
6
5
  import { ServeGibHandlerBase } from '../../handler-base.mjs';
@@ -9,15 +8,6 @@ import { RequestContext, ResponseResult, ServeGibHttpMethod } from '../../../typ
9
8
 
10
9
  const logalot = GLOBAL_LOG_A_LOT;
11
10
 
12
- let custodian: SsoCustodianService | undefined;
13
- function getCustodian(): SsoCustodianService {
14
- if (!custodian) {
15
- const config = loadSsoServerConfig();
16
- custodian = new SsoCustodianService(config);
17
- }
18
- return custodian;
19
- }
20
-
21
11
  /**
22
12
  * GET /api/identity/sso/config
23
13
  *
@@ -46,7 +36,7 @@ export class SsoConfigHandler extends ServeGibHandlerBase<any, any> {
46
36
  try {
47
37
  if (logalot) { console.log(`${lc} starting...`); }
48
38
 
49
- const service = getCustodian();
39
+ const service = getSsoCustodianService();
50
40
  const hostUrl = `${reqCtx.protocol}://${reqCtx.headers.host}`;
51
41
  const providers = service.getSsoClientConfigs(hostUrl);
52
42