@ibgib/web-gib 0.0.56 → 0.0.58

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 (65) hide show
  1. package/dist/AUTO-GENERATED-version.d.mts +1 -1
  2. package/dist/AUTO-GENERATED-version.mjs +1 -1
  3. package/dist/api/api-index.mjs +1 -1
  4. package/dist/api/api-index.mjs.map +1 -1
  5. package/dist/api/ibgib-api-bridge.d.mts +97 -0
  6. package/dist/api/ibgib-api-bridge.d.mts.map +1 -1
  7. package/dist/api/ibgib-api-bridge.mjs +205 -3
  8. package/dist/api/ibgib-api-bridge.mjs.map +1 -1
  9. package/dist/identity/custodian-delegate-helper.d.mts +22 -0
  10. package/dist/identity/custodian-delegate-helper.d.mts.map +1 -0
  11. package/dist/identity/custodian-delegate-helper.mjs +50 -0
  12. package/dist/identity/custodian-delegate-helper.mjs.map +1 -0
  13. package/dist/identity/sso-popup-helper.d.mts +33 -0
  14. package/dist/identity/sso-popup-helper.d.mts.map +1 -0
  15. package/dist/identity/sso-popup-helper.mjs +124 -0
  16. package/dist/identity/sso-popup-helper.mjs.map +1 -0
  17. package/dist/index.d.mts +2 -0
  18. package/dist/index.d.mts.map +1 -1
  19. package/dist/index.mjs +3 -0
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/ui/component/identity/add-device/add-device.d.mts.map +1 -1
  22. package/dist/ui/component/identity/add-device/add-device.html +6 -7
  23. package/dist/ui/component/identity/add-device/add-device.mjs +13 -10
  24. package/dist/ui/component/identity/add-device/add-device.mjs.map +1 -1
  25. package/dist/ui/component/identity/identity-header/identity-header.d.mts.map +1 -1
  26. package/dist/ui/component/identity/identity-header/identity-header.mjs +2 -1
  27. package/dist/ui/component/identity/identity-header/identity-header.mjs.map +1 -1
  28. package/dist/ui/component/identity/keystone-creator/keystone-creator.css +114 -9
  29. package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts +23 -0
  30. package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts.map +1 -1
  31. package/dist/ui/component/identity/keystone-creator/keystone-creator.html +122 -50
  32. package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs +529 -83
  33. package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs.map +1 -1
  34. package/dist/ui/component/identity/keystone-details/keystone-details.d.mts.map +1 -1
  35. package/dist/ui/component/identity/keystone-details/keystone-details.mjs +19 -61
  36. package/dist/ui/component/identity/keystone-details/keystone-details.mjs.map +1 -1
  37. package/package.json +4 -4
  38. package/src/AUTO-GENERATED-version.mts +1 -1
  39. package/src/api/api-index.mts +1 -1
  40. package/src/api/ibgib-api-bridge.mts +288 -4
  41. package/src/identity/custodian-delegate-helper.mts +61 -0
  42. package/src/identity/sso/{sso-custodian-service.mts → sso-custodian-service.mts.bak} +65 -38
  43. package/src/identity/sso/{sso-custodian-service.respec.mts → sso-custodian-service.respec.mts.bak} +15 -14
  44. package/src/identity/sso-popup-helper.mts +155 -0
  45. package/src/index.mts +3 -0
  46. package/src/ui/component/identity/add-device/add-device.html +6 -7
  47. package/src/ui/component/identity/add-device/add-device.mts +17 -16
  48. package/src/ui/component/identity/identity-header/identity-header.mts +2 -1
  49. package/src/ui/component/identity/keystone-creator/keystone-creator.css +114 -9
  50. package/src/ui/component/identity/keystone-creator/keystone-creator.html +122 -50
  51. package/src/ui/component/identity/keystone-creator/keystone-creator.mts +599 -100
  52. package/src/ui/component/identity/keystone-details/keystone-details.mts +26 -77
  53. package/dist/identity/sso/sso-config-helper.d.mts +0 -7
  54. package/dist/identity/sso/sso-config-helper.d.mts.map +0 -1
  55. package/dist/identity/sso/sso-config-helper.mjs +0 -42
  56. package/dist/identity/sso/sso-config-helper.mjs.map +0 -1
  57. package/dist/identity/sso/sso-custodian-service.d.mts +0 -139
  58. package/dist/identity/sso/sso-custodian-service.d.mts.map +0 -1
  59. package/dist/identity/sso/sso-custodian-service.mjs +0 -443
  60. package/dist/identity/sso/sso-custodian-service.mjs.map +0 -1
  61. package/dist/identity/sso/sso-custodian-service.respec.d.mts +0 -2
  62. package/dist/identity/sso/sso-custodian-service.respec.d.mts.map +0 -1
  63. package/dist/identity/sso/sso-custodian-service.respec.mjs +0 -254
  64. package/dist/identity/sso/sso-custodian-service.respec.mjs.map +0 -1
  65. /package/src/identity/sso/{sso-config-helper.mts → sso-config-helper.mts.bak} +0 -0
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { extractErrorMsg } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
8
8
  import { IbGib_V1 } from "@ibgib/ts-gib/dist/V1/types.mjs";
9
+ import { FlatIbGibGraph } from "@ibgib/core-gib/dist/common/other/graph-types.mjs";
9
10
 
10
11
  const logalot = false;
11
12
 
@@ -27,7 +28,7 @@ export class IbGibApiBridge {
27
28
  /**
28
29
  * Posts a single genesis keystone ibgib to the server to create a new domain.
29
30
  */
30
- public async postGenesisKeystone(keystoneIbGib: IbGib_V1): Promise<{ success: boolean; message?: string; addr?: string }> {
31
+ public async postGenesisKeystone(keystoneIbGib: IbGib_V1): Promise<{ success: boolean; pending?: boolean; statusCode?: number; message?: string; addr?: string }> {
31
32
  const lc = `${this.lc}[${this.postGenesisKeystone.name}]`;
32
33
  try {
33
34
  if (logalot) { console.log(`${lc} starting... (I: eecd082fc4a4381c8857928e8b090826)`); }
@@ -46,11 +47,11 @@ export class IbGibApiBridge {
46
47
  if (!response.ok) {
47
48
  const errorMsg = data.message || data.error || 'Unknown error';
48
49
  console.error(`${lc} server rejected genesis keystone: ${errorMsg}`);
49
- return { success: false, message: errorMsg, addr: data.addr };
50
+ return { success: false, message: errorMsg, addr: data.addr, statusCode: response.status };
50
51
  }
51
52
 
52
- if (logalot) { console.log(`${lc} genesis keystone posted successfully.`); }
53
- return { success: true, addr: data.addr };
53
+ if (logalot) { console.log(`${lc} genesis keystone posted successfully. pending: ${data.pending}`); }
54
+ return { success: true, pending: !!data.pending, statusCode: response.status, addr: data.addr, message: data.message };
54
55
 
55
56
  } catch (error) {
56
57
  const errorMsg = extractErrorMsg(error);
@@ -270,6 +271,289 @@ export class IbGibApiBridge {
270
271
  return { success: false, message: errorMsg };
271
272
  }
272
273
  }
274
+
275
+ /**
276
+ * Posts a request to generate a 6-digit verification code, save it to pending staging, and dispatch an email.
277
+ */
278
+ public async postVerificationCode({
279
+ email,
280
+ primaryTjpAddr
281
+ }: {
282
+ email: string;
283
+ primaryTjpAddr?: string;
284
+ }): Promise<{ success: boolean; message?: string }> {
285
+ const lc = `${this.lc}[${this.postVerificationCode.name}]`;
286
+ try {
287
+ const url = '/api/email/verification-code';
288
+ const response = await fetch(url, {
289
+ method: 'POST',
290
+ headers: {
291
+ 'Content-Type': 'application/json',
292
+ },
293
+ body: JSON.stringify({ email, primaryTjpAddr }),
294
+ });
295
+
296
+ const data = await this.parseResponseJson(response, url);
297
+
298
+ if (!response.ok) {
299
+ const errorMsg = data.message || data.error || 'Verification code request failed';
300
+ console.error(`${lc} server verification code post failed: ${errorMsg}`);
301
+ return { success: false, message: errorMsg };
302
+ }
303
+
304
+ return { success: true, message: data.message };
305
+ } catch (error) {
306
+ const errorMsg = extractErrorMsg(error);
307
+ console.error(`${lc} post verification code error: ${errorMsg}`);
308
+ return { success: false, message: errorMsg };
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Alias for postVerificationCode (deprecated name).
314
+ *
315
+ * @deprecated Use postVerificationCode instead.
316
+ */
317
+ public async postMagicLink(opts: {
318
+ email: string;
319
+ primaryTjpAddr: string;
320
+ }): Promise<{ success: boolean; message?: string }> {
321
+ return this.postVerificationCode(opts);
322
+ }
323
+
324
+ /**
325
+ * Posts a 6-digit verification code to activate an identity and verify email.
326
+ */
327
+ public async postVerifyCode({
328
+ email,
329
+ primaryTjpAddr,
330
+ code
331
+ }: {
332
+ email: string;
333
+ primaryTjpAddr: string;
334
+ code: string;
335
+ }): Promise<{ success: boolean; message?: string }> {
336
+ const lc = `${this.lc}[${this.postVerifyCode.name}]`;
337
+ try {
338
+ const url = '/api/email/verify-code';
339
+ const response = await fetch(url, {
340
+ method: 'POST',
341
+ headers: {
342
+ 'Content-Type': 'application/json',
343
+ },
344
+ body: JSON.stringify({ email, primaryTjpAddr, code }),
345
+ });
346
+
347
+ const data = await this.parseResponseJson(response, url);
348
+
349
+ if (!response.ok) {
350
+ const errorMsg = data.message || data.error || 'Code verification failed';
351
+ console.error(`${lc} server verify-code failed: ${errorMsg}`);
352
+ return { success: false, message: errorMsg };
353
+ }
354
+
355
+ if (logalot) { console.log(`${lc} verification code accepted.`); }
356
+ return { success: true, message: data.message };
357
+
358
+ } catch (error) {
359
+ const errorMsg = extractErrorMsg(error);
360
+ console.error(`${lc} fetch error: ${errorMsg}`);
361
+ return { success: false, message: errorMsg };
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Posts a custodian genesis request (`POST /api/identity/custodian/genesis`)
367
+ * to obtain the primary identity keystone (or primaryTjpAddr) and a fresh, single-turn
368
+ * `CustodianRequirements` payload for delegate registration.
369
+ */
370
+ public async postCustodianGenesis({
371
+ email,
372
+ code,
373
+ username
374
+ }: {
375
+ email: string;
376
+ code?: string;
377
+ username?: string;
378
+ }): Promise<{
379
+ success: boolean;
380
+ isNewIdentity?: boolean;
381
+ primaryTjpAddr?: string;
382
+ primaryKeystone?: IbGib_V1;
383
+ primaryGraph?: FlatIbGibGraph;
384
+ custodianReqs?: any;
385
+ message?: string;
386
+ }> {
387
+ const lc = `${this.lc}[${this.postCustodianGenesis.name}]`;
388
+ try {
389
+ const url = '/api/identity/custodian/genesis';
390
+ const response = await fetch(url, {
391
+ method: 'POST',
392
+ headers: {
393
+ 'Content-Type': 'application/json',
394
+ },
395
+ body: JSON.stringify({ email, code, username }),
396
+ });
397
+
398
+ const data = await this.parseResponseJson(response, url);
399
+
400
+ if (!response.ok) {
401
+ const errorMsg = data.message || data.error || 'Custodian genesis failed';
402
+ console.error(`${lc} custodian genesis failed: ${errorMsg}`);
403
+ return { success: false, message: errorMsg };
404
+ }
405
+
406
+ return {
407
+ success: true,
408
+ isNewIdentity: !!data.isNewIdentity,
409
+ primaryTjpAddr: data.primaryTjpAddr,
410
+ primaryKeystone: data.primaryKeystone,
411
+ primaryGraph: data.primaryGraph,
412
+ custodianReqs: data.custodianReqs,
413
+ message: data.message
414
+ };
415
+ } catch (error) {
416
+ const errorMsg = extractErrorMsg(error);
417
+ console.error(`${lc} post custodian genesis error: ${errorMsg}`);
418
+ return { success: false, message: errorMsg };
419
+ }
420
+ }
421
+
422
+ /**
423
+ * Posts a client delegate keystone containing single-turn `custodianReqs`
424
+ * to register it with the server custodian node (`POST /api/identity/custodian/register-delegate`).
425
+ */
426
+ public async postCustodianRegisterDelegate({
427
+ delegateKeystone
428
+ }: {
429
+ delegateKeystone: IbGib_V1;
430
+ }): Promise<{
431
+ success: boolean;
432
+ primaryTjpAddr?: string;
433
+ evolvedPrimaryKeystone?: IbGib_V1;
434
+ primaryGraph?: FlatIbGibGraph;
435
+ message?: string;
436
+ }> {
437
+ const lc = `${this.lc}[${this.postCustodianRegisterDelegate.name}]`;
438
+ try {
439
+ const url = '/api/identity/custodian/register-delegate';
440
+ const response = await fetch(url, {
441
+ method: 'POST',
442
+ headers: {
443
+ 'Content-Type': 'application/json',
444
+ },
445
+ body: JSON.stringify({ delegateKeystone }),
446
+ });
447
+
448
+ const data = await this.parseResponseJson(response, url);
449
+
450
+ if (!response.ok) {
451
+ const errorMsg = data.message || data.error || 'Custodian delegate registration failed';
452
+ console.error(`${lc} custodian delegate registration failed: ${errorMsg}`);
453
+ return { success: false, message: errorMsg };
454
+ }
455
+
456
+ return {
457
+ success: true,
458
+ primaryTjpAddr: data.primaryTjpAddr,
459
+ evolvedPrimaryKeystone: data.evolvedPrimaryKeystone,
460
+ primaryGraph: data.primaryGraph,
461
+ message: data.message
462
+ };
463
+ } catch (error) {
464
+ const errorMsg = extractErrorMsg(error);
465
+ console.error(`${lc} post custodian register delegate error: ${errorMsg}`);
466
+ return { success: false, message: errorMsg };
467
+ }
468
+ }
469
+
470
+ /**
471
+ * Retrieves safe public SSO client configuration (`GET /api/identity/sso/config`).
472
+ */
473
+ public async getSsoConfig(): Promise<{
474
+ success: boolean;
475
+ providers?: Record<string, { providerId: string; clientId: string; authUrl: string }>;
476
+ message?: string;
477
+ }> {
478
+ const lc = `${this.lc}[${this.getSsoConfig.name}]`;
479
+ try {
480
+ const url = '/api/identity/sso/config';
481
+ const response = await fetch(url, { method: 'GET' });
482
+ const data = await this.parseResponseJson(response, url);
483
+
484
+ if (!response.ok) {
485
+ const errorMsg = data.message || data.error || 'Failed to fetch SSO config';
486
+ return { success: false, message: errorMsg };
487
+ }
488
+
489
+ return {
490
+ success: true,
491
+ providers: data.providers
492
+ };
493
+ } catch (error) {
494
+ const errorMsg = extractErrorMsg(error);
495
+ console.error(`${lc} fetch SSO config error: ${errorMsg}`);
496
+ return { success: false, message: errorMsg };
497
+ }
498
+ }
499
+
500
+ /**
501
+ * Posts an OAuth authorization code (`POST /api/identity/sso/login`)
502
+ * to complete SSO token exchange and obtain custodian requirements.
503
+ */
504
+ public async postSsoLogin({
505
+ code,
506
+ providerId,
507
+ redirectUri
508
+ }: {
509
+ code: string;
510
+ providerId: string;
511
+ redirectUri: string;
512
+ }): Promise<{
513
+ success: boolean;
514
+ isNewIdentity?: boolean;
515
+ primaryTjpAddr?: string;
516
+ primaryKeystone?: IbGib_V1;
517
+ primaryGraph?: FlatIbGibGraph;
518
+ custodianReqs?: any;
519
+ userInfo?: any;
520
+ message?: string;
521
+ }> {
522
+ const lc = `${this.lc}[${this.postSsoLogin.name}]`;
523
+ try {
524
+ const url = '/api/identity/sso/login';
525
+ const response = await fetch(url, {
526
+ method: 'POST',
527
+ headers: {
528
+ 'Content-Type': 'application/json',
529
+ },
530
+ body: JSON.stringify({ code, providerId, redirectUri }),
531
+ });
532
+
533
+ const data = await this.parseResponseJson(response, url);
534
+
535
+ if (!response.ok) {
536
+ const errorMsg = data.message || data.error || 'SSO login code exchange failed';
537
+ console.error(`${lc} SSO login code exchange failed: ${errorMsg}`);
538
+ return { success: false, message: errorMsg };
539
+ }
540
+
541
+ return {
542
+ success: true,
543
+ isNewIdentity: !!data.isNewIdentity,
544
+ primaryTjpAddr: data.primaryTjpAddr,
545
+ primaryKeystone: data.primaryKeystone,
546
+ primaryGraph: data.primaryGraph,
547
+ custodianReqs: data.custodianReqs,
548
+ userInfo: data.userInfo,
549
+ message: data.message
550
+ };
551
+ } catch (error) {
552
+ const errorMsg = extractErrorMsg(error);
553
+ console.error(`${lc} post SSO login error: ${errorMsg}`);
554
+ return { success: false, message: errorMsg };
555
+ }
556
+ }
273
557
  }
274
558
 
275
559
  let _apiBridge: IbGibApiBridge | undefined;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @module identity/custodian-delegate-helper
3
+ *
4
+ * Client-side helper functions for deriving and storing local cryptographic delegate secrets
5
+ * for full-custodian / passwordless identities.
6
+ */
7
+
8
+ import { getUUID, hash, HashAlgorithm, extractErrorMsg } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
9
+
10
+ const LOCAL_STORAGE_PREFIX = 'ibgib_custodian_delegate_secret_';
11
+
12
+ /**
13
+ * Retrieves or generates a high-entropy local delegate secret for a passwordless custodian identity.
14
+ * Stored locally in `localStorage` keyed by `primaryTjpAddr`.
15
+ *
16
+ * @param primaryTjpAddr The primary identity keystone TJP address (`ib^gib`).
17
+ */
18
+ export async function deriveLocalCustodianDelegateSecret({
19
+ primaryTjpAddr,
20
+ }: {
21
+ primaryTjpAddr: string;
22
+ }): Promise<string> {
23
+ const lc = `[${deriveLocalCustodianDelegateSecret.name}]`;
24
+ try {
25
+ if (!primaryTjpAddr) {
26
+ throw new Error('primaryTjpAddr is required to derive local custodian delegate secret (E: a1b2c3d4e5f67890123456789a000101)');
27
+ }
28
+
29
+ const key = `${LOCAL_STORAGE_PREFIX}${primaryTjpAddr}`;
30
+ const existingSecret = localStorage.getItem(key);
31
+ if (existingSecret) {
32
+ return existingSecret;
33
+ }
34
+
35
+ // Generate high-entropy 512-bit SHA hash secret from fresh UUIDs
36
+ const rawEntropy = `${await getUUID()}-${await getUUID()}-${await getUUID()}-${Date.now()}`;
37
+ const newSecret = await hash({
38
+ s: rawEntropy,
39
+ algorithm: HashAlgorithm.sha_512
40
+ });
41
+
42
+ localStorage.setItem(key, newSecret);
43
+ return newSecret;
44
+ } catch (error) {
45
+ console.error(`${lc} ${extractErrorMsg(error)}`);
46
+ throw error;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Removes the stored local delegate secret for a custodian identity.
52
+ */
53
+ export function clearLocalCustodianDelegateSecret({
54
+ primaryTjpAddr,
55
+ }: {
56
+ primaryTjpAddr: string;
57
+ }): void {
58
+ if (!primaryTjpAddr) { return; }
59
+ const key = `${LOCAL_STORAGE_PREFIX}${primaryTjpAddr}`;
60
+ localStorage.removeItem(key);
61
+ }
@@ -1,20 +1,17 @@
1
1
  import { createPublicKey, verify } from 'node:crypto';
2
+
2
3
  import { hash, HashAlgorithm, extractErrorMsg } from '@ibgib/helper-gib/dist/helpers/utils-helper.mjs';
3
4
  import { kdf_recursiveSaltWrap } from '@ibgib/core-gib/dist/keystone/kdf/kdf-helpers.mjs';
4
5
  import { KeystoneService_V1 } from '@ibgib/core-gib/dist/keystone/keystone-service-v1.mjs';
5
- import { KeystoneProfileBuilder } from '@ibgib/core-gib/dist/keystone/policy/keystone-profile-builder.mjs';
6
- import { KeystoneIbGib_V1, KeystoneChallengePool, KeystonePoolConfig } from '@ibgib/core-gib/dist/keystone/keystone-types.mjs';
7
- import { POOL_ID_CUSTODIAN_MANAGE, KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE, POOL_ID_MANAGE, KEYSTONE_POOL_ID_REGEXP } from '@ibgib/core-gib/dist/keystone/keystone-constants.mjs';
8
- import { createStandardPoolConfig } from '@ibgib/core-gib/dist/keystone/keystone-config-builder.mjs';
6
+ import { KeystoneChallengePool, KeystonePoolConfig } from '@ibgib/core-gib/dist/keystone/keystone-types.mjs';
7
+ import { KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE, POOL_ID_MANAGE, KEYSTONE_POOL_ID_REGEXP } from '@ibgib/core-gib/dist/keystone/keystone-constants.mjs';
9
8
  import { generateOpaqueChallengeId } from '@ibgib/core-gib/dist/keystone/keystone-helpers.mjs';
10
9
  import { KeystoneStrategyFactory } from '@ibgib/core-gib/dist/keystone/strategy/keystone-strategy-factory.mjs';
11
10
  import { MetaspaceService } from '@ibgib/core-gib/dist/witness/space/metaspace/metaspace-types.mjs';
12
11
  import { IbGibSpaceAny } from '@ibgib/core-gib/dist/witness/space/space-base-v1.mjs';
13
- import {
14
- SsoServerConfig,
15
- SsoProviderId,
16
- OAuthUserInfo,
17
- SsoProviderServerConfig,
12
+
13
+ import {
14
+ SsoServerConfig, SsoProviderId, OAuthUserInfo, SsoProviderServerConfig,
18
15
  SsoProviderClientConfig
19
16
  } from './sso-types.mjs';
20
17
 
@@ -37,7 +34,7 @@ interface JwksResponse {
37
34
  */
38
35
  export class SsoCustodianService {
39
36
  private lc = `[${SsoCustodianService.name}]`;
40
-
37
+
41
38
  // In-memory cache for JSON Web Key Sets (JWKS) per provider
42
39
  private jwksCache: Map<SsoProviderId, { keys: JwkKey[]; expiresAt: number }> = new Map();
43
40
  private jwksTtlMs = 24 * 60 * 60 * 1000; // Cache certs for 24 hours
@@ -47,19 +44,19 @@ export class SsoCustodianService {
47
44
  * - Visibility: Private (Contains high-entropy secrets like `ssoServerKdfSecret` and `sessionSecret`).
48
45
  * - Generator: Server (Bootstrapped from node environment variables on server initialization).
49
46
  */
50
- constructor(private config: SsoServerConfig) {}
47
+ constructor(private config: SsoServerConfig) { }
51
48
 
52
49
  /**
53
50
  * Exchanges an authorization code for an OAuth2 token and retrieves normalized user info.
54
- *
51
+ *
55
52
  * @param providerId The string identifier of the OAuth provider (e.g. 'google' or 'github').
56
53
  * - Visibility: Public (Non-sensitive metadata string).
57
54
  * - Generator: Client (Sent from browser UI selection).
58
- *
55
+ *
59
56
  * @param code The temporary authorization code returned by the OAuth provider.
60
57
  * - Visibility: Private (Confidential short-lived bearer credential. Must be kept private).
61
58
  * - Generator: Provider (Created by provider's OAuth page and sent to client redirect URI).
62
- *
59
+ *
63
60
  * @param redirectUri The redirect URI registered in the provider portal.
64
61
  * - Visibility: Public (Configuration URL).
65
62
  * - Generator: Client/Server (Derived from environmental configuration and sent by client).
@@ -97,7 +94,7 @@ export class SsoCustodianService {
97
94
  /**
98
95
  * Directly validates a Google ID Token (JWT) locally and returns normalized user info.
99
96
  * Useful for client-side authentication handshakes.
100
- *
97
+ *
101
98
  * @param idToken The raw base64-encoded signed JSON Web Token (JWT) ID token returned by Google.
102
99
  * - Visibility: Private (Sensitive bearer credential containing signature and claims. Must be kept private).
103
100
  * - Generator: Provider (Generated and signed cryptographically by Google authentication servers).
@@ -125,6 +122,10 @@ export class SsoCustodianService {
125
122
  throw new Error(`Google JWT is missing subject claim ('sub'). (E: s28900abfefd23e89cfa1f89)`);
126
123
  }
127
124
 
125
+ if (payload.email && payload.email_verified !== true && (payload.email_verified as any) !== 'true') {
126
+ throw new Error(`Google email is not verified: ${payload.email}. (E: v28900abfefd23e89cfb2f90)`);
127
+ }
128
+
128
129
  return {
129
130
  providerKey: `google:${payload.sub}`,
130
131
  providerId: 'google',
@@ -137,22 +138,22 @@ export class SsoCustodianService {
137
138
 
138
139
  /**
139
140
  * Internal: Exchange authorization code for access token / ID token.
140
- *
141
+ *
141
142
  * @param providerConfig Server configuration settings for the specific provider.
142
143
  * - Visibility: Private (Contains provider client secrets. Must stay server-side).
143
144
  * - Generator: Server (Configured from node environment variables).
144
- *
145
+ *
145
146
  * @param code The temporary authorization code to exchange.
146
147
  * - Visibility: Private (Short-lived credential. Must be kept private).
147
148
  * - Generator: Provider (Via OAuth flow).
148
- *
149
+ *
149
150
  * @param redirectUri The redirect URI registered for the exchange.
150
151
  * - Visibility: Public (Configuration URL).
151
152
  * - Generator: Client/Server (From app configuration).
152
153
  */
153
154
  private async exchangeCodeForTokens(
154
- providerConfig: SsoProviderServerConfig,
155
- code: string,
155
+ providerConfig: SsoProviderServerConfig,
156
+ code: string,
156
157
  redirectUri: string
157
158
  ): Promise<any> {
158
159
  const body = new URLSearchParams({
@@ -188,17 +189,17 @@ export class SsoCustodianService {
188
189
 
189
190
  /**
190
191
  * Internal: Fetch GitHub user profile and normalize.
191
- *
192
+ *
192
193
  * @param providerConfig Server configuration settings for GitHub.
193
194
  * - Visibility: Private (Contains GitHub client secret).
194
195
  * - Generator: Server (From environment config).
195
- *
196
+ *
196
197
  * @param accessToken The active OAuth2 access token to authorize the API call.
197
198
  * - Visibility: Private (Bearer token. Must be kept private).
198
199
  * - Generator: Provider (GitHub token endpoint).
199
200
  */
200
201
  private async fetchAndNormalizeGithubUser(
201
- providerConfig: SsoProviderServerConfig,
202
+ providerConfig: SsoProviderServerConfig,
202
203
  accessToken: string
203
204
  ): Promise<OAuthUserInfo> {
204
205
  const res = await fetch(providerConfig.userInfoUrl, {
@@ -214,18 +215,44 @@ export class SsoCustodianService {
214
215
  }
215
216
 
216
217
  const rawUser = await res.json();
217
-
218
+
218
219
  if (!rawUser.id) {
219
220
  throw new Error(`GitHub user info response missing 'id' field.`);
220
221
  }
221
222
 
222
223
  const sub = String(rawUser.id);
224
+ let email = rawUser.email || undefined;
225
+
226
+ // If email is missing or private, fetch /user/emails to extract primary verified email
227
+ if (!email) {
228
+ try {
229
+ const emailsRes = await fetch('https://api.github.com/user/emails', {
230
+ headers: {
231
+ 'Authorization': `Bearer ${accessToken}`,
232
+ 'User-Agent': 'space-gib-auth-agent'
233
+ }
234
+ });
235
+ if (emailsRes.ok) {
236
+ const emailsList: any[] = await emailsRes.json();
237
+ const primaryVerified = emailsList.find((e: any) => e.verified && e.primary) || emailsList.find((e: any) => e.verified);
238
+ if (primaryVerified?.email) {
239
+ email = primaryVerified.email;
240
+ }
241
+ }
242
+ } catch (err) {
243
+ console.warn(`[${this.fetchAndNormalizeGithubUser.name}] Could not fetch GitHub /user/emails fallback: ${extractErrorMsg(err)}`);
244
+ }
245
+ }
246
+
247
+ if (!email) {
248
+ throw new Error(`Your GitHub account does not have a verified primary email address. Please verify your primary email on GitHub and try again. (E: e28900abfefd23e89cfc3f01)`);
249
+ }
223
250
 
224
251
  return {
225
252
  providerKey: `github:${sub}`,
226
253
  providerId: 'github',
227
254
  sub,
228
- email: rawUser.email || undefined,
255
+ email,
229
256
  name: rawUser.name || rawUser.login || undefined,
230
257
  picture: rawUser.avatar_url || undefined
231
258
  };
@@ -233,11 +260,11 @@ export class SsoCustodianService {
233
260
 
234
261
  /**
235
262
  * Internal: Decodes and cryptographically verifies a JWT signature using JWKS public keys.
236
- *
263
+ *
237
264
  * @param token The raw base64-encoded JWT token to verify.
238
265
  * - Visibility: Private (Sensitive token. Must be kept private).
239
266
  * - Generator: Provider (Google / OIDC server).
240
- *
267
+ *
241
268
  * @param providerConfig The target provider server settings containing `jwksUrl`.
242
269
  * - Visibility: Private (Contains secrets).
243
270
  * - Generator: Server (From environment config).
@@ -299,7 +326,7 @@ export class SsoCustodianService {
299
326
 
300
327
  /**
301
328
  * Internal: Retrieves JWKS keys from cache or fetches them from the provider's jwksUrl.
302
- *
329
+ *
303
330
  * @param providerConfig The provider config containing the target `jwksUrl`.
304
331
  * - Visibility: Private (Contains client secrets).
305
332
  * - Generator: Server (From environment config).
@@ -307,7 +334,7 @@ export class SsoCustodianService {
307
334
  private async getJwksKeys(providerConfig: SsoProviderServerConfig): Promise<JwkKey[]> {
308
335
  const providerId = providerConfig.providerId;
309
336
  const cached = this.jwksCache.get(providerId);
310
-
337
+
311
338
  if (cached && cached.expiresAt > Date.now()) {
312
339
  return cached.keys;
313
340
  }
@@ -337,11 +364,11 @@ export class SsoCustodianService {
337
364
  /**
338
365
  * Derives a server-delegate master secret deterministically using the server's private KDF secret,
339
366
  * the providerKey, and the user's public nonce.
340
- *
367
+ *
341
368
  * @param providerKey The unique key identifying the provider and user sub ID (e.g. `google:123...`).
342
369
  * - Visibility: Public (Identifier saved on parent's delegate claim, safe for log/ledger storage).
343
370
  * - Generator: Provider/Server (Derived by mapping provider user sub ID after authentication).
344
- *
371
+ *
345
372
  * @param userNonce The public, high-entropy nonce generated by the client to salt the KDF derivation.
346
373
  * - Visibility: Public (Salt value stored on parent's delegate claim, safe for ledger storage).
347
374
  * - Generator: Client (Generated offline on user's device).
@@ -362,11 +389,11 @@ export class SsoCustodianService {
362
389
  /**
363
390
  * Creates a public custodian challenge pool (custodian-manage) derived deterministically
364
391
  * from the server's private KDF secret, providerKey, and userNonce.
365
- *
392
+ *
366
393
  * @param providerKey The unique key identifying the provider and user sub ID (e.g. `google:123...`).
367
394
  * - Visibility: Public (Mapped identifier, safe for ledger).
368
395
  * - Generator: Provider/Server (Derived after successful token exchange).
369
- *
396
+ *
370
397
  * @param userNonce The public, high-entropy nonce.
371
398
  * - Visibility: Public (Derivation salt, safe for ledger).
372
399
  * - Generator: Client (Generated on user's device).
@@ -412,7 +439,7 @@ export class SsoCustodianService {
412
439
 
413
440
  // 4. Derive delegate secrets
414
441
  const custodianSecret = await this.deriveServerDelegateSecret(providerKey, userNonce);
415
-
442
+
416
443
  const poolSalt = await hash({
417
444
  s: `${providerKey}:${userNonce}:salt`,
418
445
  algorithm: HashAlgorithm.sha_256
@@ -460,14 +487,14 @@ export class SsoCustodianService {
460
487
 
461
488
  /**
462
489
  * Maps private server configuration to safe public client configurations.
463
- *
490
+ *
464
491
  * @param hostUrl The root URL host address of the server (e.g. `https://space-gib.localhost` or `https://ibgib.space`).
465
492
  * - Visibility: Public (Non-sensitive hostname configuration).
466
493
  * - Generator: Server (Extracted from the HTTP request context headers/protocol).
467
494
  */
468
495
  getSsoClientConfigs(hostUrl: string): SsoProviderClientConfig[] {
469
496
  const configs: SsoProviderClientConfig[] = [];
470
-
497
+
471
498
  if (this.config.providers.google) {
472
499
  configs.push({
473
500
  providerId: 'google',
@@ -477,7 +504,7 @@ export class SsoCustodianService {
477
504
  scope: 'openid email profile'
478
505
  });
479
506
  }
480
-
507
+
481
508
  if (this.config.providers.github) {
482
509
  configs.push({
483
510
  providerId: 'github',
@@ -487,7 +514,7 @@ export class SsoCustodianService {
487
514
  scope: 'read:user user:email'
488
515
  });
489
516
  }
490
-
517
+
491
518
  return configs;
492
519
  }
493
520