@ankhorage/permissions 0.2.3 → 0.2.4

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 (44) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +3 -3
  3. package/dist/expo/client.js +16 -13
  4. package/dist/expo/client.js.map +1 -1
  5. package/dist/expo/client.testSupport.d.ts +54 -0
  6. package/dist/expo/client.testSupport.d.ts.map +1 -0
  7. package/dist/expo/client.testSupport.js +183 -0
  8. package/dist/expo/client.testSupport.js.map +1 -0
  9. package/dist/expo/manifest.d.ts.map +1 -1
  10. package/dist/expo/manifest.js.map +1 -1
  11. package/dist/expo/permissionResponse.js +4 -4
  12. package/dist/expo/permissionResponse.js.map +1 -1
  13. package/dist/react/index.d.ts.map +1 -1
  14. package/dist/react/index.js +16 -11
  15. package/dist/react/index.js.map +1 -1
  16. package/dist/state/permissionState.d.ts.map +1 -1
  17. package/dist/state/permissionState.js.map +1 -1
  18. package/dist/testing/index.d.ts.map +1 -1
  19. package/dist/testing/index.js +27 -17
  20. package/dist/testing/index.js.map +1 -1
  21. package/dist/web/index.d.ts +1 -2
  22. package/dist/web/index.d.ts.map +1 -1
  23. package/dist/web/index.js +2 -76
  24. package/dist/web/index.js.map +1 -1
  25. package/dist/web/permissionStates.d.ts +10 -0
  26. package/dist/web/permissionStates.d.ts.map +1 -0
  27. package/dist/web/permissionStates.js +64 -0
  28. package/dist/web/permissionStates.js.map +1 -0
  29. package/package.json +10 -11
  30. package/src/expo/client.notifications.test.ts +106 -0
  31. package/src/expo/client.test.ts +110 -535
  32. package/src/expo/client.testSupport.ts +188 -0
  33. package/src/expo/client.ts +17 -13
  34. package/src/expo/manifest.test.ts +48 -50
  35. package/src/expo/manifest.ts +1 -5
  36. package/src/expo/permissionResponse.ts +4 -4
  37. package/src/manager/createPermissionManager.test.ts +48 -50
  38. package/src/react/index.tsx +21 -13
  39. package/src/registry/permissions.test.ts +4 -3
  40. package/src/state/permissionState.ts +1 -6
  41. package/src/testing/index.ts +36 -25
  42. package/src/web/index.test.ts +149 -151
  43. package/src/web/index.ts +9 -102
  44. package/src/web/permissionStates.ts +83 -0
@@ -45,6 +45,12 @@ export interface FakePermissionClient extends PermissionClient {
45
45
  getSnapshot(): readonly PermissionState[];
46
46
  }
47
47
 
48
+ interface FakePermissionContext {
49
+ readonly now: () => Date;
50
+ readonly requestStates: Map<Permission, PermissionState>;
51
+ readonly states: Map<Permission, PermissionState>;
52
+ }
53
+
48
54
  /***
49
55
  * Creates a deterministic in-memory client for tests and examples.
50
56
  *
@@ -55,12 +61,21 @@ export interface FakePermissionClient extends PermissionClient {
55
61
  export function createFakePermissionClient(
56
62
  options: FakePermissionClientOptions = {},
57
63
  ): FakePermissionClient {
58
- const now = options.now ?? (() => new Date());
59
- const states = new Map<Permission, PermissionState>();
60
- const requestStates = new Map<Permission, PermissionState>();
64
+ const context = createFakePermissionContext(options);
65
+ const client = createFakePermissionOperations(context, options.openSettings);
66
+ if (options.openSettings === undefined) delete client.openSettings;
67
+ return client;
68
+ }
69
+
70
+ function createFakePermissionContext(options: FakePermissionClientOptions): FakePermissionContext {
71
+ const context: FakePermissionContext = {
72
+ now: options.now ?? (() => new Date()),
73
+ states: new Map<Permission, PermissionState>(),
74
+ requestStates: new Map<Permission, PermissionState>(),
75
+ };
61
76
 
62
77
  for (const permission of PERMISSIONS) {
63
- states.set(
78
+ context.states.set(
64
79
  permission,
65
80
  createPermissionState({
66
81
  permission,
@@ -71,45 +86,47 @@ export function createFakePermissionClient(
71
86
  }
72
87
 
73
88
  for (const state of options.initialStates ?? []) {
74
- states.set(state.permission, normalizeSeed(state));
89
+ context.states.set(state.permission, normalizeSeed(state));
75
90
  }
76
91
 
77
92
  for (const state of options.requestStates ?? []) {
78
- requestStates.set(state.permission, normalizeSeed(state));
93
+ context.requestStates.set(state.permission, normalizeSeed(state));
79
94
  }
95
+ return context;
96
+ }
80
97
 
98
+ function createFakePermissionOperations(
99
+ context: FakePermissionContext,
100
+ openSettings: (() => Promise<void>) | undefined,
101
+ ): FakePermissionClient {
81
102
  const client: FakePermissionClient = {
82
103
  getStatus(permission) {
83
- return Promise.resolve(getStoredState(states, assertKnownPermission(permission)));
104
+ return Promise.resolve(getStoredState(context.states, assertKnownPermission(permission)));
84
105
  },
85
-
86
106
  request(permission) {
87
107
  const knownPermission = assertKnownPermission(permission);
88
108
  const requestState =
89
- requestStates.get(knownPermission) ?? getStoredState(states, knownPermission);
109
+ context.requestStates.get(knownPermission) ??
110
+ getStoredState(context.states, knownPermission);
90
111
  const nextState = createPermissionState({
91
112
  permission: knownPermission,
92
113
  status: requestState.status,
93
114
  canAskAgain: requestState.canAskAgain,
94
- requestedAt: now(),
115
+ requestedAt: context.now(),
95
116
  reason: requestState.reason,
96
117
  });
97
118
 
98
- states.set(knownPermission, nextState);
99
-
119
+ context.states.set(knownPermission, nextState);
100
120
  return Promise.resolve(nextState);
101
121
  },
102
-
103
- openSettings: options.openSettings,
104
-
122
+ openSettings,
105
123
  setState(state) {
106
124
  const normalizedState = normalizeSeed(state);
107
- states.set(normalizedState.permission, normalizedState);
125
+ context.states.set(normalizedState.permission, normalizedState);
108
126
  },
109
-
110
127
  setStatus(permission, status, statusOptions = {}) {
111
128
  const knownPermission = assertKnownPermission(permission);
112
- states.set(
129
+ context.states.set(
113
130
  knownPermission,
114
131
  createPermissionState({
115
132
  permission: knownPermission,
@@ -119,16 +136,10 @@ export function createFakePermissionClient(
119
136
  }),
120
137
  );
121
138
  },
122
-
123
139
  getSnapshot() {
124
- return PERMISSIONS.map((permission) => getStoredState(states, permission));
140
+ return PERMISSIONS.map((permission) => getStoredState(context.states, permission));
125
141
  },
126
142
  };
127
-
128
- if (options.openSettings === undefined) {
129
- delete client.openSettings;
130
- }
131
-
132
143
  return client;
133
144
  }
134
145
 
@@ -1,205 +1,203 @@
1
- import { describe, expect, test } from 'bun:test';
1
+ import { expect, test } from 'bun:test';
2
2
 
3
3
  import { Permission } from '../registry/permissions';
4
4
  import { createWebPermissionClient, type WebPermissionClientOptions } from './index';
5
5
 
6
- describe('createWebPermissionClient', () => {
7
- test('returns unavailable for unsupported web permissions', async () => {
8
- const client = createWebPermissionClient({ global: {} });
6
+ test('returns unavailable for unsupported web permissions', async () => {
7
+ const client = createWebPermissionClient({ global: {} });
9
8
 
10
- const state = await client.getStatus(Permission.MediaLibrary);
9
+ const state = await client.getStatus(Permission.MediaLibrary);
11
10
 
12
- expect(state).toMatchObject({
13
- status: 'unavailable',
14
- canAskAgain: false,
15
- });
11
+ expect(state).toMatchObject({
12
+ status: 'unavailable',
13
+ canAskAgain: false,
16
14
  });
15
+ });
17
16
 
18
- test('reads notification permission structurally', async () => {
19
- const client = createWebPermissionClient({
20
- global: {
21
- Notification: {
22
- permission: 'granted',
23
- requestPermission: () => 'granted',
24
- },
17
+ test('reads notification permission structurally', async () => {
18
+ const client = createWebPermissionClient({
19
+ global: {
20
+ Notification: {
21
+ permission: 'granted',
22
+ requestPermission: () => 'granted',
25
23
  },
26
- });
24
+ },
25
+ });
27
26
 
28
- const state = await client.getStatus(Permission.Notifications);
27
+ const state = await client.getStatus(Permission.Notifications);
29
28
 
30
- expect(state).toMatchObject({
31
- status: 'granted',
32
- granted: true,
33
- });
29
+ expect(state).toMatchObject({
30
+ status: 'granted',
31
+ granted: true,
34
32
  });
33
+ });
35
34
 
36
- test('requests notifications without throwing on denial', async () => {
37
- const client = createWebPermissionClient({
38
- global: {
39
- Notification: {
40
- permission: 'default',
41
- requestPermission: () => Promise.resolve('denied'),
42
- },
35
+ test('requests notifications without throwing on denial', async () => {
36
+ const client = createWebPermissionClient({
37
+ global: {
38
+ Notification: {
39
+ permission: 'default',
40
+ requestPermission: () => Promise.resolve('denied'),
43
41
  },
44
- });
42
+ },
43
+ });
45
44
 
46
- const state = await client.request(Permission.Notifications);
45
+ const state = await client.request(Permission.Notifications);
47
46
 
48
- expect(state).toMatchObject({
49
- status: 'denied',
50
- granted: false,
51
- });
47
+ expect(state).toMatchObject({
48
+ status: 'denied',
49
+ granted: false,
52
50
  });
51
+ });
53
52
 
54
- test('returns unavailable when notification request throws', async () => {
55
- const client = createWebPermissionClient({
56
- global: {
57
- Notification: {
58
- permission: 'default',
59
- requestPermission: () => {
60
- throw Object.assign(new Error('blocked'), { name: 'SecurityError' });
61
- },
53
+ test('returns unavailable when notification request throws', async () => {
54
+ const client = createWebPermissionClient({
55
+ global: {
56
+ Notification: {
57
+ permission: 'default',
58
+ requestPermission: () => {
59
+ throw Object.assign(new Error('blocked'), { name: 'SecurityError' });
62
60
  },
63
61
  },
64
- });
62
+ },
63
+ });
65
64
 
66
- const state = await client.request(Permission.Notifications);
65
+ const state = await client.request(Permission.Notifications);
67
66
 
68
- expect(state).toMatchObject({
69
- status: 'unavailable',
70
- granted: false,
71
- reason: 'Notification permission request failed: SecurityError.',
72
- });
67
+ expect(state).toMatchObject({
68
+ status: 'unavailable',
69
+ granted: false,
70
+ reason: 'Notification permission request failed: SecurityError.',
73
71
  });
72
+ });
74
73
 
75
- test('uses Permissions API for location status when available', async () => {
76
- const client = createWebPermissionClient({
77
- global: {
78
- navigator: {
79
- permissions: {
80
- query: () => Promise.resolve({ state: 'prompt' }),
81
- },
74
+ test('uses Permissions API for location status when available', async () => {
75
+ const client = createWebPermissionClient({
76
+ global: {
77
+ navigator: {
78
+ permissions: {
79
+ query: () => Promise.resolve({ state: 'prompt' }),
82
80
  },
83
81
  },
84
- });
82
+ },
83
+ });
85
84
 
86
- const state = await client.getStatus(Permission.LocationForeground);
85
+ const state = await client.getStatus(Permission.LocationForeground);
87
86
 
88
- expect(state).toMatchObject({
89
- status: 'unknown',
90
- canAskAgain: true,
91
- });
87
+ expect(state).toMatchObject({
88
+ status: 'unknown',
89
+ canAskAgain: true,
92
90
  });
91
+ });
93
92
 
94
- test('returns blocked for media in insecure contexts', async () => {
95
- const client = createWebPermissionClient({
96
- global: {
97
- isSecureContext: false,
98
- },
99
- });
100
-
101
- const state = await client.getStatus(Permission.Camera);
102
-
103
- expect(state).toMatchObject({
104
- status: 'blocked',
105
- canAskAgain: false,
106
- });
107
- });
108
-
109
- test('requests geolocation without a 1ms timeout', async () => {
110
- let requestedOptions:
111
- | {
112
- readonly maximumAge?: number;
113
- readonly timeout?: number;
114
- }
115
- | undefined;
116
- const client = createWebPermissionClient({
117
- global: {
118
- navigator: {
119
- geolocation: {
120
- getCurrentPosition: (success, _error, options) => {
121
- requestedOptions = options;
122
- success();
123
- },
93
+ test('returns blocked for media in insecure contexts', async () => {
94
+ const client = createWebPermissionClient({
95
+ global: {
96
+ isSecureContext: false,
97
+ },
98
+ });
99
+
100
+ const state = await client.getStatus(Permission.Camera);
101
+
102
+ expect(state).toMatchObject({
103
+ status: 'blocked',
104
+ canAskAgain: false,
105
+ });
106
+ });
107
+
108
+ test('requests geolocation without a 1ms timeout', async () => {
109
+ let requestedOptions:
110
+ | {
111
+ readonly maximumAge?: number;
112
+ readonly timeout?: number;
113
+ }
114
+ | undefined;
115
+ const client = createWebPermissionClient({
116
+ global: {
117
+ navigator: {
118
+ geolocation: {
119
+ getCurrentPosition: (success, _error, options) => {
120
+ requestedOptions = options;
121
+ success();
124
122
  },
125
123
  },
126
124
  },
127
- });
125
+ },
126
+ });
128
127
 
129
- const state = await client.request(Permission.LocationForeground);
128
+ const state = await client.request(Permission.LocationForeground);
130
129
 
131
- expect(state.status).toBe('granted');
132
- expect(requestedOptions).toEqual({ maximumAge: 0 });
133
- });
130
+ expect(state.status).toBe('granted');
131
+ expect(requestedOptions).toEqual({ maximumAge: 0 });
132
+ });
134
133
 
135
- test('requests media with getUserMedia and stops tracks', async () => {
136
- let stopped = false;
137
- const global: WebPermissionClientOptions['global'] = {
138
- navigator: {
139
- mediaDevices: {
140
- getUserMedia: () =>
141
- Promise.resolve({
142
- getTracks: () => [
143
- {
144
- stop: () => {
145
- stopped = true;
146
- },
134
+ test('requests media with getUserMedia and stops tracks', async () => {
135
+ let stopped = false;
136
+ const global: WebPermissionClientOptions['global'] = {
137
+ navigator: {
138
+ mediaDevices: {
139
+ getUserMedia: () =>
140
+ Promise.resolve({
141
+ getTracks: () => [
142
+ {
143
+ stop: () => {
144
+ stopped = true;
147
145
  },
148
- ],
149
- }),
150
- },
146
+ },
147
+ ],
148
+ }),
151
149
  },
152
- };
153
- const client = createWebPermissionClient({ global });
150
+ },
151
+ };
152
+ const client = createWebPermissionClient({ global });
154
153
 
155
- const state = await client.request(Permission.Microphone);
154
+ const state = await client.request(Permission.Microphone);
156
155
 
157
- expect(state).toMatchObject({
158
- status: 'granted',
159
- granted: true,
160
- });
161
- expect(stopped).toBe(true);
156
+ expect(state).toMatchObject({
157
+ status: 'granted',
158
+ granted: true,
162
159
  });
160
+ expect(stopped).toBe(true);
161
+ });
163
162
 
164
- test('maps missing media devices to unavailable', async () => {
165
- const client = createWebPermissionClient({
166
- global: {
167
- navigator: {
168
- mediaDevices: {
169
- getUserMedia: () =>
170
- Promise.reject(Object.assign(new Error('missing'), { name: 'NotFoundError' })),
171
- },
163
+ test('maps missing media devices to unavailable', async () => {
164
+ const client = createWebPermissionClient({
165
+ global: {
166
+ navigator: {
167
+ mediaDevices: {
168
+ getUserMedia: () =>
169
+ Promise.reject(Object.assign(new Error('missing'), { name: 'NotFoundError' })),
172
170
  },
173
171
  },
174
- });
172
+ },
173
+ });
175
174
 
176
- const state = await client.request(Permission.Camera);
175
+ const state = await client.request(Permission.Camera);
177
176
 
178
- expect(state).toMatchObject({
179
- status: 'unavailable',
180
- granted: false,
181
- reason: 'Media permission request failed: NotFoundError.',
182
- });
177
+ expect(state).toMatchObject({
178
+ status: 'unavailable',
179
+ granted: false,
180
+ reason: 'Media permission request failed: NotFoundError.',
183
181
  });
182
+ });
184
183
 
185
- test('maps user media denial to denied', async () => {
186
- const client = createWebPermissionClient({
187
- global: {
188
- navigator: {
189
- mediaDevices: {
190
- getUserMedia: () =>
191
- Promise.reject(Object.assign(new Error('denied'), { name: 'NotAllowedError' })),
192
- },
184
+ test('maps user media denial to denied', async () => {
185
+ const client = createWebPermissionClient({
186
+ global: {
187
+ navigator: {
188
+ mediaDevices: {
189
+ getUserMedia: () =>
190
+ Promise.reject(Object.assign(new Error('denied'), { name: 'NotAllowedError' })),
193
191
  },
194
192
  },
195
- });
193
+ },
194
+ });
196
195
 
197
- const state = await client.request(Permission.Camera);
196
+ const state = await client.request(Permission.Camera);
198
197
 
199
- expect(state).toMatchObject({
200
- status: 'denied',
201
- granted: false,
202
- reason: 'Media permission request was denied: NotAllowedError.',
203
- });
198
+ expect(state).toMatchObject({
199
+ status: 'denied',
200
+ granted: false,
201
+ reason: 'Media permission request was denied: NotAllowedError.',
204
202
  });
205
203
  });
package/src/web/index.ts CHANGED
@@ -1,15 +1,17 @@
1
1
  import type { PermissionClient } from '../client/types';
2
2
  import { assertKnownPermission, Permission } from '../registry/permissions';
3
+ import { createPermissionState, type PermissionState } from '../state/permissionState';
3
4
  import {
4
- createPermissionState,
5
- type PermissionState,
6
- type PermissionStatus,
7
- } from '../state/permissionState';
5
+ type BrowserPermissionState,
6
+ formatErrorName,
7
+ type NotificationPermissionState,
8
+ stateFromBrowserPermission,
9
+ stateFromMediaError,
10
+ stateFromNotificationPermission,
11
+ unavailable,
12
+ } from './permissionStates';
8
13
 
9
14
  type BrowserPermissionName = 'camera' | 'microphone' | 'geolocation' | 'notifications';
10
- type BrowserPermissionState = 'granted' | 'denied' | 'prompt';
11
- type NotificationPermissionState = 'default' | 'denied' | 'granted';
12
-
13
15
  interface BrowserPermissionStatus {
14
16
  readonly state: BrowserPermissionState;
15
17
  }
@@ -293,98 +295,3 @@ async function requestMedia(
293
295
  return stateFromMediaError(permission, error);
294
296
  }
295
297
  }
296
-
297
- function stateFromMediaError(permission: Permission, error: unknown): PermissionState {
298
- const errorName = getErrorName(error);
299
- const unavailableErrorNames = new Set([
300
- 'AbortError',
301
- 'NotFoundError',
302
- 'NotReadableError',
303
- 'OverconstrainedError',
304
- 'SecurityError',
305
- ]);
306
-
307
- if (errorName !== undefined && unavailableErrorNames.has(errorName)) {
308
- return createPermissionState({
309
- permission,
310
- status: 'unavailable',
311
- canAskAgain: false,
312
- requestedAt: new Date(),
313
- reason: `Media permission request failed: ${errorName}.`,
314
- });
315
- }
316
-
317
- return createPermissionState({
318
- permission,
319
- status: 'denied',
320
- canAskAgain: false,
321
- requestedAt: new Date(),
322
- reason:
323
- errorName === undefined
324
- ? 'Media permission request was denied.'
325
- : `Media permission request was denied: ${errorName}.`,
326
- });
327
- }
328
-
329
- function stateFromNotificationPermission(
330
- permission: Permission,
331
- status: NotificationPermissionState,
332
- ): PermissionState {
333
- if (status === 'granted') {
334
- return createPermissionState({
335
- permission,
336
- status: 'granted',
337
- canAskAgain: true,
338
- });
339
- }
340
-
341
- if (status === 'denied') {
342
- return createPermissionState({
343
- permission,
344
- status: 'denied',
345
- canAskAgain: false,
346
- });
347
- }
348
-
349
- return createPermissionState({
350
- permission,
351
- status: 'unknown',
352
- canAskAgain: true,
353
- });
354
- }
355
-
356
- function stateFromBrowserPermission(
357
- permission: Permission,
358
- status: BrowserPermissionState,
359
- ): PermissionState {
360
- const normalizedStatus: PermissionStatus = status === 'prompt' ? 'unknown' : status;
361
-
362
- return createPermissionState({
363
- permission,
364
- status: normalizedStatus,
365
- canAskAgain: status === 'prompt' || status === 'granted',
366
- });
367
- }
368
-
369
- function unavailable(permission: Permission, reason: string): PermissionState {
370
- return createPermissionState({
371
- permission,
372
- status: 'unavailable',
373
- canAskAgain: false,
374
- reason,
375
- });
376
- }
377
-
378
- function formatErrorName(error: unknown): string {
379
- const errorName = getErrorName(error);
380
-
381
- return errorName === undefined ? '' : `: ${errorName}`;
382
- }
383
-
384
- function getErrorName(error: unknown): string | undefined {
385
- if (typeof error !== 'object' || error === null || !('name' in error)) {
386
- return undefined;
387
- }
388
-
389
- return typeof error.name === 'string' && error.name.length > 0 ? error.name : undefined;
390
- }
@@ -0,0 +1,83 @@
1
+ import type { Permission } from '../registry/permissions';
2
+ import {
3
+ createPermissionState,
4
+ type PermissionState,
5
+ type PermissionStatus,
6
+ } from '../state/permissionState';
7
+
8
+ export type BrowserPermissionState = 'granted' | 'denied' | 'prompt';
9
+ export type NotificationPermissionState = 'default' | 'denied' | 'granted';
10
+
11
+ export function stateFromMediaError(permission: Permission, error: unknown): PermissionState {
12
+ const errorName = getErrorName(error);
13
+ const unavailableErrorNames = new Set([
14
+ 'AbortError',
15
+ 'NotFoundError',
16
+ 'NotReadableError',
17
+ 'OverconstrainedError',
18
+ 'SecurityError',
19
+ ]);
20
+ if (errorName !== undefined && unavailableErrorNames.has(errorName)) {
21
+ return createPermissionState({
22
+ permission,
23
+ status: 'unavailable',
24
+ canAskAgain: false,
25
+ requestedAt: new Date(),
26
+ reason: `Media permission request failed: ${errorName}.`,
27
+ });
28
+ }
29
+ return createPermissionState({
30
+ permission,
31
+ status: 'denied',
32
+ canAskAgain: false,
33
+ requestedAt: new Date(),
34
+ reason:
35
+ errorName === undefined
36
+ ? 'Media permission request was denied.'
37
+ : `Media permission request was denied: ${errorName}.`,
38
+ });
39
+ }
40
+
41
+ export function stateFromNotificationPermission(
42
+ permission: Permission,
43
+ status: NotificationPermissionState,
44
+ ): PermissionState {
45
+ if (status === 'granted') {
46
+ return createPermissionState({ permission, status: 'granted', canAskAgain: true });
47
+ }
48
+ if (status === 'denied') {
49
+ return createPermissionState({ permission, status: 'denied', canAskAgain: false });
50
+ }
51
+ return createPermissionState({ permission, status: 'unknown', canAskAgain: true });
52
+ }
53
+
54
+ export function stateFromBrowserPermission(
55
+ permission: Permission,
56
+ status: BrowserPermissionState,
57
+ ): PermissionState {
58
+ const normalizedStatus: PermissionStatus = status === 'prompt' ? 'unknown' : status;
59
+ return createPermissionState({
60
+ permission,
61
+ status: normalizedStatus,
62
+ canAskAgain: status === 'prompt' || status === 'granted',
63
+ });
64
+ }
65
+
66
+ export function unavailable(permission: Permission, reason: string): PermissionState {
67
+ return createPermissionState({
68
+ permission,
69
+ status: 'unavailable',
70
+ canAskAgain: false,
71
+ reason,
72
+ });
73
+ }
74
+
75
+ export function formatErrorName(error: unknown): string {
76
+ const errorName = getErrorName(error);
77
+ return errorName === undefined ? '' : `: ${errorName}`;
78
+ }
79
+
80
+ function getErrorName(error: unknown): string | undefined {
81
+ if (typeof error !== 'object' || error === null || !('name' in error)) return undefined;
82
+ return typeof error.name === 'string' && error.name.length > 0 ? error.name : undefined;
83
+ }