@ibgib/space-gib 0.0.7 → 0.0.8
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/client/bootstrap.mjs +1 -1
- package/dist/client/{chunk-ULFNE26Y.mjs → chunk-DBHMHCGD.mjs} +190 -65
- package/dist/client/chunk-DBHMHCGD.mjs.map +7 -0
- package/dist/client/chunk-SUQ5QJH4.mjs +42 -0
- package/dist/client/chunk-SUQ5QJH4.mjs.map +7 -0
- package/dist/client/index.mjs +1 -1
- package/dist/client/script.mjs +1 -1
- package/dist/server/server.mjs +5446 -5165
- package/dist/server/server.mjs.map +4 -4
- package/package.json +4 -4
- package/src/client/AUTO-GENERATED-version.mts +1 -1
- package/src/client/api/space-gib-api-bridge.mts +7 -2
- package/src/client/components/keystone-details/keystone-details.css +117 -20
- package/src/client/components/keystone-details/keystone-details.html +33 -5
- package/src/client/components/keystone-details/keystone-details.mts +206 -14
- package/src/server/serve-gib/handlers/api/keystone/keystone-evolve.handler.mts +33 -0
- package/src/server/serve-gib/handlers/api/keystone/sso-link.handler.mts +25 -4
- package/src/server/serve-gib/handlers/api/keystone/sso-login.handler.mts +4 -3
- package/dist/client/chunk-RXFIBFRK.mjs +0 -42
- package/dist/client/chunk-RXFIBFRK.mjs.map +0 -7
- package/dist/client/chunk-ULFNE26Y.mjs.map +0 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ibgib/space-gib",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "ibgib storage and provider service — SaaS at ibgib.space",
|
|
6
6
|
"scripts": {
|
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
"build:test": "tsc -p tsconfig.test.json"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@ibgib/core-gib": "^0.1.
|
|
15
|
+
"@ibgib/core-gib": "^0.1.65",
|
|
16
16
|
"@ibgib/helper-gib": "^0.0.36",
|
|
17
17
|
"@ibgib/ts-gib": "^0.5.33",
|
|
18
|
-
"@ibgib/web-gib": "^0.0.
|
|
19
|
-
"@ibgib/node-gib": "^0.0.
|
|
18
|
+
"@ibgib/web-gib": "^0.0.51",
|
|
19
|
+
"@ibgib/node-gib": "^0.0.9"
|
|
20
20
|
},
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": ">=20.0.0"
|
|
@@ -53,7 +53,12 @@ export class SpaceGibApiBridge {
|
|
|
53
53
|
/**
|
|
54
54
|
* PUTs an evolved domain keystone and any related ibgibs (e.g. S^Stjp) to the server.
|
|
55
55
|
*/
|
|
56
|
-
public async putEvolveKeystone(
|
|
56
|
+
public async putEvolveKeystone(
|
|
57
|
+
domainAddr: string,
|
|
58
|
+
keystoneIbGib: IbGib_V1,
|
|
59
|
+
relatedIbGibs: IbGib_V1[] = [],
|
|
60
|
+
revokeTokens?: Record<string, string>
|
|
61
|
+
): Promise<{ success: boolean; message?: string }> {
|
|
57
62
|
const lc = `${this.lc}[${this.putEvolveKeystone.name}]`;
|
|
58
63
|
try {
|
|
59
64
|
console.log(`${lc} posting evolved keystone...`);
|
|
@@ -65,7 +70,7 @@ export class SpaceGibApiBridge {
|
|
|
65
70
|
headers: {
|
|
66
71
|
'Content-Type': 'application/json',
|
|
67
72
|
},
|
|
68
|
-
body: JSON.stringify({ keystoneIbGib, relatedIbGibs }),
|
|
73
|
+
body: JSON.stringify({ keystoneIbGib, relatedIbGibs, revokeTokens }),
|
|
69
74
|
});
|
|
70
75
|
|
|
71
76
|
const data = await response.json();
|
|
@@ -433,40 +433,137 @@
|
|
|
433
433
|
}
|
|
434
434
|
}
|
|
435
435
|
|
|
436
|
-
|
|
436
|
+
/* Grouped Toggle Switch for Identity Providers */
|
|
437
|
+
.sso-toggle-group {
|
|
437
438
|
display: flex;
|
|
438
|
-
gap:
|
|
439
|
+
gap: 1.5rem;
|
|
439
440
|
margin-top: 1rem;
|
|
440
441
|
flex-wrap: wrap;
|
|
441
442
|
}
|
|
442
443
|
|
|
443
|
-
.sso-
|
|
444
|
+
.sso-toggle-card {
|
|
444
445
|
flex: 1;
|
|
445
|
-
min-width:
|
|
446
|
-
|
|
447
|
-
|
|
446
|
+
min-width: 180px;
|
|
447
|
+
max-width: 240px;
|
|
448
|
+
height: 180px;
|
|
449
|
+
background: rgba(255, 255, 255, 0.02);
|
|
450
|
+
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
451
|
+
border-radius: var(--radius-md, 8px);
|
|
452
|
+
display: flex;
|
|
453
|
+
flex-direction: column;
|
|
454
|
+
justify-content: space-between;
|
|
448
455
|
align-items: center;
|
|
449
|
-
|
|
450
|
-
|
|
456
|
+
cursor: pointer;
|
|
457
|
+
overflow: hidden;
|
|
458
|
+
position: relative;
|
|
459
|
+
user-select: none;
|
|
460
|
+
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.sso-toggle-card:hover {
|
|
464
|
+
background: rgba(255, 255, 255, 0.04);
|
|
465
|
+
border-color: rgba(255, 255, 255, 0.12);
|
|
466
|
+
transform: translateY(-2px);
|
|
467
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.sso-toggle-card:focus-visible {
|
|
471
|
+
outline: 2px solid var(--clr-accent, #50fa7b);
|
|
472
|
+
outline-offset: 2px;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/* Status Indicator bars: Top (Green/Active) and Bottom (Red/Faded) */
|
|
476
|
+
.toggle-status-indicator {
|
|
477
|
+
width: 100%;
|
|
478
|
+
height: 6px;
|
|
479
|
+
transition: all 0.25s ease-in-out;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
.toggle-status-indicator.top {
|
|
483
|
+
background: transparent;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.toggle-status-indicator.bottom {
|
|
487
|
+
background: transparent;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/* Middle Content Wrapper */
|
|
491
|
+
.toggle-content {
|
|
492
|
+
display: flex;
|
|
493
|
+
flex-direction: column;
|
|
494
|
+
align-items: center;
|
|
495
|
+
justify-content: center;
|
|
496
|
+
gap: 0.6rem;
|
|
497
|
+
padding: 1rem;
|
|
498
|
+
text-align: center;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.sso-icon {
|
|
502
|
+
width: 2.25rem;
|
|
503
|
+
height: 2.25rem;
|
|
504
|
+
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
.sso-toggle-card:hover .sso-icon {
|
|
508
|
+
transform: scale(1.1);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.provider-label {
|
|
512
|
+
font-size: 0.95rem;
|
|
451
513
|
font-weight: 600;
|
|
514
|
+
color: var(--clr-text-primary, #ffffff);
|
|
452
515
|
}
|
|
453
516
|
|
|
454
|
-
.
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
517
|
+
.toggle-state-text {
|
|
518
|
+
font-size: 0.75rem;
|
|
519
|
+
font-weight: 500;
|
|
520
|
+
text-transform: uppercase;
|
|
521
|
+
letter-spacing: 0.05em;
|
|
522
|
+
color: var(--clr-text-secondary, #8fa08f);
|
|
523
|
+
transition: color 0.25s ease-in-out;
|
|
458
524
|
}
|
|
459
525
|
|
|
460
|
-
|
|
461
|
-
|
|
526
|
+
/* Linked (Active State) styles */
|
|
527
|
+
.sso-toggle-card.linked {
|
|
528
|
+
background: rgba(80, 250, 123, 0.02);
|
|
529
|
+
border-color: rgba(80, 250, 123, 0.2);
|
|
530
|
+
box-shadow: 0 4px 16px rgba(80, 250, 123, 0.04);
|
|
462
531
|
}
|
|
463
532
|
|
|
464
|
-
.sso-
|
|
465
|
-
background:
|
|
466
|
-
border-color:
|
|
467
|
-
|
|
533
|
+
.sso-toggle-card.linked:hover {
|
|
534
|
+
background: rgba(80, 250, 123, 0.04);
|
|
535
|
+
border-color: rgba(80, 250, 123, 0.35);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
.sso-toggle-card.linked .toggle-status-indicator.top {
|
|
539
|
+
background: var(--clr-accent, #50fa7b);
|
|
540
|
+
box-shadow: 0 2px 10px rgba(80, 250, 123, 0.5);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
.sso-toggle-card.linked .toggle-state-text {
|
|
544
|
+
color: var(--clr-accent, #50fa7b);
|
|
545
|
+
font-weight: bold;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/* Unlinked (Inactive/Faded State) styles */
|
|
549
|
+
.sso-toggle-card.unlinked {
|
|
550
|
+
opacity: 0.75;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
.sso-toggle-card.unlinked:hover {
|
|
554
|
+
opacity: 1.0;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.sso-toggle-card.unlinked .toggle-status-indicator.bottom {
|
|
558
|
+
background: rgba(255, 85, 85, 0.4);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
.sso-toggle-card.unlinked .toggle-state-text {
|
|
562
|
+
color: var(--clr-text-secondary, #8fa08f);
|
|
468
563
|
}
|
|
469
564
|
|
|
470
|
-
|
|
471
|
-
|
|
565
|
+
/* Historical/Disabled state */
|
|
566
|
+
.sso-toggle-card.disabled {
|
|
567
|
+
pointer-events: none;
|
|
568
|
+
opacity: 0.4;
|
|
472
569
|
}
|
|
@@ -76,11 +76,39 @@
|
|
|
76
76
|
|
|
77
77
|
<!-- SSO Identity Linking Card -->
|
|
78
78
|
<div id="sso-link-card" class="card hidden">
|
|
79
|
-
<h3>
|
|
80
|
-
<p class="description">
|
|
81
|
-
<div class="sso-
|
|
82
|
-
|
|
83
|
-
<
|
|
79
|
+
<h3>Identity Providers</h3>
|
|
80
|
+
<p class="description">Securely link or unlink external OAuth2 providers to delegate sync access to this identity.</p>
|
|
81
|
+
<div class="sso-toggle-group">
|
|
82
|
+
<!-- Google Card Toggle -->
|
|
83
|
+
<div id="btn-link-google" class="sso-toggle-card google" role="button" tabindex="0">
|
|
84
|
+
<div class="toggle-status-indicator top"></div>
|
|
85
|
+
<div class="toggle-content">
|
|
86
|
+
<!-- Custom Google SVG Icon -->
|
|
87
|
+
<svg class="sso-icon" viewBox="0 0 24 24">
|
|
88
|
+
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
|
89
|
+
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
|
90
|
+
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.06H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.94l2.85-2.22.81-.63z"/>
|
|
91
|
+
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.06l3.66 2.84c.87-2.6 3.3-4.52 6.16-4.52z"/>
|
|
92
|
+
</svg>
|
|
93
|
+
<span class="provider-label">Google</span>
|
|
94
|
+
<span class="toggle-state-text">Unlinked</span>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="toggle-status-indicator bottom"></div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<!-- GitHub Card Toggle -->
|
|
100
|
+
<div id="btn-link-github" class="sso-toggle-card github" role="button" tabindex="0">
|
|
101
|
+
<div class="toggle-status-indicator top"></div>
|
|
102
|
+
<div class="toggle-content">
|
|
103
|
+
<!-- Custom GitHub SVG Icon -->
|
|
104
|
+
<svg class="sso-icon" viewBox="0 0 24 24" fill="currentColor">
|
|
105
|
+
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>
|
|
106
|
+
</svg>
|
|
107
|
+
<span class="provider-label">GitHub</span>
|
|
108
|
+
<span class="toggle-state-text">Unlinked</span>
|
|
109
|
+
</div>
|
|
110
|
+
<div class="toggle-status-indicator bottom"></div>
|
|
111
|
+
</div>
|
|
84
112
|
</div>
|
|
85
113
|
<div id="sso-status-msg" class="status-msg hidden"></div>
|
|
86
114
|
</div>
|
|
@@ -24,6 +24,8 @@ import { toDto } from "@ibgib/core-gib/dist/common/other/ibgib-helper.mjs";
|
|
|
24
24
|
import { GLOBAL_LOG_A_LOT, APP_CONFIG, } from "../../constants.mjs";
|
|
25
25
|
import { getComponentCtorArg, getIbGibGlobalThis_SpaceGib } from "../../helpers.web.mjs";
|
|
26
26
|
import { devLog } from "../../dev-tools.mjs";
|
|
27
|
+
import { promptForSecret } from "@ibgib/web-gib/dist/helpers.web.mjs";
|
|
28
|
+
import { isSsoProviderLinked } from "@ibgib/web-gib/dist/identity/sso/sso-helpers.mjs";
|
|
27
29
|
|
|
28
30
|
const logalot = GLOBAL_LOG_A_LOT || true;
|
|
29
31
|
|
|
@@ -84,8 +86,8 @@ interface KeystoneDetailsElements extends ElementsBase {
|
|
|
84
86
|
historicalWarningBannerEl: HTMLDivElement;
|
|
85
87
|
containerEl: HTMLDivElement;
|
|
86
88
|
ssoLinkCardEl: HTMLDivElement;
|
|
87
|
-
btnLinkGoogleEl:
|
|
88
|
-
btnLinkGithubEl:
|
|
89
|
+
btnLinkGoogleEl: HTMLDivElement;
|
|
90
|
+
btnLinkGithubEl: HTMLDivElement;
|
|
89
91
|
ssoStatusMsgEl: HTMLDivElement;
|
|
90
92
|
}
|
|
91
93
|
|
|
@@ -190,8 +192,8 @@ export class KeystoneDetailsComponentInstance
|
|
|
190
192
|
this.elements.containerEl = shadow.getElementById('container') as HTMLDivElement;
|
|
191
193
|
this.elements.historicalWarningBannerEl = shadow.getElementById('historical-warning-banner') as HTMLDivElement;
|
|
192
194
|
this.elements.ssoLinkCardEl = shadow.getElementById('sso-link-card') as HTMLDivElement;
|
|
193
|
-
this.elements.btnLinkGoogleEl = shadow.getElementById('btn-link-google') as
|
|
194
|
-
this.elements.btnLinkGithubEl = shadow.getElementById('btn-link-github') as
|
|
195
|
+
this.elements.btnLinkGoogleEl = shadow.getElementById('btn-link-google') as HTMLDivElement;
|
|
196
|
+
this.elements.btnLinkGithubEl = shadow.getElementById('btn-link-github') as HTMLDivElement;
|
|
195
197
|
this.elements.ssoStatusMsgEl = shadow.getElementById('sso-status-msg') as HTMLDivElement;
|
|
196
198
|
|
|
197
199
|
// ElementBase requires contentEl
|
|
@@ -229,8 +231,8 @@ export class KeystoneDetailsComponentInstance
|
|
|
229
231
|
// Buttons
|
|
230
232
|
this.elements!.btnVerifyChainEl.addEventListener('click', () => this.handleVerifyIdentityChain());
|
|
231
233
|
this.elements!.btnSetActiveEl.addEventListener('click', () => this.handleSetActiveIdentity());
|
|
232
|
-
this.elements!.btnLinkGoogleEl.addEventListener('click', () => this.
|
|
233
|
-
this.elements!.btnLinkGithubEl.addEventListener('click', () => this.
|
|
234
|
+
this.elements!.btnLinkGoogleEl.addEventListener('click', () => this.handleSSOClick('google'));
|
|
235
|
+
this.elements!.btnLinkGithubEl.addEventListener('click', () => this.handleSSOClick('github'));
|
|
234
236
|
}
|
|
235
237
|
|
|
236
238
|
private toggleAccordion() {
|
|
@@ -348,9 +350,43 @@ export class KeystoneDetailsComponentInstance
|
|
|
348
350
|
if (this.isHistoricalFrame) {
|
|
349
351
|
this.elements.btnSetActiveEl.disabled = true;
|
|
350
352
|
this.elements.btnVerifyChainEl.disabled = true;
|
|
353
|
+
this.elements.btnLinkGoogleEl.classList.add('disabled');
|
|
354
|
+
this.elements.btnLinkGithubEl.classList.add('disabled');
|
|
351
355
|
} else {
|
|
352
356
|
this.elements.btnVerifyChainEl.disabled = false;
|
|
353
357
|
this.elements.btnSetActiveEl.disabled = isTimelineActive;
|
|
358
|
+
this.elements.btnLinkGoogleEl.classList.remove('disabled');
|
|
359
|
+
this.elements.btnLinkGithubEl.classList.remove('disabled');
|
|
360
|
+
|
|
361
|
+
// Check if already linked
|
|
362
|
+
const isGoogleLinked = isSsoProviderLinked({ keystone: this.ibGib, providerId: 'google' });
|
|
363
|
+
const isGithubLinked = isSsoProviderLinked({ keystone: this.ibGib, providerId: 'github' });
|
|
364
|
+
|
|
365
|
+
// Google Card States
|
|
366
|
+
const googleCard = this.elements.btnLinkGoogleEl;
|
|
367
|
+
const googleLabel = googleCard.querySelector('.toggle-state-text')!;
|
|
368
|
+
if (isGoogleLinked) {
|
|
369
|
+
googleCard.classList.remove('unlinked');
|
|
370
|
+
googleCard.classList.add('linked');
|
|
371
|
+
googleLabel.textContent = "Google Linked ✅";
|
|
372
|
+
} else {
|
|
373
|
+
googleCard.classList.remove('linked');
|
|
374
|
+
googleCard.classList.add('unlinked');
|
|
375
|
+
googleLabel.textContent = "Unlinked";
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// GitHub Card States
|
|
379
|
+
const githubCard = this.elements.btnLinkGithubEl;
|
|
380
|
+
const githubLabel = githubCard.querySelector('.toggle-state-text')!;
|
|
381
|
+
if (isGithubLinked) {
|
|
382
|
+
githubCard.classList.remove('unlinked');
|
|
383
|
+
githubCard.classList.add('linked');
|
|
384
|
+
githubLabel.textContent = "GitHub Linked ✅";
|
|
385
|
+
} else {
|
|
386
|
+
githubCard.classList.remove('linked');
|
|
387
|
+
githubCard.classList.add('unlinked');
|
|
388
|
+
githubLabel.textContent = "Unlinked";
|
|
389
|
+
}
|
|
354
390
|
}
|
|
355
391
|
}
|
|
356
392
|
|
|
@@ -687,7 +723,7 @@ export class KeystoneDetailsComponentInstance
|
|
|
687
723
|
|
|
688
724
|
// 2. Generate a secure, high-entropy user nonce
|
|
689
725
|
const userNonce = (Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2)).substring(0, 32);
|
|
690
|
-
|
|
726
|
+
|
|
691
727
|
// 3. Construct state payload
|
|
692
728
|
const stateObj = {
|
|
693
729
|
providerId,
|
|
@@ -710,16 +746,30 @@ export class KeystoneDetailsComponentInstance
|
|
|
710
746
|
}
|
|
711
747
|
|
|
712
748
|
// 6. Listen for callback from pop-up
|
|
749
|
+
let checkClosedInterval: any;
|
|
713
750
|
const messageListener = async (event: MessageEvent) => {
|
|
714
751
|
if (event.origin !== window.location.origin) return;
|
|
715
752
|
const callbackData = event.data;
|
|
716
753
|
if (callbackData && callbackData.type === 'sso-oauth-callback') {
|
|
717
754
|
window.removeEventListener('message', messageListener);
|
|
755
|
+
clearInterval(checkClosedInterval);
|
|
718
756
|
await this.handleSSOLinkCallback(callbackData.code, stateObj, providerConfig.redirectUri);
|
|
719
757
|
}
|
|
720
758
|
};
|
|
721
759
|
window.addEventListener('message', messageListener);
|
|
722
760
|
|
|
761
|
+
// 7. Poll for manual closure of the popup window
|
|
762
|
+
checkClosedInterval = setInterval(() => {
|
|
763
|
+
if (popup.closed) {
|
|
764
|
+
clearInterval(checkClosedInterval);
|
|
765
|
+
window.removeEventListener('message', messageListener);
|
|
766
|
+
const statusEl = this.elements!.ssoStatusMsgEl;
|
|
767
|
+
if (statusEl.textContent && statusEl.textContent.startsWith('Linking')) {
|
|
768
|
+
this.setSSOStatus('Linking cancelled (window closed).', 'error');
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}, 500);
|
|
772
|
+
|
|
723
773
|
} catch (error) {
|
|
724
774
|
console.error(`${lc} SSO link initiation failed: ${extractErrorMsg(error)}`);
|
|
725
775
|
this.setSSOStatus(`Link failed: ${extractErrorMsg(error)}`, "error");
|
|
@@ -762,10 +812,47 @@ export class KeystoneDetailsComponentInstance
|
|
|
762
812
|
|
|
763
813
|
const custodianPool = linkData.custodianPool;
|
|
764
814
|
|
|
765
|
-
// 2. Prompt user for master secret
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
|
|
815
|
+
// 2. Prompt user for master secret and verify it
|
|
816
|
+
const keystoneService = new KeystoneService_V1();
|
|
817
|
+
let masterSecret: string | undefined;
|
|
818
|
+
let isCorrect = false;
|
|
819
|
+
let tries = 0;
|
|
820
|
+
const maxTries = 8;
|
|
821
|
+
|
|
822
|
+
while (tries < maxTries && !isCorrect) {
|
|
823
|
+
try {
|
|
824
|
+
const promptMsg = tries === 0
|
|
825
|
+
? "Link successful! Enter your local Master Secret (Passphrase) to sign and register the custodian pool"
|
|
826
|
+
: `Incorrect passphrase. Please try again (${maxTries - tries} tries remaining):`;
|
|
827
|
+
masterSecret = await promptForSecret({
|
|
828
|
+
msg: promptMsg,
|
|
829
|
+
confirm: false
|
|
830
|
+
});
|
|
831
|
+
} catch (error) {
|
|
832
|
+
// cancelled
|
|
833
|
+
if (logalot) { console.log(`${lc}[promptForSecret] user cancelled (I: 911ff1fb0234949228481968e9087626)`); }
|
|
834
|
+
masterSecret = undefined;
|
|
835
|
+
break;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (!masterSecret) {
|
|
839
|
+
break;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Verify secret using verifySigningSecret (requires manage poolId)
|
|
843
|
+
isCorrect = await keystoneService.verifySigningSecret({
|
|
844
|
+
keystoneIbGib: this.ibGib!,
|
|
845
|
+
signingSecret: masterSecret,
|
|
846
|
+
poolId: 'manage'
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
if (!isCorrect) {
|
|
850
|
+
tries++;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
if (!isCorrect) {
|
|
855
|
+
this.setSSOStatus("Link cancelled or failed (incorrect passphrase).", "error");
|
|
769
856
|
return;
|
|
770
857
|
}
|
|
771
858
|
|
|
@@ -774,13 +861,12 @@ export class KeystoneDetailsComponentInstance
|
|
|
774
861
|
const metaspace = getIbGibGlobalThis_SpaceGib(APP_CONFIG).metaspace;
|
|
775
862
|
if (!metaspace) throw new Error("Metaspace not found");
|
|
776
863
|
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
777
|
-
if (!space) throw new Error("Local space not found");
|
|
864
|
+
if (!space) { throw new Error("Local space not found"); }
|
|
778
865
|
|
|
779
866
|
// 3. Evolve parent keystone to include the custodian pool
|
|
780
|
-
const keystoneService = new KeystoneService_V1();
|
|
781
867
|
const evolvedParent = await keystoneService.addPools({
|
|
782
868
|
latestKeystone: this.ibGib!,
|
|
783
|
-
masterSecret
|
|
869
|
+
masterSecret: masterSecret!,
|
|
784
870
|
newPools: [custodianPool],
|
|
785
871
|
metaspace,
|
|
786
872
|
space
|
|
@@ -818,4 +904,110 @@ export class KeystoneDetailsComponentInstance
|
|
|
818
904
|
this.setSSOStatus(`Link failed: ${extractErrorMsg(error)}`, "error");
|
|
819
905
|
}
|
|
820
906
|
}
|
|
907
|
+
|
|
908
|
+
protected async handleSSOClick(providerId: 'google' | 'github') {
|
|
909
|
+
const isLinked = isSsoProviderLinked({ keystone: this.ibGib, providerId });
|
|
910
|
+
if (isLinked) {
|
|
911
|
+
await this.handleSSOUnlink(providerId);
|
|
912
|
+
} else {
|
|
913
|
+
await this.handleSSOLink(providerId);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
protected async handleSSOUnlink(providerId: 'google' | 'github') {
|
|
918
|
+
const lc = `${this.lc}[${this.handleSSOUnlink.name}]`;
|
|
919
|
+
this.setSSOStatus(`Unlinking ${providerId}...`, "info");
|
|
920
|
+
|
|
921
|
+
try {
|
|
922
|
+
// 1. Prompt user for passphrase
|
|
923
|
+
const keystoneService = new KeystoneService_V1();
|
|
924
|
+
let masterSecret: string | undefined;
|
|
925
|
+
let isCorrect = false;
|
|
926
|
+
let tries = 0;
|
|
927
|
+
const maxTries = 8;
|
|
928
|
+
|
|
929
|
+
while (tries < maxTries && !isCorrect) {
|
|
930
|
+
try {
|
|
931
|
+
const promptMsg = tries === 0
|
|
932
|
+
? `Are you sure you want to unlink your ${providerId} account? If so, enter your local Master Secret (Passphrase) to authorize. Otherwise, click Cancel.`
|
|
933
|
+
: `Incorrect passphrase. Please try again (${maxTries - tries} tries remaining):`;
|
|
934
|
+
masterSecret = await promptForSecret({
|
|
935
|
+
msg: promptMsg,
|
|
936
|
+
confirm: false
|
|
937
|
+
});
|
|
938
|
+
} catch (error) {
|
|
939
|
+
masterSecret = undefined;
|
|
940
|
+
break;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if (!masterSecret) {
|
|
944
|
+
break;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// Verify secret using verifySigningSecret (requires manage poolId)
|
|
948
|
+
isCorrect = await keystoneService.verifySigningSecret({
|
|
949
|
+
keystoneIbGib: this.ibGib!,
|
|
950
|
+
signingSecret: masterSecret,
|
|
951
|
+
poolId: 'manage'
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
if (!isCorrect) {
|
|
955
|
+
tries++;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (!isCorrect) {
|
|
960
|
+
this.setSSOStatus("Unlink cancelled or failed (incorrect passphrase).", "error");
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
this.setSSOStatus("Evolving identity locally and removing custodian pool...", "info");
|
|
965
|
+
|
|
966
|
+
const metaspace = getIbGibGlobalThis_SpaceGib(APP_CONFIG).metaspace;
|
|
967
|
+
if (!metaspace) throw new Error("Metaspace not found");
|
|
968
|
+
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
969
|
+
if (!space) { throw new Error("Local space not found"); }
|
|
970
|
+
|
|
971
|
+
// 2. Evolve parent keystone to remove the custodian pool
|
|
972
|
+
const targetPoolId = `custodian-manage-${providerId}`;
|
|
973
|
+
const evolvedParent = await keystoneService.removePools({
|
|
974
|
+
latestKeystone: this.ibGib!,
|
|
975
|
+
masterSecret: masterSecret!,
|
|
976
|
+
poolIds: [targetPoolId],
|
|
977
|
+
metaspace,
|
|
978
|
+
space
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
// 3. Sync evolved parent keystone back to server
|
|
982
|
+
this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
|
|
983
|
+
const syncRes = await spaceGibApiBridge.putEvolveKeystone(
|
|
984
|
+
getIbGibAddr({ ibGib: this.ibGib! }),
|
|
985
|
+
evolvedParent,
|
|
986
|
+
[] // No outward delegate timelines
|
|
987
|
+
);
|
|
988
|
+
if (!syncRes.success) {
|
|
989
|
+
throw new Error(`Server synchronization rejected: ${syncRes.message}`);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// 4. Update index locally
|
|
993
|
+
await updateSpecialIndex({
|
|
994
|
+
type: "keystones",
|
|
995
|
+
rel8nInfos: [
|
|
996
|
+
{
|
|
997
|
+
rel8nName: "keystone",
|
|
998
|
+
ibGibs: [toDto({ ibGib: evolvedParent })]
|
|
999
|
+
}
|
|
1000
|
+
],
|
|
1001
|
+
metaspace,
|
|
1002
|
+
space
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
this.setSSOStatus(`Successfully unlinked ${providerId}!`, "success");
|
|
1006
|
+
await this.loadIbGib({ getLatest: false });
|
|
1007
|
+
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
console.error(`${lc} SSO unlink failed: ${extractErrorMsg(error)}`);
|
|
1010
|
+
this.setSSOStatus(`Unlink failed: ${extractErrorMsg(error)}`, "error");
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
821
1013
|
}
|
|
@@ -15,6 +15,8 @@ import { getIbGibAddr } from '@ibgib/ts-gib/dist/helper.mjs';
|
|
|
15
15
|
import { IbGib_V1 } from '@ibgib/ts-gib/dist/V1/types.mjs';
|
|
16
16
|
import { validateAndRegisterEvolveKeystone } from '@ibgib/core-gib/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-receiver/sync-websocket-peer-helpers.mjs';
|
|
17
17
|
import { KeystoneIbGib_V1 } from '@ibgib/core-gib/dist/keystone/keystone-types.mjs';
|
|
18
|
+
import { loadSsoServerConfig } from '@ibgib/web-gib/dist/identity/sso/sso-config-helper.mjs';
|
|
19
|
+
import { SsoCustodianService } from '@ibgib/web-gib/dist/identity/sso/sso-custodian-service.mjs';
|
|
18
20
|
|
|
19
21
|
import { GLOBAL_LOG_A_LOT } from '../../../constants.mjs';
|
|
20
22
|
import { ServeGibHandlerWithMetaspaceBase } from '../../handler-base.mjs';
|
|
@@ -30,6 +32,7 @@ interface EvolveParams extends ParamsWithDomain {
|
|
|
30
32
|
interface EvolveBody {
|
|
31
33
|
keystoneIbGib: KeystoneIbGib_V1;
|
|
32
34
|
relatedIbGibs?: IbGib_V1[];
|
|
35
|
+
revokeTokens?: Record<string, string>;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
export class KeystoneEvolveHandler extends ServeGibHandlerWithMetaspaceBase<EvolveParams, EvolveBody> {
|
|
@@ -94,6 +97,36 @@ export class KeystoneEvolveHandler extends ServeGibHandlerWithMetaspaceBase<Evol
|
|
|
94
97
|
space
|
|
95
98
|
});
|
|
96
99
|
|
|
100
|
+
// Post-Evolution Hook: Handle SSO provider unlinking
|
|
101
|
+
const claim = keystoneIbGib.data?.proofs?.[0]?.claim;
|
|
102
|
+
if (claim?.details?.type === 'remove-pool') {
|
|
103
|
+
const removeInfo = claim.details.info;
|
|
104
|
+
if (removeInfo && Array.isArray(removeInfo.remove)) {
|
|
105
|
+
// Lazy-load custodian service config
|
|
106
|
+
const config = loadSsoServerConfig();
|
|
107
|
+
const custodian = new SsoCustodianService(config);
|
|
108
|
+
|
|
109
|
+
for (const poolId of removeInfo.remove) {
|
|
110
|
+
if (poolId.startsWith('custodian-manage-')) {
|
|
111
|
+
const providerId = poolId.replace('custodian-manage-', '');
|
|
112
|
+
console.log(`${lc} Evolved keystone removed custodian pool: ${poolId}. Performing unlink cleanup for provider: ${providerId}`);
|
|
113
|
+
|
|
114
|
+
// Revoke token if supplied by the client
|
|
115
|
+
const token = parsed.revokeTokens?.[providerId];
|
|
116
|
+
if (token) {
|
|
117
|
+
try {
|
|
118
|
+
await custodian.revokeToken(providerId as any, token);
|
|
119
|
+
} catch (e) {
|
|
120
|
+
console.warn(`${lc} Failed to revoke token for provider ${providerId}: ${extractErrorMsg(e)}`);
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
console.log(`${lc} No OAuth token supplied by client for revocation of provider: ${providerId}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
97
130
|
return this.ok({
|
|
98
131
|
message: `Successfully evolved domain keystone with session keystone`,
|
|
99
132
|
});
|
|
@@ -3,6 +3,8 @@ import { getIbGibAddr } from '@ibgib/ts-gib/dist/helper.mjs';
|
|
|
3
3
|
import { loadSsoServerConfig } from '@ibgib/web-gib/dist/identity/sso/sso-config-helper.mjs';
|
|
4
4
|
import { SsoCustodianService } from '@ibgib/web-gib/dist/identity/sso/sso-custodian-service.mjs';
|
|
5
5
|
import { SsoProviderId } from '@ibgib/web-gib/dist/identity/sso/sso-types.mjs';
|
|
6
|
+
import { isSsoProviderLinked } from '@ibgib/web-gib/dist/identity/sso/sso-helpers.mjs';
|
|
7
|
+
import { KeystoneService_V1 } from '@ibgib/core-gib/dist/keystone/keystone-service-v1.mjs';
|
|
6
8
|
|
|
7
9
|
import { GLOBAL_LOG_A_LOT } from '../../../constants.mjs';
|
|
8
10
|
import { ServeGibHandlerWithMetaspaceBase } from '../../handler-base.mjs';
|
|
@@ -91,15 +93,34 @@ export class SsoLinkHandler extends ServeGibHandlerWithMetaspaceBase<LinkParams,
|
|
|
91
93
|
return this.error(500, 'No local user space found in domain metaspace');
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
// 0. Fetch target keystone and verify if already linked
|
|
97
|
+
const keystoneService = new KeystoneService_V1();
|
|
98
|
+
const targetKeystone = await keystoneService.getLatestKeystone({
|
|
99
|
+
addr: parentTjpAddr,
|
|
100
|
+
metaspace: reqCtx.metaspace,
|
|
101
|
+
space
|
|
102
|
+
});
|
|
103
|
+
if (!targetKeystone) {
|
|
104
|
+
return this.error(400, `Keystone not found for address: ${parentTjpAddr}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (isSsoProviderLinked({ keystone: targetKeystone, providerId })) {
|
|
108
|
+
return this.error(400, `SSO provider "${providerId}" is already linked to this keystone.`);
|
|
109
|
+
}
|
|
110
|
+
|
|
94
111
|
// 1. Authenticate with provider and retrieve user info
|
|
95
112
|
const service = getCustodian();
|
|
96
113
|
const userInfo = await service.getOAuthUserInfo(providerId, code, redirectUri);
|
|
97
114
|
|
|
98
115
|
// 2. Generate the custodian foreign challenge pool
|
|
99
|
-
const custodianPool = await service.createCustodianChallengePool(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
116
|
+
const custodianPool = await service.createCustodianChallengePool({
|
|
117
|
+
providerId,
|
|
118
|
+
providerKey: userInfo.providerKey,
|
|
119
|
+
userNonce,
|
|
120
|
+
keystoneAddr: parentTjpAddr,
|
|
121
|
+
metaspace: reqCtx.metaspace,
|
|
122
|
+
space
|
|
123
|
+
});
|
|
103
124
|
|
|
104
125
|
console.log(`${lc} created custodian pool for: ${userInfo.providerKey}`);
|
|
105
126
|
|