@equinor/fusion-framework-module-msal 10.0.2 → 11.0.0-next.0

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 (73) hide show
  1. package/CHANGELOG.md +163 -0
  2. package/README.md +17 -368
  3. package/dist/esm/MsalConfigurator.js +176 -88
  4. package/dist/esm/MsalConfigurator.js.map +1 -1
  5. package/dist/esm/__tests__/MsalConfigurator.test.js +75 -0
  6. package/dist/esm/__tests__/MsalConfigurator.test.js.map +1 -1
  7. package/dist/esm/__tests__/create-proxy-provider.test.js +53 -0
  8. package/dist/esm/__tests__/create-proxy-provider.test.js.map +1 -0
  9. package/dist/esm/__tests__/mock/msal-mock.test.js +399 -0
  10. package/dist/esm/__tests__/mock/msal-mock.test.js.map +1 -0
  11. package/dist/esm/index.js +5 -0
  12. package/dist/esm/index.js.map +1 -1
  13. package/dist/esm/mock/MsalMockClient.js +467 -0
  14. package/dist/esm/mock/MsalMockClient.js.map +1 -0
  15. package/dist/esm/mock/MsalMockConfigurator.js +187 -0
  16. package/dist/esm/mock/MsalMockConfigurator.js.map +1 -0
  17. package/dist/esm/mock/create-mock-token.js +60 -0
  18. package/dist/esm/mock/create-mock-token.js.map +1 -0
  19. package/dist/esm/mock/create-msal-mock-client.js +20 -0
  20. package/dist/esm/mock/create-msal-mock-client.js.map +1 -0
  21. package/dist/esm/mock/decode-jwt-segment.js +22 -0
  22. package/dist/esm/mock/decode-jwt-segment.js.map +1 -0
  23. package/dist/esm/mock/index.js +30 -0
  24. package/dist/esm/mock/index.js.map +1 -0
  25. package/dist/esm/mock/module.js +38 -0
  26. package/dist/esm/mock/module.js.map +1 -0
  27. package/dist/esm/msal-config-schema.js +37 -0
  28. package/dist/esm/msal-config-schema.js.map +1 -0
  29. package/dist/esm/telemetry-config-schema.js +16 -0
  30. package/dist/esm/telemetry-config-schema.js.map +1 -0
  31. package/dist/esm/version.js +1 -1
  32. package/dist/esm/version.js.map +1 -1
  33. package/dist/esm/versioning/resolve-version.js +0 -1
  34. package/dist/esm/versioning/resolve-version.js.map +1 -1
  35. package/dist/tsconfig.tsbuildinfo +1 -1
  36. package/dist/types/MsalConfigurator.d.ts +96 -20
  37. package/dist/types/__tests__/create-proxy-provider.test.d.ts +1 -0
  38. package/dist/types/__tests__/mock/msal-mock.test.d.ts +1 -0
  39. package/dist/types/index.d.ts +5 -0
  40. package/dist/types/mock/MsalMockClient.d.ts +270 -0
  41. package/dist/types/mock/MsalMockConfigurator.d.ts +156 -0
  42. package/dist/types/mock/create-mock-token.d.ts +54 -0
  43. package/dist/types/mock/create-msal-mock-client.d.ts +14 -0
  44. package/dist/types/mock/decode-jwt-segment.d.ts +13 -0
  45. package/dist/types/mock/index.d.ts +29 -0
  46. package/dist/types/mock/module.d.ts +35 -0
  47. package/dist/types/msal-config-schema.d.ts +64 -0
  48. package/dist/types/telemetry-config-schema.d.ts +8 -0
  49. package/dist/types/version.d.ts +1 -1
  50. package/docs/api-reference.md +85 -0
  51. package/docs/auth-code-flow.md +86 -0
  52. package/docs/migration-v2-to-v4.md +115 -0
  53. package/docs/testing.md +167 -0
  54. package/docs/troubleshooting.md +17 -0
  55. package/docs/version-management.md +67 -0
  56. package/package.json +12 -5
  57. package/src/MsalConfigurator.ts +202 -115
  58. package/src/__tests__/MsalConfigurator.test.ts +106 -0
  59. package/src/__tests__/create-proxy-provider.test.ts +77 -0
  60. package/src/__tests__/mock/msal-mock.test.ts +544 -0
  61. package/src/index.ts +6 -0
  62. package/src/mock/MsalMockClient.ts +599 -0
  63. package/src/mock/MsalMockConfigurator.ts +241 -0
  64. package/src/mock/create-mock-token.ts +92 -0
  65. package/src/mock/create-msal-mock-client.ts +25 -0
  66. package/src/mock/decode-jwt-segment.ts +22 -0
  67. package/src/mock/index.ts +29 -0
  68. package/src/mock/module.ts +54 -0
  69. package/src/msal-config-schema.ts +81 -0
  70. package/src/telemetry-config-schema.ts +25 -0
  71. package/src/version.ts +1 -1
  72. package/src/versioning/resolve-version.ts +0 -1
  73. package/vitest.config.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,168 @@
1
1
  # Change Log
2
2
 
3
+ ## 11.0.0-next.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 2836e0b: Move the MSAL configuration schema into `MsalConfig.schema.ts` and add `MsalConfigExtension`, an extension point for variants of this module.
8
+
9
+ `MsalConfig` is now the schema's inferred type intersected with `MsalConfigExtension`, an empty interface a variant merges its own configuration into:
10
+
11
+ ```typescript
12
+ declare module "@equinor/fusion-framework-module-msal" {
13
+ interface MsalConfigExtension {
14
+ mock?: { account?: MsalMockUser };
15
+ }
16
+ }
17
+ ```
18
+
19
+ `BaseConfigBuilder._set` derives its target from `MsalConfig`, so before this a key the type did not know about could only be set by casting past the builder. Making the configurator generic over its configuration cannot solve that: a dot-path union over an unresolved type parameter defers, which takes every existing literal path down with it.
20
+
21
+ The schema is unchanged and still describes exactly what reaches `MsalProvider` — it strips anything merged in, so an extension carries a declaration across the builder and stops there. `MsalConfigSchema`, `TelemetryConfigSchema` and their types are re-exported from `MsalConfigurator` as before.
22
+
23
+ - 2836e0b: Extract client construction from `MsalConfigurator._processConfig` into overridable seams.
24
+
25
+ `_processConfig` now only decides _whether_ a client is needed; `protected _createClient(config, init)` decides _what_ to build, and `protected _createClientConfig(config)` resolves the `MsalClientConfig` it is built from — authority derived from the tenant, cache location, telemetry-backed logging and cache lookup policy included.
26
+
27
+ Behaviour is unchanged for consumers: the client is still auto-created from `setClientConfig`, and a client supplied through `setClient` still wins, because `_createClient` is consulted only when no client was set.
28
+
29
+ This gives a supported seam for authenticating through something other than Entra ID:
30
+
31
+ ```typescript
32
+ class MyConfigurator extends MsalConfigurator {
33
+ protected override async _createClient(
34
+ config: MsalConfig,
35
+ ): Promise<IMsalClient> {
36
+ // same fully resolved configuration the real client is built from
37
+ return new MyOwnMsalClient(this._createClientConfig(config));
38
+ }
39
+ }
40
+ ```
41
+
42
+ Overriding it replaces only the client, leaving the builder, the schema validation and `MsalProvider` untouched.
43
+
44
+ No client is built when the module is hoisted onto a host application's provider — an app running inside a portal authenticates through the host, so a client built during configuration would be discarded, or worse, shadow the host's signed-in user. `protected _isHoisted(init)` exposes that decision to subclasses.
45
+
46
+ Also adds `getClientConfig()`, the counterpart to `setClientConfig`, so a subclass can tell "nothing was declared" apart from "declared, and here it is".
47
+
48
+ `_createClientConfig` applies its defaults to a copy of the declared configuration, not the object itself, so a caller reusing or asserting on it never sees it rewritten, and a shared constant can be used as a default without one configurator's client contaminating the next.
49
+
50
+ - 2836e0b: Give `MsalMockClient` a real account cache, so its account APIs agree with each other and with MSAL.
51
+
52
+ Previously the client held a single nullable account, which made its surface inconsistent: `getAccount(filter)` ignored the filter, `getAllAccounts()` reported an account even after one had only been made active, and signing out merely blanked a field.
53
+
54
+ The client now keeps a cache keyed by `homeAccountId` alongside an active account, matching MSAL:
55
+
56
+ - `getAccount(filter)` matches on `homeAccountId`, `localAccountId`, `username` and `tenantId`.
57
+ - `getAllAccounts()` returns everything cached.
58
+ - Signing in adds the account and activates it; signing out removes it, rather than leaving a stale entry behind.
59
+ - `setUser` replaces the session, so declaring a second user never leaves two accounts cached.
60
+
61
+ `setActiveAccount` deliberately departs from MSAL in one respect: an account that was never issued by a sign-in is accepted and added to the cache. That makes swapping the user between tests a single line, without rebuilding the framework:
62
+
63
+ ```typescript
64
+ beforeEach(() => {
65
+ fusion.modules.auth.client.setActiveAccount(account);
66
+ });
67
+ ```
68
+
69
+ - 2836e0b: Added a `./mock` entry point so applications can run against the auth module without credentials or network access.
70
+
71
+ ```ts
72
+ import {
73
+ enableMsalMock,
74
+ createMsalMockClient,
75
+ } from "@equinor/fusion-framework-module-msal/mock";
76
+
77
+ // default mock user
78
+ enableMsalMock(configurator);
79
+
80
+ // or a specific one
81
+ enableMsalMock(configurator, (builder) => {
82
+ builder.setAccount({ name: "Ada Lovelace" });
83
+ });
84
+ ```
85
+
86
+ Only the MSAL **client** is substituted. `MsalMockClient` resolves tokens in-process and takes the same `MsalClientConfig` as the real `MsalClient`, so `setClientConfig` means the same thing whether a test runs against Entra ID or in-process. `MsalMockConfigurator` builds it through the `_createClient` seam, and `msalMockModule` differs from the real module in its `configure` alone — `initialize` is the production one, untouched, so `MsalProvider`, schema validation and the whole start-up path run exactly as they do in production. `IMsalProvider` and `MsalConfigurator` are untouched.
87
+
88
+ The same client works with the plain module, without the mock module:
89
+
90
+ ```ts
91
+ enableMSAL(configurator, (builder) =>
92
+ builder.setClient(
93
+ createMsalMockClient(
94
+ { auth: { clientId: "my-app" } },
95
+ { name: "Ada Lovelace" },
96
+ ),
97
+ ),
98
+ );
99
+ ```
100
+
101
+ Tokens are structurally valid, unsigned JWTs and are identical between runs. They are not cryptographically valid and are rejected by any real service.
102
+
103
+ Exports `enableMsalMock`, `msalMockModule`, `MsalMockConfigurator`, `MsalMockClient`, `createMsalMockClient` and `createMockToken`. The entry point has no test-runner dependency.
104
+
105
+ - 2836e0b: Add `setAccount` to `MsalMockConfigurator`, so a test declares the signed-in user on the builder instead of baking it into a client.
106
+
107
+ ```typescript
108
+ enableMsalMock(configurator, (builder) => {
109
+ builder.setAccount({ name: "Ada Lovelace", username: "ada@equinor.com" });
110
+ });
111
+ ```
112
+
113
+ It takes an object, `null` when nobody is signed in, or an ordinary `ConfigBuilderCallback` resolving either:
114
+
115
+ ```typescript
116
+ builder.setAccount(null);
117
+ builder.setAccount(async ({ hasModule }) => ({
118
+ name: hasModule("app") ? "App User" : "Portal User",
119
+ }));
120
+ ```
121
+
122
+ The callback is the builder's own type rather than a bespoke one, so it is handed the same arguments every other configuration callback receives and is resolved by the same machinery.
123
+
124
+ `null` and `{ signedOut: true }` both start without a session, and differ in what a later login resolves to: `null` forgets the identity, `signedOut` keeps it. A declared `null` is a declaration in its own right, so it overrides the default signed-in user — only an absent declaration means the test said nothing.
125
+
126
+ `setAccount` writes to `mock.account` on the configuration rather than to a field on the builder, so the user travels the ordinary builder pipeline: a callback is resolved by `_buildConfig` with the same arguments every other configuration callback receives, and the result is on the raw configuration before validation. The schema strips the key, so nothing about a test reaches `MsalProvider` — the branch exists purely to carry the declaration across the builder. `MsalMockConfig` is exported for code that reads it.
127
+
128
+ `setAccount` records configuration only — the user is signed in on the client as it is built, so it may be declared at any point before initialization and the last declaration wins. Keeping the user off `MsalClientConfig` is what lets `MsalMockClient` take the same argument the real `MsalClient` takes: a client is configured with _what it talks to_, never with _who is signed in_.
129
+
130
+ Because the user is in place before `MsalProvider.initialize()` runs, the provider's own start-up path acts on it. Pairing `{ signedOut: true }` with `setRequiresAuth(true)` therefore exercises the real automatic login, rather than a state assigned after initialization had already finished.
131
+
132
+ Because who is signed in is session state rather than client configuration, the user is applied to whichever client the module ends up authenticating through — wherever that client was built. When the module is hoisted onto a host application's provider, no client is built here, so the user is signed in on the _host's_ client instead. Without that, a declaration made in an application's test would silently do nothing precisely when the application is being tested inside a portal. The session is shared, so the host sees the same user, as it does in production; when the host does not authenticate through a mock client the declaration throws rather than failing quietly.
133
+
134
+ `setClient` replaces the client, but not the rule: a mock client supplied that way receives the declared user too.
135
+
136
+ ### Patch Changes
137
+
138
+ - e8aae1f: Internal: publish every package on the `next` pre-release tag so the whole framework can be installed as a coherent set.
139
+
140
+ Packages without their own changes are bumped only to receive a `-next.N` version and the `next` dist-tag on npm. Install with:
141
+
142
+ ```bash
143
+ pnpm add @equinor/fusion-framework-react-app@next
144
+ ```
145
+
146
+ - 2836e0b: Restructure documentation so each README is an entry point rather than a manual.
147
+
148
+ Long-form content moved into per-package `docs/` folders, matching the convention already used by `@equinor/fusion-framework-module` and `@equinor/fusion-framework-module-http`. Each README now keeps the elevator pitch, the shortest working example and a documentation table linking to the rest.
149
+
150
+ - **msal** — `docs/api-reference.md`, `docs/auth-code-flow.md`, `docs/testing.md`, `docs/version-management.md`, `docs/migration-v2-to-v4.md`, `docs/troubleshooting.md`. The README also gained the top-level heading it was missing.
151
+ - **service-discovery** — `docs/configuration.md`, `docs/testing.md`, `docs/session-overrides.md`, `docs/api-reference.md`.
152
+ - **framework** — `docs/testing-choosing-a-layer.md`, `docs/testing.md`, `docs/testing-design.md`, `docs/testing-extending.md`, `docs/testing-api.md`.
153
+
154
+ Both module READMEs now document their `/mock` entry point, which was previously undocumented, and state that spying on an individual call is the test runner's job rather than something these packages provide.
155
+
156
+ - 2836e0b: Remove a stray `console.log` left in `resolveVersion`'s minor-version-mismatch warning path.
157
+ - Updated dependencies [e8aae1f]
158
+ - Updated dependencies [2836e0b]
159
+ - Updated dependencies [2836e0b]
160
+ - Updated dependencies [2836e0b]
161
+ - Updated dependencies [2836e0b]
162
+ - Updated dependencies [2836e0b]
163
+ - @equinor/fusion-framework-module@6.1.3-next.0
164
+ - @equinor/fusion-framework-module-telemetry@8.0.0-next.0
165
+
3
166
  ## 10.0.2
4
167
 
5
168
  ### Patch Changes
package/README.md CHANGED
@@ -1,3 +1,4 @@
1
+ # `@equinor/fusion-framework-module-msal`
1
2
 
2
3
  `@equinor/fusion-framework-module-msal` provides secure Azure AD authentication for browser applications using Microsoft's MSAL (Microsoft Authentication Library). Perfect for web applications, SPAs, and React apps that need to authenticate with Microsoft services.
3
4
 
@@ -78,92 +79,16 @@ try {
78
79
  > [!IMPORTANT]
79
80
  > The `@equinor/fusion-framework-app` enables this package by default, so applications using the app package do not need to enable this module manually.
80
81
 
81
- ## Backend-Issued Auth Code Flow
82
+ ## Documentation
82
83
 
83
- Enable automatic sign-in using a backend-issued authorization code without interactive login prompts.
84
-
85
- ### Overview
86
-
87
- When your backend authenticates a user and generates a short-lived SPA auth code, the MSAL module can exchange it for tokens during initialization, eliminating double-login issues and providing seamless authentication.
88
-
89
- ### Usage
90
-
91
- ```typescript
92
- import { enableMSAL } from '@equinor/fusion-framework-module-msal';
93
-
94
- enableMSAL(configurator, (builder) => {
95
- builder.setClientConfig({
96
- auth: {
97
- clientId: 'your-client-id',
98
- tenantId: 'your-tenant-id'
99
- }
100
- });
101
-
102
- // Backend injects auth code as window.MSAL_AUTH_CODE during initial first page load
103
- // This is the most secure approach - only available on first render, cleared after use
104
- if (typeof window !== 'undefined' && window.MSAL_AUTH_CODE) {
105
- builder.setAuthCode(window.MSAL_AUTH_CODE);
106
- delete (window as any).MSAL_AUTH_CODE; // Clear after consuming
107
- }
108
-
109
- builder.setRequiresAuth(true);
110
- });
111
- ```
112
-
113
- ### How It Works
114
-
115
- 1. Backend authenticates user and generates short-lived auth code
116
- 2. Frontend passes auth code to MSAL: `builder.setAuthCode(authCode)`
117
- 3. During `initialize()`: auth code exchanged for tokens (before `requiresAuth` check)
118
- 4. Tokens cached by MSAL → user automatically signed in
119
- 5. Falls back to standard MSAL flows on exchange failure
120
-
121
- ### API: `setAuthCode(authCode?: string)`
122
-
123
- Sets backend-issued auth code for token exchange during initialization.
124
-
125
- Pass `undefined` to clear/reset a previously configured auth code.
126
-
127
- **Returns:** configurator instance (chainable)
128
-
129
- **Behavior:**
130
- - Exchange happens before `requiresAuth` check
131
- - On success: user auto-authenticated, no login prompt
132
- - On failure: falls back to standard MSAL login
133
- - Auth code cleared after exchange (no reuse)
134
- - `setAuthCode(undefined)` clears configured auth code
135
- - `setAuthCode('')` is treated as absent auth code
136
- - `setAuthCode(' ')` is trimmed and treated as absent auth code
137
- - No auth-code exchange is attempted when auth code is absent/cleared
138
-
139
- **Example:**
140
-
141
- ```typescript
142
- // Best practice: Backend injects auth code on initial page load as window.MSAL_AUTH_CODE
143
- if (typeof window !== 'undefined' && window.MSAL_AUTH_CODE) {
144
- builder.setAuthCode(window.MSAL_AUTH_CODE);
145
- delete (window as any).MSAL_AUTH_CODE; // Clear after consuming to prevent reuse
146
- }
147
-
148
- // Clear/reset auth code when input is missing
149
- builder.setAuthCode(undefined);
150
- ```
151
-
152
- ### Security
153
-
154
- - ✅ Auth codes: single-use, short-lived (5-10 min)
155
- - ✅ MSAL validates tokens from Microsoft authority
156
- - ✅ Tokens stored securely, refresh tokens auto-managed
157
- - ⚠️ Pass codes securely: HTTPS, HTTP-only cookies, or encrypted channels
158
-
159
- ### Troubleshooting
160
-
161
- | Issue | Solution |
162
- |-------|----------|
163
- | Auth code not exchanged | Verify `setAuthCode()` called before init |
164
- | Invalid auth code error | Confirm backend `WithSpaAuthCode` enabled, code is fresh |
165
- | Still shows login prompt | Check auth code exchange completes before `requiresAuth` check |
166
- | Exchange fails | Auth code may have expired; backend should generate fresh code per load |
84
+ | Guide | Covers |
85
+ | --- | --- |
86
+ | [Configuration](./docs/api-reference.md) | `enableMSAL`, builder methods, `IMsalProvider`, and type definitions |
87
+ | [Backend-Issued Auth Code Flow](./docs/auth-code-flow.md) | Signing a user in from a backend-issued SPA auth code, without an interactive prompt |
88
+ | [Testing](./docs/testing.md) | The `/mock` entry point: in-process authentication, deterministic tokens, and spying |
89
+ | [Version Management](./docs/version-management.md) | Version resolution, compatibility checking, and related errors |
90
+ | [Migration v2 to v4](./docs/migration-v2-to-v4.md) | Moving from MSAL Browser v2 to v4, including the compatibility proxy |
91
+ | [Troubleshooting](./docs/troubleshooting.md) | Common failures and where to get help |
167
92
 
168
93
  ## Configuration
169
94
 
@@ -193,92 +118,21 @@ AZURE_TENANT_ID=your-tenant-id
193
118
  AZURE_REDIRECT_URI=https://your-app.com/callback
194
119
  ```
195
120
 
196
- ## API Reference
197
-
198
- ### `enableMSAL(configurator, configure?)`
199
-
200
- Enables the MSAL module in your Fusion Framework application.
201
-
202
- **Parameters:**
203
- - `configurator`: `IModulesConfigurator` - The modules configurator instance
204
- - `configure?`: `(builder: { setClientConfig, setRequiresAuth }) => void` - Optional configuration function
205
-
206
- **Returns:** `void`
207
-
208
- **Example:**
209
- ```typescript
210
- enableMSAL(configurator, (builder) => {
211
- builder.setClientConfig({ auth: { clientId: '...', tenantId: '...' } });
212
- builder.setRequiresAuth(true);
213
- });
214
- ```
215
-
216
- ### Type Definitions
121
+ ## Testing
217
122
 
218
- #### `LoginOptions`
123
+ Import from `@equinor/fusion-framework-module-msal/mock` to authenticate in-process instead of against Entra ID. The real configurator, provider and schema validation still run — only the client that would contact Entra ID is substituted.
219
124
 
220
125
  ```typescript
221
- type LoginOptions = {
222
- request: PopupRequest | RedirectRequest; // MSAL request object
223
- behavior?: 'popup' | 'redirect'; // Auth method (default: 'redirect')
224
- silent?: boolean; // Attempt silent auth first (default: true)
225
- };
226
- ```
227
-
228
- #### `LogoutOptions`
126
+ import { enableMsalMock } from '@equinor/fusion-framework-module-msal/mock';
229
127
 
230
- ```typescript
231
- type LogoutOptions = {
232
- redirectUri?: string; // Redirect after logout
233
- account?: AccountInfo; // Account to logout (defaults to active)
234
- };
128
+ enableMsalMock(configurator);
235
129
  ```
236
130
 
237
- #### `AcquireTokenOptions`
131
+ A user named `Test User` is signed in, and tokens are real JWTs minted in-process with a fixed issue time, so they are identical across runs and machines.
238
132
 
239
- ```typescript
240
- type AcquireTokenOptions = {
241
- request: PopupRequest | RedirectRequest; // MSAL request with scopes
242
- behavior?: 'popup' | 'redirect'; // Auth method (default: 'redirect')
243
- silent?: boolean; // Attempt silent first (default: true if account available)
244
- };
245
- ```
246
-
247
- ### `IMsalProvider`
248
-
249
- The authentication provider interface available at `framework.auth`:
250
-
251
- ```typescript
252
- interface IMsalProvider {
253
- // The MSAL PublicClientApplication instance
254
- readonly client: IMsalClient;
255
-
256
- // Current user account information
257
- readonly account: AccountInfo | null;
258
-
259
- // Initialize the MSAL provider
260
- initialize(): Promise<void>;
261
-
262
- // Acquire an access token for the specified scopes
263
- acquireAccessToken(options: AcquireTokenOptionsLegacy): Promise<string | undefined>;
264
-
265
- // Acquire full authentication result
266
- acquireToken(options: AcquireTokenOptionsLegacy): Promise<AcquireTokenResult>;
267
-
268
- // Login user interactively
269
- login(options: LoginOptions): Promise<LoginResult>;
270
-
271
- // Logout user (returns boolean)
272
- logout(options?: LogoutOptions): Promise<boolean>;
273
-
274
- // Handle authentication redirect (returns AuthenticationResult | null)
275
- handleRedirect(): Promise<AuthenticationResult | null>;
276
- }
277
-
278
- // Note: defaultAccount and other deprecated v2 properties are available only
279
- // when using a v2-compatible proxy via createProxyProvider()
280
- ```
133
+ The entry point has **no test-runner dependency**, and ships no mocking API of its own — spying on a call is your test runner's job.
281
134
 
135
+ See [Testing](./docs/testing.md) for choosing the signed-in user, signed-out behaviour and runner guidance, or [`@equinor/fusion-framework/mock`](../../framework/docs/testing.md) to mock every framework boundary at once.
282
136
 
283
137
  ## Module Hoisting
284
138
 
@@ -287,208 +141,6 @@ The module implements a hoisting pattern where the authentication provider is cr
287
141
  > [!IMPORTANT]
288
142
  > **Configure the auth module only in the root Fusion Framework instance** - Sub-instances will automatically inherit the authentication configuration from the parent.
289
143
 
290
- ## Migration Guide
291
-
292
- ### MSAL v2 to v4 Migration
293
-
294
- This package has been upgraded from MSAL Browser v2 to v4, providing the latest security improvements and features from Microsoft.
295
-
296
- #### What Changed in v4
297
-
298
- **New MSAL Browser v4 Features:**
299
- - Enhanced security with improved token management
300
- - Better performance and memory usage
301
- - New authentication API structure with nested request objects
302
- - Improved error handling and retry mechanisms
303
-
304
- **Architecture Changes:**
305
- - **Module Hoisting**: The module uses module hoisting, meaning sub-module instances proxy the parent module instance
306
- - **Shared Authentication State**: Authentication state is shared across all module instances
307
- - **Async Initialization**: New `initialize()` method must be called before using the provider
308
-
309
- #### Breaking Changes
310
-
311
- 1. **Auto-initialization via Framework**
312
- ```typescript
313
- // The provider initializes automatically when framework loads
314
- const framework = await initialize(configurator);
315
- const auth = framework.auth; // Already initialized
316
-
317
- // Manual initialization is only needed for standalone usage
318
- const provider = new MsalProvider(config);
319
- await provider.initialize();
320
- ```
321
-
322
- 2. **API Method Signature Updates**
323
- - `logout()` now returns `Promise<boolean>` instead of `Promise<void>`
324
- - `handleRedirect()` now returns `Promise<AuthenticationResult | null>` instead of `Promise<void>`
325
- - Methods now expect nested request objects (v4 format)
326
-
327
- 3. **Account Property Changes**
328
- - Use `account` property (returns `AccountInfo | null`) - v4 native
329
- - `defaultAccount` is deprecated and only available via v2 proxy layer
330
- - Migration: Replace `defaultAccount` with `account` throughout your code
331
-
332
- #### Migration Steps
333
-
334
- 1. **Update Token Acquisition** (Recommended)
335
- ```typescript
336
- // Before (v2 format - still works via proxy)
337
- const token = await framework.auth.acquireAccessToken({
338
- scopes: ['api.read']
339
- });
340
-
341
- // After (v4 format - recommended)
342
- const token = await framework.auth.acquireAccessToken({
343
- request: { scopes: ['api.read'] }
344
- });
345
- ```
346
-
347
- 2. **Update Logout Handling**
348
- ```typescript
349
- // Before
350
- await framework.auth.logout();
351
-
352
- // After (check return value)
353
- const success = await framework.auth.logout();
354
- if (success) {
355
- // Handle successful logout
356
- }
357
- ```
358
-
359
- 3. **Update Redirect Handling**
360
- ```typescript
361
- // Before
362
- await framework.auth.handleRedirect();
363
-
364
- // After (handle result)
365
- const result = await framework.auth.handleRedirect();
366
- if (result?.account) {
367
- // User authenticated successfully
368
- console.log('Logged in as:', result.account.username);
369
- }
370
- ```
371
-
372
- 4. **Update Configuration** (if needed)
373
- ```typescript
374
- // Ensure only the root module configures MSAL
375
- enableMSAL(configurator, (builder) => {
376
- builder.setClientConfig({
377
- auth: {
378
- clientId: 'your-client-id',
379
- tenantId: 'your-tenant-id',
380
- redirectUri: 'https://your-app.com/callback'
381
- }
382
- });
383
- builder.setRequiresAuth(true);
384
- });
385
- ```
386
-
387
- 5. **Remove Duplicate Configurations**: Remove MSAL configuration from child modules
388
-
389
- #### Backward Compatibility
390
-
391
- The module includes a **v2 proxy layer** that automatically converts v2 API calls to v4 format. This means:
392
- - ✅ Existing code continues to work without changes
393
- - ✅ Legacy format `{ scopes: [] }` is still supported
394
- - ✅ Deprecated v2 properties like `defaultAccount` are available via v2 proxy (with deprecation warnings)
395
- - ⚠️ New v4 features require using v4 format
396
-
397
- #### Benefits of Migration
398
-
399
- - **Better Security**: Latest MSAL v4 security improvements and token handling
400
- - **Improved Performance**: Faster token acquisition, better caching, reduced memory usage
401
- - **Enhanced Error Handling**: More robust error recovery and retry mechanisms
402
- - **Future-Proof**: Access to latest Microsoft authentication features and updates
403
- - **Shared State**: Improved authentication state management across app scopes via module hoisting
404
- - **Better Developer Experience**: Cleaner API, better TypeScript support, comprehensive documentation
405
-
406
- ## Troubleshooting
407
-
408
- ### Common Issues
409
-
410
- | Issue | Solution |
411
- |-------|----------|
412
- | **Authentication Loop** | Ensure redirect URIs match your application's routing |
413
- | **Token Acquisition Fails** | Check that required scopes are properly configured |
414
- | **Module Not Found** | Ensure the module is properly configured and framework is initialized |
415
- | **Multiple MSAL Instances** | Remove duplicate configurations from child modules |
416
- | **Redirect Returns Void** | For redirect flows, use `handleRedirect()` after navigation completes |
417
- | **Token Empty/Undefined** | Verify user is authenticated and scopes are correct |
418
-
419
- ### Getting Help
420
-
421
- - 📖 [MSAL Cookbook](https://github.com/equinor/fusion-framework/tree/main/cookbooks/app-react-msal) - Complete working examples
422
- - 🐛 [Report Issues](https://github.com/equinor/fusion/issues) - Bug reports and feature requests
423
-
424
- ## Version Management
425
-
426
- The MSAL module includes built-in version checking to ensure compatibility between different MSAL library versions.
427
-
428
- ### Version Resolution
429
-
430
- ```typescript
431
- import { resolveVersion, VersionError } from '@equinor/fusion-framework-module-msal/versioning';
432
-
433
- // Resolve and validate a version
434
- const result = resolveVersion('2.0.0');
435
- console.log(result.isLatest); // false
436
- console.log(result.satisfiesLatest); // true
437
- console.log(result.enumVersion); // MsalModuleVersion.V2
438
- ```
439
-
440
- ### Version Checking Behavior
441
-
442
- - **Major Version Incompatibility**: Throws `VersionError` if requested major version is greater than latest
443
- - **Minor Version Mismatch**: Logs warning but allows execution
444
- - **Patch Differences**: Ignored for compatibility
445
- - **Invalid Versions**: Throws `VersionError` with descriptive message
446
-
447
- ### API Reference
448
-
449
- #### `resolveVersion(version: string | SemVer): ResolvedVersion`
450
-
451
- Resolves and validates a version string against the latest available MSAL version.
452
-
453
- **Parameters:**
454
- - `version` - Version string or SemVer object to resolve
455
-
456
- **Returns:** `ResolvedVersion` object containing:
457
- - `wantedVersion: SemVer` - The parsed requested version
458
- - `latestVersion: SemVer` - The latest available version
459
- - `isLatest: boolean` - Whether the version is exactly the latest
460
- - `satisfiesLatest: boolean` - Whether the major version matches latest
461
- - `enumVersion: MsalModuleVersion` - Corresponding enum version
462
-
463
- **Throws:** `VersionError` for invalid or incompatible versions
464
-
465
- #### `VersionError`
466
-
467
- Error class for version-related issues with the following types:
468
- - `InvalidVersion` - Requested version is not a valid semver
469
- - `InvalidLatestVersion` - Latest version parsing failed (build issue)
470
- - `MajorIncompatibility` - Major version is greater than latest
471
- - `MinorMismatch` - Minor version differs (warning only)
472
- - `PatchDifference` - Patch version differs (info only)
473
- - `IncompatibleVersion` - General incompatibility
474
-
475
- ### Error Handling
476
-
477
- ```typescript
478
- import { resolveVersion, VersionError } from '@equinor/fusion-framework-module-msal/versioning';
479
-
480
- try {
481
- const result = resolveVersion('3.0.0'); // Assuming latest is 2.x
482
- } catch (error) {
483
- if (error instanceof VersionError) {
484
- console.error('Version error:', error.message);
485
- console.error('Requested:', error.requestedVersion);
486
- console.error('Latest:', error.latestVersion);
487
- console.error('Type:', error.type);
488
- }
489
- }
490
- ```
491
-
492
144
  ## Additional Resources
493
145
 
494
146
  ### Official Documentation
@@ -507,6 +159,3 @@ try {
507
159
  - 💬 For questions: [Fusion Framework Discussions](https://github.com/equinor/fusion-framework/discussions)
508
160
  - 🐛 Report bugs: [Fusion Framework Issues](https://github.com/equinor/fusion-framework/issues)
509
161
  - 📧 Contact: Equinor Fusion Framework Team
510
-
511
-
512
-