@equinor/fusion-framework-module-msal 11.0.0 → 11.0.1

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 (60) hide show
  1. package/dist/esm/version.js +1 -1
  2. package/dist/tsconfig.tsbuildinfo +1 -1
  3. package/dist/types/version.d.ts +1 -1
  4. package/package.json +8 -5
  5. package/CHANGELOG.md +0 -1212
  6. package/docs/api-reference.md +0 -85
  7. package/docs/auth-code-flow.md +0 -86
  8. package/docs/migration-v2-to-v4.md +0 -115
  9. package/docs/testing.md +0 -191
  10. package/docs/troubleshooting.md +0 -17
  11. package/docs/version-management.md +0 -67
  12. package/src/MsalClient.interface.ts +0 -139
  13. package/src/MsalClient.ts +0 -326
  14. package/src/MsalConfigurator.ts +0 -486
  15. package/src/MsalProvider.interface.ts +0 -179
  16. package/src/MsalProvider.ts +0 -776
  17. package/src/MsalProxyProvider.interface.ts +0 -72
  18. package/src/__tests__/MsalConfigurator.test.ts +0 -222
  19. package/src/__tests__/MsalProvider.test.ts +0 -74
  20. package/src/__tests__/create-proxy-provider.test.ts +0 -77
  21. package/src/__tests__/mock/create-mock-user-from-token.test.ts +0 -46
  22. package/src/__tests__/mock/msal-mock.test.ts +0 -613
  23. package/src/__tests__/versioning/resolve-version.test.ts +0 -161
  24. package/src/create-client-log-callback.ts +0 -102
  25. package/src/create-proxy-provider.ts +0 -97
  26. package/src/index.ts +0 -48
  27. package/src/mock/MsalMockClient.ts +0 -618
  28. package/src/mock/MsalMockConfigurator.ts +0 -305
  29. package/src/mock/create-mock-token.ts +0 -92
  30. package/src/mock/create-mock-user-from-token.ts +0 -46
  31. package/src/mock/create-msal-mock-client.ts +0 -25
  32. package/src/mock/decode-jwt-segment.ts +0 -22
  33. package/src/mock/index.ts +0 -30
  34. package/src/mock/module.ts +0 -54
  35. package/src/module.ts +0 -142
  36. package/src/msal-config-schema.ts +0 -81
  37. package/src/static.ts +0 -38
  38. package/src/telemetry-config-schema.ts +0 -25
  39. package/src/types.ts +0 -16
  40. package/src/util/compare-origin.ts +0 -18
  41. package/src/util/normalize-uri.ts +0 -24
  42. package/src/util/redirect.ts +0 -19
  43. package/src/v2/IAuthClient.interface.ts +0 -114
  44. package/src/v2/Logger.ts +0 -204
  45. package/src/v2/MsalProvider.interface.ts +0 -102
  46. package/src/v2/create-proxy-client.ts +0 -195
  47. package/src/v2/create-proxy-provider.ts +0 -177
  48. package/src/v2/map-account-info.ts +0 -23
  49. package/src/v2/map-authentication-result.ts +0 -28
  50. package/src/v2/types.ts +0 -674
  51. package/src/v4/create-proxy-provider.ts +0 -75
  52. package/src/v4/index.ts +0 -13
  53. package/src/v4/types.ts +0 -727
  54. package/src/version.ts +0 -2
  55. package/src/versioning/VersionError.ts +0 -64
  56. package/src/versioning/index.ts +0 -29
  57. package/src/versioning/resolve-version.ts +0 -154
  58. package/src/versioning/types.ts +0 -60
  59. package/tsconfig.json +0 -18
  60. package/vitest.config.ts +0 -11
@@ -1,72 +0,0 @@
1
- import type { SemVer } from 'semver';
2
- import type { MsalModuleVersion } from './static';
3
-
4
- import type { IMsalProvider } from './MsalProvider.interface';
5
- import type { IMsalProvider as IMsalProvider_v2 } from './v2/MsalProvider.interface';
6
-
7
- /**
8
- * Type mapping between MSAL module versions and their corresponding provider interfaces.
9
- *
10
- * This mapping ensures type-safe creation of proxy providers for different MSAL versions.
11
- * Each version maps to its appropriate provider interface type.
12
- *
13
- * @internal
14
- */
15
- type ProxyProviderMap = {
16
- [MsalModuleVersion.V2]: IMsalProvider_v2;
17
- [MsalModuleVersion.V4]: IMsalProvider;
18
- [MsalModuleVersion.V5]: IMsalProvider;
19
- };
20
-
21
- /**
22
- * Interface for providers that can create version-compatible proxy providers.
23
- *
24
- * This interface enables backward compatibility by allowing providers to create
25
- * proxies that adapt their API to match different MSAL version signatures. The proxy
26
- * wraps the v4 implementation and exposes it through older version interfaces.
27
- *
28
- * @remarks
29
- * This interface should ideally be defined in the @equinor/fusion-framework-module package
30
- * for broader framework compatibility.
31
- *
32
- * @property version - The semantic version of the provider
33
- * @property msalVersion - The MSAL module version enum value
34
- * @property createProxyProvider - Method to create a version-specific proxy provider
35
- *
36
- * @example
37
- * ```typescript
38
- * const provider: IMsalProvider = new MsalProvider(config);
39
- *
40
- * // Create a v2-compatible proxy
41
- * const v2Proxy = provider.createProxyProvider('2.0.0');
42
- * // v2Proxy now has v2-compatible method signatures
43
- * ```
44
- */
45
- export interface IProxyProvider {
46
- /**
47
- * The semantic version of the provider.
48
- *
49
- * This represents the actual version number of the MSAL implementation,
50
- * following semantic versioning (semver) standards.
51
- */
52
- readonly version: string | SemVer;
53
-
54
- /**
55
- * The MSAL module version enum value indicating the API compatibility level.
56
- *
57
- * This property specifies which MSAL version's API surface this provider implements,
58
- * allowing for version-specific behavior and proxy provider creation.
59
- */
60
- msalVersion: MsalModuleVersion;
61
-
62
- /**
63
- * Creates a proxy provider compatible with the specified MSAL version.
64
- *
65
- * The proxy adapts the provider's v4 API to match the requested version's interface,
66
- * enabling backward compatibility during migration scenarios.
67
- *
68
- * @param version - Target version key (V2, V4, or Latest)
69
- * @returns Proxy provider with version-specific type
70
- */
71
- createProxyProvider<T extends keyof ProxyProviderMap>(version: T): ProxyProviderMap[T];
72
- }
@@ -1,222 +0,0 @@
1
- import type { ConfigBuilderCallbackArgs } from '@equinor/fusion-framework-module';
2
- import { CacheLookupPolicy } from '@azure/msal-browser';
3
- import { describe, expect, it, vi } from 'vitest';
4
- import type { IMsalClient } from '../MsalClient.interface';
5
- import { type MsalConfig, MsalConfigurator } from '../MsalConfigurator';
6
-
7
- const createConfigCallbackArgs = (): ConfigBuilderCallbackArgs => ({
8
- config: {},
9
- hasModule: vi.fn().mockReturnValue(false),
10
- requireInstance: vi.fn(),
11
- });
12
-
13
- const createClient = (): IMsalClient => ({}) as IMsalClient;
14
-
15
- const createInitialConfig = (): Pick<MsalConfig, 'telemetry'> => ({
16
- telemetry: {
17
- metadata: {},
18
- scope: [],
19
- },
20
- });
21
-
22
- describe('MsalConfigurator', () => {
23
- it('enriches a copy, leaving the declared client configuration untouched', async () => {
24
- // A caller may reuse or assert on the object it passed, and the defaults
25
- // applied here are derived — rewriting it behind their back is not ours to do
26
- const declared = { auth: { clientId: 'my-app', tenantId: 'my-tenant' } };
27
- const configurator = new MsalConfigurator();
28
-
29
- configurator.setClientConfig(declared);
30
-
31
- const config = await configurator.createConfigAsync(
32
- createConfigCallbackArgs(),
33
- createInitialConfig(),
34
- );
35
-
36
- expect(declared).toEqual({ auth: { clientId: 'my-app', tenantId: 'my-tenant' } });
37
- expect(config.client?.tenantId).toBe('my-tenant');
38
- });
39
-
40
- it('setAuthCode should normalize surrounding whitespace', async () => {
41
- const configurator = new MsalConfigurator();
42
-
43
- configurator.setClient(createClient());
44
- configurator.setAuthCode(' auth-code ');
45
-
46
- const config = await configurator.createConfigAsync(
47
- createConfigCallbackArgs(),
48
- createInitialConfig(),
49
- );
50
-
51
- expect(config.authCode).toBe('auth-code');
52
- });
53
-
54
- it('setAuthCode should allow clearing with undefined', async () => {
55
- const configurator = new MsalConfigurator();
56
-
57
- configurator.setClient(createClient());
58
- configurator.setAuthCode('auth-code');
59
- configurator.setAuthCode(undefined);
60
-
61
- const config = await configurator.createConfigAsync(
62
- createConfigCallbackArgs(),
63
- createInitialConfig(),
64
- );
65
-
66
- expect(config.authCode).toBeUndefined();
67
- });
68
-
69
- it('setAuthCode should treat whitespace-only values as undefined', async () => {
70
- const configurator = new MsalConfigurator();
71
-
72
- configurator.setClient(createClient());
73
- configurator.setAuthCode(' ');
74
-
75
- const config = await configurator.createConfigAsync(
76
- createConfigCallbackArgs(),
77
- createInitialConfig(),
78
- );
79
-
80
- expect(config.authCode).toBeUndefined();
81
- });
82
-
83
- it('createConfigAsync should succeed when client is omitted', async () => {
84
- const configurator = new MsalConfigurator();
85
-
86
- const config = await configurator.createConfigAsync(
87
- createConfigCallbackArgs(),
88
- createInitialConfig(),
89
- );
90
-
91
- expect(config.client).toBeUndefined();
92
- });
93
-
94
- describe('_createClient', () => {
95
- it('builds from the same resolved client config the real client would get', async () => {
96
- // The mock relies on this: substituting the client must not also mean
97
- // re-implementing authority, cache and telemetry resolution
98
- const received: unknown[] = [];
99
- class CustomConfigurator extends MsalConfigurator {
100
- protected override async _createClient(config: MsalConfig): Promise<IMsalClient> {
101
- received.push(this._createClientConfig(config));
102
- return createClient();
103
- }
104
- }
105
-
106
- const configurator = new CustomConfigurator();
107
- configurator.setClientConfig({ auth: { clientId: 'client-id', tenantId: 'tenant-id' } });
108
-
109
- await configurator.createConfigAsync(createConfigCallbackArgs(), createInitialConfig());
110
-
111
- expect(received).toEqual([
112
- expect.objectContaining({
113
- auth: expect.objectContaining({
114
- clientId: 'client-id',
115
- // derived by the configurator, not by the caller
116
- authority: 'https://login.microsoftonline.com/tenant-id',
117
- }),
118
- cache: { cacheLocation: 'localStorage' },
119
- }),
120
- ]);
121
- });
122
-
123
- it('supplies the client when none was set', async () => {
124
- const client = createClient();
125
- class CustomConfigurator extends MsalConfigurator {
126
- protected override async _createClient(): Promise<IMsalClient> {
127
- return client;
128
- }
129
- }
130
-
131
- const config = await new CustomConfigurator().createConfigAsync(
132
- createConfigCallbackArgs(),
133
- createInitialConfig(),
134
- );
135
-
136
- expect(config.client).toBe(client);
137
- });
138
-
139
- it('is not consulted when a client was set, so setClient always wins', async () => {
140
- const own = createClient();
141
- const createOther = vi.fn().mockResolvedValue(createClient());
142
- class CustomConfigurator extends MsalConfigurator {
143
- protected override _createClient(): Promise<IMsalClient> {
144
- return createOther();
145
- }
146
- }
147
-
148
- const configurator = new CustomConfigurator();
149
- configurator.setClient(own);
150
-
151
- const config = await configurator.createConfigAsync(
152
- createConfigCallbackArgs(),
153
- createInitialConfig(),
154
- );
155
-
156
- expect(config.client).toBe(own);
157
- expect(createOther).not.toHaveBeenCalled();
158
- });
159
-
160
- it('is not consulted when hoisted, so a host provider is never shadowed', async () => {
161
- // A hoisted module authenticates through the host's provider, so anything
162
- // built here would be discarded — or worse, shadow the host's user
163
- const createOther = vi.fn().mockResolvedValue(createClient());
164
- class CustomConfigurator extends MsalConfigurator {
165
- protected override _createClient(): Promise<IMsalClient> {
166
- return createOther();
167
- }
168
- }
169
-
170
- const configurator = new CustomConfigurator();
171
- configurator.setClientConfig({ auth: { clientId: 'client-id', tenantId: 'tenant-id' } });
172
-
173
- const config = await configurator.createConfigAsync(
174
- { ...createConfigCallbackArgs(), ref: { auth: {} } },
175
- createInitialConfig(),
176
- );
177
-
178
- expect(config.client).toBeUndefined();
179
- expect(createOther).not.toHaveBeenCalled();
180
- });
181
- });
182
-
183
- describe('cacheLookupPolicy', () => {
184
- it('defaults to CacheLookupPolicy.AccessTokenAndRefreshToken', async () => {
185
- const configurator = new MsalConfigurator();
186
- configurator.setClient(createClient());
187
-
188
- const config = await configurator.createConfigAsync(
189
- createConfigCallbackArgs(),
190
- createInitialConfig(),
191
- );
192
-
193
- expect(config.cacheLookupPolicy).toBe(CacheLookupPolicy.AccessTokenAndRefreshToken);
194
- });
195
-
196
- it('setCacheLookupPolicy(undefined) clears the policy so MSAL default applies', async () => {
197
- const configurator = new MsalConfigurator();
198
- configurator.setClient(createClient());
199
- configurator.setCacheLookupPolicy(undefined);
200
-
201
- const config = await configurator.createConfigAsync(
202
- createConfigCallbackArgs(),
203
- createInitialConfig(),
204
- );
205
-
206
- expect(config.cacheLookupPolicy).toBeUndefined();
207
- });
208
-
209
- it('setCacheLookupPolicy overrides the default', async () => {
210
- const configurator = new MsalConfigurator();
211
- configurator.setClient(createClient());
212
- configurator.setCacheLookupPolicy(CacheLookupPolicy.Default);
213
-
214
- const config = await configurator.createConfigAsync(
215
- createConfigCallbackArgs(),
216
- createInitialConfig(),
217
- );
218
-
219
- expect(config.cacheLookupPolicy).toBe(CacheLookupPolicy.Default);
220
- });
221
- });
222
- });
@@ -1,74 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
- import type { IMsalClient } from '../MsalClient.interface';
3
- import type { MsalConfig } from '../MsalConfigurator';
4
- import { MsalProvider } from '../MsalProvider';
5
- import type { AuthenticationResult } from '../types';
6
-
7
- type MockMsalClient = {
8
- client: IMsalClient;
9
- acquireTokenByCode: ReturnType<typeof vi.fn>;
10
- initialize: ReturnType<typeof vi.fn>;
11
- };
12
-
13
- const createClient = (): MockMsalClient => {
14
- const initialize = vi.fn(async () => undefined);
15
- const acquireTokenByCode = vi.fn(async () => ({}) as AuthenticationResult);
16
-
17
- return {
18
- client: {
19
- clientId: 'test-client-id',
20
- initialize,
21
- acquireTokenByCode,
22
- setActiveAccount: vi.fn(),
23
- } as unknown as IMsalClient,
24
- acquireTokenByCode,
25
- initialize,
26
- };
27
- };
28
-
29
- const createConfig = (client: IMsalClient, authCode?: string): MsalConfig => ({
30
- client,
31
- version: '7.0.0',
32
- requiresAuth: false,
33
- authCode,
34
- telemetry: {
35
- metadata: { module: 'msal', version: '7.0.0' },
36
- scope: ['framework', 'authentication'],
37
- },
38
- });
39
-
40
- describe('MsalProvider.initialize', () => {
41
- it('should not attempt auth code exchange when auth code is undefined', async () => {
42
- const mockClient = createClient();
43
-
44
- const provider = new MsalProvider(createConfig(mockClient.client, undefined));
45
- await provider.initialize();
46
-
47
- expect(mockClient.initialize).toHaveBeenCalledTimes(1);
48
- expect(mockClient.acquireTokenByCode).not.toHaveBeenCalled();
49
- });
50
-
51
- it('should not attempt auth code exchange when auth code is whitespace-only', async () => {
52
- const mockClient = createClient();
53
-
54
- const provider = new MsalProvider(createConfig(mockClient.client, ' '));
55
- await provider.initialize();
56
-
57
- expect(mockClient.acquireTokenByCode).not.toHaveBeenCalled();
58
- });
59
-
60
- it('should exchange auth code once and clear it afterwards', async () => {
61
- const mockClient = createClient();
62
-
63
- const provider = new MsalProvider(createConfig(mockClient.client, 'auth-code'));
64
-
65
- await provider.initialize();
66
- await provider.initialize();
67
-
68
- expect(mockClient.acquireTokenByCode).toHaveBeenCalledTimes(1);
69
- expect(mockClient.acquireTokenByCode).toHaveBeenCalledWith({
70
- code: 'auth-code',
71
- scopes: ['test-client-id/.default'],
72
- });
73
- });
74
- });
@@ -1,77 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
- import type { IMsalClient } from '../MsalClient.interface';
3
- import type { MsalConfig } from '../MsalConfigurator';
4
- import { MsalProvider } from '../MsalProvider';
5
- import { MsalModuleVersion } from '../static';
6
- import type { AuthenticationResult } from '../types';
7
- import type { IMsalProvider as IMsalProvider_v2 } from '../v2/MsalProvider.interface';
8
-
9
- type MockMsalClient = {
10
- client: IMsalClient;
11
- acquireToken: ReturnType<typeof vi.fn>;
12
- handleRedirectPromise: ReturnType<typeof vi.fn>;
13
- };
14
-
15
- const createClient = (): MockMsalClient => {
16
- const acquireToken = vi.fn(async () => ({ accessToken: 'v4-token' }) as AuthenticationResult);
17
- const handleRedirectPromise = vi.fn(
18
- async () => ({ accessToken: 'redirect-token' }) as AuthenticationResult,
19
- );
20
-
21
- return {
22
- client: {
23
- clientId: 'test-client-id',
24
- initialize: vi.fn(async () => undefined),
25
- acquireToken,
26
- handleRedirectPromise,
27
- setActiveAccount: vi.fn(),
28
- getActiveAccount: vi.fn(() => null),
29
- } as unknown as IMsalClient,
30
- acquireToken,
31
- handleRedirectPromise,
32
- };
33
- };
34
-
35
- const createConfig = (client: IMsalClient): MsalConfig => ({
36
- client,
37
- version: '7.0.0',
38
- requiresAuth: false,
39
- telemetry: {
40
- metadata: { module: 'msal', version: '7.0.0' },
41
- scope: ['framework', 'authentication'],
42
- },
43
- });
44
-
45
- describe('MsalProvider.createProxyProvider (v2)', () => {
46
- it('tags the proxy as v2 while delegating through the same v4 provider', () => {
47
- const mockClient = createClient();
48
- const provider = new MsalProvider(createConfig(mockClient.client));
49
-
50
- const v2Provider = provider.createProxyProvider<IMsalProvider_v2>(MsalModuleVersion.V2);
51
-
52
- expect(v2Provider.msalVersion).toBe(MsalModuleVersion.V2);
53
- });
54
-
55
- it('adapts the legacy v2 acquireToken shape into a v4 request before delegating', async () => {
56
- const mockClient = createClient();
57
- const provider = new MsalProvider(createConfig(mockClient.client));
58
- const v2Provider = provider.createProxyProvider<IMsalProvider_v2>(MsalModuleVersion.V2);
59
-
60
- await expect(v2Provider.acquireToken({ scopes: ['User.Read'] })).resolves.toMatchObject({
61
- accessToken: 'v4-token',
62
- });
63
-
64
- expect(mockClient.acquireToken).toHaveBeenCalledWith(
65
- expect.objectContaining({ request: expect.objectContaining({ scopes: ['User.Read'] }) }),
66
- );
67
- });
68
-
69
- it('discards the host’s redirect result, honoring v2’s null contract, while still processing it', async () => {
70
- const mockClient = createClient();
71
- const provider = new MsalProvider(createConfig(mockClient.client));
72
- const v2Provider = provider.createProxyProvider<IMsalProvider_v2>(MsalModuleVersion.V2);
73
-
74
- await expect(v2Provider.handleRedirect()).resolves.toBeNull();
75
- expect(mockClient.handleRedirectPromise).toHaveBeenCalledTimes(1);
76
- });
77
- });
@@ -1,46 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
-
3
- import { createMockToken } from '../../mock/create-mock-token';
4
- import { createMockUserFromToken } from '../../mock/create-mock-user-from-token';
5
-
6
- describe('createMockUserFromToken', () => {
7
- it('maps identity claims onto the matching MsalMockUser fields', () => {
8
- const token = createMockToken({
9
- name: 'Ada Lovelace',
10
- preferred_username: 'ada@equinor.com',
11
- oid: 'ada-object-id',
12
- tid: 'ada-tenant-id',
13
- scp: 'User.Read Files.Read',
14
- });
15
-
16
- expect(createMockUserFromToken(token)).toEqual({
17
- name: 'Ada Lovelace',
18
- username: 'ada@equinor.com',
19
- userId: 'ada-object-id',
20
- tenantId: 'ada-tenant-id',
21
- scopes: ['User.Read', 'Files.Read'],
22
- });
23
- });
24
-
25
- it('leaves a field undefined rather than fabricating one, when a claim is absent', () => {
26
- const token = createMockToken({
27
- name: undefined,
28
- preferred_username: undefined,
29
- scp: undefined,
30
- });
31
-
32
- const user = createMockUserFromToken(token);
33
-
34
- expect(user.name).toBeUndefined();
35
- expect(user.username).toBeUndefined();
36
- expect(user.scopes).toBeUndefined();
37
- });
38
-
39
- it('throws for a token with no payload segment', () => {
40
- expect(() => createMockUserFromToken('not-a-jwt')).toThrow(/payload segment/);
41
- });
42
-
43
- it('throws for an empty string', () => {
44
- expect(() => createMockUserFromToken('')).toThrow(/payload segment/);
45
- });
46
- });