@microsoft/power-apps-native-host 0.3.3 → 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.
Files changed (111) hide show
  1. package/README.md +60 -0
  2. package/compatibility/current.json +1 -1
  3. package/lib/NOTICE.txt +2957 -371
  4. package/lib/commonjs/auth/AuthContext.js +49 -22
  5. package/lib/commonjs/auth/targetAuthorization.js +53 -0
  6. package/lib/commonjs/bridge/NativePowerAppsBridge.js +33 -2
  7. package/lib/commonjs/bridge/PowerPagesBridge.js +1 -3
  8. package/lib/commonjs/bridge/setupNativeHost.js +6 -0
  9. package/lib/commonjs/config/expoConfig.js +14 -0
  10. package/lib/commonjs/config/runtimeTenantMode.js +16 -0
  11. package/lib/commonjs/config/runtimeTenantMode.web.js +9 -0
  12. package/lib/commonjs/config/tenantMode.js +68 -0
  13. package/lib/commonjs/connections/PowerPlatformConnectionApi.js +24 -27
  14. package/lib/commonjs/connections/connectionMetadata.js +2 -2
  15. package/lib/commonjs/connections/resolveConnections.js +8 -3
  16. package/lib/commonjs/connections/types.js +9 -2
  17. package/lib/commonjs/connections/useConnectionRefs.js +19 -10
  18. package/lib/commonjs/index.js +41 -2
  19. package/lib/commonjs/platform/CanvasAppResolution.js +33 -0
  20. package/lib/commonjs/platform/CanvasAppResolutionApi.js +69 -0
  21. package/lib/commonjs/platform/DataverseGlobalDiscoveryApi.js +50 -13
  22. package/lib/commonjs/platform/GlobalDiscovery.js +40 -1
  23. package/lib/commonjs/provider/PowerAppsHostProvider.js +47 -8
  24. package/lib/commonjs/provider/PowerAppsProvider.js +175 -9
  25. package/lib/commonjs/target/DefaultPowerAppsExperience.js +564 -0
  26. package/lib/commonjs/target/PowerAppsExperience.js +41 -0
  27. package/lib/commonjs/target/PowerAppsTargetBootstrap.js +534 -0
  28. package/lib/commonjs/target/errors.js +24 -0
  29. package/lib/commonjs/target/types.js +1 -0
  30. package/lib/commonjs/telemetry/index.js +2 -2
  31. package/lib/module/auth/AuthContext.js +49 -22
  32. package/lib/module/auth/targetAuthorization.js +48 -0
  33. package/lib/module/bridge/NativePowerAppsBridge.js +33 -2
  34. package/lib/module/bridge/PowerPagesBridge.js +1 -3
  35. package/lib/module/bridge/setupNativeHost.js +5 -0
  36. package/lib/module/config/expoConfig.js +14 -0
  37. package/lib/module/config/runtimeTenantMode.js +12 -0
  38. package/lib/module/config/runtimeTenantMode.web.js +5 -0
  39. package/lib/module/config/tenantMode.js +61 -0
  40. package/lib/module/connections/PowerPlatformConnectionApi.js +24 -27
  41. package/lib/module/connections/connectionMetadata.js +2 -2
  42. package/lib/module/connections/resolveConnections.js +9 -4
  43. package/lib/module/connections/types.js +9 -2
  44. package/lib/module/connections/useConnectionRefs.js +20 -11
  45. package/lib/module/index.js +5 -2
  46. package/lib/module/platform/CanvasAppResolution.js +27 -0
  47. package/lib/module/platform/CanvasAppResolutionApi.js +65 -0
  48. package/lib/module/platform/DataverseGlobalDiscoveryApi.js +49 -14
  49. package/lib/module/platform/GlobalDiscovery.js +37 -1
  50. package/lib/module/provider/PowerAppsHostProvider.js +47 -8
  51. package/lib/module/provider/PowerAppsProvider.js +179 -13
  52. package/lib/module/target/DefaultPowerAppsExperience.js +559 -0
  53. package/lib/module/target/PowerAppsExperience.js +34 -0
  54. package/lib/module/target/PowerAppsTargetBootstrap.js +529 -0
  55. package/lib/module/target/errors.js +19 -0
  56. package/lib/module/target/types.js +1 -0
  57. package/lib/module/telemetry/index.js +1 -1
  58. package/lib/typescript/commonjs/auth/AuthContext.d.ts +11 -2
  59. package/lib/typescript/commonjs/auth/targetAuthorization.d.ts +15 -0
  60. package/lib/typescript/commonjs/bridge/NativePowerAppsBridge.d.ts +6 -0
  61. package/lib/typescript/commonjs/bridge/setupNativeHost.d.ts +2 -0
  62. package/lib/typescript/commonjs/config/runtimeTenantMode.d.ts +3 -0
  63. package/lib/typescript/commonjs/config/runtimeTenantMode.web.d.ts +3 -0
  64. package/lib/typescript/commonjs/config/tenantMode.d.ts +14 -0
  65. package/lib/typescript/commonjs/connections/PowerPlatformConnectionApi.d.ts +5 -2
  66. package/lib/typescript/commonjs/connections/connectionMetadata.d.ts +1 -1
  67. package/lib/typescript/commonjs/connections/types.d.ts +7 -1
  68. package/lib/typescript/commonjs/connections/useConnectionRefs.d.ts +11 -2
  69. package/lib/typescript/commonjs/index.d.ts +7 -3
  70. package/lib/typescript/commonjs/platform/CanvasAppResolution.d.ts +4 -0
  71. package/lib/typescript/commonjs/platform/CanvasAppResolutionApi.d.ts +6 -0
  72. package/lib/typescript/commonjs/platform/DataverseGlobalDiscoveryApi.d.ts +12 -2
  73. package/lib/typescript/commonjs/platform/GlobalDiscovery.d.ts +12 -0
  74. package/lib/typescript/commonjs/provider/PowerAppsHostProvider.d.ts +9 -1
  75. package/lib/typescript/commonjs/provider/PowerAppsProvider.d.ts +14 -16
  76. package/lib/typescript/commonjs/target/DefaultPowerAppsExperience.d.ts +6 -0
  77. package/lib/typescript/commonjs/target/PowerAppsExperience.d.ts +61 -0
  78. package/lib/typescript/commonjs/target/PowerAppsTargetBootstrap.d.ts +15 -0
  79. package/lib/typescript/commonjs/target/errors.d.ts +13 -0
  80. package/lib/typescript/commonjs/target/types.d.ts +34 -0
  81. package/lib/typescript/commonjs/telemetry/index.d.ts +2 -2
  82. package/lib/typescript/module/auth/AuthContext.d.ts +11 -2
  83. package/lib/typescript/module/auth/targetAuthorization.d.ts +15 -0
  84. package/lib/typescript/module/bridge/NativePowerAppsBridge.d.ts +6 -0
  85. package/lib/typescript/module/bridge/setupNativeHost.d.ts +2 -0
  86. package/lib/typescript/module/config/runtimeTenantMode.d.ts +3 -0
  87. package/lib/typescript/module/config/runtimeTenantMode.web.d.ts +3 -0
  88. package/lib/typescript/module/config/tenantMode.d.ts +14 -0
  89. package/lib/typescript/module/connections/PowerPlatformConnectionApi.d.ts +5 -2
  90. package/lib/typescript/module/connections/connectionMetadata.d.ts +1 -1
  91. package/lib/typescript/module/connections/types.d.ts +7 -1
  92. package/lib/typescript/module/connections/useConnectionRefs.d.ts +11 -2
  93. package/lib/typescript/module/index.d.ts +7 -3
  94. package/lib/typescript/module/platform/CanvasAppResolution.d.ts +4 -0
  95. package/lib/typescript/module/platform/CanvasAppResolutionApi.d.ts +6 -0
  96. package/lib/typescript/module/platform/DataverseGlobalDiscoveryApi.d.ts +12 -2
  97. package/lib/typescript/module/platform/GlobalDiscovery.d.ts +12 -0
  98. package/lib/typescript/module/provider/PowerAppsHostProvider.d.ts +9 -1
  99. package/lib/typescript/module/provider/PowerAppsProvider.d.ts +14 -16
  100. package/lib/typescript/module/target/DefaultPowerAppsExperience.d.ts +6 -0
  101. package/lib/typescript/module/target/PowerAppsExperience.d.ts +61 -0
  102. package/lib/typescript/module/target/PowerAppsTargetBootstrap.d.ts +15 -0
  103. package/lib/typescript/module/target/errors.d.ts +13 -0
  104. package/lib/typescript/module/target/types.d.ts +34 -0
  105. package/lib/typescript/module/telemetry/index.d.ts +2 -2
  106. package/package.json +12 -5
  107. package/scripts/build-codegen-package.js +34 -3
  108. package/scripts/dev.js +405 -0
  109. package/scripts/upgrade-authoring.js +85 -28
  110. package/scripts/wrap.js +42 -5
  111. 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') ?? parsedNSError?.[1],
48
- nativeErrorCode: getErrorProperty(error, 'nativeErrorCode') ?? parsedNSError?.[2],
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/${effectiveTenantId}`,
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
- let restoredAccount = accounts[0] ?? null;
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(e instanceof Error ? e : new Error(String(e)));
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, effectiveTenantId, effectiveRedirectUri, requiredScopeKey, useBroker]);
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
- setError(e instanceof Error ? e : new Error(String(e)));
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
- return this._options.getConnectorToken(params[0]);
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
- return this._options.getDataverseToken(params[0]);
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})${detail}`, undefined, {
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,5 @@
1
+ "use strict";
2
+
3
+ export function assertNativeTenantMode(_tenantMode) {
4
+ // Native wrap metadata is not available on web.
5
+ }
@@ -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
+ }