@forge/react 11.9.2-next.0-experimental-919607a → 11.10.0-next.1-experimental-cb1ecce

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,10 +1,26 @@
1
1
  # @forge/react
2
2
 
3
- ## 11.9.2-next.0-experimental-919607a
3
+ ## 11.10.0-next.1-experimental-cb1ecce
4
+
5
+ ### Minor Changes
6
+
7
+ - f058dd8: Expose Permissions API in @forge/bridge for Custom UI apps
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [f058dd8]
12
+ - @forge/bridge@5.11.0-next.3-experimental-cb1ecce
13
+
14
+ ## 11.10.0-next.1
15
+
16
+ ### Minor Changes
17
+
18
+ - f058dd8: Expose Permissions API in @forge/bridge for Custom UI apps
4
19
 
5
20
  ### Patch Changes
6
21
 
7
- - @forge/bridge@5.10.3-next.1-experimental-919607a
22
+ - Updated dependencies [f058dd8]
23
+ - @forge/bridge@5.11.0-next.2
8
24
 
9
25
  ## 11.9.2-next.0
10
26
 
@@ -4,11 +4,25 @@ const react_hooks_1 = require("@testing-library/react-hooks");
4
4
  const usePermissions_1 = require("../usePermissions");
5
5
  const testUtils_1 = require("../../__test__/testUtils");
6
6
  // Mock @forge/bridge
7
- jest.mock('@forge/bridge', () => ({
8
- view: {
9
- getContext: jest.fn()
7
+ jest.mock('@forge/bridge', () => {
8
+ // Set up window before requiring actual bridge to avoid initialization issues
9
+ if (typeof window === 'undefined') {
10
+ // @ts-ignore
11
+ global.window = global;
12
+ // @ts-ignore
13
+ global.window.__bridge = {
14
+ callBridge: jest.fn()
15
+ };
10
16
  }
11
- }));
17
+ const actualBridge = jest.requireActual('@forge/bridge');
18
+ return {
19
+ ...actualBridge,
20
+ view: {
21
+ ...actualBridge.view,
22
+ getContext: jest.fn()
23
+ }
24
+ };
25
+ });
12
26
  const mockGetContext = jest.fn();
13
27
  describe('usePermissions', () => {
14
28
  beforeEach(() => {
@@ -1,13 +1,5 @@
1
- /**
2
- * Resource types that can be loaded externally
3
- */
4
- declare const RESOURCE_TYPES: readonly ["fonts", "styles", "frames", "images", "media", "scripts"];
5
- export declare type ResourceType = (typeof RESOURCE_TYPES)[number];
6
- /**
7
- * Fetch types for external requests
8
- */
9
- declare const FETCH_TYPES: readonly ["backend", "client"];
10
- export declare type FetchType = (typeof FETCH_TYPES)[number];
1
+ import { type PermissionRequirements, type MissingPermissions, type PermissionCheckResult, type ResourceType, type FetchType } from '@forge/bridge';
2
+ export type { ResourceType, FetchType };
11
3
  export interface Permissions {
12
4
  scopes?: string[];
13
5
  external?: {
@@ -24,21 +16,7 @@ export interface Permissions {
24
16
  };
25
17
  content?: Record<string, unknown>;
26
18
  }
27
- /**
28
- * Required permissions for a component
29
- */
30
- export declare type PermissionRequirements = Permissions;
31
- /**
32
- * Missing permissions information
33
- */
34
- export declare type MissingPermissions = Permissions;
35
- /**
36
- * Permission check result
37
- */
38
- export interface PermissionCheckResult {
39
- granted: boolean;
40
- missing: MissingPermissions | null;
41
- }
19
+ export type { PermissionRequirements, MissingPermissions, PermissionCheckResult };
42
20
  /**
43
21
  * Hook for checking permissions in Forge apps
44
22
  *
@@ -78,8 +56,7 @@ export interface PermissionCheckResult {
78
56
  export declare const usePermissions: (requiredPermissions: PermissionRequirements) => {
79
57
  hasPermission: boolean;
80
58
  isLoading: boolean;
81
- missingPermissions: Permissions | null;
59
+ missingPermissions: PermissionRequirements | null;
82
60
  error: Error | null;
83
61
  };
84
- export {};
85
62
  //# sourceMappingURL=usePermissions.d.ts.map
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"usePermissions.d.ts","sourceRoot":"","sources":["../../src/hooks/usePermissions.ts"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EACvB,KAAK,qBAAqB,EAC1B,KAAK,YAAY,EACjB,KAAK,SAAS,EACf,MAAM,eAAe,CAAC;AAGvB,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC;AAExC,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;AAGD,YAAY,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,CAAC;AAElF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,cAAc,wBAAyB,sBAAsB;;;;;CAoEzE,CAAC"}
@@ -3,32 +3,6 @@ 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 egress_1 = require("@forge/egress");
7
- /**
8
- * https://ecosystem-platform.atlassian.net/browse/DEPLOY-1411
9
- * Uses @forge/egress for URL matching (same logic as @forge/api)
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
- }
24
- /**
25
- * Resource types that can be loaded externally
26
- */
27
- const RESOURCE_TYPES = ['fonts', 'styles', 'frames', 'images', 'media', 'scripts'];
28
- /**
29
- * Fetch types for external requests
30
- */
31
- const FETCH_TYPES = ['backend', 'client'];
32
6
  /**
33
7
  * Hook for checking permissions in Forge apps
34
8
  *
@@ -69,6 +43,10 @@ const usePermissions = (requiredPermissions) => {
69
43
  const [context, setContext] = (0, react_1.useState)();
70
44
  const [isLoading, setIsLoading] = (0, react_1.useState)(true);
71
45
  const [error, setError] = (0, react_1.useState)(null);
46
+ const [permissionResult, setPermissionResult] = (0, react_1.useState)({
47
+ granted: false,
48
+ missing: null
49
+ });
72
50
  // Load context on mount
73
51
  (0, react_1.useEffect)(() => {
74
52
  const loadContext = async () => {
@@ -87,106 +65,37 @@ const usePermissions = (requiredPermissions) => {
87
65
  };
88
66
  void loadContext();
89
67
  }, []);
90
- // Permission checking utilities
91
- const permissionUtils = (0, react_1.useMemo)(() => {
92
- if (!context?.permissions)
93
- return null;
94
- const { scopes, external = {} } = context.permissions;
95
- const scopeArray = Array.isArray(scopes) ? scopes : Object.keys(scopes || {});
96
- return {
97
- hasScope: (scope) => scopeArray.includes(scope),
98
- canFetchFrom: (type, url) => {
99
- const fetchUrls = external.fetch?.[type];
100
- if (!fetchUrls?.length)
101
- return false;
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);
109
- },
110
- canLoadResource: (type, url) => {
111
- const resourceUrls = external[type];
112
- if (!resourceUrls?.length)
113
- return false;
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);
121
- },
122
- getScopes: () => scopeArray,
123
- getExternalPermissions: () => external,
124
- hasAnyPermissions: () => scopeArray.length > 0 || Object.keys(external).length > 0
125
- };
126
- }, [context?.permissions]);
127
- // Check permissions
128
- const permissionResult = (0, react_1.useMemo)(() => {
129
- if (!requiredPermissions) {
130
- return { granted: false, missing: null };
131
- }
132
- if (!permissionUtils) {
133
- // If still loading or there's an error, return null for missing permissions
134
- if (isLoading || error) {
135
- return { granted: false, missing: null };
136
- }
137
- throw new Error('This feature is not available yet');
68
+ // Check permissions using shared utility
69
+ (0, react_1.useEffect)(() => {
70
+ // Skip if still loading context
71
+ if (isLoading) {
72
+ return;
138
73
  }
139
- const missing = {};
140
- let hasAllRequiredPermissions = true;
141
- // Check scopes
142
- if (requiredPermissions.scopes?.length) {
143
- const missingScopes = requiredPermissions.scopes.filter((scope) => !permissionUtils.hasScope(scope));
144
- if (missingScopes.length > 0) {
145
- missing.scopes = missingScopes;
146
- hasAllRequiredPermissions = false;
74
+ const checkPerms = async () => {
75
+ if (!requiredPermissions) {
76
+ setPermissionResult({ granted: false, missing: null });
77
+ return;
147
78
  }
148
- }
149
- // Check external permissions
150
- if (requiredPermissions.external) {
151
- const missingExternal = {};
152
- // Check fetch permissions
153
- if (requiredPermissions.external.fetch) {
154
- const missingFetch = {};
155
- FETCH_TYPES.forEach((type) => {
156
- const requiredUrls = requiredPermissions.external?.fetch?.[type];
157
- if (requiredUrls?.length) {
158
- const missingUrls = requiredUrls.filter((url) => !permissionUtils.canFetchFrom(type, url));
159
- if (missingUrls.length > 0) {
160
- missingFetch[type] = missingUrls;
161
- hasAllRequiredPermissions = false;
162
- }
163
- }
164
- });
165
- if (Object.keys(missingFetch).length > 0) {
166
- missingExternal.fetch = missingFetch;
79
+ if (!context?.permissions) {
80
+ // If context loaded but has no permissions, set error
81
+ if (context !== undefined) {
82
+ setError(new Error('This feature is not available yet'));
83
+ setPermissionResult({ granted: false, missing: null });
167
84
  }
85
+ return;
168
86
  }
169
- // Check resource permissions
170
- RESOURCE_TYPES.forEach((type) => {
171
- const requiredUrls = requiredPermissions.external?.[type];
172
- if (requiredUrls?.length) {
173
- const missingUrls = requiredUrls.filter((url) => !permissionUtils.canLoadResource(type, url));
174
- if (missingUrls.length > 0) {
175
- missingExternal[type] = missingUrls;
176
- hasAllRequiredPermissions = false;
177
- }
178
- }
179
- });
180
- if (Object.keys(missingExternal).length > 0) {
181
- missing.external = missingExternal;
87
+ try {
88
+ setError(null); // Clear any previous errors
89
+ const result = await (0, bridge_1.checkPermissions)(requiredPermissions, context.permissions);
90
+ setPermissionResult(result);
91
+ }
92
+ catch (err) {
93
+ setError(err instanceof Error ? err : new Error('Failed to check permissions'));
94
+ setPermissionResult({ granted: false, missing: null });
182
95
  }
183
- }
184
- // Note: Content permissions are not supported in the current RuntimePermissions type
185
- return {
186
- granted: hasAllRequiredPermissions,
187
- missing: hasAllRequiredPermissions ? null : missing
188
96
  };
189
- }, [permissionUtils, requiredPermissions]);
97
+ void checkPerms();
98
+ }, [context, requiredPermissions, isLoading]);
190
99
  return {
191
100
  hasPermission: permissionResult.granted,
192
101
  isLoading,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forge/react",
3
- "version": "11.9.2-next.0-experimental-919607a",
3
+ "version": "11.10.0-next.1-experimental-cb1ecce",
4
4
  "description": "Forge React reconciler",
5
5
  "author": "Atlassian",
6
6
  "license": "SEE LICENSE IN LICENSE.txt",
@@ -28,7 +28,7 @@
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.3-next.1-experimental-919607a",
31
+ "@forge/bridge": "^5.11.0-next.3-experimental-cb1ecce",
32
32
  "@forge/egress": "^2.3.1",
33
33
  "@forge/i18n": "0.0.7",
34
34
  "@types/react": "^18.2.64",