@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.
- package/dist/email/verify/email-verify-helpers.d.mts +6 -1
- package/dist/email/verify/email-verify-helpers.d.mts.map +1 -1
- package/dist/email/verify/email-verify-helpers.mjs +6 -2
- package/dist/email/verify/email-verify-helpers.mjs.map +1 -1
- package/dist/identity/sso/sso-custodian-service.d.mts +8 -0
- package/dist/identity/sso/sso-custodian-service.d.mts.map +1 -1
- package/dist/identity/sso/sso-custodian-service.mjs +126 -77
- package/dist/identity/sso/sso-custodian-service.mjs.map +1 -1
- package/dist/serve-gib/handlers/api/email/email-verification-code.handler.d.mts.map +1 -1
- package/dist/serve-gib/handlers/api/email/email-verification-code.handler.mjs +50 -4
- package/dist/serve-gib/handlers/api/email/email-verification-code.handler.mjs.map +1 -1
- package/dist/serve-gib/handlers/api/identity/keystone-custodian-add-master-password.handler.d.mts +42 -0
- package/dist/serve-gib/handlers/api/identity/keystone-custodian-add-master-password.handler.d.mts.map +1 -0
- package/dist/serve-gib/handlers/api/identity/keystone-custodian-add-master-password.handler.mjs +201 -0
- package/dist/serve-gib/handlers/api/identity/keystone-custodian-add-master-password.handler.mjs.map +1 -0
- package/dist/serve-gib/handlers/api/identity/keystone-custodian-register-delegate.handler.d.mts.map +1 -1
- package/dist/serve-gib/handlers/api/identity/keystone-custodian-register-delegate.handler.mjs +5 -1
- package/dist/serve-gib/handlers/api/identity/keystone-custodian-register-delegate.handler.mjs.map +1 -1
- package/dist/serve-gib/handlers/api/keystone/keystone-evolve.handler.d.mts.map +1 -1
- package/dist/serve-gib/handlers/api/keystone/keystone-evolve.handler.mjs +6 -1
- package/dist/serve-gib/handlers/api/keystone/keystone-evolve.handler.mjs.map +1 -1
- package/dist/serve-gib/handlers/api/keystone/sso-config.handler.d.mts.map +1 -1
- package/dist/serve-gib/handlers/api/keystone/sso-config.handler.mjs +2 -11
- package/dist/serve-gib/handlers/api/keystone/sso-config.handler.mjs.map +1 -1
- package/dist/serve-gib/handlers/api/keystone/sso-link.handler.d.mts.map +1 -1
- package/dist/serve-gib/handlers/api/keystone/sso-link.handler.mjs +66 -26
- package/dist/serve-gib/handlers/api/keystone/sso-link.handler.mjs.map +1 -1
- package/dist/serve-gib/handlers/api/keystone/sso-login.handler.d.mts.map +1 -1
- package/dist/serve-gib/handlers/api/keystone/sso-login.handler.mjs +3 -10
- package/dist/serve-gib/handlers/api/keystone/sso-login.handler.mjs.map +1 -1
- package/dist/serve-gib/identity-index/identity-index-service.d.mts +5 -1
- package/dist/serve-gib/identity-index/identity-index-service.d.mts.map +1 -1
- package/dist/serve-gib/identity-index/identity-index-service.mjs +44 -20
- package/dist/serve-gib/identity-index/identity-index-service.mjs.map +1 -1
- package/dist/serve-gib/index.d.mts +1 -0
- package/dist/serve-gib/index.d.mts.map +1 -1
- package/dist/serve-gib/index.mjs +3 -0
- package/dist/serve-gib/index.mjs.map +1 -1
- package/dist/serve-gib/path-constants.d.mts +4 -0
- package/dist/serve-gib/path-constants.d.mts.map +1 -1
- package/dist/serve-gib/path-constants.mjs +4 -0
- package/dist/serve-gib/path-constants.mjs.map +1 -1
- package/e2e/identity-custodian-lifecycle.e2e.md +37 -0
- package/package.json +2 -2
- package/src/email/verify/email-verify-helpers.mts +13 -2
- package/src/identity/sso/sso-custodian-service.mts +129 -84
- package/src/serve-gib/handlers/api/email/email-verification-code.handler.mts +52 -5
- package/src/serve-gib/handlers/api/identity/keystone-custodian-add-master-password.handler.mts +252 -0
- package/src/serve-gib/handlers/api/identity/keystone-custodian-add-master-password.respec.mts +167 -0
- package/src/serve-gib/handlers/api/identity/keystone-custodian-register-delegate.handler.mts +6 -1
- package/src/serve-gib/handlers/api/identity/keystone-custodian-register-delegate.respec.mts +2 -2
- package/src/serve-gib/handlers/api/keystone/keystone-evolve.handler.mts +6 -1
- package/src/serve-gib/handlers/api/keystone/sso-config.handler.mts +2 -12
- package/src/serve-gib/handlers/api/keystone/sso-link.handler.mts +61 -26
- package/src/serve-gib/handlers/api/keystone/sso-login.handler.mts +3 -11
- package/src/serve-gib/identity-index/identity-index-service.mts +39 -19
- package/src/serve-gib/index.mts +3 -0
- 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
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
}
|
package/src/serve-gib/handlers/api/identity/keystone-custodian-add-master-password.handler.mts
ADDED
|
@@ -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
|
+
});
|
package/src/serve-gib/handlers/api/identity/keystone-custodian-register-delegate.handler.mts
CHANGED
|
@@ -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:
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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 =
|
|
39
|
+
const service = getSsoCustodianService();
|
|
50
40
|
const hostUrl = `${reqCtx.protocol}://${reqCtx.headers.host}`;
|
|
51
41
|
const providers = service.getSsoClientConfigs(hostUrl);
|
|
52
42
|
|