@microsoft/power-apps-native-host 0.4.0 → 0.5.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/README.md +60 -0
- package/compatibility/current.json +1 -1
- package/lib/NOTICE.txt +2957 -371
- package/lib/commonjs/auth/AuthContext.js +49 -22
- package/lib/commonjs/auth/targetAuthorization.js +53 -0
- package/lib/commonjs/bridge/NativePowerAppsBridge.js +33 -2
- package/lib/commonjs/bridge/PowerPagesBridge.js +1 -3
- package/lib/commonjs/bridge/setupNativeHost.js +6 -0
- package/lib/commonjs/config/expoConfig.js +14 -0
- package/lib/commonjs/config/runtimeTenantMode.js +16 -0
- package/lib/commonjs/config/runtimeTenantMode.web.js +9 -0
- package/lib/commonjs/config/tenantMode.js +68 -0
- package/lib/commonjs/connections/PowerPlatformConnectionApi.js +24 -27
- package/lib/commonjs/connections/connectionMetadata.js +2 -2
- package/lib/commonjs/connections/resolveConnections.js +8 -3
- package/lib/commonjs/connections/types.js +9 -2
- package/lib/commonjs/connections/useConnectionRefs.js +19 -10
- package/lib/commonjs/index.js +39 -0
- package/lib/commonjs/platform/CanvasAppResolution.js +33 -0
- package/lib/commonjs/platform/CanvasAppResolutionApi.js +69 -0
- package/lib/commonjs/platform/DataverseGlobalDiscoveryApi.js +50 -13
- package/lib/commonjs/platform/GlobalDiscovery.js +40 -1
- package/lib/commonjs/provider/PowerAppsHostProvider.js +47 -8
- package/lib/commonjs/provider/PowerAppsProvider.js +172 -6
- package/lib/commonjs/target/DefaultPowerAppsExperience.js +564 -0
- package/lib/commonjs/target/PowerAppsExperience.js +41 -0
- package/lib/commonjs/target/PowerAppsTargetBootstrap.js +534 -0
- package/lib/commonjs/target/errors.js +24 -0
- package/lib/commonjs/target/types.js +1 -0
- package/lib/module/auth/AuthContext.js +49 -22
- package/lib/module/auth/targetAuthorization.js +48 -0
- package/lib/module/bridge/NativePowerAppsBridge.js +33 -2
- package/lib/module/bridge/PowerPagesBridge.js +1 -3
- package/lib/module/bridge/setupNativeHost.js +5 -0
- package/lib/module/config/expoConfig.js +14 -0
- package/lib/module/config/runtimeTenantMode.js +12 -0
- package/lib/module/config/runtimeTenantMode.web.js +5 -0
- package/lib/module/config/tenantMode.js +61 -0
- package/lib/module/connections/PowerPlatformConnectionApi.js +24 -27
- package/lib/module/connections/connectionMetadata.js +2 -2
- package/lib/module/connections/resolveConnections.js +9 -4
- package/lib/module/connections/types.js +9 -2
- package/lib/module/connections/useConnectionRefs.js +20 -11
- package/lib/module/index.js +4 -1
- package/lib/module/platform/CanvasAppResolution.js +27 -0
- package/lib/module/platform/CanvasAppResolutionApi.js +65 -0
- package/lib/module/platform/DataverseGlobalDiscoveryApi.js +49 -14
- package/lib/module/platform/GlobalDiscovery.js +37 -1
- package/lib/module/provider/PowerAppsHostProvider.js +47 -8
- package/lib/module/provider/PowerAppsProvider.js +176 -10
- package/lib/module/target/DefaultPowerAppsExperience.js +559 -0
- package/lib/module/target/PowerAppsExperience.js +34 -0
- package/lib/module/target/PowerAppsTargetBootstrap.js +529 -0
- package/lib/module/target/errors.js +19 -0
- package/lib/module/target/types.js +1 -0
- package/lib/typescript/commonjs/auth/AuthContext.d.ts +11 -2
- package/lib/typescript/commonjs/auth/targetAuthorization.d.ts +15 -0
- package/lib/typescript/commonjs/bridge/NativePowerAppsBridge.d.ts +6 -0
- package/lib/typescript/commonjs/bridge/setupNativeHost.d.ts +2 -0
- package/lib/typescript/commonjs/config/runtimeTenantMode.d.ts +3 -0
- package/lib/typescript/commonjs/config/runtimeTenantMode.web.d.ts +3 -0
- package/lib/typescript/commonjs/config/tenantMode.d.ts +14 -0
- package/lib/typescript/commonjs/connections/PowerPlatformConnectionApi.d.ts +5 -2
- package/lib/typescript/commonjs/connections/connectionMetadata.d.ts +1 -1
- package/lib/typescript/commonjs/connections/types.d.ts +7 -1
- package/lib/typescript/commonjs/connections/useConnectionRefs.d.ts +11 -2
- package/lib/typescript/commonjs/index.d.ts +5 -1
- package/lib/typescript/commonjs/platform/CanvasAppResolution.d.ts +4 -0
- package/lib/typescript/commonjs/platform/CanvasAppResolutionApi.d.ts +6 -0
- package/lib/typescript/commonjs/platform/DataverseGlobalDiscoveryApi.d.ts +12 -2
- package/lib/typescript/commonjs/platform/GlobalDiscovery.d.ts +12 -0
- package/lib/typescript/commonjs/provider/PowerAppsHostProvider.d.ts +9 -1
- package/lib/typescript/commonjs/provider/PowerAppsProvider.d.ts +13 -15
- package/lib/typescript/commonjs/target/DefaultPowerAppsExperience.d.ts +6 -0
- package/lib/typescript/commonjs/target/PowerAppsExperience.d.ts +61 -0
- package/lib/typescript/commonjs/target/PowerAppsTargetBootstrap.d.ts +15 -0
- package/lib/typescript/commonjs/target/errors.d.ts +13 -0
- package/lib/typescript/commonjs/target/types.d.ts +34 -0
- package/lib/typescript/module/auth/AuthContext.d.ts +11 -2
- package/lib/typescript/module/auth/targetAuthorization.d.ts +15 -0
- package/lib/typescript/module/bridge/NativePowerAppsBridge.d.ts +6 -0
- package/lib/typescript/module/bridge/setupNativeHost.d.ts +2 -0
- package/lib/typescript/module/config/runtimeTenantMode.d.ts +3 -0
- package/lib/typescript/module/config/runtimeTenantMode.web.d.ts +3 -0
- package/lib/typescript/module/config/tenantMode.d.ts +14 -0
- package/lib/typescript/module/connections/PowerPlatformConnectionApi.d.ts +5 -2
- package/lib/typescript/module/connections/connectionMetadata.d.ts +1 -1
- package/lib/typescript/module/connections/types.d.ts +7 -1
- package/lib/typescript/module/connections/useConnectionRefs.d.ts +11 -2
- package/lib/typescript/module/index.d.ts +5 -1
- package/lib/typescript/module/platform/CanvasAppResolution.d.ts +4 -0
- package/lib/typescript/module/platform/CanvasAppResolutionApi.d.ts +6 -0
- package/lib/typescript/module/platform/DataverseGlobalDiscoveryApi.d.ts +12 -2
- package/lib/typescript/module/platform/GlobalDiscovery.d.ts +12 -0
- package/lib/typescript/module/provider/PowerAppsHostProvider.d.ts +9 -1
- package/lib/typescript/module/provider/PowerAppsProvider.d.ts +13 -15
- package/lib/typescript/module/target/DefaultPowerAppsExperience.d.ts +6 -0
- package/lib/typescript/module/target/PowerAppsExperience.d.ts +61 -0
- package/lib/typescript/module/target/PowerAppsTargetBootstrap.d.ts +15 -0
- package/lib/typescript/module/target/errors.d.ts +13 -0
- package/lib/typescript/module/target/types.d.ts +34 -0
- package/package.json +12 -5
- package/scripts/build-codegen-package.js +34 -3
- package/scripts/dev.js +405 -0
- package/scripts/upgrade-authoring.js +85 -28
- package/scripts/wrap.js +42 -5
- package/src/config/generated-config.d.ts +21 -0
|
@@ -21,6 +21,7 @@ import { Platform } from 'react-native';
|
|
|
21
21
|
import { PublicClientApplication, registerAndEnrollAccount, deRegisterAndUnenrollAccount, computeMsalRedirectUri, getWrapConfig } from '@microsoft/power-apps-native-auth';
|
|
22
22
|
import { getAppLogger, HostOperationNames } from "../telemetry/index.js";
|
|
23
23
|
import { debugLog, debugWarn } from "../logger.js";
|
|
24
|
+
import { authorizeRequiredScopes } from "./targetAuthorization.js";
|
|
24
25
|
|
|
25
26
|
// The Power Platform API (api.powerplatform.com) exposes its own scopes, not raw
|
|
26
27
|
// OIDC scopes. Requesting 'openid'/'profile' alone causes AAD to decline.
|
|
@@ -28,25 +29,19 @@ import { debugLog, debugWarn } from "../logger.js";
|
|
|
28
29
|
// implicitly includes openid + profile + offline_access for the sign-in flow.
|
|
29
30
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
30
31
|
const SIGN_IN_SCOPES = ['https://api.powerplatform.com/.default'];
|
|
32
|
+
const AUTHENTICATION_ERROR_MESSAGE = 'We could not sign you in. Try again.';
|
|
31
33
|
function getSafeErrorName(error) {
|
|
32
34
|
return error instanceof Error ? error.constructor.name : typeof error;
|
|
33
35
|
}
|
|
34
36
|
function getErrorProperty(error, propertyName) {
|
|
35
37
|
return error && typeof error === 'object' ? error[propertyName] : undefined;
|
|
36
38
|
}
|
|
37
|
-
function getSafeErrorMessage(error) {
|
|
38
|
-
if (error instanceof Error) return error.message;
|
|
39
|
-
return typeof error === 'string' ? error : String(error);
|
|
40
|
-
}
|
|
41
39
|
function getAuthErrorTelemetry(error) {
|
|
42
|
-
const errorMessage = getSafeErrorMessage(error);
|
|
43
|
-
const parsedNSError = /domain=([^\s]+) code=(-?\d+)/.exec(errorMessage);
|
|
44
40
|
return {
|
|
45
41
|
errorName: getSafeErrorName(error),
|
|
46
42
|
errorCode: getErrorProperty(error, 'code'),
|
|
47
|
-
nativeErrorDomain: getErrorProperty(error, 'nativeErrorDomain')
|
|
48
|
-
nativeErrorCode: getErrorProperty(error, 'nativeErrorCode')
|
|
49
|
-
nativeErrorMessage: errorMessage.slice(0, 512)
|
|
43
|
+
nativeErrorDomain: getErrorProperty(error, 'nativeErrorDomain'),
|
|
44
|
+
nativeErrorCode: getErrorProperty(error, 'nativeErrorCode')
|
|
50
45
|
};
|
|
51
46
|
}
|
|
52
47
|
function getAuthTelemetryProperties(useBroker, useEmbeddedWebView) {
|
|
@@ -60,6 +55,14 @@ function getAuthTelemetryProperties(useBroker, useEmbeddedWebView) {
|
|
|
60
55
|
function accountsMatch(left, right) {
|
|
61
56
|
return left.identifier === right.identifier && left.tenantId === right.tenantId;
|
|
62
57
|
}
|
|
58
|
+
function normalizeTenantId(tenantId) {
|
|
59
|
+
return tenantId.trim().toLowerCase();
|
|
60
|
+
}
|
|
61
|
+
function assertExpectedTenant(account, expectedTenantId) {
|
|
62
|
+
if (expectedTenantId && normalizeTenantId(account.tenantId) !== normalizeTenantId(expectedTenantId)) {
|
|
63
|
+
throw new Error('The signed-in account is not available in the selected environment tenant.');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
63
66
|
async function validateRequiredScopes(pca, account, scopes) {
|
|
64
67
|
for (const scope of scopes) {
|
|
65
68
|
await pca.acquireTokenSilent({
|
|
@@ -68,13 +71,14 @@ async function validateRequiredScopes(pca, account, scopes) {
|
|
|
68
71
|
});
|
|
69
72
|
}
|
|
70
73
|
}
|
|
71
|
-
async function acquireRequiredScopesInteractively(pca, scopes) {
|
|
74
|
+
async function acquireRequiredScopesInteractively(pca, scopes, expectedTenantId) {
|
|
72
75
|
const [primaryScope, ...additionalScopes] = scopes;
|
|
73
76
|
const primaryResult = await pca.acquireToken({
|
|
74
77
|
scopes: [primaryScope],
|
|
75
78
|
prompt: 'select_account'
|
|
76
79
|
});
|
|
77
80
|
const signedInAccount = primaryResult.account;
|
|
81
|
+
assertExpectedTenant(signedInAccount, expectedTenantId);
|
|
78
82
|
for (const scope of additionalScopes) {
|
|
79
83
|
try {
|
|
80
84
|
await pca.acquireTokenSilent({
|
|
@@ -90,6 +94,7 @@ async function acquireRequiredScopesInteractively(pca, scopes) {
|
|
|
90
94
|
if (!accountsMatch(signedInAccount, result.account)) {
|
|
91
95
|
throw new Error('Sign in with the same account for all required Power Platform permissions.');
|
|
92
96
|
}
|
|
97
|
+
assertExpectedTenant(result.account, expectedTenantId);
|
|
93
98
|
}
|
|
94
99
|
}
|
|
95
100
|
await validateRequiredScopes(pca, signedInAccount, scopes);
|
|
@@ -100,6 +105,10 @@ export function AuthProvider({
|
|
|
100
105
|
msalConfig,
|
|
101
106
|
intuneConfig,
|
|
102
107
|
requiredScopes = [],
|
|
108
|
+
expectedTenantId,
|
|
109
|
+
authorityTenant,
|
|
110
|
+
preferredAccountIdentifier,
|
|
111
|
+
autoAuthorizeMissingScopes = false,
|
|
103
112
|
children
|
|
104
113
|
}) {
|
|
105
114
|
const [isAuthReady, setIsAuthReady] = useState(false);
|
|
@@ -142,6 +151,7 @@ export function AuthProvider({
|
|
|
142
151
|
return msalConfig.tenantId;
|
|
143
152
|
}
|
|
144
153
|
})();
|
|
154
|
+
const effectiveAuthorityTenant = authorityTenant ?? effectiveTenantId;
|
|
145
155
|
|
|
146
156
|
// When broker is off (Android dev/embedded-WebView mode) the msauth:// redirect
|
|
147
157
|
// URI is not registered in AAD and will cause "multiple apps listening" errors
|
|
@@ -167,7 +177,12 @@ export function AuthProvider({
|
|
|
167
177
|
const requiredScopeKey = [...new Set(requiredScopes)].join('|');
|
|
168
178
|
const effectiveRequiredScopes = requiredScopeKey ? requiredScopeKey.split('|') : SIGN_IN_SCOPES;
|
|
169
179
|
const pcaRef = useRef(null);
|
|
180
|
+
const accountRef = useRef(null);
|
|
181
|
+
accountRef.current = account;
|
|
182
|
+
const authGenerationRef = useRef(0);
|
|
170
183
|
useEffect(() => {
|
|
184
|
+
const generation = ++authGenerationRef.current;
|
|
185
|
+
const isCurrent = () => authGenerationRef.current === generation;
|
|
171
186
|
if (noAuthMode) {
|
|
172
187
|
setIsAuthReady(true);
|
|
173
188
|
setIsLoading(false);
|
|
@@ -176,7 +191,7 @@ export function AuthProvider({
|
|
|
176
191
|
const pca = new PublicClientApplication({
|
|
177
192
|
auth: {
|
|
178
193
|
clientId: effectiveClientId,
|
|
179
|
-
authority: `https://login.microsoftonline.com/${
|
|
194
|
+
authority: `https://login.microsoftonline.com/${effectiveAuthorityTenant}`,
|
|
180
195
|
redirectUri: effectiveRedirectUri
|
|
181
196
|
},
|
|
182
197
|
useBroker,
|
|
@@ -190,19 +205,20 @@ export function AuthProvider({
|
|
|
190
205
|
const authTelemetryProperties = getAuthTelemetryProperties(useBroker, useEmbeddedWebView);
|
|
191
206
|
const authInitScenario = getAppLogger().trackScenario(HostOperationNames.AuthInit, authTelemetryProperties);
|
|
192
207
|
pca.init().then(() => pca.getAccounts()).then(async accounts => {
|
|
193
|
-
|
|
208
|
+
if (!isCurrent()) return;
|
|
209
|
+
const matchesExpectedTenant = candidate => !expectedTenantId || normalizeTenantId(candidate.tenantId) === normalizeTenantId(expectedTenantId);
|
|
210
|
+
let restoredAccount = preferredAccountIdentifier ? accounts.find(candidate => candidate.identifier === preferredAccountIdentifier && matchesExpectedTenant(candidate)) ?? null : accounts.find(matchesExpectedTenant) ?? null;
|
|
194
211
|
if (restoredAccount) {
|
|
195
212
|
try {
|
|
196
|
-
await validateRequiredScopes(pca, restoredAccount, effectiveRequiredScopes);
|
|
213
|
+
restoredAccount = autoAuthorizeMissingScopes ? await authorizeRequiredScopes(pca, restoredAccount, effectiveRequiredScopes, expectedTenantId) : (await validateRequiredScopes(pca, restoredAccount, effectiveRequiredScopes), restoredAccount);
|
|
197
214
|
} catch (tokenError) {
|
|
198
|
-
await pca.signOut({
|
|
199
|
-
account: restoredAccount
|
|
200
|
-
}).catch(() => undefined);
|
|
201
215
|
restoredAccount = null;
|
|
216
|
+
if (!isCurrent()) return;
|
|
202
217
|
setError(new Error('Your session has expired. Sign in again to continue.'));
|
|
203
218
|
debugWarn('[AuthProvider] restored account is missing a required token:', tokenError);
|
|
204
219
|
}
|
|
205
220
|
}
|
|
221
|
+
if (!isCurrent()) return;
|
|
206
222
|
setAccount(restoredAccount);
|
|
207
223
|
if (restoredAccount) {
|
|
208
224
|
getAppLogger().identifyUser({
|
|
@@ -216,15 +232,19 @@ export function AuthProvider({
|
|
|
216
232
|
accountFound: accounts.length > 0
|
|
217
233
|
});
|
|
218
234
|
}).catch(e => {
|
|
235
|
+
if (!isCurrent()) return;
|
|
219
236
|
debugLog('[AuthProvider] MSAL init failed:', e);
|
|
220
|
-
setError(
|
|
237
|
+
setError(new Error(AUTHENTICATION_ERROR_MESSAGE));
|
|
221
238
|
setIsAuthReady(true);
|
|
222
239
|
setIsLoading(false);
|
|
223
240
|
authInitScenario.fail(undefined, getAuthErrorTelemetry(e));
|
|
224
241
|
});
|
|
242
|
+
return () => {
|
|
243
|
+
if (isCurrent()) authGenerationRef.current += 1;
|
|
244
|
+
};
|
|
225
245
|
// Re-initialize only when the effective broker/identity settings change.
|
|
226
246
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
227
|
-
}, [effectiveClientId,
|
|
247
|
+
}, [autoAuthorizeMissingScopes, effectiveClientId, effectiveAuthorityTenant, effectiveRedirectUri, expectedTenantId, preferredAccountIdentifier, requiredScopeKey, useBroker, useEmbeddedWebView]);
|
|
228
248
|
const signIn = useCallback(async () => {
|
|
229
249
|
const pca = pcaRef.current;
|
|
230
250
|
if (!pca) return;
|
|
@@ -237,7 +257,8 @@ export function AuthProvider({
|
|
|
237
257
|
if (!pca.isInitialized) {
|
|
238
258
|
await pca.init();
|
|
239
259
|
}
|
|
240
|
-
const signedInAccount = await acquireRequiredScopesInteractively(pca, effectiveRequiredScopes);
|
|
260
|
+
const signedInAccount = await acquireRequiredScopesInteractively(pca, effectiveRequiredScopes, expectedTenantId);
|
|
261
|
+
if (pcaRef.current !== pca) return;
|
|
241
262
|
setAccount(signedInAccount);
|
|
242
263
|
getAppLogger().identifyUser({
|
|
243
264
|
objectId: signedInAccount.identifier,
|
|
@@ -267,12 +288,13 @@ export function AuthProvider({
|
|
|
267
288
|
}
|
|
268
289
|
signInScenario.complete();
|
|
269
290
|
} catch (e) {
|
|
270
|
-
|
|
291
|
+
debugWarn('[AuthProvider] sign-in failed:', e);
|
|
292
|
+
setError(new Error(AUTHENTICATION_ERROR_MESSAGE));
|
|
271
293
|
signInScenario.fail(undefined, getAuthErrorTelemetry(e));
|
|
272
294
|
} finally {
|
|
273
295
|
setIsLoading(false);
|
|
274
296
|
}
|
|
275
|
-
}, [intuneEnabled, requiredScopeKey, useBroker]);
|
|
297
|
+
}, [expectedTenantId, intuneEnabled, requiredScopeKey, useBroker, useEmbeddedWebView]);
|
|
276
298
|
const acquireToken = useCallback(async scopes => {
|
|
277
299
|
const pca = pcaRef.current;
|
|
278
300
|
if (!pca || !account) return null;
|
|
@@ -281,6 +303,10 @@ export function AuthProvider({
|
|
|
281
303
|
scopes,
|
|
282
304
|
account
|
|
283
305
|
});
|
|
306
|
+
assertExpectedTenant(result.account, expectedTenantId);
|
|
307
|
+
if (pcaRef.current !== pca || !accountRef.current || !accountsMatch(accountRef.current, account)) {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
284
310
|
return result.accessToken;
|
|
285
311
|
} catch (e) {
|
|
286
312
|
debugWarn('[AuthContext] acquireTokenSilent failed for scopes:', scopes, e);
|
|
@@ -300,7 +326,7 @@ export function AuthProvider({
|
|
|
300
326
|
}
|
|
301
327
|
return null;
|
|
302
328
|
}
|
|
303
|
-
}, [account, requiredScopeKey]);
|
|
329
|
+
}, [account, expectedTenantId, requiredScopeKey]);
|
|
304
330
|
const signOut = useCallback(async () => {
|
|
305
331
|
const pca = pcaRef.current;
|
|
306
332
|
if (!pca || !account) return;
|
|
@@ -333,6 +359,7 @@ export function AuthProvider({
|
|
|
333
359
|
isAuthReady,
|
|
334
360
|
isSignedIn: noAuthMode || account !== null,
|
|
335
361
|
hasRealAccount: account !== null,
|
|
362
|
+
account,
|
|
336
363
|
error,
|
|
337
364
|
acquireToken,
|
|
338
365
|
signIn,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function getErrorProperty(error, propertyName) {
|
|
4
|
+
return error && typeof error === 'object' ? error[propertyName] : undefined;
|
|
5
|
+
}
|
|
6
|
+
function getErrorMessage(error) {
|
|
7
|
+
if (error instanceof Error) return error.message;
|
|
8
|
+
return typeof error === 'string' ? error : String(error);
|
|
9
|
+
}
|
|
10
|
+
function normalizeTenantId(tenantId) {
|
|
11
|
+
return tenantId.trim().toLowerCase();
|
|
12
|
+
}
|
|
13
|
+
function assertExpectedTenant(account, expectedTenantId) {
|
|
14
|
+
if (expectedTenantId && normalizeTenantId(account.tenantId) !== normalizeTenantId(expectedTenantId)) {
|
|
15
|
+
throw new Error('The signed-in account is not available in the selected environment tenant.');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function accountsMatch(left, right) {
|
|
19
|
+
return left.identifier === right.identifier && normalizeTenantId(left.tenantId) === normalizeTenantId(right.tenantId);
|
|
20
|
+
}
|
|
21
|
+
export function isInteractionRequired(error) {
|
|
22
|
+
const code = getErrorProperty(error, 'code');
|
|
23
|
+
return code === 1004 || code === '1004' || code === 'E_INTERACTION_REQUIRED' || /interaction[ _-]?required/i.test(getErrorMessage(error));
|
|
24
|
+
}
|
|
25
|
+
export async function authorizeRequiredScopes(client, account, scopes, expectedTenantId) {
|
|
26
|
+
let authorizedAccount = account;
|
|
27
|
+
for (const scope of scopes) {
|
|
28
|
+
try {
|
|
29
|
+
const result = await client.acquireTokenSilent({
|
|
30
|
+
scopes: [scope],
|
|
31
|
+
account: authorizedAccount
|
|
32
|
+
});
|
|
33
|
+
authorizedAccount = result.account;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (!isInteractionRequired(error)) throw error;
|
|
36
|
+
const result = await client.acquireToken({
|
|
37
|
+
scopes: [scope],
|
|
38
|
+
loginHint: authorizedAccount.username
|
|
39
|
+
});
|
|
40
|
+
if (!accountsMatch(authorizedAccount, result.account)) {
|
|
41
|
+
throw new Error('Sign in with the same account for all required Power Platform permissions.');
|
|
42
|
+
}
|
|
43
|
+
authorizedAccount = result.account;
|
|
44
|
+
}
|
|
45
|
+
assertExpectedTenant(authorizedAccount, expectedTenantId);
|
|
46
|
+
}
|
|
47
|
+
return authorizedAccount;
|
|
48
|
+
}
|
|
@@ -104,6 +104,7 @@ function arrayBufferViewToArrayBuffer(view) {
|
|
|
104
104
|
return copy.buffer;
|
|
105
105
|
}
|
|
106
106
|
export class NativePowerAppsBridge {
|
|
107
|
+
_runtimeEpoch = 0;
|
|
107
108
|
constructor(_options) {
|
|
108
109
|
this._options = _options;
|
|
109
110
|
}
|
|
@@ -117,8 +118,14 @@ export class NativePowerAppsBridge {
|
|
|
117
118
|
* via globalThis would have no effect — mutating options is the correct path.
|
|
118
119
|
*/
|
|
119
120
|
updateOptions(options) {
|
|
121
|
+
if (this._options.runtimeIdentity !== options.runtimeIdentity) {
|
|
122
|
+
this._runtimeEpoch += 1;
|
|
123
|
+
}
|
|
120
124
|
this._options = updateBridgeOptionsInPlace(this._options, options);
|
|
121
125
|
}
|
|
126
|
+
invalidateRuntime() {
|
|
127
|
+
this._runtimeEpoch += 1;
|
|
128
|
+
}
|
|
122
129
|
async initialize() {
|
|
123
130
|
// No handshake required for the native path.
|
|
124
131
|
}
|
|
@@ -147,9 +154,27 @@ export class NativePowerAppsBridge {
|
|
|
147
154
|
});
|
|
148
155
|
switch (key) {
|
|
149
156
|
case 'AppIdentityServicePlugin.getAppAccessTokenAsync':
|
|
150
|
-
|
|
157
|
+
{
|
|
158
|
+
const runtimeEpoch = this._runtimeEpoch;
|
|
159
|
+
const generation = this._options.targetGeneration;
|
|
160
|
+
const runtimeIdentity = this._options.runtimeIdentity;
|
|
161
|
+
const token = await this._options.getConnectorToken(params[0]);
|
|
162
|
+
if (this._runtimeEpoch !== runtimeEpoch || this._options.targetGeneration !== generation || this._options.runtimeIdentity !== runtimeIdentity) {
|
|
163
|
+
throw new Error('NativePowerAppsBridge: target changed while acquiring a connector token');
|
|
164
|
+
}
|
|
165
|
+
return token;
|
|
166
|
+
}
|
|
151
167
|
case 'AppIdentityServicePlugin.getAppDynamicResourceAccessTokenAsync':
|
|
152
|
-
|
|
168
|
+
{
|
|
169
|
+
const runtimeEpoch = this._runtimeEpoch;
|
|
170
|
+
const generation = this._options.targetGeneration;
|
|
171
|
+
const runtimeIdentity = this._options.runtimeIdentity;
|
|
172
|
+
const token = await this._options.getDataverseToken(params[0]);
|
|
173
|
+
if (this._runtimeEpoch !== runtimeEpoch || this._options.targetGeneration !== generation || this._options.runtimeIdentity !== runtimeIdentity) {
|
|
174
|
+
throw new Error('NativePowerAppsBridge: target changed while acquiring a Dataverse token');
|
|
175
|
+
}
|
|
176
|
+
return token;
|
|
177
|
+
}
|
|
153
178
|
case 'AppHttpClientPlugin.sendHttpAsync':
|
|
154
179
|
return this._sendHttpAsync(params);
|
|
155
180
|
case 'AppPowerAppsClientPlugin.getAppCdsDataSourceConfigsAsync':
|
|
@@ -193,6 +218,9 @@ export class NativePowerAppsBridge {
|
|
|
193
218
|
* [{ status: number; headers: Record<string, string> }, ArrayBuffer]
|
|
194
219
|
*/
|
|
195
220
|
async _sendHttpAsync(params) {
|
|
221
|
+
const requestRuntimeEpoch = this._runtimeEpoch;
|
|
222
|
+
const requestGeneration = this._options.targetGeneration;
|
|
223
|
+
const requestRuntimeIdentity = this._options.runtimeIdentity;
|
|
196
224
|
const requestConfig = params[0];
|
|
197
225
|
const bodyParam = params[1];
|
|
198
226
|
const {
|
|
@@ -234,6 +262,9 @@ export class NativePowerAppsBridge {
|
|
|
234
262
|
headers: headers,
|
|
235
263
|
body: bodyInit
|
|
236
264
|
});
|
|
265
|
+
if (this._runtimeEpoch !== requestRuntimeEpoch || this._options.targetGeneration !== requestGeneration || this._options.runtimeIdentity !== requestRuntimeIdentity) {
|
|
266
|
+
throw new Error('NativePowerAppsBridge: target changed while the request was in flight');
|
|
267
|
+
}
|
|
237
268
|
} catch (fetchErr) {
|
|
238
269
|
debugWarn(`[bridge] fetch THREW for ${method} ${urlLabel}:`, fetchErr);
|
|
239
270
|
debugLog(`[bridge] fetch THREW for ${method} ${urlLabel} | req body: ${typeof bodyInit === 'string' ? bodyInit.slice(0, 500) : bodyInit != null ? '<non-string body>' : '<no body>'}`);
|
|
@@ -125,11 +125,9 @@ export class PowerPagesBridge {
|
|
|
125
125
|
throw error;
|
|
126
126
|
}
|
|
127
127
|
if (!response.ok) {
|
|
128
|
-
const responseText = new TextDecoder().decode(responseBody).slice(0, 1000);
|
|
129
|
-
const detail = responseText ? `: ${responseText}` : '';
|
|
130
128
|
// RuntimeDataClient treats JSON tuples as successful before inspecting status.
|
|
131
129
|
// Reject with its HTTP-plugin error shape so 4xx/5xx responses remain failures.
|
|
132
|
-
throw [[`Power Pages Web API request failed (${response.status})
|
|
130
|
+
throw [[`Power Pages Web API request failed (${response.status})`, undefined, {
|
|
133
131
|
status: response.status,
|
|
134
132
|
headers: responseHeaders
|
|
135
133
|
}], responseBody];
|
|
@@ -59,4 +59,9 @@ export function setupNativeHost(options) {
|
|
|
59
59
|
globalThis.powerAppsBridge = _bridge;
|
|
60
60
|
debugLog('[bridge] setupNativeHost: bridge ready');
|
|
61
61
|
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Invalidates in-flight work without replacing the SDK-cached bridge singleton. */
|
|
65
|
+
export function invalidateNativeHostRuntime() {
|
|
66
|
+
_bridge?.invalidateRuntime();
|
|
62
67
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
import { resolveMultiTenantConfig, resolveTenantMode } from "./tenantMode.js";
|
|
3
4
|
/**
|
|
4
5
|
* Branding placeholder used when the app supplies no icon/splash of its own.
|
|
5
6
|
*
|
|
@@ -13,6 +14,12 @@
|
|
|
13
14
|
*/
|
|
14
15
|
const PLACEHOLDER_BRANDING_IMAGE = './assets/adaptive-icon.png';
|
|
15
16
|
export function createPowerAppsExpoConfig(baseConfig, settings, customize = config => config) {
|
|
17
|
+
const tenantMode = resolveTenantMode({
|
|
18
|
+
expo: baseConfig
|
|
19
|
+
});
|
|
20
|
+
if (tenantMode === 'multi') resolveMultiTenantConfig({
|
|
21
|
+
expo: baseConfig
|
|
22
|
+
});
|
|
16
23
|
const resolvedIconPath = settings.iconPath || PLACEHOLDER_BRANDING_IMAGE;
|
|
17
24
|
const resolvedSplashPath = settings.splashPath || process.env.APP_SPLASH_PATH || PLACEHOLDER_BRANDING_IMAGE;
|
|
18
25
|
const icon = {
|
|
@@ -40,6 +47,13 @@ export function createPowerAppsExpoConfig(baseConfig, settings, customize = conf
|
|
|
40
47
|
experiments: {
|
|
41
48
|
typedRoutes: true
|
|
42
49
|
},
|
|
50
|
+
extra: {
|
|
51
|
+
...baseConfig.extra,
|
|
52
|
+
powerappsNative: {
|
|
53
|
+
...baseConfig.extra?.powerappsNative,
|
|
54
|
+
tenantMode
|
|
55
|
+
}
|
|
56
|
+
},
|
|
43
57
|
plugins: [...(settings.isDevClient ? ['expo-dev-client'] : []), 'expo-asset', 'expo-audio', 'expo-font', 'expo-image', 'expo-mail-composer', 'expo-router', 'expo-secure-store', 'expo-sharing', 'expo-video', 'expo-web-browser', '@microsoft/power-apps-native-host', '@microsoft/power-apps-native-offline', '@react-native-community/datetimepicker',
|
|
44
58
|
// Configured explicitly rather than left to Expo's default so prebuild
|
|
45
59
|
// emits named, resource-table-registered splash assets the wrap pipeline
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { getWrapConfig } from '@microsoft/power-apps-native-auth';
|
|
4
|
+
import { TargetRuntimeError } from "../target/errors.js";
|
|
5
|
+
import { isDevPlayer } from "../utils/isDevPlayer.js";
|
|
6
|
+
export function assertNativeTenantMode(tenantMode) {
|
|
7
|
+
if (__DEV__ || isDevPlayer) return;
|
|
8
|
+
const native = getWrapConfig();
|
|
9
|
+
if (native.authContractVersion !== '2' || native.tenantMode !== 'single' && native.tenantMode !== 'multi' || native.tenantMode !== tenantMode) {
|
|
10
|
+
throw new TargetRuntimeError('invalid-build-config', 'configuration', false, 'The app authentication configuration does not match this build. Rebuild the app or contact your administrator.');
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
export const MULTI_TENANT_AUTHORITY = 'common';
|
|
4
|
+
const GUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
5
|
+
const CANVAS_APP_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_]{0,99}$/;
|
|
6
|
+
function getPowerAppsNativeConfig(appConfig) {
|
|
7
|
+
if (!appConfig || typeof appConfig !== 'object') return {};
|
|
8
|
+
const config = appConfig;
|
|
9
|
+
return config.expo?.extra?.powerappsNative ?? {};
|
|
10
|
+
}
|
|
11
|
+
export function resolveTenantMode(appConfig) {
|
|
12
|
+
const value = getPowerAppsNativeConfig(appConfig).tenantMode;
|
|
13
|
+
if (value === undefined) return 'single';
|
|
14
|
+
if (value === 'single' || value === 'multi') return value;
|
|
15
|
+
throw new Error('app.json expo.extra.powerappsNative.tenantMode must be "single" or "multi"');
|
|
16
|
+
}
|
|
17
|
+
export function resolveMultiTenantConfig(appConfig) {
|
|
18
|
+
const config = getPowerAppsNativeConfig(appConfig);
|
|
19
|
+
if (typeof config.appDisplayName !== 'string' || config.appDisplayName.trim().length === 0) {
|
|
20
|
+
throw new Error('app.json expo.extra.powerappsNative.appDisplayName is required in multi-tenant mode');
|
|
21
|
+
}
|
|
22
|
+
if (typeof config.appName !== 'string' || !CANVAS_APP_NAME_PATTERN.test(config.appName.trim())) {
|
|
23
|
+
throw new Error('app.json expo.extra.powerappsNative.appName must start with a letter and contain only letters, numbers, or underscores (maximum 100 characters)');
|
|
24
|
+
}
|
|
25
|
+
const appIdentity = {
|
|
26
|
+
appDisplayName: config.appDisplayName.trim(),
|
|
27
|
+
appName: config.appName.trim()
|
|
28
|
+
};
|
|
29
|
+
if (config.tenantPolicy === 'organizations') {
|
|
30
|
+
return {
|
|
31
|
+
...appIdentity,
|
|
32
|
+
tenantPolicy: 'organizations'
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (!config.tenantPolicy || typeof config.tenantPolicy !== 'object' || Array.isArray(config.tenantPolicy)) {
|
|
36
|
+
throw new Error('app.json expo.extra.powerappsNative.tenantPolicy must be "organizations" or an allowedTenantIds policy');
|
|
37
|
+
}
|
|
38
|
+
const values = config.tenantPolicy.allowedTenantIds;
|
|
39
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
40
|
+
throw new Error('app.json tenantPolicy.allowedTenantIds must be a non-empty tenant GUID array');
|
|
41
|
+
}
|
|
42
|
+
const allowedTenantIds = values.map(value => {
|
|
43
|
+
if (typeof value !== 'string' || !GUID_PATTERN.test(value.trim())) {
|
|
44
|
+
throw new Error('app.json tenantPolicy.allowedTenantIds must contain only tenant GUIDs');
|
|
45
|
+
}
|
|
46
|
+
return value.trim().toLowerCase();
|
|
47
|
+
});
|
|
48
|
+
if (new Set(allowedTenantIds).size !== allowedTenantIds.length) {
|
|
49
|
+
throw new Error('app.json tenantPolicy.allowedTenantIds must not contain duplicates');
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
...appIdentity,
|
|
53
|
+
tenantPolicy: {
|
|
54
|
+
allowedTenantIds
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function isTenantAllowed(tenantId, policy) {
|
|
59
|
+
if (!GUID_PATTERN.test(tenantId.trim())) return false;
|
|
60
|
+
return policy === 'organizations' || policy.allowedTenantIds.includes(tenantId.trim().toLowerCase());
|
|
61
|
+
}
|