@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
@@ -19,8 +19,10 @@ import { connectionIdsEqual, extractConnectionId } from "../connections/disambig
19
19
  import { applyRegisteredConnectionUsages, getUnregisteredConnectionRequirements, isTransientConnectionConsentStatus } from "./connectionState.js";
20
20
  import { getConnectionDatasetName } from "./types.js";
21
21
  import { getAppLogger, HostOperationNames, platformHttpClient } from "../telemetry/index.js";
22
+ import { tryResolveTelemetryLocationFromEnvironment } from '@microsoft/power-apps-native-common';
22
23
  import { getHttpCorrelationProperties } from '@microsoft/power-apps-native-common/http';
23
24
  import { debugLog, debugWarn } from "../logger.js";
25
+ import { TargetRuntimeError } from "../target/errors.js";
24
26
 
25
27
  // Power Platform connectivity API version used across all calls here.
26
28
  const API_VERSION = '1';
@@ -121,8 +123,7 @@ export async function fetchConnectionsForConnector(connectorId, environmentId, r
121
123
  }
122
124
  });
123
125
  if (!res.ok) {
124
- const errorText = await res.text().catch(() => res.statusText);
125
- throw new Error(`Unable to list connector connections (HTTP ${res.status}): ${errorText}`);
126
+ throw new Error(`Unable to list connector connections (HTTP ${res.status})`);
126
127
  }
127
128
  const body = await res.json();
128
129
  return (body.value ?? []).map(c => {
@@ -143,7 +144,7 @@ export async function fetchConnectionsForConnector(connectorId, environmentId, r
143
144
  });
144
145
  } catch (error) {
145
146
  if (error instanceof Error) throw error;
146
- throw new Error(`Unable to list connector connections: ${String(error)}`);
147
+ throw new Error('Unable to list connector connections');
147
148
  }
148
149
  }
149
150
 
@@ -231,10 +232,9 @@ export async function createConnection(connectorId, environmentId, region, token
231
232
  body: JSON.stringify(body)
232
233
  });
233
234
  if (!res.ok) {
234
- const errText = await res.text().catch(() => res.statusText);
235
235
  const result = {
236
236
  status: 'error',
237
- message: `HTTP ${res.status}: ${errText}`
237
+ message: `Unable to create the connection (HTTP ${res.status})`
238
238
  };
239
239
  scenario.completeWithError({
240
240
  httpStatus: res.status
@@ -267,14 +267,13 @@ export async function createConnection(connectorId, environmentId, region, token
267
267
  body: JSON.stringify(buildConsentLinkDefinition(oauthRedirectUrl))
268
268
  });
269
269
  if (!consentRes.ok) {
270
- const errorText = await consentRes.text().catch(() => consentRes.statusText);
271
270
  scenario.completeWithError({
272
271
  httpStatus: consentRes.status,
273
272
  phase: 'getConsentLink'
274
273
  });
275
274
  return {
276
275
  status: 'error',
277
- message: `Unable to start connector consent (HTTP ${consentRes.status}): ${errorText}`
276
+ message: `Unable to start connector consent (HTTP ${consentRes.status})`
278
277
  };
279
278
  }
280
279
  const consentData = await consentRes.json();
@@ -303,7 +302,7 @@ export async function createConnection(connectorId, environmentId, region, token
303
302
  });
304
303
  return {
305
304
  status: 'error',
306
- message: String(e)
305
+ message: 'Unable to create the connection.'
307
306
  };
308
307
  }
309
308
  }
@@ -331,14 +330,13 @@ export async function repairConnection(connectorId, connectionName, environmentI
331
330
  })
332
331
  });
333
332
  if (!updateResponse.ok) {
334
- const errorText = await updateResponse.text().catch(() => updateResponse.statusText);
335
333
  scenario.completeWithError({
336
334
  httpStatus: updateResponse.status,
337
335
  phase: 'update'
338
336
  });
339
337
  return {
340
338
  status: 'error',
341
- message: `Unable to update the connection (HTTP ${updateResponse.status}): ${errorText}`
339
+ message: `Unable to update the connection (HTTP ${updateResponse.status})`
342
340
  };
343
341
  }
344
342
  if (!requiresApiConsent) {
@@ -357,7 +355,7 @@ export async function repairConnection(connectorId, connectionName, environmentI
357
355
  });
358
356
  return {
359
357
  status: 'error',
360
- message: `Unable to update the connection: ${String(error)}`
358
+ message: 'Unable to update the connection.'
361
359
  };
362
360
  }
363
361
  }
@@ -382,14 +380,13 @@ export async function repairConnection(connectorId, connectionName, environmentI
382
380
  body: JSON.stringify(buildConsentLinkDefinition(oauthRedirectUrl))
383
381
  });
384
382
  if (!response.ok) {
385
- const errorText = await response.text().catch(() => response.statusText);
386
383
  scenario.completeWithError({
387
384
  httpStatus: response.status,
388
385
  phase: 'getConsentLink'
389
386
  });
390
387
  return {
391
388
  status: 'error',
392
- message: `Unable to repair connector consent (HTTP ${response.status}): ${errorText}`
389
+ message: `Unable to repair connector consent (HTTP ${response.status})`
393
390
  };
394
391
  }
395
392
  const data = await response.json();
@@ -418,7 +415,7 @@ export async function repairConnection(connectorId, connectionName, environmentI
418
415
  });
419
416
  return {
420
417
  status: 'error',
421
- message: `Unable to repair connector consent: ${String(error)}`
418
+ message: 'Unable to repair connector consent.'
422
419
  };
423
420
  }
424
421
  }
@@ -588,8 +585,7 @@ export async function fetchAppConnectionUsages(appId, environmentId, region, tok
588
585
  }
589
586
  });
590
587
  if (!response.ok) {
591
- const errorText = await response.text().catch(() => response.statusText);
592
- throw new Error(`Unable to load app connection usages (HTTP ${response.status}): ${errorText}`);
588
+ throw new Error(`Unable to load app connection usages (HTTP ${response.status})`);
593
589
  }
594
590
  const data = await response.json();
595
591
  return (data.value ?? []).filter(usage => Boolean(usage.name && usage.properties?.reference && usage.properties?.apiId)).map(usage => ({
@@ -611,8 +607,7 @@ export async function postConnectionUsage(appId, usage, environmentId, region, t
611
607
  body: JSON.stringify(buildConnectionUsageRequest(appId, usage))
612
608
  });
613
609
  if (!response.ok) {
614
- const errorText = await response.text().catch(() => response.statusText);
615
- throw new Error(`Unable to register connection usage (HTTP ${response.status}): ${errorText}`);
610
+ throw new Error(`Unable to register connection usage (HTTP ${response.status})`);
616
611
  }
617
612
  }
618
613
  export async function reportScopeConsents(appId, usages, environmentId, region, token) {
@@ -627,8 +622,7 @@ export async function reportScopeConsents(appId, usages, environmentId, region,
627
622
  body: JSON.stringify(buildScopeConsentRequest(usages))
628
623
  });
629
624
  if (!response.ok) {
630
- const errorText = await response.text().catch(() => response.statusText);
631
- throw new Error(`Unable to register connection scope consent (HTTP ${response.status}): ${errorText}`);
625
+ throw new Error(`Unable to register connection scope consent (HTTP ${response.status})`);
632
626
  }
633
627
  }
634
628
 
@@ -653,7 +647,7 @@ export async function reportScopeConsents(appId, usages, environmentId, region,
653
647
  * @param environmentId - Environment GUID.
654
648
  * @param region - Cluster region (default "prod").
655
649
  * @param token - Bearer token for {@link powerPlatformScope}`(region)`.
656
- * @returns Processed launch data, or `null` when the app is not found.
650
+ * @returns Processed launch data. Missing or inaccessible apps throw a typed error.
657
651
  */
658
652
  export async function fetchAppLaunchData(appId, environmentId, region, token) {
659
653
  const base = buildEnvironmentEndpoint(environmentId, region);
@@ -669,16 +663,17 @@ export async function fetchAppLaunchData(appId, environmentId, region, token) {
669
663
  body: '{}'
670
664
  });
671
665
  } catch (error) {
672
- throw new Error(`Unable to reach the Power Platform app launch endpoint: ${String(error)}`);
666
+ throw new TargetRuntimeError('service-unavailable', 'launch', true, 'The app launch service is temporarily unavailable. Try again.', undefined, error);
673
667
  }
674
668
  if (!res.ok) {
675
669
  const responseText = await res.text().catch(() => res.statusText);
676
670
  if (isAppLaunchAccessDenied(res.status, responseText)) {
677
- throw new AppLaunchAccessDeniedError();
671
+ throw new TargetRuntimeError('app-access-denied', 'launch', false, 'You do not have access to this app. Ask the app owner or an administrator to share it with you.');
678
672
  }
679
- const failureMessage = getAppLaunchFailureMessage(res.status, res.statusText, responseText);
680
- if (!failureMessage) return null;
681
- throw new Error(failureMessage);
673
+ if (!getAppLaunchFailureMessage(res.status, res.statusText, responseText)) {
674
+ throw new TargetRuntimeError('app-not-found', 'launch', false, 'The configured app is missing from the selected environment.');
675
+ }
676
+ throw new TargetRuntimeError('service-unavailable', 'launch', res.status === 429 || res.status >= 500, res.status === 429 || res.status >= 500 ? 'The app launch service is temporarily unavailable. Try again.' : 'The app could not be loaded from the selected environment.');
682
677
  }
683
678
  try {
684
679
  const data = await res.json();
@@ -843,9 +838,11 @@ export async function fetchAppLaunchData(appId, environmentId, region, token) {
843
838
  }
844
839
  }
845
840
  const conflictingDataSources = findConnectionDataSourceConflicts(connectionRequirements);
841
+ const telemetryLocation = tryResolveTelemetryLocationFromEnvironment(data.app?.properties?.environment?.properties?.location, data.app?.properties?.environment?.properties?.cluster?.geoShortName);
846
842
  const result = {
847
843
  connectionRefs,
848
844
  dataverseOrgUrl,
845
+ telemetryLocation,
849
846
  databaseReferences: data.app?.properties?.databaseReferences ?? {},
850
847
  missingConnectors,
851
848
  missingConnectionReferences,
@@ -881,7 +878,7 @@ export async function fetchAppLaunchData(appId, environmentId, region, token) {
881
878
  }));
882
879
  return result;
883
880
  } catch (error) {
884
- throw new Error(`The Power Platform app launch response was invalid: ${String(error)}`);
881
+ throw new Error('The Power Platform app launch response was invalid');
885
882
  }
886
883
  }
887
884
 
@@ -45,9 +45,9 @@ export function hasOAuthParameter(parameters) {
45
45
  export function buildConnectionUserConsentUrl(environmentEndpoint, connectorId, connectionId) {
46
46
  return `${environmentEndpoint}/connectivity/connectors/${encodeURIComponent(getConnectorId(connectorId))}` + `/connections/${encodeURIComponent(connectionId)}/userConsent?api-version=1`;
47
47
  }
48
- export function getAppLaunchFailureMessage(status, statusText, responseText) {
48
+ export function getAppLaunchFailureMessage(status, _statusText, _responseText) {
49
49
  if (status === 404) return null;
50
- return `Unable to load the published app (HTTP ${status}): ${responseText || statusText}`;
50
+ return `Unable to load the published app (HTTP ${status})`;
51
51
  }
52
52
  export function isAppLaunchAccessDenied(status, responseText) {
53
53
  if (status !== 403) return false;
@@ -21,7 +21,7 @@
21
21
  import { fetchConnectorMetadata, grantConnectionUserConsent } from "./PowerPlatformConnectionApi.js";
22
22
  import { getConnectionNamesRequiringAclGrant, shouldGrantConnectionUserConsent } from "./connectionState.js";
23
23
  import { connectionUserConsentScope, buildEnvironmentEndpoint } from "../platform/PowerPlatformEnvironmentApi.js";
24
- import { fetchInstanceUrl } from "../platform/DataverseGlobalDiscoveryApi.js";
24
+ import { fetchInstanceMetadata } from "../platform/DataverseGlobalDiscoveryApi.js";
25
25
  import { connectionIdsEqual, disambiguateConnections, extractConnectionId } from "../connections/disambiguate.js";
26
26
  import { buildConnectionRequirements, ConnectionStatus, findConnectionDataSourceConflicts, getConnectionDatasetName, getFlowConnectionName, getFlowDataSourceNames, isCompositeConnectionApiId, getConnectorId, normalizeApiId } from "./types.js";
27
27
  import { platformHttpClient } from "../telemetry/index.js";
@@ -95,8 +95,12 @@ export async function resolveViaStaticRefs({
95
95
  Authorization: `Bearer ${token}`
96
96
  };
97
97
 
98
- // Fetch the Dataverse instanceUrl only when the app uses Dataverse.
99
- const [instanceUrl, ...connResults] = await Promise.all([hasDataverse ? fetchInstanceUrl(environmentId, region, acquireToken, token) : Promise.resolve(undefined), ...Object.entries(directConnectionReferences).map(async ([localRefId, ref]) => {
98
+ // GDS provides authoritative geography when the environment has Dataverse,
99
+ // even if this app does not use it. Only Dataverse apps require the lookup.
100
+ const [instanceMetadata, ...connResults] = await Promise.all([fetchInstanceMetadata(environmentId, region, acquireToken, token).catch(error => {
101
+ if (hasDataverse) throw error;
102
+ return undefined;
103
+ }), ...Object.entries(directConnectionReferences).map(async ([localRefId, ref]) => {
100
104
  const apiId = normalizeApiId(ref.id);
101
105
  const schema = schemaByApiId[apiId];
102
106
  if (!schema) {
@@ -252,7 +256,7 @@ export async function resolveViaStaticRefs({
252
256
  unresolvedApiIds.push(normalizeApiId(dr.apiId));
253
257
  }
254
258
  }
255
- const dataverseOrgUrl = (instanceUrl ?? '').replace(/\/$/, '');
259
+ const dataverseOrgUrl = (instanceMetadata?.instanceUrl ?? '').replace(/\/$/, '');
256
260
 
257
261
  // Build the final AppConnectionReference map.
258
262
  const connectionRefs = {};
@@ -295,6 +299,7 @@ export async function resolveViaStaticRefs({
295
299
  return {
296
300
  connectionRefs,
297
301
  dataverseOrgUrl,
302
+ telemetryLocation: instanceMetadata?.telemetryLocation,
298
303
  databaseReferences: {},
299
304
  missingConnectors: [...new Set(unresolvedApiIds)],
300
305
  missingConnectionReferences,
@@ -98,7 +98,7 @@ export function hasCompleteStaticConnectionConfig(connectionReferences, schemaMa
98
98
  const directReferences = references.filter(reference => !isCompositeConnectionApiId(reference.id));
99
99
  return directReferences.length > 0 && directReferences.every(reference => Boolean(reference.id) && reference.dataSources.length > 0 && reference.dataSources.every(dataSource => Boolean(schemaMap[dataSource]?.name && schemaMap[dataSource]?.properties?.primaryRuntimeUrl)));
100
100
  }
101
- export function buildConnectionResolutionKey(appId, environmentId, connectionReferences) {
101
+ export function buildConnectionResolutionKey(appId, environmentId, connectionReferences, targetIdentity) {
102
102
  const references = Object.entries(connectionReferences).sort(([left], [right]) => left.localeCompare(right)).map(([localRefId, reference]) => ({
103
103
  localRefId,
104
104
  id: reference.id,
@@ -109,7 +109,14 @@ export function buildConnectionResolutionKey(appId, environmentId, connectionRef
109
109
  return JSON.stringify({
110
110
  appId: appId ?? '',
111
111
  environmentId,
112
- references
112
+ references,
113
+ ...(targetIdentity ? {
114
+ tenantId: targetIdentity.tenantId?.toLowerCase() ?? '',
115
+ appName: targetIdentity.appName ?? '',
116
+ region: targetIdentity.region?.toLowerCase() ?? '',
117
+ generation: targetIdentity.generation ?? 0,
118
+ accountIdentifier: targetIdentity.accountIdentifier ?? ''
119
+ } : {})
113
120
  });
114
121
  }
115
122
 
@@ -36,7 +36,8 @@
36
36
  import { useState, useEffect, useRef } from 'react';
37
37
  import { isDevPlayer } from "../utils/isDevPlayer.js";
38
38
  import { powerPlatformScope } from "../platform/PowerPlatformEnvironmentApi.js";
39
- import { AppLaunchAccessDeniedError, fetchAppLaunchData } from "./PowerPlatformConnectionApi.js";
39
+ import { fetchAppLaunchData } from "./PowerPlatformConnectionApi.js";
40
+ import { TargetRuntimeError } from "../target/errors.js";
40
41
  import { applyAclGrants, resolveViaStaticRefs } from "./resolveConnections.js";
41
42
  import { applyRegisteredConnectionUsages, getConnectionNamesRequiringAclGrant, invalidateFailedAclGrants, selectLaunchConnection } from "./connectionState.js";
42
43
  import { debugLog, debugWarn } from "../logger.js";
@@ -68,7 +69,9 @@ export function useConnectionRefs({
68
69
  schemaMap,
69
70
  onDisambiguationNeeded,
70
71
  onMissingConnection,
71
- isvClientId
72
+ isvClientId,
73
+ onTelemetryLocationResolved,
74
+ targetIdentity
72
75
  }) {
73
76
  const [connectionRefs, setConnectionRefs] = useState({});
74
77
  const [isReady, setIsReady] = useState(false);
@@ -91,7 +94,10 @@ export function useConnectionRefs({
91
94
  const isBackgroundRetryRef = useRef(false);
92
95
  const MAX_TOKEN_RETRIES = 5;
93
96
  const TOKEN_RETRY_DELAY_MS = 2000;
94
- const currentConfigKey = buildConnectionResolutionKey(appId, environmentId, isDevPlayer ? connectionReferences : {});
97
+ const currentConfigKey = buildConnectionResolutionKey(appId, environmentId, isDevPlayer ? connectionReferences : {}, targetIdentity ? {
98
+ ...targetIdentity,
99
+ region
100
+ } : undefined);
95
101
  useEffect(() => {
96
102
  if (!isSignedIn) {
97
103
  tokenRetryCountRef.current = 0;
@@ -200,9 +206,6 @@ export function useConnectionRefs({
200
206
  let result = null;
201
207
  if (appId && environmentId && !isDevPlayer) {
202
208
  result = await fetchAppLaunchData(appId, environmentId, region, token);
203
- if (!result) {
204
- throw new Error(`App "${appId}" is not available in environment "${environmentId}". Push the mobile app to this environment and try again.`);
205
- }
206
209
  if (result && onDisambiguationNeeded) {
207
210
  for (const requirement of [...result.missingConnectionReferences]) {
208
211
  const candidates = result.connectionCandidates[requirement.localRefId] ?? [];
@@ -260,6 +263,9 @@ export function useConnectionRefs({
260
263
  });
261
264
  return;
262
265
  }
266
+ if (result.telemetryLocation) {
267
+ onTelemetryLocationResolved?.(result.telemetryLocation);
268
+ }
263
269
 
264
270
  // Pre-warm the Dataverse token while the user waits.
265
271
  if (result.dataverseOrgUrl) {
@@ -296,15 +302,18 @@ export function useConnectionRefs({
296
302
  errorName: e instanceof Error ? e.name : 'UnknownError'
297
303
  });
298
304
  getAppLogger().trackErrorEvent(HostOperationNames.PlatformLoadConnectionsFailed, {
299
- error: String(e),
305
+ errorName: e instanceof Error ? e.name : 'UnknownError',
306
+ errorCode: e instanceof TargetRuntimeError ? e.code : undefined,
300
307
  hasAppId: Boolean(appId),
301
308
  hasEnvironmentId: Boolean(environmentId),
302
309
  connectionReferenceCount: Object.keys(connectionReferences).length,
303
310
  runningInDevPlayer: isDevPlayer
304
311
  });
305
- const accessDenied = e instanceof AppLaunchAccessDeniedError;
306
- setErrorKind(accessDenied ? 'app-access-denied' : 'generic');
307
- setError(accessDenied ? 'You don’t have access to this app. Ask the app owner or an administrator to share it with you, then try again.' : 'Unable to load this app from Power Platform. Try again later.');
312
+ const targetError = e instanceof TargetRuntimeError ? e : null;
313
+ const accessDenied = targetError?.code === 'app-access-denied';
314
+ const appNotFound = targetError?.code === 'app-not-found';
315
+ setErrorKind(accessDenied ? 'app-access-denied' : appNotFound ? 'app-not-found' : 'generic');
316
+ setError(targetError?.userMessage ?? (accessDenied ? 'You don’t have access to this app. Ask the app owner or an administrator to share it with you, then try again.' : 'Unable to load this app from Power Platform. Try again later.'));
308
317
  setIsReady(true);
309
318
  }
310
319
  });
@@ -313,7 +322,7 @@ export function useConnectionRefs({
313
322
  if (retryTimer) clearTimeout(retryTimer);
314
323
  };
315
324
  // eslint-disable-next-line react-hooks/exhaustive-deps
316
- }, [isSignedIn, retryTick, appId, environmentId, region, connectionReferences, hasDataverse, schemaMap, isvClientId]);
325
+ }, [isSignedIn, retryTick, appId, environmentId, region, connectionReferences, hasDataverse, schemaMap, isvClientId, onTelemetryLocationResolved, targetIdentity?.tenantId, targetIdentity?.appName, targetIdentity?.generation, targetIdentity?.accountIdentifier]);
317
326
  return {
318
327
  connectionRefs,
319
328
  isReady,
@@ -13,14 +13,17 @@ import '@microsoft/power-apps-native-assets';
13
13
  export { NativePowerAppsBridge } from "./bridge/NativePowerAppsBridge.js";
14
14
  export { PowerPagesBridge, setupPowerPagesHost } from "./bridge/PowerPagesBridge.js";
15
15
  export { buildConnectionReferences } from "./bridge/NativePowerAppsBridge.js";
16
- export { setupNativeHost } from "./bridge/setupNativeHost.js";
16
+ export { invalidateNativeHostRuntime, setupNativeHost } from "./bridge/setupNativeHost.js";
17
17
  export { updateBridgeOptionsInPlace } from "./bridge/bridgeState.js";
18
18
  export { AuthProvider, useAuth } from './auth/AuthContext';
19
19
  export { completePowerAppsAuthSession } from "./auth/completePowerAppsAuthSession.js";
20
20
  export { PowerAppsHostProvider } from './provider/PowerAppsHostProvider';
21
21
  export { PowerAppsProvider } from "./provider/PowerAppsProvider.js";
22
+ export { PowerAppsRuntimeGate, usePowerAppsExperience } from "./target/PowerAppsExperience.js";
22
23
  export { buildEnvironmentEndpoint, POWER_PLATFORM_API_SCOPE } from "./platform/PowerPlatformEnvironmentApi.js";
23
24
  export { fetchInstanceUrl } from "./platform/DataverseGlobalDiscoveryApi.js";
25
+ export { buildCanvasAppResolutionUrl, canonicalizeDataverseOrigin } from "./platform/CanvasAppResolution.js";
26
+ export { resolveCanvasApp } from "./platform/CanvasAppResolutionApi.js";
24
27
  export { buildCdsDataSources } from "./platform/DataverseDataSources.js";
25
28
  export { useConnectionRefs } from "./connections/useConnectionRefs.js";
26
29
  // Connection creation & OAuth flow
@@ -48,6 +51,6 @@ export { isDevPlayer } from "./utils/isDevPlayer.js";
48
51
  export { base64ToDataverseUploadFile, base64ToUint8Array, createDataverseUploadFile } from "./utils/dataverseUtils.js";
49
52
 
50
53
  // ── Telemetry ─────────────────────────────────────────────────────────────────
51
- export { getAppLogger, configureAppLogger, getCustomerTelemetryLogger, HostOperationNames } from "./telemetry/index.js";
54
+ export { getAppLogger, configureAppLogger, getCustomEventsLogger, HostOperationNames } from "./telemetry/index.js";
52
55
  // ── Error boundary ────────────────────────────────────────────────────────────
53
56
  export { ErrorBoundary } from "./ErrorBoundary.js";
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+
3
+ export function canonicalizeDataverseOrigin(value) {
4
+ const url = new URL(value);
5
+ if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || url.pathname !== '' && url.pathname !== '/') {
6
+ throw new Error('Dataverse URL must be an HTTPS origin');
7
+ }
8
+ return url.origin.toLowerCase();
9
+ }
10
+ export function areDataverseOriginAliases(left, right) {
11
+ const normalizeHost = value => {
12
+ const url = new URL(canonicalizeDataverseOrigin(value));
13
+ const labels = url.hostname.split('.');
14
+ if (labels[1] === 'api') labels.splice(1, 1);
15
+ return `${labels.join('.')}:${url.port}`;
16
+ };
17
+ return normalizeHost(left) === normalizeHost(right);
18
+ }
19
+ export function buildCanvasAppResolutionUrl(dataverseApiUrl, appName) {
20
+ const origin = canonicalizeDataverseOrigin(dataverseApiUrl);
21
+ const query = new URLSearchParams({
22
+ '$select': 'name,uniquecanvasappid,canvasapptype,componentstate',
23
+ '$filter': 'name eq @appName and componentstate eq 0',
24
+ '@appName': `'${appName.replace(/'/g, "''")}'`
25
+ });
26
+ return `${origin}/api/data/v9.2/canvasapps?${query.toString()}`;
27
+ }
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+
3
+ import { getHttpCorrelationProperties } from '@microsoft/power-apps-native-common/http';
4
+ import { getAppLogger, HostOperationNames, platformHttpClient } from "../telemetry/index.js";
5
+ import { TargetRuntimeError } from "../target/errors.js";
6
+ import { buildCanvasAppResolutionUrl, canonicalizeDataverseOrigin } from "./CanvasAppResolution.js";
7
+ const GUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
8
+ export async function resolveCanvasApp(target, dataverseApiUrl, acquireToken, generation) {
9
+ const assertCurrent = () => {
10
+ if (generation && !generation.isCurrent(generation.value)) {
11
+ throw new TargetRuntimeError('stale-operation', 'app-resolution', false, 'The selected environment changed before app resolution completed.', target.id);
12
+ }
13
+ };
14
+ const origin = canonicalizeDataverseOrigin(dataverseApiUrl);
15
+ const scenario = getAppLogger().trackScenario(HostOperationNames.PlatformFetchInstanceUrl, {
16
+ targetId: target.id,
17
+ generation: generation?.value
18
+ });
19
+ try {
20
+ const token = await acquireToken([`${origin}/user_impersonation`]);
21
+ assertCurrent();
22
+ if (!token) {
23
+ throw new TargetRuntimeError('app-metadata-access-denied', 'app-resolution', false, 'You do not have access to app metadata in the selected environment.', target.id);
24
+ }
25
+ const response = await platformHttpClient.request(buildCanvasAppResolutionUrl(origin, target.appName), {
26
+ headers: {
27
+ Authorization: `Bearer ${token}`,
28
+ Accept: 'application/json'
29
+ }
30
+ });
31
+ assertCurrent();
32
+ if (response.status === 401 || response.status === 403) {
33
+ throw new TargetRuntimeError('app-metadata-access-denied', 'app-resolution', false, 'You do not have access to app metadata in the selected environment.', target.id);
34
+ }
35
+ if (!response.ok) throw new Error(`Canvas App metadata request failed with status ${response.status}`);
36
+ const payload = await response.json();
37
+ assertCurrent();
38
+ const rows = payload.value ?? [];
39
+ if (rows.length === 0) {
40
+ throw new TargetRuntimeError('app-metadata-missing', 'app-resolution', false, 'The configured app is missing from the selected environment.', target.id);
41
+ }
42
+ if (rows.length !== 1) {
43
+ throw new TargetRuntimeError('app-metadata-ambiguous', 'app-resolution', false, 'More than one app matches the selected environment configuration.', target.id);
44
+ }
45
+ const row = rows[0];
46
+ const appId = row.uniquecanvasappid?.toLowerCase();
47
+ if (row.name !== target.appName || row.componentstate !== 0 || !appId || !GUID_PATTERN.test(appId)) {
48
+ throw new TargetRuntimeError('app-metadata-missing', 'app-resolution', false, 'The configured app metadata is invalid in the selected environment.', target.id);
49
+ }
50
+ scenario.complete({
51
+ ...getHttpCorrelationProperties(response.headers)
52
+ });
53
+ return {
54
+ ...target,
55
+ appId,
56
+ dataverseApiUrl: origin
57
+ };
58
+ } catch (error) {
59
+ scenario.fail(undefined, {
60
+ errorCode: error instanceof TargetRuntimeError ? error.code : 'service-unavailable'
61
+ });
62
+ if (error instanceof TargetRuntimeError) throw error;
63
+ throw new TargetRuntimeError('service-unavailable', 'app-resolution', true, 'App metadata is temporarily unavailable. Try again.', target.id, error);
64
+ }
65
+ }
@@ -1,33 +1,38 @@
1
1
  "use strict";
2
2
 
3
+ import { tryResolveTelemetryLocationFromDataverseRegion } from '@microsoft/power-apps-native-common';
3
4
  import { getHttpCorrelationProperties } from '@microsoft/power-apps-native-common/http';
4
5
  import * as SecureStore from 'expo-secure-store';
5
6
  import { getAppLogger, HostOperationNames, platformHttpClient } from "../telemetry/index.js";
6
- import { buildGlobalDiscoveryUrl, globalDiscoveryScope } from "./GlobalDiscovery.js";
7
- const INSTANCE_URL_CACHE_KEY_PREFIX = 'microsoft.power-apps-native-host.instance-url.v1';
7
+ import { buildGlobalDiscoveryListUrl, buildGlobalDiscoveryUrl, globalDiscoveryScope, parseDiscoveredEnvironments } from "./GlobalDiscovery.js";
8
+ import { TargetRuntimeError } from "../target/errors.js";
9
+ const INSTANCE_URL_CACHE_KEY_PREFIX = 'microsoft.power-apps-native-host.instance-url.v3';
8
10
  const INSTANCE_URL_CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
9
11
  function getInstanceUrlCacheKey(environmentId, region) {
10
12
  return `${INSTANCE_URL_CACHE_KEY_PREFIX}.${region.toLowerCase()}.${environmentId.toLowerCase()}`;
11
13
  }
12
14
  async function getCachedInstanceUrl(environmentId, region) {
13
15
  try {
14
- const cachedValue = await SecureStore.getItemAsync(getInstanceUrlCacheKey(environmentId, region));
16
+ const cacheKey = getInstanceUrlCacheKey(environmentId, region);
17
+ const cachedValue = await SecureStore.getItemAsync(cacheKey);
15
18
  if (!cachedValue) {
16
19
  return null;
17
20
  }
18
21
  const entry = JSON.parse(cachedValue);
19
- if (typeof entry.instanceUrl !== 'string' || typeof entry.expiresAt !== 'number' || entry.expiresAt <= Date.now()) {
22
+ if (typeof entry.instanceUrl !== 'string' || entry.telemetryLocation !== undefined && entry.telemetryLocation !== 'eu' && entry.telemetryLocation !== 'global' || typeof entry.expiresAt !== 'number' || entry.expiresAt <= Date.now()) {
23
+ await SecureStore.deleteItemAsync(cacheKey);
20
24
  return null;
21
25
  }
22
- return entry.instanceUrl;
26
+ return entry;
23
27
  } catch {
24
28
  return null;
25
29
  }
26
30
  }
27
- async function cacheInstanceUrl(environmentId, region, instanceUrl) {
31
+ async function cacheInstanceUrl(environmentId, region, instanceUrl, telemetryLocation) {
28
32
  try {
29
33
  const entry = {
30
34
  instanceUrl,
35
+ telemetryLocation,
31
36
  expiresAt: Date.now() + INSTANCE_URL_CACHE_TTL_MS
32
37
  };
33
38
  await SecureStore.setItemAsync(getInstanceUrlCacheKey(environmentId, region), JSON.stringify(entry));
@@ -39,20 +44,40 @@ function getSafeErrorName(error) {
39
44
  return error instanceof Error ? error.constructor.name : typeof error;
40
45
  }
41
46
 
42
- /** Fetch the Dataverse instance URL from the Dataverse Global Discovery Service. */
43
- export async function fetchInstanceUrl(environmentId, region, acquireToken, _bearerToken) {
47
+ /** Enumerate enabled commercial-cloud Dataverse environments available to an authenticated tenant account. */
48
+ export async function fetchAccessibleEnvironments(tenantId, appDisplayName, appName, acquireToken) {
49
+ try {
50
+ const region = 'prod';
51
+ const token = await acquireToken([globalDiscoveryScope(region)]);
52
+ if (!token) throw new Error('Global Discovery token is unavailable');
53
+ const response = await platformHttpClient.request(buildGlobalDiscoveryListUrl(region), {
54
+ method: 'GET',
55
+ headers: {
56
+ Authorization: `Bearer ${token}`,
57
+ 'Content-Type': 'application/json'
58
+ }
59
+ });
60
+ if (!response.ok) throw new Error(`Global Discovery returned ${response.status} ${response.statusText}`);
61
+ const result = await response.json();
62
+ return parseDiscoveredEnvironments(Array.isArray(result.value) ? result.value : [], tenantId, appDisplayName, appName);
63
+ } catch (error) {
64
+ throw new TargetRuntimeError('discovery-failed', 'discovery', true, 'Your environments could not be loaded. Try again.', undefined, error);
65
+ }
66
+ }
67
+ /** Fetch Dataverse environment metadata from the Global Discovery Service. */
68
+ export async function fetchInstanceMetadata(environmentId, region, acquireToken, _bearerToken) {
44
69
  const scenario = getAppLogger().trackScenario(HostOperationNames.PlatformFetchInstanceUrl, {
45
70
  environmentId,
46
71
  region
47
72
  });
48
73
  try {
49
- const cachedInstanceUrl = await getCachedInstanceUrl(environmentId, region);
50
- if (cachedInstanceUrl) {
74
+ const cachedMetadata = await getCachedInstanceUrl(environmentId, region);
75
+ if (cachedMetadata) {
51
76
  scenario.complete({
52
77
  hasInstanceUrl: true,
53
78
  cacheHit: true
54
79
  });
55
- return cachedInstanceUrl;
80
+ return cachedMetadata;
56
81
  }
57
82
  const url = buildGlobalDiscoveryUrl(environmentId, region);
58
83
  const token = await acquireToken([globalDiscoveryScope(region)]);
@@ -70,20 +95,30 @@ export async function fetchInstanceUrl(environmentId, region, acquireToken, _bea
70
95
  throw new Error(`fetchInstanceUrl: Global Discovery returned ${response.status} ${response.statusText}`);
71
96
  }
72
97
  const result = await response.json();
73
- const instanceUrl = result.value?.find(instance => instance.EnvironmentId?.toLowerCase() === environmentId.toLowerCase())?.ApiUrl;
98
+ const instance = result.value?.find(instance => instance.EnvironmentId?.toLowerCase() === environmentId.toLowerCase());
99
+ const instanceUrl = instance?.ApiUrl;
100
+ const telemetryLocation = tryResolveTelemetryLocationFromDataverseRegion(instance?.Region);
74
101
  if (instanceUrl) {
75
- await cacheInstanceUrl(environmentId, region, instanceUrl);
102
+ await cacheInstanceUrl(environmentId, region, instanceUrl, telemetryLocation);
76
103
  }
77
104
  scenario.complete({
78
105
  hasInstanceUrl: instanceUrl !== undefined,
79
106
  cacheHit: false,
80
107
  ...getHttpCorrelationProperties(response.headers)
81
108
  });
82
- return instanceUrl;
109
+ return instanceUrl ? {
110
+ instanceUrl,
111
+ telemetryLocation
112
+ } : undefined;
83
113
  } catch (error) {
84
114
  scenario.fail(undefined, {
85
115
  errorName: getSafeErrorName(error)
86
116
  });
87
117
  throw error;
88
118
  }
119
+ }
120
+
121
+ /** Fetch only the Dataverse instance URL for callers that do not need region metadata. */
122
+ export async function fetchInstanceUrl(environmentId, region, acquireToken, bearerToken) {
123
+ return (await fetchInstanceMetadata(environmentId, region, acquireToken, bearerToken))?.instanceUrl;
89
124
  }