@forge/react 11.4.1-next.0 → 11.5.0-next.1-experimental-75d036a

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # @forge/react
2
2
 
3
+ ## 11.5.0-next.1-experimental-75d036a
4
+
5
+ ### Minor Changes
6
+
7
+ - 01521ac: add usePermissions hook
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [75d036a]
12
+ - Updated dependencies [41d8011]
13
+ - Updated dependencies [4c1e39d]
14
+ - Updated dependencies [58a20e7]
15
+ - Updated dependencies [01521ac]
16
+ - Updated dependencies [f67f026]
17
+ - @forge/bridge@5.6.0-next.4-experimental-75d036a
18
+
19
+ ## 11.5.0-next.1
20
+
21
+ ### Minor Changes
22
+
23
+ - 01521ac: add usePermissions hook
24
+
25
+ ### Patch Changes
26
+
27
+ - Updated dependencies [01521ac]
28
+ - @forge/bridge@5.6.0-next.1
29
+
3
30
  ## 11.4.1-next.0
4
31
 
5
32
  ### Patch Changes
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=usePermissions.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usePermissions.test.d.ts","sourceRoot":"","sources":["../../../src/hooks/__test__/usePermissions.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,413 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const react_hooks_1 = require("@testing-library/react-hooks");
4
+ const usePermissions_1 = require("../usePermissions");
5
+ const testUtils_1 = require("../../__test__/testUtils");
6
+ // Mock @forge/bridge
7
+ jest.mock('@forge/bridge', () => ({
8
+ view: {
9
+ getContext: jest.fn()
10
+ }
11
+ }));
12
+ const mockGetContext = jest.fn();
13
+ describe('usePermissions', () => {
14
+ beforeEach(() => {
15
+ jest.clearAllMocks();
16
+ (0, testUtils_1.setupBridge)();
17
+ // Set up the mock to use our mockGetContext function
18
+ const { view } = require('@forge/bridge');
19
+ view.getContext.mockImplementation(mockGetContext);
20
+ });
21
+ describe('Interface inheritance', () => {
22
+ it('should allow PermissionRequirements to be used as Permissions', () => {
23
+ const requiredPermissions = {
24
+ scopes: ['read:confluence-content']
25
+ };
26
+ // This should compile without errors, demonstrating inheritance
27
+ const permissions = requiredPermissions;
28
+ expect(permissions.scopes).toEqual(['read:confluence-content']);
29
+ });
30
+ it('should allow MissingPermissions to be used as Permissions', () => {
31
+ const missingPermissions = {
32
+ scopes: ['write:jira-work']
33
+ };
34
+ // This should compile without errors, demonstrating inheritance
35
+ const permissions = missingPermissions;
36
+ expect(permissions.scopes).toEqual(['write:jira-work']);
37
+ });
38
+ });
39
+ describe('Loading state', () => {
40
+ it('should start with loading true', () => {
41
+ const requiredPermissions = {
42
+ scopes: ['read:confluence-content']
43
+ };
44
+ const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
45
+ expect(result.current.isLoading).toBe(true);
46
+ expect(result.current.hasPermission).toBe(false);
47
+ expect(result.current.missingPermissions).toBe(null);
48
+ expect(result.current.error).toBe(null);
49
+ });
50
+ it('should set loading to false after context loads', async () => {
51
+ const mockContext = {
52
+ permissions: {
53
+ scopes: ['read:confluence-content']
54
+ }
55
+ };
56
+ mockGetContext.mockResolvedValue(mockContext);
57
+ const requiredPermissions = {
58
+ scopes: ['read:confluence-content']
59
+ };
60
+ const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
61
+ await (0, react_hooks_1.act)(async () => {
62
+ await new Promise((resolve) => setTimeout(resolve, 10));
63
+ });
64
+ expect(result.current.hasPermission).toBe(true);
65
+ expect(result.current.missingPermissions).toBe(null);
66
+ });
67
+ });
68
+ describe('Error handling', () => {
69
+ it('should handle context loading errors', async () => {
70
+ const error = new Error('Failed to load context');
71
+ mockGetContext.mockRejectedValue(error);
72
+ const requiredPermissions = {
73
+ scopes: ['read:confluence-content']
74
+ };
75
+ const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
76
+ await (0, react_hooks_1.act)(async () => {
77
+ await new Promise((resolve) => setTimeout(resolve, 10));
78
+ });
79
+ expect(result.current.isLoading).toBe(false);
80
+ expect(result.current.error).toEqual(error);
81
+ expect(result.current.hasPermission).toBe(false);
82
+ expect(result.current.missingPermissions).toBe(null);
83
+ });
84
+ it('should handle non-Error objects in catch block', async () => {
85
+ mockGetContext.mockRejectedValue('String error');
86
+ const requiredPermissions = {
87
+ scopes: ['read:confluence-content']
88
+ };
89
+ const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
90
+ await (0, react_hooks_1.act)(async () => {
91
+ await new Promise((resolve) => setTimeout(resolve, 10));
92
+ });
93
+ expect(result.current.error).toEqual(new Error('Failed to load context'));
94
+ });
95
+ });
96
+ describe('Scope permissions', () => {
97
+ it('should grant permission when all required scopes are present', async () => {
98
+ const mockContext = {
99
+ permissions: {
100
+ scopes: ['read:confluence-content', 'write:confluence-content']
101
+ }
102
+ };
103
+ mockGetContext.mockResolvedValue(mockContext);
104
+ const requiredPermissions = {
105
+ scopes: ['read:confluence-content']
106
+ };
107
+ const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
108
+ await (0, react_hooks_1.act)(async () => {
109
+ await new Promise((resolve) => setTimeout(resolve, 10));
110
+ });
111
+ expect(result.current.isLoading).toBe(false);
112
+ expect(result.current.hasPermission).toBe(true);
113
+ expect(result.current.missingPermissions).toBe(null);
114
+ });
115
+ it('should deny permission when required scopes are missing', async () => {
116
+ const mockContext = {
117
+ permissions: {
118
+ scopes: ['read:confluence-content']
119
+ }
120
+ };
121
+ mockGetContext.mockResolvedValue(mockContext);
122
+ const requiredPermissions = {
123
+ scopes: ['read:confluence-content', 'write:jira-work']
124
+ };
125
+ const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
126
+ await (0, react_hooks_1.act)(async () => {
127
+ await new Promise((resolve) => setTimeout(resolve, 10));
128
+ });
129
+ expect(result.current.isLoading).toBe(false);
130
+ expect(result.current.hasPermission).toBe(false);
131
+ expect(result.current.missingPermissions).toEqual({
132
+ scopes: ['write:jira-work']
133
+ });
134
+ });
135
+ it('should handle scopes as object format', async () => {
136
+ const mockContext = {
137
+ permissions: {
138
+ scopes: {
139
+ 'read:confluence-content': { allowImpersonation: false },
140
+ 'write:confluence-content': { allowImpersonation: true }
141
+ }
142
+ }
143
+ };
144
+ mockGetContext.mockResolvedValue(mockContext);
145
+ const requiredPermissions = {
146
+ scopes: ['read:confluence-content']
147
+ };
148
+ const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
149
+ await (0, react_hooks_1.act)(async () => {
150
+ await new Promise((resolve) => setTimeout(resolve, 10));
151
+ });
152
+ expect(result.current.isLoading).toBe(false);
153
+ expect(result.current.hasPermission).toBe(true);
154
+ });
155
+ it('should handle empty scopes array', async () => {
156
+ const mockContext = {
157
+ permissions: {
158
+ scopes: []
159
+ }
160
+ };
161
+ mockGetContext.mockResolvedValue(mockContext);
162
+ const requiredPermissions = {
163
+ scopes: []
164
+ };
165
+ const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
166
+ await (0, react_hooks_1.act)(async () => {
167
+ await new Promise((resolve) => setTimeout(resolve, 10));
168
+ });
169
+ expect(result.current.isLoading).toBe(false);
170
+ expect(result.current.hasPermission).toBe(true);
171
+ });
172
+ });
173
+ describe('External permissions', () => {
174
+ it('should grant permission for allowed fetch URLs', async () => {
175
+ const mockContext = {
176
+ permissions: {
177
+ external: {
178
+ fetch: {
179
+ backend: ['https://api.example.com', 'https://api.test.com'],
180
+ client: ['https://cdn.example.com']
181
+ }
182
+ }
183
+ }
184
+ };
185
+ mockGetContext.mockResolvedValue(mockContext);
186
+ const requiredPermissions = {
187
+ external: {
188
+ fetch: {
189
+ backend: ['https://api.example.com'],
190
+ client: ['https://cdn.example.com']
191
+ }
192
+ }
193
+ };
194
+ const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
195
+ await (0, react_hooks_1.act)(async () => {
196
+ await new Promise((resolve) => setTimeout(resolve, 10));
197
+ });
198
+ expect(result.current.isLoading).toBe(false);
199
+ expect(result.current.hasPermission).toBe(true);
200
+ });
201
+ it('should deny permission for disallowed fetch URLs', async () => {
202
+ const mockContext = {
203
+ permissions: {
204
+ external: {
205
+ fetch: {
206
+ backend: ['https://api.example.com']
207
+ }
208
+ }
209
+ }
210
+ };
211
+ mockGetContext.mockResolvedValue(mockContext);
212
+ const requiredPermissions = {
213
+ external: {
214
+ fetch: {
215
+ backend: ['https://api.example.com', 'https://api.unauthorized.com']
216
+ }
217
+ }
218
+ };
219
+ const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
220
+ await (0, react_hooks_1.act)(async () => {
221
+ await new Promise((resolve) => setTimeout(resolve, 10));
222
+ });
223
+ expect(result.current.isLoading).toBe(false);
224
+ expect(result.current.hasPermission).toBe(false);
225
+ expect(result.current.missingPermissions).toEqual({
226
+ external: {
227
+ fetch: {
228
+ backend: ['https://api.unauthorized.com']
229
+ }
230
+ }
231
+ });
232
+ });
233
+ it('should handle wildcard URL patterns', async () => {
234
+ const mockContext = {
235
+ permissions: {
236
+ external: {
237
+ fetch: {
238
+ backend: ['https://api.example.com/*', 'https://*.test.com/**']
239
+ }
240
+ }
241
+ }
242
+ };
243
+ mockGetContext.mockResolvedValue(mockContext);
244
+ const requiredPermissions = {
245
+ external: {
246
+ fetch: {
247
+ backend: ['https://api.example.com/users', 'https://subdomain.test.com/api']
248
+ }
249
+ }
250
+ };
251
+ const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
252
+ await (0, react_hooks_1.act)(async () => {
253
+ await new Promise((resolve) => setTimeout(resolve, 10));
254
+ });
255
+ expect(result.current.isLoading).toBe(false);
256
+ expect(result.current.hasPermission).toBe(true);
257
+ });
258
+ it('should handle resource permissions (fonts, images, etc.)', async () => {
259
+ const mockContext = {
260
+ permissions: {
261
+ external: {
262
+ fonts: ['https://fonts.googleapis.com'],
263
+ images: ['https://images.example.com'],
264
+ scripts: ['https://scripts.example.com']
265
+ }
266
+ }
267
+ };
268
+ mockGetContext.mockResolvedValue(mockContext);
269
+ const requiredPermissions = {
270
+ external: {
271
+ fonts: ['https://fonts.googleapis.com'],
272
+ images: ['https://images.example.com']
273
+ }
274
+ };
275
+ const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
276
+ await (0, react_hooks_1.act)(async () => {
277
+ await new Promise((resolve) => setTimeout(resolve, 10));
278
+ });
279
+ expect(result.current.isLoading).toBe(false);
280
+ expect(result.current.hasPermission).toBe(true);
281
+ });
282
+ });
283
+ describe('Complex permission scenarios', () => {
284
+ it('should handle mixed permission types', async () => {
285
+ const mockContext = {
286
+ permissions: {
287
+ scopes: ['read:confluence-content', 'write:confluence-content', 'read:jira-work'],
288
+ external: {
289
+ fetch: {
290
+ backend: ['https://api.example.com', 'https://api.test.com'],
291
+ client: ['https://cdn.example.com', 'https://cdn.test.com']
292
+ },
293
+ images: ['https://images.example.com', 'https://images.test.com'],
294
+ fonts: ['https://fonts.googleapis.com', 'https://fonts.gstatic.com'],
295
+ scripts: ['https://scripts.example.com']
296
+ }
297
+ }
298
+ };
299
+ mockGetContext.mockResolvedValue(mockContext);
300
+ const requiredPermissions = {
301
+ scopes: ['read:confluence-content', 'write:confluence-content'],
302
+ external: {
303
+ fetch: {
304
+ backend: ['https://api.example.com'],
305
+ client: ['https://cdn.example.com']
306
+ },
307
+ images: ['https://images.example.com'],
308
+ fonts: ['https://fonts.googleapis.com'],
309
+ scripts: ['https://scripts.example.com']
310
+ }
311
+ };
312
+ const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
313
+ await (0, react_hooks_1.act)(async () => {
314
+ await new Promise((resolve) => setTimeout(resolve, 10));
315
+ });
316
+ expect(result.current.isLoading).toBe(false);
317
+ expect(result.current.hasPermission).toBe(true);
318
+ });
319
+ it('should identify missing permissions across different types', async () => {
320
+ const mockContext = {
321
+ permissions: {
322
+ scopes: ['read:confluence-content', 'read:jira-work'],
323
+ external: {
324
+ fetch: {
325
+ backend: ['https://api.example.com', 'https://api.authorized.com'],
326
+ client: ['https://cdn.authorized.com']
327
+ },
328
+ images: ['https://images.authorized.com'],
329
+ fonts: ['https://fonts.authorized.com']
330
+ }
331
+ }
332
+ };
333
+ mockGetContext.mockResolvedValue(mockContext);
334
+ const requiredPermissions = {
335
+ scopes: ['read:confluence-content', 'write:jira-work', 'admin:confluence'],
336
+ external: {
337
+ fetch: {
338
+ backend: ['https://api.example.com', 'https://api.unauthorized.com', 'https://api.missing.com'],
339
+ client: ['https://cdn.example.com', 'https://cdn.unauthorized.com']
340
+ },
341
+ images: ['https://images.example.com', 'https://images.unauthorized.com'],
342
+ fonts: ['https://fonts.example.com'],
343
+ scripts: ['https://scripts.example.com', 'https://scripts.unauthorized.com']
344
+ }
345
+ };
346
+ const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
347
+ await (0, react_hooks_1.act)(async () => {
348
+ await new Promise((resolve) => setTimeout(resolve, 10));
349
+ });
350
+ expect(result.current.isLoading).toBe(false);
351
+ expect(result.current.hasPermission).toBe(false);
352
+ expect(result.current.missingPermissions).toEqual({
353
+ scopes: ['write:jira-work', 'admin:confluence'],
354
+ external: {
355
+ fetch: {
356
+ backend: ['https://api.unauthorized.com', 'https://api.missing.com'],
357
+ client: ['https://cdn.example.com', 'https://cdn.unauthorized.com']
358
+ },
359
+ images: ['https://images.example.com', 'https://images.unauthorized.com'],
360
+ fonts: ['https://fonts.example.com'],
361
+ scripts: ['https://scripts.example.com', 'https://scripts.unauthorized.com']
362
+ }
363
+ });
364
+ });
365
+ });
366
+ describe('Edge cases', () => {
367
+ it('should handle empty required permissions', async () => {
368
+ const mockContext = {
369
+ permissions: {
370
+ scopes: ['read:confluence-content']
371
+ }
372
+ };
373
+ mockGetContext.mockResolvedValue(mockContext);
374
+ const requiredPermissions = {};
375
+ const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
376
+ await (0, react_hooks_1.act)(async () => {
377
+ await new Promise((resolve) => setTimeout(resolve, 10));
378
+ });
379
+ expect(result.current.isLoading).toBe(false);
380
+ expect(result.current.hasPermission).toBe(true);
381
+ expect(result.current.missingPermissions).toBe(null);
382
+ });
383
+ it('should handle complex external permission objects', async () => {
384
+ const mockContext = {
385
+ permissions: {
386
+ external: {
387
+ fetch: {
388
+ backend: [
389
+ 'https://api.example.com',
390
+ { address: 'https://api.address.com' },
391
+ { remote: 'https://api.remote.com' }
392
+ ]
393
+ }
394
+ }
395
+ }
396
+ };
397
+ mockGetContext.mockResolvedValue(mockContext);
398
+ const requiredPermissions = {
399
+ external: {
400
+ fetch: {
401
+ backend: ['https://api.example.com', 'https://api.address.com']
402
+ }
403
+ }
404
+ };
405
+ const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
406
+ await (0, react_hooks_1.act)(async () => {
407
+ await new Promise((resolve) => setTimeout(resolve, 10));
408
+ });
409
+ expect(result.current.isLoading).toBe(false);
410
+ expect(result.current.hasPermission).toBe(true);
411
+ });
412
+ });
413
+ });
@@ -0,0 +1,89 @@
1
+ /**
2
+ * https://ecosystem-platform.atlassian.net/browse/DEPLOY-1411
3
+ * reuse logic from @forge/api
4
+ */
5
+ /**
6
+ * Resource types that can be loaded externally
7
+ */
8
+ declare const RESOURCE_TYPES: readonly ["fonts", "styles", "frames", "images", "media", "scripts"];
9
+ export declare type ResourceType = (typeof RESOURCE_TYPES)[number];
10
+ /**
11
+ * Fetch types for external requests
12
+ */
13
+ declare const FETCH_TYPES: readonly ["backend", "client"];
14
+ export declare type FetchType = (typeof FETCH_TYPES)[number];
15
+ export interface Permissions {
16
+ scopes?: string[];
17
+ external?: {
18
+ fetch?: {
19
+ backend?: string[];
20
+ client?: string[];
21
+ };
22
+ fonts?: string[];
23
+ styles?: string[];
24
+ frames?: string[];
25
+ images?: string[];
26
+ media?: string[];
27
+ scripts?: string[];
28
+ };
29
+ content?: Record<string, unknown>;
30
+ }
31
+ /**
32
+ * Required permissions for a component
33
+ */
34
+ export declare type PermissionRequirements = Permissions;
35
+ /**
36
+ * Missing permissions information
37
+ */
38
+ export declare type MissingPermissions = Permissions;
39
+ /**
40
+ * Permission check result
41
+ */
42
+ export interface PermissionCheckResult {
43
+ granted: boolean;
44
+ missing: MissingPermissions | null;
45
+ }
46
+ /**
47
+ * Hook for checking permissions in Forge apps
48
+ *
49
+ * @param requiredPermissions - The permissions required for the component
50
+ * @returns Object containing permission state, loading status, and error information
51
+ * @returns hasPermission - Whether all required permissions are granted
52
+ * @returns isLoading - Whether the permission check is still in progress
53
+ * @returns missingPermissions - Details about which permissions are missing (null if all granted)
54
+ * @returns error - Any error that occurred during permission checking (null if no error)
55
+ *
56
+ * @example
57
+ * ```tsx
58
+ * const MyComponent: React.FC = () => {
59
+ * const { hasPermission, isLoading, missingPermissions, error } = usePermissions({
60
+ * scopes: ['write:confluence-content'],
61
+ * external: {
62
+ * fetch: {
63
+ * backend: ['https://api.example.com']
64
+ * }
65
+ * }
66
+ * });
67
+ *
68
+ * if (isLoading) return <LoadingSpinner />;
69
+ *
70
+ * if (error) {
71
+ * return <ErrorMessage error={error} />;
72
+ * }
73
+ *
74
+ * if (!hasPermission) {
75
+ * return <PermissionDenied missingPermissions={missingPermissions} />;
76
+ * }
77
+ *
78
+ * return <ProtectedFeature />;
79
+ * };
80
+ * ```
81
+ */
82
+ export declare const usePermissions: (requiredPermissions: PermissionRequirements) => {
83
+ hasPermission: boolean;
84
+ isLoading: boolean;
85
+ missingPermissions: Permissions | null;
86
+ error: Error | null;
87
+ };
88
+ export {};
89
+ //# sourceMappingURL=usePermissions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usePermissions.d.ts","sourceRoot":"","sources":["../../src/hooks/usePermissions.ts"],"names":[],"mappings":"AAIA;;;GAGG;AAEH;;GAEG;AACH,QAAA,MAAM,cAAc,sEAAuE,CAAC;AAC5F,oBAAY,YAAY,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;AAE3D;;GAEG;AACH,QAAA,MAAM,WAAW,gCAAiC,CAAC;AACnD,oBAAY,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAErD,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,CAAC,EAAE;QACT,KAAK,CAAC,EAAE;YACN,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;YACnB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;SACnB,CAAC;QACF,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;QAClB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;QAClB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC;IACF,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED;;GAEG;AACH,oBAAY,sBAAsB,GAAG,WAAW,CAAC;AAEjD;;GAEG;AACH,oBAAY,kBAAkB,GAAG,WAAW,CAAC;AAE7C;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACpC;AAaD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,cAAc,wBAAyB,sBAAsB;;;;;CA2JzE,CAAC"}