@ibgib/web-gib 0.0.57 → 0.0.59

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 (46) 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/ibgib-api-bridge.d.mts +63 -1
  4. package/dist/api/ibgib-api-bridge.d.mts.map +1 -1
  5. package/dist/api/ibgib-api-bridge.mjs +135 -0
  6. package/dist/api/ibgib-api-bridge.mjs.map +1 -1
  7. package/dist/identity/custodian-delegate-helper.d.mts +22 -0
  8. package/dist/identity/custodian-delegate-helper.d.mts.map +1 -0
  9. package/dist/identity/custodian-delegate-helper.mjs +50 -0
  10. package/dist/identity/custodian-delegate-helper.mjs.map +1 -0
  11. package/dist/identity/sso-popup-helper.d.mts +33 -0
  12. package/dist/identity/sso-popup-helper.d.mts.map +1 -0
  13. package/dist/identity/sso-popup-helper.mjs +124 -0
  14. package/dist/identity/sso-popup-helper.mjs.map +1 -0
  15. package/dist/index.d.mts +2 -0
  16. package/dist/index.d.mts.map +1 -1
  17. package/dist/index.mjs +3 -0
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/ui/component/identity/add-device/add-device.d.mts.map +1 -1
  20. package/dist/ui/component/identity/add-device/add-device.mjs +5 -1
  21. package/dist/ui/component/identity/add-device/add-device.mjs.map +1 -1
  22. package/dist/ui/component/identity/identity-header/identity-header.d.mts.map +1 -1
  23. package/dist/ui/component/identity/identity-header/identity-header.mjs +2 -1
  24. package/dist/ui/component/identity/identity-header/identity-header.mjs.map +1 -1
  25. package/dist/ui/component/identity/keystone-creator/keystone-creator.css +109 -23
  26. package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts +22 -0
  27. package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts.map +1 -1
  28. package/dist/ui/component/identity/keystone-creator/keystone-creator.html +122 -60
  29. package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs +445 -73
  30. package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs.map +1 -1
  31. package/dist/ui/component/identity/keystone-details/keystone-details.d.mts.map +1 -1
  32. package/dist/ui/component/identity/keystone-details/keystone-details.mjs +10 -53
  33. package/dist/ui/component/identity/keystone-details/keystone-details.mjs.map +1 -1
  34. package/package.json +4 -4
  35. package/src/AUTO-GENERATED-version.mts +1 -1
  36. package/src/api/ibgib-api-bridge.mts +195 -1
  37. package/src/identity/custodian-delegate-helper.mts +61 -0
  38. package/src/identity/sso-popup-helper.mts +155 -0
  39. package/src/index.mts +3 -0
  40. package/src/ui/component/identity/add-device/add-device.mts +5 -1
  41. package/src/ui/component/identity/identity-header/identity-header.mts +2 -1
  42. package/src/ui/component/identity/keystone-creator/keystone-creator.css +109 -23
  43. package/src/ui/component/identity/keystone-creator/keystone-creator.html +122 -60
  44. package/src/ui/component/identity/keystone-creator/keystone-creator.mts +507 -77
  45. package/src/ui/component/identity/keystone-details/keystone-details.mts +10 -62
  46. package/tools/auto-generated-agent-skills/skills/ibgib-implementation-plan/SKILL.md +3 -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
 
@@ -279,7 +280,7 @@ export class IbGibApiBridge {
279
280
  primaryTjpAddr
280
281
  }: {
281
282
  email: string;
282
- primaryTjpAddr: string;
283
+ primaryTjpAddr?: string;
283
284
  }): Promise<{ success: boolean; message?: string }> {
284
285
  const lc = `${this.lc}[${this.postVerificationCode.name}]`;
285
286
  try {
@@ -360,6 +361,199 @@ export class IbGibApiBridge {
360
361
  return { success: false, message: errorMsg };
361
362
  }
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
+ }
363
557
  }
364
558
 
365
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
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * @module identity/sso-popup-helper
3
+ *
4
+ * DRY client helper for initiating OAuth2 SSO authentication via a popup window
5
+ * and capturing the returned authorization code.
6
+ */
7
+
8
+ import { extractErrorMsg, getUUID } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
9
+ import { getApiBridge } from "../api/ibgib-api-bridge.mjs";
10
+
11
+ export interface SsoPopupOpts {
12
+ providerId: 'google' | 'github';
13
+ parentTjpAddr?: string;
14
+ }
15
+
16
+ export interface SsoPopupResult {
17
+ success: boolean;
18
+ code?: string;
19
+ stateObj?: any;
20
+ redirectUri?: string;
21
+ message?: string;
22
+ }
23
+
24
+ /**
25
+ * Executes an OAuth2 SSO login/link authorization flow using a popup window.
26
+ *
27
+ * 1. Queries `GET /api/identity/sso/config` for safe public client configs.
28
+ * 2. Constructs the provider authorization URL with encoded state payload.
29
+ * 3. Opens a 600x700 popup window and listens for `sso-oauth-callback` postMessage.
30
+ * 4. Returns `{ success: true, code, stateObj, redirectUri }` upon authorization.
31
+ */
32
+ export async function executeSsoOAuthPopup({
33
+ providerId,
34
+ parentTjpAddr,
35
+ }: SsoPopupOpts): Promise<SsoPopupResult> {
36
+ const lc = `[${executeSsoOAuthPopup.name}]`;
37
+ try {
38
+ // 1. Fetch public SSO configs from server
39
+ const apiBridge = getApiBridge();
40
+ const resConfig = await apiBridge.getSsoConfig();
41
+ if (!resConfig.success || !resConfig.providers) {
42
+ return { success: false, message: resConfig.message || 'SSO configuration retrieval failed.' };
43
+ }
44
+
45
+ const providers = resConfig.providers as any;
46
+ const providerConfig = Array.isArray(providers)
47
+ ? providers.find((p: any) => p.providerId === providerId)
48
+ : providers[providerId];
49
+
50
+ if (!providerConfig) {
51
+ return {
52
+ success: false,
53
+ message: `SSO provider '${providerId}' is not configured on this server environment.`
54
+ };
55
+ }
56
+
57
+ // 2. Generate high-entropy user nonce & state
58
+ /**
59
+ * {@link getUUID} uses {@link globalThis.crypto.getRandomValues}
60
+ */
61
+ const userNonce = await getUUID();
62
+ const stateObj = {
63
+ providerId,
64
+ parentTjpAddr,
65
+ userNonce
66
+ };
67
+ const state = btoa(JSON.stringify(stateObj));
68
+
69
+ // 3. Construct Auth URL if not pre-constructed
70
+ let authUrl = providerConfig.authUrl;
71
+ if (!authUrl.includes('client_id=')) {
72
+ authUrl = `${authUrl}?client_id=${providerConfig.clientId}&redirect_uri=${encodeURIComponent(providerConfig.redirectUri)}&response_type=code&scope=${encodeURIComponent(providerConfig.scope)}&state=${encodeURIComponent(state)}`;
73
+ } else if (!authUrl.includes('state=')) {
74
+ authUrl = `${authUrl}&state=${encodeURIComponent(state)}`;
75
+ }
76
+
77
+ // 4. Open popup window
78
+ const popup = window.open(
79
+ authUrl,
80
+ 'sso-oauth-popup',
81
+ 'width=600,height=700,status=yes,toolbar=no,menubar=no,location=yes'
82
+ );
83
+
84
+ if (!popup) {
85
+ return { success: false, message: 'Popup blocked by browser. Please enable popups for this site.' };
86
+ }
87
+
88
+ // 5. Promise wrapper listening for postMessage or window closure
89
+ return await new Promise<SsoPopupResult>((resolve) => {
90
+ let checkClosedInterval: any;
91
+
92
+ const messageListener = (event: MessageEvent) => {
93
+ if (event.origin !== window.location.origin) return;
94
+ const callbackData = event.data;
95
+ if (callbackData && callbackData.type === 'sso-oauth-callback') {
96
+ window.removeEventListener('message', messageListener);
97
+ clearInterval(checkClosedInterval);
98
+ resolve({
99
+ success: true,
100
+ code: callbackData.code,
101
+ stateObj: callbackData.stateObj || stateObj,
102
+ redirectUri: providerConfig.redirectUri
103
+ });
104
+ }
105
+ };
106
+
107
+ window.addEventListener('message', messageListener);
108
+
109
+ checkClosedInterval = setInterval(() => {
110
+ if (popup.closed) {
111
+ clearInterval(checkClosedInterval);
112
+ window.removeEventListener('message', messageListener);
113
+ resolve({ success: false, message: 'Authentication cancelled (window closed).' });
114
+ }
115
+ }, 500);
116
+ });
117
+
118
+ } catch (error) {
119
+ const emsg = extractErrorMsg(error);
120
+ console.error(`${lc} ${emsg}`);
121
+ return { success: false, message: emsg };
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Checks if the current window was opened as an OAuth popup (`window.opener` exists)
127
+ * and has received an authorization code in the URL query string (`?code=...`).
128
+ * If so, posts `sso-oauth-callback` to the parent window and closes itself.
129
+ */
130
+ export function checkAndNotifyPopupOpener(): boolean {
131
+ if (typeof window === 'undefined' || !window.opener) {
132
+ return false;
133
+ }
134
+
135
+ const url = new URL(window.location.href);
136
+ const code = url.searchParams.get('code');
137
+ if (!code) {
138
+ return false;
139
+ }
140
+
141
+ const rawState = url.searchParams.get('state');
142
+ let stateObj: any;
143
+ if (rawState) {
144
+ try { stateObj = JSON.parse(atob(rawState)); } catch { }
145
+ }
146
+
147
+ window.opener.postMessage({
148
+ type: 'sso-oauth-callback',
149
+ code,
150
+ stateObj
151
+ }, window.location.origin);
152
+
153
+ window.close();
154
+ return true;
155
+ }
package/src/index.mts CHANGED
@@ -18,6 +18,9 @@ export * from "./api/ibgib-api-bridge.mjs";
18
18
 
19
19
  // Identity & Sync services
20
20
  export * from "./identity/keystone-sync-service.mjs";
21
+ export * from "./identity/custodian-delegate-helper.mjs";
22
+ // export * from "./identity/sso-callback-helper.mjs";
23
+ export * from "./identity/sso-popup-helper.mjs";
21
24
 
22
25
  // Identity UI components
23
26
  export * from "./ui/component/identity/index.mjs";
@@ -277,7 +277,11 @@ export class AddDeviceComponentInstance
277
277
  const primaryTjpAddr = getTjpAddr({ ibGib: primaryKeystone, defaultIfNone: 'incomingAddr' });
278
278
  if (!primaryTjpAddr) { throw new Error(`(UNEXPECTED) primaryTjpAddr falsy? couldn't get tjpAddr from primaryKeystone? (E: bc8cc8678e1ce4194ab02646160fde26)`); }
279
279
  const delegateSecret = await deriveDelegateSecret({ masterSecret });
280
- const delegateBuilder = KeystoneProfileBuilder.buildKeystone('sync')
280
+ const syncProfileName = process.env.KEYSTONE_PROFILE_SOVEREIGN_SYNC;
281
+ if (!syncProfileName || !syncProfileName.trim()) {
282
+ throw new Error('KEYSTONE_PROFILE_SOVEREIGN_SYNC environment variable is missing. Expected valid profile name like "sync-profile.dev" or "sync-profile.prod". (E: 8f9e0a1b2c3d4e5f67890123456789ab)');
283
+ }
284
+ const delegateBuilder = KeystoneProfileBuilder.buildKeystone(syncProfileName.trim() as any)
281
285
  .withUsername(`${primaryUsername}-sync-${deviceTag}`)
282
286
  .withDescription(`Sync Delegate for ${primaryUsername} on ${deviceTag}`)
283
287
  .withDetails({
@@ -146,7 +146,8 @@ export class IdentityHeaderComponentInstance
146
146
  };
147
147
  this.keystoneCache.set(addr, details);
148
148
  return details;
149
- } catch {
149
+ } catch (error) {
150
+ console.error(`${this.lc}[getKeystoneDetails] Registered keystone address ${addr} not found in local browser space: ${extractErrorMsg(error)} (E: a123456789abcdef0123456789c0002)`);
150
151
  return { username: '', description: '', isPrimary: false };
151
152
  }
152
153
  }
@@ -10,17 +10,105 @@ h2 {
10
10
  color: var(--clr-accent, #50fa7b);
11
11
  }
12
12
 
13
+ .mode-tabs {
14
+ display: flex;
15
+ gap: 0.5rem;
16
+ background: rgba(0, 0, 0, 0.25);
17
+ padding: 0.25rem;
18
+ border-radius: 10px;
19
+ border: 1px solid var(--clr-border, rgba(255, 255, 255, 0.1));
20
+ margin-bottom: 1rem;
21
+ }
22
+
23
+ .mode-tab {
24
+ flex: 1;
25
+ padding: 0.6rem 0.75rem;
26
+ border: none;
27
+ border-radius: 8px;
28
+ background: transparent;
29
+ color: var(--clr-text-secondary, #a6adc8);
30
+ font-weight: 600;
31
+ font-size: 0.9rem;
32
+ cursor: pointer;
33
+ transition: all 0.2s ease;
34
+ }
35
+
36
+ .mode-tab:hover {
37
+ color: var(--clr-text, #cdd6f4);
38
+ background: rgba(255, 255, 255, 0.05);
39
+ }
40
+
41
+ .mode-tab.active {
42
+ background: #45475a;
43
+ color: #89b4fa;
44
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
45
+ }
46
+
13
47
  .subtitle {
14
48
  color: var(--clr-text-secondary, #a6adc8);
15
- font-size: 0.95rem;
49
+ font-size: 0.92rem;
16
50
  margin: 0;
51
+ line-height: 1.45;
17
52
  }
18
53
 
19
54
  .content {
20
- margin-top: 1.5rem;
55
+ margin-top: 1.25rem;
56
+ display: flex;
57
+ flex-direction: column;
58
+ gap: 1.25rem;
59
+ }
60
+
61
+ .tab-panel.hidden, .hidden {
62
+ display: none !important;
63
+ }
64
+
65
+ .sso-buttons {
21
66
  display: flex;
22
67
  flex-direction: column;
23
- gap: 1.5rem;
68
+ gap: 0.75rem;
69
+ }
70
+
71
+ .sso-btn {
72
+ display: flex;
73
+ align-items: center;
74
+ justify-content: center;
75
+ gap: 0.75rem;
76
+ padding: 0.8rem 1.25rem;
77
+ border: 1px solid rgba(255, 255, 255, 0.15);
78
+ border-radius: 8px;
79
+ background: #1e1e2e;
80
+ color: #cdd6f4;
81
+ font-size: 0.95rem;
82
+ font-weight: 600;
83
+ cursor: pointer;
84
+ transition: transform 0.15s ease, background 0.15s ease;
85
+ }
86
+
87
+ .sso-btn:hover {
88
+ transform: translateY(-1px);
89
+ background: #313244;
90
+ border-color: #89b4fa;
91
+ }
92
+
93
+ .divider {
94
+ display: flex;
95
+ align-items: center;
96
+ text-align: center;
97
+ margin: 0.5rem 0;
98
+ color: #a6adc8;
99
+ font-size: 0.75rem;
100
+ font-weight: 700;
101
+ letter-spacing: 1px;
102
+ }
103
+
104
+ .divider::before, .divider::after {
105
+ content: '';
106
+ flex: 1;
107
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
108
+ }
109
+
110
+ .divider span {
111
+ padding: 0 0.75rem;
24
112
  }
25
113
 
26
114
  .input-group {
@@ -34,8 +122,25 @@ h2 {
34
122
  color: var(--clr-text-secondary, #a6adc8);
35
123
  }
36
124
 
125
+ .text-input {
126
+ width: 100%;
127
+ padding: 0.75rem 1rem;
128
+ border-radius: 8px;
129
+ border: 1px solid rgba(255, 255, 255, 0.15);
130
+ background: #11111b;
131
+ color: #cdd6f4;
132
+ font-size: 0.95rem;
133
+ box-sizing: border-box;
134
+ }
135
+
136
+ .text-input:focus {
137
+ outline: none;
138
+ border-color: #89b4fa;
139
+ box-shadow: 0 0 0 2px rgba(137, 180, 250, 0.2);
140
+ }
141
+
37
142
  .action-row {
38
- margin-top: 1rem;
143
+ margin-top: 0.5rem;
39
144
  width: 100%;
40
145
  }
41
146
 
@@ -65,10 +170,6 @@ h2 {
65
170
  margin-top: 0.5rem;
66
171
  }
67
172
 
68
- .status-area.hidden {
69
- display: none;
70
- }
71
-
72
173
  .result-area {
73
174
  padding: 1.5rem;
74
175
  background: rgba(255, 255, 255, 0.03);
@@ -77,7 +178,6 @@ h2 {
77
178
  display: flex;
78
179
  flex-direction: column;
79
180
  gap: 0.75rem;
80
- transition: opacity var(--t-med, 0.2s ease);
81
181
  }
82
182
 
83
183
  .result-area.collapsed {
@@ -99,18 +199,4 @@ h2 {
99
199
  input::placeholder {
100
200
  color: rgba(255, 255, 255, 0.35);
101
201
  opacity: 1;
102
- }
103
-
104
- .section-code-verification {
105
- margin-top: 1.5rem;
106
- padding: 1.25rem;
107
- border: 2px dashed #f59e0b;
108
- background: rgba(245, 158, 11, 0.05);
109
- box-shadow: 0 0 15px rgba(245, 158, 11, 0.2);
110
- border-radius: 12px;
111
- transition: all 0.3s ease;
112
- }
113
-
114
- .section-code-verification.hidden {
115
- display: none;
116
202
  }