@forge/react 11.9.0-next.1 → 11.9.1-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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @forge/react
2
2
 
3
+ ## 11.9.1-next.0
4
+
5
+ ### Patch Changes
6
+
7
+ - f57dd69: Bug fix for usePermissions hook
8
+
9
+ ## 11.9.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 92c8fe8: Added useTheme hook to access the current theme and subscribe to theme updates
14
+
15
+ ### Patch Changes
16
+
17
+ - @forge/bridge@5.10.1
18
+
3
19
  ## 11.9.0-next.1
4
20
 
5
21
  ### Minor Changes
@@ -363,6 +363,369 @@ describe('usePermissions', () => {
363
363
  });
364
364
  });
365
365
  });
366
+ describe('CSP path matching for client fetch', () => {
367
+ it('should allow any path when allowlist has no path', async () => {
368
+ const mockContext = {
369
+ permissions: {
370
+ external: {
371
+ fetch: {
372
+ client: ['https://cdn.example.com']
373
+ }
374
+ }
375
+ }
376
+ };
377
+ mockGetContext.mockResolvedValue(mockContext);
378
+ const requiredPermissions = {
379
+ external: {
380
+ fetch: {
381
+ client: [
382
+ 'https://cdn.example.com',
383
+ 'https://cdn.example.com/',
384
+ 'https://cdn.example.com/any/path',
385
+ 'https://cdn.example.com/file.js'
386
+ ]
387
+ }
388
+ }
389
+ };
390
+ const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
391
+ await (0, react_hooks_1.act)(async () => {
392
+ await new Promise((resolve) => setTimeout(resolve, 10));
393
+ });
394
+ expect(result.current.isLoading).toBe(false);
395
+ expect(result.current.hasPermission).toBe(true);
396
+ });
397
+ it('should use prefix matching when allowlist has trailing slash', async () => {
398
+ const mockContext = {
399
+ permissions: {
400
+ external: {
401
+ fetch: {
402
+ client: ['https://cdn.example.com/api/']
403
+ }
404
+ }
405
+ }
406
+ };
407
+ mockGetContext.mockResolvedValue(mockContext);
408
+ const requiredPermissions = {
409
+ external: {
410
+ fetch: {
411
+ client: [
412
+ 'https://cdn.example.com/api/',
413
+ 'https://cdn.example.com/api/users',
414
+ 'https://cdn.example.com/api/v1/data'
415
+ ]
416
+ }
417
+ }
418
+ };
419
+ const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
420
+ await (0, react_hooks_1.act)(async () => {
421
+ await new Promise((resolve) => setTimeout(resolve, 10));
422
+ });
423
+ expect(result.current.isLoading).toBe(false);
424
+ expect(result.current.hasPermission).toBe(true);
425
+ });
426
+ it('should block paths outside prefix when using trailing slash', async () => {
427
+ const mockContext = {
428
+ permissions: {
429
+ external: {
430
+ fetch: {
431
+ client: ['https://cdn.example.com/api/']
432
+ }
433
+ }
434
+ }
435
+ };
436
+ mockGetContext.mockResolvedValue(mockContext);
437
+ const requiredPermissions = {
438
+ external: {
439
+ fetch: {
440
+ client: ['https://cdn.example.com/other', 'https://cdn.example.com/']
441
+ }
442
+ }
443
+ };
444
+ const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
445
+ await (0, react_hooks_1.act)(async () => {
446
+ await new Promise((resolve) => setTimeout(resolve, 10));
447
+ });
448
+ expect(result.current.isLoading).toBe(false);
449
+ expect(result.current.hasPermission).toBe(false);
450
+ expect(result.current.missingPermissions?.external?.fetch?.client).toEqual([
451
+ 'https://cdn.example.com/other',
452
+ 'https://cdn.example.com/'
453
+ ]);
454
+ });
455
+ it('should use exact matching when allowlist has no trailing slash', async () => {
456
+ const mockContext = {
457
+ permissions: {
458
+ external: {
459
+ fetch: {
460
+ client: ['https://cdn.example.com/bundle.js']
461
+ }
462
+ }
463
+ }
464
+ };
465
+ mockGetContext.mockResolvedValue(mockContext);
466
+ const requiredPermissions = {
467
+ external: {
468
+ fetch: {
469
+ client: ['https://cdn.example.com/bundle.js']
470
+ }
471
+ }
472
+ };
473
+ const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
474
+ await (0, react_hooks_1.act)(async () => {
475
+ await new Promise((resolve) => setTimeout(resolve, 10));
476
+ });
477
+ expect(result.current.isLoading).toBe(false);
478
+ expect(result.current.hasPermission).toBe(true);
479
+ });
480
+ it('should block non-exact paths when using exact matching', async () => {
481
+ const mockContext = {
482
+ permissions: {
483
+ external: {
484
+ fetch: {
485
+ client: ['https://cdn.example.com/bundle.js']
486
+ }
487
+ }
488
+ }
489
+ };
490
+ mockGetContext.mockResolvedValue(mockContext);
491
+ const requiredPermissions = {
492
+ external: {
493
+ fetch: {
494
+ client: [
495
+ 'https://cdn.example.com/bundle.js/extra',
496
+ 'https://cdn.example.com/other.js',
497
+ 'https://cdn.example.com/'
498
+ ]
499
+ }
500
+ }
501
+ };
502
+ const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
503
+ await (0, react_hooks_1.act)(async () => {
504
+ await new Promise((resolve) => setTimeout(resolve, 10));
505
+ });
506
+ expect(result.current.isLoading).toBe(false);
507
+ expect(result.current.hasPermission).toBe(false);
508
+ expect(result.current.missingPermissions?.external?.fetch?.client).toEqual([
509
+ 'https://cdn.example.com/bundle.js/extra',
510
+ 'https://cdn.example.com/other.js',
511
+ 'https://cdn.example.com/'
512
+ ]);
513
+ });
514
+ });
515
+ describe('Backend vs Client CSP differences', () => {
516
+ it('should allow any path for backend (hostname-only matching)', async () => {
517
+ const mockContext = {
518
+ permissions: {
519
+ external: {
520
+ fetch: {
521
+ backend: ['https://api.example.com/specific/path']
522
+ }
523
+ }
524
+ }
525
+ };
526
+ mockGetContext.mockResolvedValue(mockContext);
527
+ const requiredPermissions = {
528
+ external: {
529
+ fetch: {
530
+ backend: [
531
+ 'https://api.example.com',
532
+ 'https://api.example.com/different/path',
533
+ 'https://api.example.com/specific/path'
534
+ ]
535
+ }
536
+ }
537
+ };
538
+ const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
539
+ await (0, react_hooks_1.act)(async () => {
540
+ await new Promise((resolve) => setTimeout(resolve, 10));
541
+ });
542
+ expect(result.current.isLoading).toBe(false);
543
+ expect(result.current.hasPermission).toBe(true);
544
+ });
545
+ it('should demonstrate backend allows but client blocks different paths', async () => {
546
+ const mockContext = {
547
+ permissions: {
548
+ external: {
549
+ fetch: {
550
+ backend: ['https://api.example.com/api/'],
551
+ client: ['https://api.example.com/api/']
552
+ }
553
+ }
554
+ }
555
+ };
556
+ mockGetContext.mockResolvedValue(mockContext);
557
+ const requiredPermissions = {
558
+ external: {
559
+ fetch: {
560
+ backend: ['https://api.example.com/private/secret.json'],
561
+ client: ['https://api.example.com/private/secret.json']
562
+ }
563
+ }
564
+ };
565
+ const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
566
+ await (0, react_hooks_1.act)(async () => {
567
+ await new Promise((resolve) => setTimeout(resolve, 10));
568
+ });
569
+ expect(result.current.isLoading).toBe(false);
570
+ expect(result.current.hasPermission).toBe(false);
571
+ // Backend should pass (hostname-only), client should fail (CSP path check)
572
+ expect(result.current.missingPermissions?.external?.fetch?.backend).toBeUndefined();
573
+ expect(result.current.missingPermissions?.external?.fetch?.client).toEqual([
574
+ 'https://api.example.com/private/secret.json'
575
+ ]);
576
+ });
577
+ });
578
+ describe('CSP for resource types', () => {
579
+ it('should use CSP validation for images with paths', async () => {
580
+ const mockContext = {
581
+ permissions: {
582
+ external: {
583
+ images: ['https://cdn.example.com/public/']
584
+ }
585
+ }
586
+ };
587
+ mockGetContext.mockResolvedValue(mockContext);
588
+ const requiredPermissions = {
589
+ external: {
590
+ images: ['https://cdn.example.com/public/image.png', 'https://cdn.example.com/public/nested/image.jpg']
591
+ }
592
+ };
593
+ const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
594
+ await (0, react_hooks_1.act)(async () => {
595
+ await new Promise((resolve) => setTimeout(resolve, 10));
596
+ });
597
+ expect(result.current.isLoading).toBe(false);
598
+ expect(result.current.hasPermission).toBe(true);
599
+ });
600
+ it('should block images outside allowed directory', async () => {
601
+ const mockContext = {
602
+ permissions: {
603
+ external: {
604
+ images: ['https://cdn.example.com/public/']
605
+ }
606
+ }
607
+ };
608
+ mockGetContext.mockResolvedValue(mockContext);
609
+ const requiredPermissions = {
610
+ external: {
611
+ images: ['https://cdn.example.com/private/image.png', 'https://cdn.example.com/image.png']
612
+ }
613
+ };
614
+ const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
615
+ await (0, react_hooks_1.act)(async () => {
616
+ await new Promise((resolve) => setTimeout(resolve, 10));
617
+ });
618
+ expect(result.current.isLoading).toBe(false);
619
+ expect(result.current.hasPermission).toBe(false);
620
+ expect(result.current.missingPermissions?.external?.images).toEqual([
621
+ 'https://cdn.example.com/private/image.png',
622
+ 'https://cdn.example.com/image.png'
623
+ ]);
624
+ });
625
+ it('should apply CSP to all resource types', async () => {
626
+ const mockContext = {
627
+ permissions: {
628
+ external: {
629
+ scripts: ['https://cdn.example.com'],
630
+ styles: ['https://cdn.example.com/css/'],
631
+ fonts: ['https://fonts.example.com/font.woff2']
632
+ }
633
+ }
634
+ };
635
+ mockGetContext.mockResolvedValue(mockContext);
636
+ const requiredPermissions = {
637
+ external: {
638
+ scripts: ['https://cdn.example.com/bundle.js'],
639
+ styles: ['https://cdn.example.com/css/main.css'],
640
+ fonts: ['https://fonts.example.com/font.woff2'] // Exact match
641
+ }
642
+ };
643
+ const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
644
+ await (0, react_hooks_1.act)(async () => {
645
+ await new Promise((resolve) => setTimeout(resolve, 10));
646
+ });
647
+ expect(result.current.isLoading).toBe(false);
648
+ expect(result.current.hasPermission).toBe(true);
649
+ });
650
+ });
651
+ describe('URL normalization and protocol handling', () => {
652
+ it('should handle URLs without protocol', async () => {
653
+ const mockContext = {
654
+ permissions: {
655
+ external: {
656
+ fetch: {
657
+ backend: ['api.example.com']
658
+ }
659
+ }
660
+ }
661
+ };
662
+ mockGetContext.mockResolvedValue(mockContext);
663
+ const requiredPermissions = {
664
+ external: {
665
+ fetch: {
666
+ backend: ['https://api.example.com', 'api.example.com']
667
+ }
668
+ }
669
+ };
670
+ const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
671
+ await (0, react_hooks_1.act)(async () => {
672
+ await new Promise((resolve) => setTimeout(resolve, 10));
673
+ });
674
+ expect(result.current.isLoading).toBe(false);
675
+ expect(result.current.hasPermission).toBe(true);
676
+ });
677
+ it('should handle CSP secure protocol upgrades (http -> https)', async () => {
678
+ const mockContext = {
679
+ permissions: {
680
+ external: {
681
+ fetch: {
682
+ client: ['http://example.com']
683
+ }
684
+ }
685
+ }
686
+ };
687
+ mockGetContext.mockResolvedValue(mockContext);
688
+ const requiredPermissions = {
689
+ external: {
690
+ fetch: {
691
+ client: ['http://example.com', 'https://example.com']
692
+ }
693
+ }
694
+ };
695
+ const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
696
+ await (0, react_hooks_1.act)(async () => {
697
+ await new Promise((resolve) => setTimeout(resolve, 10));
698
+ });
699
+ expect(result.current.isLoading).toBe(false);
700
+ expect(result.current.hasPermission).toBe(true);
701
+ });
702
+ it('should not allow protocol downgrades (https -> http)', async () => {
703
+ const mockContext = {
704
+ permissions: {
705
+ external: {
706
+ fetch: {
707
+ client: ['https://example.com']
708
+ }
709
+ }
710
+ }
711
+ };
712
+ mockGetContext.mockResolvedValue(mockContext);
713
+ const requiredPermissions = {
714
+ external: {
715
+ fetch: {
716
+ client: ['http://example.com']
717
+ }
718
+ }
719
+ };
720
+ const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
721
+ await (0, react_hooks_1.act)(async () => {
722
+ await new Promise((resolve) => setTimeout(resolve, 10));
723
+ });
724
+ expect(result.current.isLoading).toBe(false);
725
+ expect(result.current.hasPermission).toBe(false);
726
+ expect(result.current.missingPermissions?.external?.fetch?.client).toEqual(['http://example.com']);
727
+ });
728
+ });
366
729
  describe('Edge cases', () => {
367
730
  it('should handle empty required permissions', async () => {
368
731
  const mockContext = {
@@ -1,7 +1,3 @@
1
- /**
2
- * https://ecosystem-platform.atlassian.net/browse/DEPLOY-1411
3
- * reuse logic from @forge/api
4
- */
5
1
  /**
6
2
  * Resource types that can be loaded externally
7
3
  */
@@ -1 +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"}
1
+ {"version":3,"file":"usePermissions.d.ts","sourceRoot":"","sources":["../../src/hooks/usePermissions.ts"],"names":[],"mappings":"AAuBA;;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;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,cAAc,wBAAyB,sBAAsB;;;;;CAqJzE,CAAC"}
@@ -3,11 +3,24 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.usePermissions = void 0;
4
4
  const react_1 = require("react");
5
5
  const bridge_1 = require("@forge/bridge");
6
- const minimatch_1 = require("minimatch");
6
+ const egress_1 = require("@forge/egress");
7
7
  /**
8
8
  * https://ecosystem-platform.atlassian.net/browse/DEPLOY-1411
9
- * reuse logic from @forge/api
9
+ * Uses @forge/egress for URL matching (same logic as @forge/api)
10
10
  */
11
+ /**
12
+ * Helper function to extract URL string from external URL permissions.
13
+ * Matches the implementation in @forge/api for consistency.
14
+ */
15
+ function extractUrlString(url) {
16
+ if (typeof url === 'string') {
17
+ return url;
18
+ }
19
+ if ('address' in url && url.address) {
20
+ return url.address;
21
+ }
22
+ return url.remote || '';
23
+ }
11
24
  /**
12
25
  * Resource types that can be loaded externally
13
26
  */
@@ -16,16 +29,6 @@ const RESOURCE_TYPES = ['fonts', 'styles', 'frames', 'images', 'media', 'scripts
16
29
  * Fetch types for external requests
17
30
  */
18
31
  const FETCH_TYPES = ['backend', 'client'];
19
- /**
20
- * Helper function to check if a URL matches any of the allowed patterns
21
- * Uses minimatch for robust pattern matching with wildcards
22
- */
23
- const matchesAllowedUrl = (url, allowedUrls) => {
24
- return allowedUrls.some((allowedUrl) => {
25
- // Use minimatch for pattern matching
26
- return (0, minimatch_1.minimatch)(url, allowedUrl);
27
- });
28
- };
29
32
  /**
30
33
  * Hook for checking permissions in Forge apps
31
34
  *
@@ -96,29 +99,25 @@ const usePermissions = (requiredPermissions) => {
96
99
  const fetchUrls = external.fetch?.[type];
97
100
  if (!fetchUrls?.length)
98
101
  return false;
99
- // Extract string URLs from fetch URLs array
100
- const allowedUrls = fetchUrls
101
- .map((item) => {
102
- // If item is already a string, use it directly
103
- if (typeof item === 'string') {
104
- return item;
105
- }
106
- // If item has an address property, use that
107
- if ('address' in item && item.address) {
108
- return item.address;
109
- }
110
- // Otherwise, use the remote property (if it exists)
111
- return item.remote;
112
- })
113
- .filter((url) => typeof url === 'string');
114
- return matchesAllowedUrl(url, allowedUrls);
102
+ // Extract URLs and create egress filter
103
+ const allowList = fetchUrls.map(extractUrlString).filter((u) => u.length > 0);
104
+ if (allowList.length === 0)
105
+ return false;
106
+ const egressFilter = new egress_1.EgressFilteringService(allowList);
107
+ // Backend: hostname-only matching, Client: CSP validation (includes paths)
108
+ return type === 'client' ? egressFilter.isValidUrlCSP(url) : egressFilter.isValidUrl(url);
115
109
  },
116
110
  canLoadResource: (type, url) => {
117
111
  const resourceUrls = external[type];
118
112
  if (!resourceUrls?.length)
119
113
  return false;
120
- const stringUrls = resourceUrls.filter((item) => typeof item === 'string');
121
- return matchesAllowedUrl(url, stringUrls);
114
+ // Extract URLs and create egress filter
115
+ const allowList = resourceUrls.map(extractUrlString).filter((u) => u.length > 0);
116
+ if (allowList.length === 0)
117
+ return false;
118
+ const egressFilter = new egress_1.EgressFilteringService(allowList);
119
+ // All resources use CSP validation (checks protocol + hostname + paths)
120
+ return egressFilter.isValidUrlCSP(url);
122
121
  },
123
122
  getScopes: () => scopeArray,
124
123
  getExternalPermissions: () => external,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forge/react",
3
- "version": "11.9.0-next.1",
3
+ "version": "11.9.1-next.0",
4
4
  "description": "Forge React reconciler",
5
5
  "author": "Atlassian",
6
6
  "license": "SEE LICENSE IN LICENSE.txt",
@@ -28,12 +28,12 @@
28
28
  "@atlaskit/adf-schema": "^48.0.0",
29
29
  "@atlaskit/adf-utils": "^19.19.0",
30
30
  "@atlaskit/forge-react-types": "^0.48.0",
31
- "@forge/bridge": "^5.10.1-next.0",
31
+ "@forge/bridge": "^5.10.1",
32
+ "@forge/egress": "^2.3.1",
32
33
  "@forge/i18n": "0.0.7",
33
34
  "@types/react": "^18.2.64",
34
35
  "@types/react-reconciler": "^0.28.8",
35
36
  "lodash": "^4.17.21",
36
- "minimatch": "^9.0.5",
37
37
  "react": "^18.2.0",
38
38
  "react-hook-form": "7.65.0",
39
39
  "react-reconciler": "^0.29.0",