Skip to content

Commit 42813f6

Browse files
committed
fix(module-services): preserve caller headers on App State wipe, fix TSDoc example
- me.delete.ts / admin-app.delete.ts: merge caller headers via Headers instead of\n Object.assign shallow-replacing the headers object, so callers can add headers\n without stripping the mandatory X-Confirm-Wipe header\n- client.ts / changeset: fix TSDoc/example importing HttpClient from the wrong\n path and constructing it with an unsupported { baseUri } argument\n- client.ts: remove inaccurate "defaults to 'json'" claim on @template TMethod
1 parent 7db2e4a commit 42813f6

5 files changed

Lines changed: 46 additions & 30 deletions

File tree

.changeset/module-services_add-app-state-client.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ The client follows the same versioned method pattern as `bookmarks`/`context`/`n
88

99
```typescript
1010
import { AppStateApiClient } from '@equinor/fusion-framework-module-services/app-state';
11-
import { HttpClient } from '@equinor/fusion-framework-module-http';
11+
import { HttpClient } from '@equinor/fusion-framework-module-http/client';
1212

13-
const httpClient = new HttpClient({ baseUri: 'https://app-state-api.example.com/' });
13+
const httpClient = new HttpClient('https://app-state-api.example.com/');
1414
const client = new AppStateApiClient(httpClient, 'json');
1515

1616
const apps = await client.listMyApps('v1');

packages/modules/services/src/app-state/client.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,16 +72,16 @@ import {
7272
* @example
7373
* ```typescript
7474
* import { AppStateApiClient } from '@equinor/fusion-framework-module-services/app-state';
75-
* import { HttpClient } from '@equinor/fusion-framework-module-http';
75+
* import { HttpClient } from '@equinor/fusion-framework-module-http/client';
7676
*
77-
* const httpClient = new HttpClient({ baseUri: 'https://my-app-state-api.com/' });
77+
* const httpClient = new HttpClient('https://my-app-state-api.com/');
7878
* const client = new AppStateApiClient(httpClient, 'json');
7979
*
8080
* const apps = await client.listMyApps('v1');
8181
* await client.wipeMyAppState('v1', { appKey: 'my-app' });
8282
* ```
8383
*
84-
* @template TMethod - The client method to use for the request, defaults to 'json'.
84+
* @template TMethod - The client method to use for the request.
8585
* @template TClient - The HTTP client to use for executing the request.
8686
*/
8787
export class AppStateApiClient<

packages/modules/services/src/app-state/endpoints/admin-app.delete.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,15 @@ const generateRequestParameters = <TResult, TVersion extends AvailableVersions>(
5656
// Select the response schema that matches the requested API version.
5757
switch (version) {
5858
case ApiVersion.v1: {
59-
// The API requires an explicit opt-in header before it will wipe every user's state for the app.
6059
const baseInit: FetchRequestInit<ApiResponse<ApiVersion.v1>, JsonRequest> = {
6160
method: 'DELETE',
62-
headers: { 'X-Confirm-Wipe': 'true' },
6361
selector: schemaSelector(ApiResponseSchema[version]),
6462
};
65-
// Merge caller overrides on top of the generated version-specific defaults.
66-
return Object.assign({}, baseInit, init);
63+
// Preserve caller headers, but force the API's required confirmation header last so it can't be stripped.
64+
const headers = new Headers(init?.headers);
65+
headers.set('X-Confirm-Wipe', 'true');
66+
// Merge caller overrides on top of the generated defaults, with the confirmation header applied last.
67+
return Object.assign({}, baseInit, init, { headers });
6768
}
6869
}
6970
throw Error(`Unknown API version: ${version}`);

packages/modules/services/src/app-state/endpoints/me.delete.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,15 @@ const generateRequestParameters = <TResult, TVersion extends AvailableVersions>(
4949
// Select the response schema that matches the requested API version.
5050
switch (version) {
5151
case ApiVersion.v1: {
52-
// The API requires an explicit opt-in header before it will perform a full GDPR erasure.
5352
const baseInit: FetchRequestInit<ApiResponse<ApiVersion.v1>, JsonRequest> = {
5453
method: 'DELETE',
55-
headers: { 'X-Confirm-Wipe': 'true' },
5654
selector: schemaSelector(ApiResponseSchema[version]),
5755
};
58-
// Merge caller overrides on top of the generated version-specific defaults.
59-
return Object.assign({}, baseInit, init);
56+
// Preserve caller headers, but force the API's required confirmation header last so it can't be stripped.
57+
const headers = new Headers(init?.headers);
58+
headers.set('X-Confirm-Wipe', 'true');
59+
// Merge caller overrides on top of the generated defaults, with the confirmation header applied last.
60+
return Object.assign({}, baseInit, init, { headers });
6061
}
6162
}
6263
throw Error(`Unknown API version: ${version}`);

packages/modules/services/tests/app-state.test.ts

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,22 @@ describe('AppState', () => {
6565
expect(result).toMatchObject({ wiped: true });
6666
expect(httpClientWatcher).toHaveBeenCalledWith(
6767
`/persons/me?api-version=${ApiVersion.v1}`,
68-
expect.objectContaining({
69-
method: 'DELETE',
70-
headers: { 'X-Confirm-Wipe': 'true' },
71-
}),
68+
expect.objectContaining({ method: 'DELETE' }),
7269
);
70+
const [, init] = httpClientWatcher.mock.calls.at(-1) ?? [];
71+
expect(new Headers(init?.headers).get('X-Confirm-Wipe')).toBe('true');
72+
});
73+
74+
it('preserves caller headers without stripping the confirmation header', async () => {
75+
const result = await appStateClient.wipeAllMyState('v1', {
76+
headers: { 'X-Custom': 'yes' },
77+
});
78+
79+
expect(result).toMatchObject({ wiped: true });
80+
const [, init] = httpClientWatcher.mock.calls.at(-1) ?? [];
81+
const headers = new Headers(init?.headers);
82+
expect(headers.get('X-Custom')).toBe('yes');
83+
expect(headers.get('X-Confirm-Wipe')).toBe('true');
7384
});
7485
});
7586

@@ -118,22 +129,25 @@ describe('AppState', () => {
118129
expect(result).toMatchObject({ wiped: true });
119130
expect(httpClientWatcher).toHaveBeenCalledWith(
120131
`/admin/apps/my-app?api-version=${ApiVersion.v1}`,
121-
expect.objectContaining({
122-
method: 'DELETE',
123-
headers: { 'X-Confirm-Wipe': 'true' },
124-
}),
132+
expect.objectContaining({ method: 'DELETE' }),
125133
);
134+
const [, init] = httpClientWatcher.mock.calls.at(-1) ?? [];
135+
expect(new Headers(init?.headers).get('X-Confirm-Wipe')).toBe('true');
126136
});
127137

128-
it('rejects when the API rejects a missing confirmation header', async () => {
129-
// Simulate the upstream API rejecting the request when the confirmation header is stripped
130-
await expect(
131-
appStateClient.wipeAllAppUsersState(
132-
'v1',
133-
{ appKey: 'my-app' },
134-
{ headers: {} as unknown as HeadersInit },
135-
),
136-
).rejects.toBeTruthy();
138+
it('preserves caller headers without stripping the confirmation header', async () => {
139+
// The caller's own headers must survive alongside the mandatory confirmation header, not replace it.
140+
const result = await appStateClient.wipeAllAppUsersState(
141+
'v1',
142+
{ appKey: 'my-app' },
143+
{ headers: { 'X-Custom': 'yes' } },
144+
);
145+
146+
expect(result).toMatchObject({ wiped: true });
147+
const [, init] = httpClientWatcher.mock.calls.at(-1) ?? [];
148+
const headers = new Headers(init?.headers);
149+
expect(headers.get('X-Custom')).toBe('yes');
150+
expect(headers.get('X-Confirm-Wipe')).toBe('true');
137151
});
138152
});
139153
});

0 commit comments

Comments
 (0)