@forge/react 11.9.2-next.0 → 11.10.0-next.1
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,16 @@
|
|
|
1
1
|
# @forge/react
|
|
2
2
|
|
|
3
|
+
## 11.10.0-next.1
|
|
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.2
|
|
13
|
+
|
|
3
14
|
## 11.9.2-next.0
|
|
4
15
|
|
|
5
16
|
### Patch Changes
|
|
@@ -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
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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:
|
|
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":"
|
|
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
|
-
//
|
|
91
|
-
|
|
92
|
-
if
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "11.10.0-next.1",
|
|
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.
|
|
31
|
+
"@forge/bridge": "^5.11.0-next.2",
|
|
32
32
|
"@forge/egress": "^2.3.1",
|
|
33
33
|
"@forge/i18n": "0.0.7",
|
|
34
34
|
"@types/react": "^18.2.64",
|