@forge/react
11.9.1-experimental-60ea29e11.9.2-next.0
~
Modified (7 files)
Index: package/out/hooks/usePermissions.js
===================================================================
--- package/out/hooks/usePermissions.js
+++ package/out/hooks/usePermissions.js
@@ -2,9 +2,35 @@
Object.defineProperty(exports, "__esModule", { value: true });
exports.usePermissions = void 0;
const react_1 = require("react");
const bridge_1 = require("@forge/bridge");
+const egress_1 = require("@forge/egress");
/**
+ * https://ecosystem-platform.atlassian.net/browse/DEPLOY-1411
+ * Uses @forge/egress for URL matching (same logic as @forge/api)
+ */
+/**
+ * Helper function to extract URL string from external URL permissions.
+ * Matches the implementation in @forge/api for consistency.
+ */
+function extractUrlString(url) {
+ if (typeof url === 'string') {
+ return url;
+ }
+ if ('address' in url && url.address) {
+ return url.address;
+ }
+ return url.remote || '';
+}
+/**
+ * Resource types that can be loaded externally
+ */
+const RESOURCE_TYPES = ['fonts', 'styles', 'frames', 'images', 'media', 'scripts'];
+/**
+ * Fetch types for external requests
+ */
+const FETCH_TYPES = ['backend', 'client'];
+/**
* Hook for checking permissions in Forge apps
*
* @param requiredPermissions - The permissions required for the component
* @returns Object containing permission state, loading status, and error information
@@ -42,12 +68,8 @@
const usePermissions = (requiredPermissions) => {
const [context, setContext] = (0, react_1.useState)();
const [isLoading, setIsLoading] = (0, react_1.useState)(true);
const [error, setError] = (0, react_1.useState)(null);
- const [permissionResult, setPermissionResult] = (0, react_1.useState)({
- granted: false,
- missing: null
- });
// Load context on mount
(0, react_1.useEffect)(() => {
const loadContext = async () => {
try {
@@ -64,39 +86,108 @@
}
};
void loadContext();
}, []);
- // Check permissions using shared utility
- (0, react_1.useEffect)(() => {
- // Skip if still loading context
- if (isLoading) {
- return;
+ // Permission checking utilities
+ const permissionUtils = (0, react_1.useMemo)(() => {
+ if (!context?.permissions)
+ return null;
+ const { scopes, external = {} } = context.permissions;
+ const scopeArray = Array.isArray(scopes) ? scopes : Object.keys(scopes || {});
+ return {
+ hasScope: (scope) => scopeArray.includes(scope),
+ canFetchFrom: (type, url) => {
+ const fetchUrls = external.fetch?.[type];
+ if (!fetchUrls?.length)
+ return false;
+ // Extract URLs and create egress filter
+ const allowList = fetchUrls.map(extractUrlString).filter((u) => u.length > 0);
+ if (allowList.length === 0)
+ return false;
+ const egressFilter = new egress_1.EgressFilteringService(allowList);
+ // Backend: hostname-only matching, Client: CSP validation (includes paths)
+ return type === 'client' ? egressFilter.isValidUrlCSP(url) : egressFilter.isValidUrl(url);
+ },
+ canLoadResource: (type, url) => {
+ const resourceUrls = external[type];
+ if (!resourceUrls?.length)
+ return false;
+ // Extract URLs and create egress filter
+ const allowList = resourceUrls.map(extractUrlString).filter((u) => u.length > 0);
+ if (allowList.length === 0)
+ return false;
+ const egressFilter = new egress_1.EgressFilteringService(allowList);
+ // All resources use CSP validation (checks protocol + hostname + paths)
+ return egressFilter.isValidUrlCSP(url);
+ },
+ getScopes: () => scopeArray,
+ getExternalPermissions: () => external,
+ hasAnyPermissions: () => scopeArray.length > 0 || Object.keys(external).length > 0
+ };
+ }, [context?.permissions]);
+ // Check permissions
+ const permissionResult = (0, react_1.useMemo)(() => {
+ if (!requiredPermissions) {
+ return { granted: false, missing: null };
}
- const checkPerms = async () => {
- if (!requiredPermissions) {
- setPermissionResult({ granted: false, missing: null });
- return;
+ if (!permissionUtils) {
+ // If still loading or there's an error, return null for missing permissions
+ if (isLoading || error) {
+ return { granted: false, missing: null };
}
- if (!context?.permissions) {
- // If context loaded but has no permissions, set error
- if (context !== undefined) {
- setError(new Error('This feature is not available yet'));
- setPermissionResult({ granted: false, missing: null });
+ throw new Error('This feature is not available yet');
+ }
+ const missing = {};
+ let hasAllRequiredPermissions = true;
+ // Check scopes
+ if (requiredPermissions.scopes?.length) {
+ const missingScopes = requiredPermissions.scopes.filter((scope) => !permissionUtils.hasScope(scope));
+ if (missingScopes.length > 0) {
+ missing.scopes = missingScopes;
+ hasAllRequiredPermissions = false;
+ }
+ }
+ // Check external permissions
+ if (requiredPermissions.external) {
+ const missingExternal = {};
+ // Check fetch permissions
+ if (requiredPermissions.external.fetch) {
+ const missingFetch = {};
+ FETCH_TYPES.forEach((type) => {
+ const requiredUrls = requiredPermissions.external?.fetch?.[type];
+ if (requiredUrls?.length) {
+ const missingUrls = requiredUrls.filter((url) => !permissionUtils.canFetchFrom(type, url));
+ if (missingUrls.length > 0) {
+ missingFetch[type] = missingUrls;
+ hasAllRequiredPermissions = false;
+ }
+ }
+ });
+ if (Object.keys(missingFetch).length > 0) {
+ missingExternal.fetch = missingFetch;
}
- return;
}
- try {
- setError(null); // Clear any previous errors
- const result = await (0, bridge_1.checkPermissions)(requiredPermissions, context.permissions);
- setPermissionResult(result);
+ // Check resource permissions
+ RESOURCE_TYPES.forEach((type) => {
+ const requiredUrls = requiredPermissions.external?.[type];
+ if (requiredUrls?.length) {
+ const missingUrls = requiredUrls.filter((url) => !permissionUtils.canLoadResource(type, url));
+ if (missingUrls.length > 0) {
+ missingExternal[type] = missingUrls;
+ hasAllRequiredPermissions = false;
+ }
+ }
+ });
+ if (Object.keys(missingExternal).length > 0) {
+ missing.external = missingExternal;
}
- catch (err) {
- setError(err instanceof Error ? err : new Error('Failed to check permissions'));
- setPermissionResult({ granted: false, missing: null });
- }
+ }
+ // Note: Content permissions are not supported in the current RuntimePermissions type
+ return {
+ granted: hasAllRequiredPermissions,
+ missing: hasAllRequiredPermissions ? null : missing
};
- void checkPerms();
- }, [context, requiredPermissions, isLoading]);
+ }, [permissionUtils, requiredPermissions]);
return {
hasPermission: permissionResult.granted,
isLoading,
missingPermissions: permissionResult.missing, Index: package/out/hooks/__test__/usePermissions.test.js
===================================================================
--- package/out/hooks/__test__/usePermissions.test.js
+++ package/out/hooks/__test__/usePermissions.test.js
@@ -3,27 +3,13 @@
const react_hooks_1 = require("@testing-library/react-hooks");
const usePermissions_1 = require("../usePermissions");
const testUtils_1 = require("../../__test__/testUtils");
// Mock @forge/bridge
-jest.mock('@forge/bridge', () => {
- // Set up window before requiring actual bridge to avoid initialization issues
- if (typeof window === 'undefined') {
- // @ts-ignore
- global.window = global;
- // @ts-ignore
- global.window.__bridge = {
- callBridge: jest.fn()
- };
+jest.mock('@forge/bridge', () => ({
+ view: {
+ getContext: jest.fn()
}
- const actualBridge = jest.requireActual('@forge/bridge');
- return {
- ...actualBridge,
- view: {
- ...actualBridge.view,
- getContext: jest.fn()
- }
- };
-});
+}));
const mockGetContext = jest.fn();
describe('usePermissions', () => {
beforeEach(() => {
jest.clearAllMocks(); Index: package/package.json
===================================================================
--- package/package.json
+++ package/package.json
@@ -1,7 +1,7 @@
{
"name": "@forge/react",
- "version": "11.9.1-experimental-60ea29e",
+ "version": "11.9.2-next.0",
"description": "Forge React reconciler",
"author": "Atlassian",
"license": "SEE LICENSE IN LICENSE.txt",
"main": "out/index.js",
@@ -27,9 +27,9 @@
"dependencies": {
"@atlaskit/adf-schema": "^48.0.0",
"@atlaskit/adf-utils": "^19.19.0",
"@atlaskit/forge-react-types": "^0.48.0",
- "@forge/bridge": "^5.10.2-experimental-60ea29e",
+ "@forge/bridge": "^5.10.3-next.0",
"@forge/egress": "^2.3.1",
"@forge/i18n": "0.0.7",
"@types/react": "^18.2.64",
"@types/react-reconciler": "^0.28.8", Index: package/out/hooks/usePermissions.d.ts.map
===================================================================
--- package/out/hooks/usePermissions.d.ts.map
+++ package/out/hooks/usePermissions.d.ts.map
@@ -1,1 +1,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"}
\ No newline at end of file
+{"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"}
\ No newline at end of file Index: package/CHANGELOG.md
===================================================================
--- package/CHANGELOG.md
+++ package/CHANGELOG.md
@@ -1,16 +1,11 @@
# @forge/react
-## 11.9.1-experimental-60ea29e
+## 11.9.2-next.0
-### Minor Changes
-
-- f058dd8: Expose Permissions API in @forge/bridge for Custom UI apps
-
### Patch Changes
-- Updated dependencies [f058dd8]
- - @forge/[email protected]
+- @forge/[email protected]
## 11.9.1
### Patch Changes Index: package/out/hooks/usePermissions.d.ts
===================================================================
--- package/out/hooks/usePermissions.d.ts
+++ package/out/hooks/usePermissions.d.ts
@@ -1,6 +1,14 @@
-import { type PermissionRequirements, type MissingPermissions, type PermissionCheckResult, type ResourceType, type FetchType } from '@forge/bridge';
-export type { ResourceType, FetchType };
+/**
+ * Resource types that can be loaded externally
+ */
+declare const RESOURCE_TYPES: readonly ["fonts", "styles", "frames", "images", "media", "scripts"];
+export declare type ResourceType = (typeof RESOURCE_TYPES)[number];
+/**
+ * Fetch types for external requests
+ */
+declare const FETCH_TYPES: readonly ["backend", "client"];
+export declare type FetchType = (typeof FETCH_TYPES)[number];
export interface Permissions {
scopes?: string[];
external?: {
fetch?: {
@@ -15,10 +23,24 @@
scripts?: string[];
};
content?: Record<string, unknown>;
}
-export type { PermissionRequirements, MissingPermissions, PermissionCheckResult };
/**
+ * Required permissions for a component
+ */
+export declare type PermissionRequirements = Permissions;
+/**
+ * Missing permissions information
+ */
+export declare type MissingPermissions = Permissions;
+/**
+ * Permission check result
+ */
+export interface PermissionCheckResult {
+ granted: boolean;
+ missing: MissingPermissions | null;
+}
+/**
* Hook for checking permissions in Forge apps
*
* @param requiredPermissions - The permissions required for the component
* @returns Object containing permission state, loading status, and error information
@@ -55,8 +77,9 @@
*/
export declare const usePermissions: (requiredPermissions: PermissionRequirements) => {
hasPermission: boolean;
isLoading: boolean;
- missingPermissions: PermissionRequirements | null;
+ missingPermissions: Permissions | null;
error: Error | null;
};
+export {};
//# sourceMappingURL=usePermissions.d.ts.map
\ No newline at end of file File too large for inline diff