@ibgib/web-gib 0.0.48 → 0.0.50

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 (37) hide show
  1. package/dist/AUTO-GENERATED-version.d.mts +1 -1
  2. package/dist/AUTO-GENERATED-version.mjs +1 -1
  3. package/dist/common/settings/settings-constants.d.mts +8 -1
  4. package/dist/common/settings/settings-constants.d.mts.map +1 -1
  5. package/dist/common/settings/settings-constants.mjs +7 -0
  6. package/dist/common/settings/settings-constants.mjs.map +1 -1
  7. package/dist/common/settings/settings-helpers.d.mts.map +1 -1
  8. package/dist/common/settings/settings-helpers.mjs +3 -1
  9. package/dist/common/settings/settings-helpers.mjs.map +1 -1
  10. package/dist/common/settings/settings-types.d.mts +5 -2
  11. package/dist/common/settings/settings-types.d.mts.map +1 -1
  12. package/dist/identity/sso/sso-config-helper.d.mts +7 -0
  13. package/dist/identity/sso/sso-config-helper.d.mts.map +1 -0
  14. package/dist/identity/sso/sso-config-helper.mjs +42 -0
  15. package/dist/identity/sso/sso-config-helper.mjs.map +1 -0
  16. package/dist/identity/sso/sso-custodian-service.d.mts +125 -0
  17. package/dist/identity/sso/sso-custodian-service.d.mts.map +1 -0
  18. package/dist/identity/sso/sso-custodian-service.mjs +373 -0
  19. package/dist/identity/sso/sso-custodian-service.mjs.map +1 -0
  20. package/dist/identity/sso/sso-custodian-service.respec.d.mts +2 -0
  21. package/dist/identity/sso/sso-custodian-service.respec.d.mts.map +1 -0
  22. package/dist/identity/sso/sso-custodian-service.respec.mjs +167 -0
  23. package/dist/identity/sso/sso-custodian-service.respec.mjs.map +1 -0
  24. package/dist/identity/sso/sso-types.d.mts +40 -0
  25. package/dist/identity/sso/sso-types.d.mts.map +1 -0
  26. package/dist/identity/sso/sso-types.mjs +7 -0
  27. package/dist/identity/sso/sso-types.mjs.map +1 -0
  28. package/package.json +4 -3
  29. package/src/AUTO-GENERATED-version.mts +1 -1
  30. package/src/common/settings/settings-constants.mts +9 -0
  31. package/src/common/settings/settings-helpers.mts +4 -1
  32. package/src/common/settings/settings-types.mts +7 -2
  33. package/src/identity/sso/sso-config-helper.mts +52 -0
  34. package/src/identity/sso/sso-custodian-service.mts +453 -0
  35. package/src/identity/sso/sso-custodian-service.respec.mts +206 -0
  36. package/src/identity/sso/sso-types.mts +44 -0
  37. package/src/ui/component/README.md +53 -1
@@ -0,0 +1,206 @@
1
+ import { generateKeyPairSync, createSign } from 'node:crypto';
2
+ import {
3
+ respecfully,
4
+ ifWeMight,
5
+ iReckon,
6
+ firstOfAll,
7
+ lastOfAll
8
+ } from '@ibgib/helper-gib/dist/respec-gib/respec-gib.mjs';
9
+ import { SsoCustodianService } from './sso-custodian-service.mjs';
10
+ import { SsoServerConfig } from './sso-types.mjs';
11
+ import { POOL_ID_CUSTODIAN_MANAGE } from '@ibgib/core-gib/dist/keystone/keystone-constants.mjs';
12
+
13
+ const maam = `[${import.meta.url}]`, sir = maam;
14
+
15
+ await respecfully(sir, 'SsoCustodianService', async () => {
16
+ let mockPrivateKey: any;
17
+ let mockPublicKeyJwk: any;
18
+ let originalFetch: any;
19
+
20
+ firstOfAll(sir, async () => {
21
+ // Generate mock RSA key pair
22
+ const { privateKey, publicKey } = generateKeyPairSync('rsa', {
23
+ modulusLength: 2048
24
+ });
25
+ mockPrivateKey = privateKey;
26
+
27
+ // Export public key as JWK
28
+ mockPublicKeyJwk = publicKey.export({ format: 'jwk' });
29
+ mockPublicKeyJwk.kid = 'mock-kid-123';
30
+ mockPublicKeyJwk.alg = 'RS256';
31
+ mockPublicKeyJwk.use = 'sig';
32
+
33
+ // Mock global fetch to return JWKS containing the mock public key
34
+ originalFetch = (globalThis as any).fetch;
35
+ (globalThis as any).fetch = async (url: string, options?: any) => {
36
+ if (url === 'https://www.googleapis.com/oauth2/v3/certs') {
37
+ return {
38
+ ok: true,
39
+ status: 200,
40
+ json: async () => ({
41
+ keys: [mockPublicKeyJwk]
42
+ })
43
+ } as any;
44
+ }
45
+ return {
46
+ ok: false,
47
+ status: 404,
48
+ text: async () => 'Not Found'
49
+ } as any;
50
+ };
51
+ });
52
+
53
+ lastOfAll(sir, async () => {
54
+ // Restore global fetch
55
+ if (originalFetch) {
56
+ (globalThis as any).fetch = originalFetch;
57
+ }
58
+ });
59
+
60
+ await ifWeMight(sir, 'verify dynamic RSA RS256 JWT signature successfully', async () => {
61
+ const config: SsoServerConfig = {
62
+ ssoServerKdfSecret: 'mock-kdf-secret',
63
+ sessionSecret: 'mock-session-secret',
64
+ providers: {
65
+ google: {
66
+ providerId: 'google',
67
+ clientId: 'google-client-id-123',
68
+ clientSecret: 'google-client-secret-123',
69
+ tokenUrl: 'https://oauth2.googleapis.com/token',
70
+ userInfoUrl: 'https://openidconnect.googleapis.com/v1/userinfo',
71
+ jwksUrl: 'https://www.googleapis.com/oauth2/v3/certs'
72
+ }
73
+ }
74
+ };
75
+
76
+ const service = new SsoCustodianService(config);
77
+
78
+ // 1. Build JWT Header & Payload
79
+ const header = { alg: 'RS256', kid: 'mock-kid-123', typ: 'JWT' };
80
+ const payload = {
81
+ sub: 'user-sub-999',
82
+ email: 'user@example.com',
83
+ name: 'John Doe',
84
+ aud: 'google-client-id-123',
85
+ iss: 'https://accounts.google.com',
86
+ exp: Math.floor(Date.now() / 1000) + 3600 // expires in 1 hour
87
+ };
88
+
89
+ const headerB64 = Buffer.from(JSON.stringify(header)).toString('base64url');
90
+ const payloadB64 = Buffer.from(JSON.stringify(payload)).toString('base64url');
91
+ const data = `${headerB64}.${payloadB64}`;
92
+
93
+ // 2. Sign JWT using private key
94
+ const sign = createSign('RSA-SHA256');
95
+ sign.update(data);
96
+ const signatureB64 = sign.sign(mockPrivateKey, 'base64url');
97
+
98
+ const mockJwtToken = `${data}.${signatureB64}`;
99
+
100
+ // 3. Verify signature via service
101
+ const decodedPayload = await service.verifyJwt(mockJwtToken, config.providers.google!);
102
+
103
+ iReckon(sir, decodedPayload.sub).isGonnaBe('user-sub-999');
104
+ iReckon(sir, decodedPayload.email).isGonnaBe('user@example.com');
105
+ iReckon(sir, decodedPayload.name).isGonnaBe('John Doe');
106
+ });
107
+
108
+ await ifWeMight(sir, 'fail validation if JWT has invalid signature', async () => {
109
+ const config: SsoServerConfig = {
110
+ ssoServerKdfSecret: 'mock-kdf-secret',
111
+ sessionSecret: 'mock-session-secret',
112
+ providers: {
113
+ google: {
114
+ providerId: 'google',
115
+ clientId: 'google-client-id-123',
116
+ clientSecret: 'google-client-secret-123',
117
+ tokenUrl: 'https://oauth2.googleapis.com/token',
118
+ userInfoUrl: 'https://openidconnect.googleapis.com/v1/userinfo',
119
+ jwksUrl: 'https://www.googleapis.com/oauth2/v3/certs'
120
+ }
121
+ }
122
+ };
123
+
124
+ const service = new SsoCustodianService(config);
125
+
126
+ const header = { alg: 'RS256', kid: 'mock-kid-123', typ: 'JWT' };
127
+ const payload = {
128
+ sub: 'user-sub-999',
129
+ aud: 'google-client-id-123',
130
+ exp: Math.floor(Date.now() / 1000) + 3600
131
+ };
132
+
133
+ const headerB64 = Buffer.from(JSON.stringify(header)).toString('base64url');
134
+ const payloadB64 = Buffer.from(JSON.stringify(payload)).toString('base64url');
135
+ const data = `${headerB64}.${payloadB64}`;
136
+
137
+ // Create signature using a different/unregistered RSA key
138
+ const { privateKey: badPrivateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });
139
+ const sign = createSign('RSA-SHA256');
140
+ sign.update(data);
141
+ const badSignatureB64 = sign.sign(badPrivateKey, 'base64url');
142
+
143
+ const badJwtToken = `${data}.${badSignatureB64}`;
144
+
145
+ let verifyError: any;
146
+ try {
147
+ await service.verifyJwt(badJwtToken, config.providers.google!);
148
+ } catch (error: any) {
149
+ verifyError = error;
150
+ }
151
+
152
+ iReckon(sir, verifyError).not.isGonnaBeUndefined();
153
+ iReckon(sir, verifyError.message).includes('Cryptographic signature verification failed');
154
+ });
155
+
156
+ await ifWeMight(sir, 'derive server-delegate secret deterministically', async () => {
157
+ const config: SsoServerConfig = {
158
+ ssoServerKdfSecret: 'high-entropy-server-kdf-secret',
159
+ sessionSecret: 'session-secret-123',
160
+ providers: {}
161
+ };
162
+ const service = new SsoCustodianService(config);
163
+
164
+ const key = 'google:123456789';
165
+ const nonce1 = 'nonce-abc-123';
166
+ const nonce2 = 'nonce-def-456';
167
+
168
+ const secret1_a = await service.deriveServerDelegateSecret(key, nonce1);
169
+ const secret1_b = await service.deriveServerDelegateSecret(key, nonce1);
170
+ const secret2 = await service.deriveServerDelegateSecret(key, nonce2);
171
+
172
+ // Identical inputs must yield identical outputs
173
+ iReckon(sir, secret1_a).isGonnaBe(secret1_b);
174
+ // Different nonces must yield different outputs
175
+ iReckon(sir, secret1_a).not.isGonnaBe(secret2);
176
+ // Derived secrets must be string type with high entropy
177
+ iReckon(sir, typeof secret1_a).isGonnaBe('string');
178
+ iReckon(sir, secret1_a.length).isGonnaBe(128); // sha512 output in hex/base64 wrap
179
+ });
180
+
181
+ await ifWeMight(sir, 'create custodian challenge pool successfully', async () => {
182
+ const config: SsoServerConfig = {
183
+ ssoServerKdfSecret: 'high-entropy-server-kdf-secret',
184
+ sessionSecret: 'session-secret-123',
185
+ providers: {}
186
+ };
187
+ const service = new SsoCustodianService(config);
188
+
189
+ const key = 'google:123456789';
190
+ const nonce = 'nonce-abc-123';
191
+
192
+ const custodianPool = await service.createCustodianChallengePool(
193
+ key,
194
+ nonce
195
+ );
196
+
197
+ iReckon(sir, custodianPool).not.isGonnaBeUndefined();
198
+ iReckon(sir, custodianPool.id).willEqual(POOL_ID_CUSTODIAN_MANAGE);
199
+ iReckon(sir, custodianPool.isForeign).isGonnaBeTrue();
200
+ iReckon(sir, custodianPool.metadata?.providerKey).willEqual(key);
201
+
202
+ // Structural validation of challenges
203
+ const challengesCount = Object.keys(custodianPool.challenges).length;
204
+ iReckon(sir, challengesCount).willEqual(custodianPool.config.behavior.size);
205
+ });
206
+ });
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @module identity/sso/sso-types
3
+ *
4
+ * Core types and interfaces for extensible SSO (OAuth2) authentication.
5
+ */
6
+
7
+ export type SsoProviderId = 'google' | 'github';
8
+
9
+ export interface SsoProviderClientConfig {
10
+ providerId: SsoProviderId;
11
+ clientId: string;
12
+ authUrl: string;
13
+ redirectUri: string;
14
+ scope: string;
15
+ }
16
+
17
+ export interface SsoProviderServerConfig {
18
+ providerId: SsoProviderId;
19
+ clientId: string;
20
+ clientSecret: string;
21
+ tokenUrl: string;
22
+ userInfoUrl: string;
23
+ jwksUrl?: string;
24
+ }
25
+
26
+ export interface SsoServerConfig {
27
+ /** Server's private KDF secret used to derive server-delegate keystones. */
28
+ ssoServerKdfSecret: string;
29
+ /** Server's private session encryption secret. */
30
+ sessionSecret: string;
31
+ /** Map of active OAuth2 providers. */
32
+ providers: Partial<Record<SsoProviderId, SsoProviderServerConfig>>;
33
+ }
34
+
35
+ export interface OAuthUserInfo {
36
+ /** Unique identity key combining provider and sub ID (e.g., `google:107632...`). */
37
+ providerKey: string;
38
+ providerId: SsoProviderId;
39
+ /** Unique user ID within the provider. */
40
+ sub: string;
41
+ email?: string;
42
+ name?: string;
43
+ picture?: string;
44
+ }
@@ -192,10 +192,62 @@ export class MyFormInstance extends IbGibFormInstanceBase {
192
192
  // ... perform business logic / space evolution ...
193
193
  this.setStatus("Form successfully submitted!", "success");
194
194
  } catch (err) {
195
- this.setStatus(`Error occurred: ${err.message}`, "error");
195
+ // ...
196
196
  } finally {
197
197
  this.setLoading(false, '#btn-submit');
198
198
  }
199
199
  }
200
200
  }
201
201
  ```
202
+
203
+ ---
204
+
205
+ ## 🗂️ Parent / Child Tab Components
206
+
207
+ For complex views that act as shells containing other interactive component tabs (e.g. `ibgib-projects` or `ibgib-identity-manager`), the framework provides specialized parent classes:
208
+
209
+ * **`IbGibDynamicComponentInstanceBase_Parent<TIbGib, TElements, TChildInfo>`**:
210
+ Base class coordinating child injection, child components mapping, and activation of a specific backing child `ibgib` address.
211
+ * **`IbGibDynamicComponentInstanceBase_ParentOfTabs<TSettings, TIbGib, TElements, TChildInfo>`**:
212
+ Extends the Parent base class to integrate tab state persistence using `SettingsWithTabs` (e.g. saving which child tabs are open and which one is active).
213
+
214
+ ### Core Tab Integration Steps
215
+
216
+ 1. **Define Tab Info**: Define a tab interface that extends `ChildInfoBase<TComponent>`:
217
+ ```typescript
218
+ interface MyTabInfo extends ChildInfoBase<IbGibDynamicComponentInstance<any, any>> {
219
+ // Add custom metadata if needed (e.g. component cache, lens modes)
220
+ }
221
+ ```
222
+ 2. **Parent Class Configuration**: Extend `IbGibDynamicComponentInstanceBase_ParentOfTabs` and define `settingsType` and `getLoadedChildInfo()`:
223
+ ```typescript
224
+ export class MyParentComponent extends IbGibDynamicComponentInstanceBase_ParentOfTabs<
225
+ Settings_MyParent, IbGib_V1, MyElements, MyTabInfo
226
+ > {
227
+ protected get settingsType() { return SettingsType.myParent; }
228
+
229
+ protected async getLoadedChildInfo({ addr, ibGib }) {
230
+ // Check if tab info exists in this.childInfos, else instantiate
231
+ // the child component using componentSvc.getComponentInstance
232
+ }
233
+
234
+ protected async addChild({ addr, ibGib }) {
235
+ // Create tab button element, attach listener, append to DOM, and return it
236
+ }
237
+
238
+ protected async removeTabBtn({ tabInfo }) {
239
+ // Remove tab button element from DOM
240
+ }
241
+ }
242
+ ```
243
+ 3. **Dynamic Delegate Tab Navigation**:
244
+ To allow deep links or delegate views (e.g. clicking a delegate key to open its details in a temporary sub-tab), have the child component dispatch a bubbling Custom Event:
245
+ ```typescript
246
+ this.dispatchEvent(new CustomEvent('ibgib-view-details-request', {
247
+ detail: { addr: delegateAddr },
248
+ bubbles: true,
249
+ composed: true
250
+ }));
251
+ ```
252
+ The parent component listens to this event in `created()` and triggers `activateIbGib({ addr })` to dynamically open and activate the sub-tab.
253
+