@microsoft/power-apps-native-host 0.2.22 → 0.3.0

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 (156) hide show
  1. package/README.md +162 -26
  2. package/compatibility/current.json +87 -0
  3. package/lib/NOTICE.txt +140 -1226
  4. package/lib/commonjs/auth/AuthContext.js +74 -18
  5. package/lib/commonjs/auth/AuthContext.web.js +32 -0
  6. package/lib/commonjs/auth/completePowerAppsAuthSession.js +16 -0
  7. package/lib/commonjs/bridge/NativePowerAppsBridge.js +20 -17
  8. package/lib/commonjs/bridge/PowerPagesBridge.js +157 -0
  9. package/lib/commonjs/bridge/bridgeState.js +40 -0
  10. package/lib/commonjs/connections/ConnectionSetupScreen.js +459 -80
  11. package/lib/commonjs/connections/PowerPlatformConnectionApi.js +946 -0
  12. package/lib/commonjs/connections/connectionMetadata.js +127 -0
  13. package/lib/commonjs/connections/connectionState.js +96 -0
  14. package/lib/commonjs/connections/disambiguate.js +19 -11
  15. package/lib/commonjs/connections/resolveConnections.js +347 -0
  16. package/lib/commonjs/connections/types.js +106 -0
  17. package/lib/commonjs/{platform → connections}/useConnectionRefs.js +128 -65
  18. package/lib/commonjs/connections/useConnectionSetup.js +111 -52
  19. package/lib/commonjs/controls/FilePicker.js +6 -0
  20. package/lib/commonjs/controls/ImagePicker.js +8 -0
  21. package/lib/commonjs/index.js +188 -6
  22. package/lib/commonjs/logger.js +9 -6
  23. package/lib/commonjs/offline/createOfflineHostActivationCallbacks.js +61 -0
  24. package/lib/commonjs/offline/useOfflineIntegration.js +12 -0
  25. package/lib/commonjs/offline/useOfflineIntegration.web.js +11 -0
  26. package/lib/commonjs/offline/useOfflineIntegrationWithStartupGate.js +46 -0
  27. package/lib/commonjs/offline/useOfflineIntegrationWithStartupGate.web.js +13 -0
  28. package/lib/commonjs/platform/DataverseDataSources.js +14 -0
  29. package/lib/commonjs/platform/DataverseGlobalDiscoveryApi.js +49 -0
  30. package/lib/commonjs/platform/GlobalDiscovery.js +51 -0
  31. package/lib/commonjs/platform/PowerPlatformEnvironmentApi.js +6 -106
  32. package/lib/commonjs/provider/PowerAppsHostProvider.js +156 -25
  33. package/lib/commonjs/provider/PowerAppsHostProvider.web.js +41 -0
  34. package/lib/commonjs/provider/PowerAppsProvider.js +229 -133
  35. package/lib/commonjs/provider/SessionDetailsGesture.js +216 -0
  36. package/lib/commonjs/telemetry/OfflineSdkTelemetry.js +95 -0
  37. package/lib/commonjs/telemetry/OfflineSdkTelemetry.web.js +9 -0
  38. package/lib/commonjs/telemetry/index.js +22 -3
  39. package/lib/module/auth/AuthContext.js +74 -18
  40. package/lib/module/auth/AuthContext.web.js +26 -0
  41. package/lib/module/auth/completePowerAppsAuthSession.js +11 -0
  42. package/lib/module/bridge/NativePowerAppsBridge.js +21 -18
  43. package/lib/module/bridge/PowerPagesBridge.js +151 -0
  44. package/lib/module/bridge/bridgeState.js +34 -0
  45. package/lib/module/connections/ConnectionSetupScreen.js +460 -81
  46. package/lib/module/connections/PowerPlatformConnectionApi.js +932 -0
  47. package/lib/module/connections/connectionMetadata.js +112 -0
  48. package/lib/module/connections/connectionState.js +82 -0
  49. package/lib/module/connections/disambiguate.js +19 -12
  50. package/lib/module/connections/resolveConnections.js +342 -0
  51. package/lib/module/connections/types.js +91 -0
  52. package/lib/module/{platform → connections}/useConnectionRefs.js +129 -66
  53. package/lib/module/connections/useConnectionSetup.js +113 -54
  54. package/lib/module/controls/FilePicker.js +6 -0
  55. package/lib/module/controls/ImagePicker.js +8 -0
  56. package/lib/module/index.js +14 -6
  57. package/lib/module/logger.js +8 -6
  58. package/lib/module/offline/createOfflineHostActivationCallbacks.js +55 -0
  59. package/lib/module/offline/useOfflineIntegration.js +3 -0
  60. package/lib/module/offline/useOfflineIntegration.web.js +7 -0
  61. package/lib/module/offline/useOfflineIntegrationWithStartupGate.js +42 -0
  62. package/lib/module/offline/useOfflineIntegrationWithStartupGate.web.js +9 -0
  63. package/lib/module/platform/DataverseDataSources.js +10 -0
  64. package/lib/module/platform/DataverseGlobalDiscoveryApi.js +45 -0
  65. package/lib/module/platform/GlobalDiscovery.js +45 -0
  66. package/lib/module/platform/PowerPlatformEnvironmentApi.js +5 -104
  67. package/lib/module/provider/PowerAppsHostProvider.js +159 -28
  68. package/lib/module/provider/PowerAppsHostProvider.web.js +36 -0
  69. package/lib/module/provider/PowerAppsProvider.js +233 -137
  70. package/lib/module/provider/SessionDetailsGesture.js +211 -0
  71. package/lib/module/telemetry/OfflineSdkTelemetry.js +91 -0
  72. package/lib/module/telemetry/OfflineSdkTelemetry.web.js +5 -0
  73. package/lib/module/telemetry/index.js +15 -4
  74. package/lib/typescript/commonjs/auth/AuthContext.d.ts +3 -1
  75. package/lib/typescript/commonjs/auth/AuthContext.web.d.ts +21 -0
  76. package/lib/typescript/commonjs/auth/completePowerAppsAuthSession.d.ts +8 -0
  77. package/lib/typescript/commonjs/bridge/PowerPagesBridge.d.ts +18 -0
  78. package/lib/typescript/commonjs/bridge/bridgeState.d.ts +5 -0
  79. package/lib/typescript/commonjs/connections/ConnectionSetupScreen.d.ts +28 -7
  80. package/lib/typescript/commonjs/{platform → connections}/PowerPlatformConnectionApi.d.ts +52 -43
  81. package/lib/typescript/commonjs/connections/connectionMetadata.d.ts +66 -0
  82. package/lib/typescript/commonjs/connections/connectionState.d.ts +16 -0
  83. package/lib/typescript/commonjs/connections/disambiguate.d.ts +4 -2
  84. package/lib/typescript/commonjs/{platform → connections}/resolveConnections.d.ts +19 -19
  85. package/lib/typescript/commonjs/connections/types.d.ts +129 -1
  86. package/lib/typescript/commonjs/{platform → connections}/useConnectionRefs.d.ts +33 -24
  87. package/lib/typescript/commonjs/connections/useConnectionSetup.d.ts +12 -8
  88. package/lib/typescript/commonjs/controls/FilePicker.d.ts +7 -1
  89. package/lib/typescript/commonjs/controls/ImagePicker.d.ts +9 -1
  90. package/lib/typescript/commonjs/index.d.ts +20 -10
  91. package/lib/typescript/commonjs/logger.d.ts +4 -4
  92. package/lib/typescript/commonjs/offline/createOfflineHostActivationCallbacks.d.ts +12 -0
  93. package/lib/typescript/commonjs/offline/useOfflineIntegration.d.ts +2 -0
  94. package/lib/typescript/commonjs/offline/useOfflineIntegration.web.d.ts +4 -0
  95. package/lib/typescript/commonjs/offline/useOfflineIntegrationWithStartupGate.d.ts +15 -0
  96. package/lib/typescript/commonjs/offline/useOfflineIntegrationWithStartupGate.web.d.ts +6 -0
  97. package/lib/typescript/commonjs/platform/DataverseDataSources.d.ts +27 -0
  98. package/lib/typescript/commonjs/platform/DataverseGlobalDiscoveryApi.d.ts +3 -0
  99. package/lib/typescript/commonjs/platform/GlobalDiscovery.d.ts +4 -0
  100. package/lib/typescript/commonjs/platform/PowerPlatformEnvironmentApi.d.ts +3 -74
  101. package/lib/typescript/commonjs/provider/PowerAppsHostProvider.d.ts +29 -18
  102. package/lib/typescript/commonjs/provider/PowerAppsHostProvider.web.d.ts +5 -0
  103. package/lib/typescript/commonjs/provider/PowerAppsProvider.d.ts +32 -10
  104. package/lib/typescript/commonjs/provider/SessionDetailsGesture.d.ts +13 -0
  105. package/lib/typescript/commonjs/telemetry/OfflineSdkTelemetry.d.ts +15 -0
  106. package/lib/typescript/commonjs/telemetry/OfflineSdkTelemetry.web.d.ts +2 -0
  107. package/lib/typescript/commonjs/telemetry/index.d.ts +13 -1
  108. package/lib/typescript/module/auth/AuthContext.d.ts +3 -1
  109. package/lib/typescript/module/auth/AuthContext.web.d.ts +21 -0
  110. package/lib/typescript/module/auth/completePowerAppsAuthSession.d.ts +8 -0
  111. package/lib/typescript/module/bridge/PowerPagesBridge.d.ts +18 -0
  112. package/lib/typescript/module/bridge/bridgeState.d.ts +5 -0
  113. package/lib/typescript/module/connections/ConnectionSetupScreen.d.ts +28 -7
  114. package/lib/typescript/module/{platform → connections}/PowerPlatformConnectionApi.d.ts +52 -43
  115. package/lib/typescript/module/connections/connectionMetadata.d.ts +66 -0
  116. package/lib/typescript/module/connections/connectionState.d.ts +16 -0
  117. package/lib/typescript/module/connections/disambiguate.d.ts +4 -2
  118. package/lib/typescript/module/{platform → connections}/resolveConnections.d.ts +19 -19
  119. package/lib/typescript/module/connections/types.d.ts +129 -1
  120. package/lib/typescript/module/{platform → connections}/useConnectionRefs.d.ts +33 -24
  121. package/lib/typescript/module/connections/useConnectionSetup.d.ts +12 -8
  122. package/lib/typescript/module/controls/FilePicker.d.ts +7 -1
  123. package/lib/typescript/module/controls/ImagePicker.d.ts +9 -1
  124. package/lib/typescript/module/index.d.ts +24 -14
  125. package/lib/typescript/module/logger.d.ts +4 -4
  126. package/lib/typescript/module/offline/createOfflineHostActivationCallbacks.d.ts +12 -0
  127. package/lib/typescript/module/offline/useOfflineIntegration.d.ts +2 -0
  128. package/lib/typescript/module/offline/useOfflineIntegration.web.d.ts +4 -0
  129. package/lib/typescript/module/offline/useOfflineIntegrationWithStartupGate.d.ts +15 -0
  130. package/lib/typescript/module/offline/useOfflineIntegrationWithStartupGate.web.d.ts +6 -0
  131. package/lib/typescript/module/platform/DataverseDataSources.d.ts +27 -0
  132. package/lib/typescript/module/platform/DataverseGlobalDiscoveryApi.d.ts +3 -0
  133. package/lib/typescript/module/platform/GlobalDiscovery.d.ts +4 -0
  134. package/lib/typescript/module/platform/PowerPlatformEnvironmentApi.d.ts +3 -74
  135. package/lib/typescript/module/provider/PowerAppsHostProvider.d.ts +29 -18
  136. package/lib/typescript/module/provider/PowerAppsHostProvider.web.d.ts +5 -0
  137. package/lib/typescript/module/provider/PowerAppsProvider.d.ts +34 -12
  138. package/lib/typescript/module/provider/SessionDetailsGesture.d.ts +13 -0
  139. package/lib/typescript/module/telemetry/OfflineSdkTelemetry.d.ts +15 -0
  140. package/lib/typescript/module/telemetry/OfflineSdkTelemetry.web.d.ts +2 -0
  141. package/lib/typescript/module/telemetry/index.d.ts +13 -1
  142. package/package.json +43 -18
  143. package/scripts/build-ios-base.js +66 -27
  144. package/scripts/bundle.js +212 -5
  145. package/scripts/generate-connector-schemas.js +8 -6
  146. package/scripts/metro-logger.js +213 -0
  147. package/scripts/start-web.mjs +137 -0
  148. package/scripts/upgrade/runtime.js +463 -0
  149. package/scripts/upgrade/system.js +244 -0
  150. package/scripts/upgrade-authoring.js +534 -0
  151. package/scripts/upgrade.js +188 -0
  152. package/scripts/wrap.js +536 -0
  153. package/lib/commonjs/platform/PowerPlatformConnectionApi.js +0 -606
  154. package/lib/commonjs/platform/resolveConnections.js +0 -264
  155. package/lib/module/platform/PowerPlatformConnectionApi.js +0 -595
  156. package/lib/module/platform/resolveConnections.js +0 -259
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+
3
+ import { getConnectorId } from "./types.js";
4
+ export function parseConnectionParameters(parameters) {
5
+ return Object.entries(parameters ?? {}).flatMap(([name, parameter]) => {
6
+ if (!parameter.uiDefinition) return [];
7
+ const constraints = parameter.uiDefinition.constraints;
8
+ const type = (parameter.type ?? 'string').toLowerCase();
9
+ const supportedType = type === 'string' || type === 'securestring' || type === 'boolean' || type === 'integer' || type === 'clientcertificate' ? type : null;
10
+ if (!supportedType) return [];
11
+ return [{
12
+ name,
13
+ displayName: parameter.uiDefinition.displayName ?? name,
14
+ type: supportedType,
15
+ required: constraints?.required === true || constraints?.required === 'true',
16
+ hidden: constraints?.hidden === true || constraints?.hidden === 'true',
17
+ capabilities: constraints?.capability,
18
+ allowedValues: constraints?.allowedValues?.filter(item => Boolean(item.value)).map(item => ({
19
+ value: item.value,
20
+ displayName: item.displayName ?? item.value
21
+ })),
22
+ defaultValue: parameter.defaultValue
23
+ }];
24
+ });
25
+ }
26
+ export function buildConnectionParameterSets(connectionParameters, parameterSets) {
27
+ const declaredSets = (parameterSets ?? []).filter(item => Boolean(item.name)).map(item => ({
28
+ name: item.name,
29
+ displayName: item.uiDefinition?.displayName ?? item.name,
30
+ parameters: parseConnectionParameters(item.connectionParameters ?? item.parameters),
31
+ requiresApiConsent: hasOAuthParameter(item.connectionParameters ?? item.parameters),
32
+ gatewayParameterName: Object.entries(item.connectionParameters ?? item.parameters ?? {}).find(([, parameter]) => parameter.type?.toLowerCase() === 'gatewaysetting')?.[0]
33
+ }));
34
+ return declaredSets.length > 0 ? declaredSets : [{
35
+ name: undefined,
36
+ displayName: 'Default',
37
+ parameters: parseConnectionParameters(connectionParameters),
38
+ requiresApiConsent: hasOAuthParameter(connectionParameters),
39
+ gatewayParameterName: Object.entries(connectionParameters ?? {}).find(([, parameter]) => parameter.type?.toLowerCase() === 'gatewaysetting')?.[0]
40
+ }];
41
+ }
42
+ export function hasOAuthParameter(parameters) {
43
+ return Object.values(parameters ?? {}).some(parameter => parameter.type?.toLowerCase() === 'oauthsetting');
44
+ }
45
+ export function buildConnectionUserConsentUrl(environmentEndpoint, connectorId, connectionId) {
46
+ return `${environmentEndpoint}/connectivity/connectors/${encodeURIComponent(getConnectorId(connectorId))}` + `/connections/${encodeURIComponent(connectionId)}/userConsent?api-version=1`;
47
+ }
48
+ export function getAppLaunchFailureMessage(status, statusText, responseText) {
49
+ if (status === 404) return null;
50
+ return `Unable to load the published app (HTTP ${status}): ${responseText || statusText}`;
51
+ }
52
+ export function isAppLaunchAccessDenied(status, responseText) {
53
+ if (status !== 403) return false;
54
+ try {
55
+ const body = JSON.parse(responseText);
56
+ return body.error?.code === 'PowerAppsNoAccess' || body.error?.details?.some(detail => detail.code === 'PowerAppsNoAccess') === true;
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
61
+ export function buildConsentLinkDefinition(redirectUrl) {
62
+ return {
63
+ redirectUrl
64
+ };
65
+ }
66
+ export function serializeConnectionParameters(input) {
67
+ const parameters = {
68
+ ...(input?.parameters ?? {}),
69
+ ...(input?.gatewayObjectId && input.gatewayParameterName ? {
70
+ [input.gatewayParameterName]: {
71
+ gatewayObjectId: input.gatewayObjectId
72
+ }
73
+ } : {})
74
+ };
75
+ if (!input?.parameterSetName) return {
76
+ connectionParameters: parameters
77
+ };
78
+ return {
79
+ connectionParametersSet: {
80
+ name: input.parameterSetName,
81
+ values: Object.fromEntries(Object.entries(parameters).map(([name, value]) => [name, {
82
+ value
83
+ }]))
84
+ }
85
+ };
86
+ }
87
+ export function buildConnectionUsageUrl(environmentBaseUrl, environmentId, usage) {
88
+ return `${environmentBaseUrl}/powerapps/apis/${encodeURIComponent(getConnectorId(usage.apiId))}` + `/connections/${encodeURIComponent(usage.connectionId)}/usages` + `?api-version=1&$filter=environment eq '${environmentId}'`;
89
+ }
90
+ export function buildConnectionUsageRequest(appId, usage) {
91
+ const resourceId = appId.startsWith('/providers/') ? appId : `/providers/Microsoft.PowerApps/apps/${appId}`;
92
+ return {
93
+ properties: {
94
+ reference: usage.localRefId,
95
+ resourceId,
96
+ appActions: usage.appActions
97
+ }
98
+ };
99
+ }
100
+ export function buildScopeConsentUrl(environmentBaseUrl, appId) {
101
+ const appName = appId.split('/').filter(Boolean).pop() ?? appId;
102
+ return `${environmentBaseUrl}/powerapps/apps/${encodeURIComponent(appName)}` + '/reportScopesConsent?api-version=1';
103
+ }
104
+ export function buildScopeConsentRequest(usages) {
105
+ return {
106
+ consents: usages.map(usage => ({
107
+ connectionId: usage.connectionId,
108
+ consentType: 'userConsent',
109
+ scopes: usage.apiScopes
110
+ }))
111
+ };
112
+ }
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+
3
+ import { normalizeApiId } from "./types.js";
4
+ import { connectionIdsEqual } from "./disambiguate.js";
5
+ export function isTransientConnectionConsentStatus(status) {
6
+ return status === 408 || status === 429 || status >= 500;
7
+ }
8
+ export function shouldGrantConnectionUserConsent(requirement, runningInDevPlayer) {
9
+ return runningInDevPlayer || !requirement.sharedConnectionId;
10
+ }
11
+ export function getConnectionNamesRequiringAclGrant(connectionRefs, connectionRequirements) {
12
+ const connectionNames = [];
13
+ const seen = new Set();
14
+ for (const requirement of connectionRequirements) {
15
+ if (!shouldGrantConnectionUserConsent(requirement, false)) continue;
16
+ const connectionName = requirement.dataSources.map(dataSource => connectionRefs[dataSource]?.connectionName).find(selectedName => Boolean(selectedName));
17
+ if (!connectionName || seen.has(connectionName)) continue;
18
+ seen.add(connectionName);
19
+ connectionNames.push(connectionName);
20
+ }
21
+ return connectionNames;
22
+ }
23
+ export function invalidateFailedAclGrants(result, failedConnectionNames) {
24
+ if (failedConnectionNames.size === 0) return;
25
+ for (const requirement of result.connectionRequirements) {
26
+ const selected = requirement.dataSources.map(dataSource => result.connectionRefs[dataSource]).find(Boolean);
27
+ if (!selected || !failedConnectionNames.has(selected.connectionName)) continue;
28
+ for (const dataSource of requirement.dataSources) {
29
+ const reference = result.connectionRefs[dataSource];
30
+ if (reference) reference.connectionName = '';
31
+ }
32
+ if (!result.missingConnectionReferences.some(item => item.localRefId === requirement.localRefId)) {
33
+ result.missingConnectionReferences.push(requirement);
34
+ }
35
+ if (!result.missingConnectors.includes(requirement.apiId)) {
36
+ result.missingConnectors.push(requirement.apiId);
37
+ }
38
+ }
39
+ }
40
+ export function selectLaunchConnection(result, localRefId, connectionId) {
41
+ const requirement = result.connectionRequirements.find(item => item.localRefId === localRefId);
42
+ const candidate = result.connectionCandidates[localRefId]?.find(item => connectionIdsEqual(item.id, connectionId));
43
+ if (!requirement || !candidate) return false;
44
+ for (const dataSource of requirement.dataSources) {
45
+ const reference = result.connectionRefs[dataSource];
46
+ if (reference) reference.connectionName = candidate.id;
47
+ }
48
+ result.missingConnectionReferences = result.missingConnectionReferences.filter(item => item.localRefId !== localRefId);
49
+ if (!result.missingConnectionReferences.some(item => item.apiId === requirement.apiId)) {
50
+ result.missingConnectors = result.missingConnectors.filter(apiId => apiId !== requirement.apiId);
51
+ }
52
+ return true;
53
+ }
54
+ export function getUnregisteredConnectionRequirements(result, usages = result.registeredConnectionUsages) {
55
+ return result.connectionRequirements.filter(requirement => {
56
+ const selectedConnectionId = requirement.dataSources.map(dataSource => result.connectionRefs[dataSource]?.connectionName).find(Boolean);
57
+ if (!selectedConnectionId) return true;
58
+ return !usages.some(usage => usage.reference.toLowerCase() === requirement.localRefId.toLowerCase() && normalizeApiId(usage.apiId) === requirement.apiId && connectionIdsEqual(usage.connectionId, selectedConnectionId) && (usage.appActions.length === 0 || (requirement.appActions ?? []).every(action => usage.appActions.includes(action))));
59
+ });
60
+ }
61
+ export function applyRegisteredConnectionSelections(result, usages) {
62
+ for (const requirement of result.connectionRequirements) {
63
+ const usage = usages.find(item => item.reference.toLowerCase() === requirement.localRefId.toLowerCase() && normalizeApiId(item.apiId) === requirement.apiId);
64
+ if (!usage) continue;
65
+ const candidate = result.connectionCandidates[requirement.localRefId]?.find(item => connectionIdsEqual(item.id, usage.connectionId));
66
+ if (candidate) selectLaunchConnection(result, requirement.localRefId, candidate.id);
67
+ }
68
+ }
69
+ export function hasRegisteredConnectionConsent(result, usages = result.registeredConnectionUsages) {
70
+ return getUnregisteredConnectionRequirements(result, usages).length === 0;
71
+ }
72
+ export function applyRegisteredConnectionUsages(result, usages) {
73
+ applyRegisteredConnectionSelections(result, usages);
74
+ result.registeredConnectionUsages = usages;
75
+ result.requiresConnectionConsent = getUnregisteredConnectionRequirements(result, usages).length > 0;
76
+ }
77
+ export function shouldPersistConnectionUsages(runningInDevPlayer, appId) {
78
+ return !runningInDevPlayer && Boolean(appId);
79
+ }
80
+ export function shouldShowConnectionReview(runningInDevPlayer, requiresConnectionConsent, hasResourceConsents) {
81
+ return !runningInDevPlayer && (requiresConnectionConsent || hasResourceConsents);
82
+ }
@@ -11,7 +11,7 @@
11
11
  * other web-host dependency.
12
12
  */
13
13
 
14
- import { ConnectionStatus } from "./types.js";
14
+ import { ConnectionStatus, normalizeApiId } from "./types.js";
15
15
  /**
16
16
  * Disambiguate a set of connection references against the user's available
17
17
  * connection instances.
@@ -30,11 +30,20 @@ import { ConnectionStatus } from "./types.js";
30
30
  export function disambiguateConnections(connectionReferences, allConnectionsByApiId) {
31
31
  const resolutions = [];
32
32
  for (const [localRefId, ref] of Object.entries(connectionReferences)) {
33
- const apiId = ref.id;
34
- const all = allConnectionsByApiId[apiId] ?? [];
33
+ const apiId = normalizeApiId(ref.id);
34
+ const all = allConnectionsByApiId[apiId] ?? allConnectionsByApiId[ref.id] ?? [];
35
35
  const connected = all.filter(c => c.status === ConnectionStatus.Connected);
36
36
  let result;
37
- if (connected.length === 1) {
37
+ if (ref.sharedConnectionId) {
38
+ const sharedId = extractConnectionId(ref.sharedConnectionId);
39
+ const shared = connected.find(connection => connectionIdsEqual(connection.id, sharedId));
40
+ result = shared ? {
41
+ kind: 'resolved',
42
+ connectionId: shared.id
43
+ } : {
44
+ kind: 'missing'
45
+ };
46
+ } else if (connected.length === 1) {
38
47
  result = {
39
48
  kind: 'resolved',
40
49
  connectionId: connected[0].id
@@ -68,16 +77,14 @@ export function disambiguateConnections(connectionReferences, allConnectionsByAp
68
77
  * "abc12345def6789012345678901234ab" (bare UUID, no dashes)
69
78
  * "abc12345-def6-7890-1234-5678901234ab" (UUID with dashes)
70
79
  *
71
- * Returns the 32-char hex ID (dashes stripped) or the raw string if no UUID
72
- * pattern is found.
80
+ * Returns the terminal resource name exactly as supplied by Power Platform.
81
+ * Some connectors require dashed GUIDs while others use compact IDs.
73
82
  */
74
83
  export function extractConnectionId(rawName) {
75
- // Match a standard UUID (with or without dashes) at the end of the string.
76
- const match = rawName.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i);
77
- if (match) {
78
- return match[1].replace(/-/g, '');
79
- }
80
- // Some connections use a 32-char hex ID without dashes, after a slash.
81
84
  const slashIdx = rawName.lastIndexOf('/');
82
85
  return slashIdx >= 0 ? rawName.slice(slashIdx + 1) : rawName;
86
+ }
87
+ export function connectionIdsEqual(left, right) {
88
+ const normalize = value => extractConnectionId(value).replace(/-/g, '').toLowerCase();
89
+ return normalize(left) === normalize(right);
83
90
  }
@@ -0,0 +1,342 @@
1
+ "use strict";
2
+
3
+ /*!
4
+ * power-apps-native-host — resolveConnections
5
+ *
6
+ * Pure async helpers for the two connection-resolution strategies used by
7
+ * useConnectionRefs:
8
+ *
9
+ * • resolveViaStaticRefs — per-connector resolution using the static
10
+ * connectionReferences map from power.config.json. Used only by Dev Player
11
+ * for unpublished local apps. Handles pre-disambiguation ACL grants so
12
+ * inaccessible connections are excluded before the user is asked to pick.
13
+ *
14
+ * • applyAclGrants — post-resolution user consent used by the fast path
15
+ * (production APKs via fetchAppLaunchData).
16
+ *
17
+ * Both helpers return / consume the shared AppLaunchResult shape so the
18
+ * hook orchestrator can handle them uniformly.
19
+ */
20
+
21
+ import { fetchConnectorMetadata, grantConnectionUserConsent } from "./PowerPlatformConnectionApi.js";
22
+ import { getConnectionNamesRequiringAclGrant, shouldGrantConnectionUserConsent } from "./connectionState.js";
23
+ import { connectionUserConsentScope, buildEnvironmentEndpoint } from "../platform/PowerPlatformEnvironmentApi.js";
24
+ import { fetchInstanceUrl } from "../platform/DataverseGlobalDiscoveryApi.js";
25
+ import { connectionIdsEqual, disambiguateConnections, extractConnectionId } from "../connections/disambiguate.js";
26
+ import { buildConnectionRequirements, ConnectionStatus, findConnectionDataSourceConflicts, getConnectionDatasetName, getFlowConnectionName, getFlowDataSourceNames, isCompositeConnectionApiId, getConnectorId, normalizeApiId } from "./types.js";
27
+ import { platformHttpClient } from "../telemetry/index.js";
28
+ const API_VERSION = '1';
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // ConnectorSchema
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /**
35
+ * Connector schema entry generated by `generate-connector-schemas`.
36
+ * Keyed by data source name in the `schemaMap` passed to `useConnectionRefs`.
37
+ */
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // resolveViaStaticRefs
41
+ // ---------------------------------------------------------------------------
42
+
43
+ /**
44
+ * Resolve connections from the static `connectionReferences` map.
45
+ *
46
+ * Steps:
47
+ * 1. Build an apiId → schema lookup.
48
+ * 2. In parallel: fetch the Dataverse instanceUrl and all connection instances
49
+ * for every connector.
50
+ * 3. If `isvClientId` is set, run pre-disambiguation user consent — connections
51
+ * where the grant fails are marked `NeedsAclGrant` and excluded from
52
+ * resolution, prompting the user to create a new connection.
53
+ * 4. Disambiguate: resolve single matches, invoke callbacks for ambiguous /
54
+ * missing cases, perform one-shot retries when `onMissingConnection` returns
55
+ * `true`.
56
+ * 5. Build and return an `AppLaunchResult`-shaped object.
57
+ */
58
+ export async function resolveViaStaticRefs({
59
+ connectionReferences,
60
+ schemaMap,
61
+ environmentId,
62
+ region,
63
+ acquireToken,
64
+ token,
65
+ isvClientId,
66
+ onDisambiguationNeeded,
67
+ onMissingConnection
68
+ }) {
69
+ const directConnectionReferences = Object.fromEntries(Object.entries(connectionReferences).filter(([, reference]) => !isCompositeConnectionApiId(reference.id)));
70
+ const flowConnectionReferences = Object.fromEntries(Object.entries(connectionReferences).filter(([, reference]) => getConnectorId(reference.id) === 'shared_logicflows'));
71
+ // Build apiId → schema lookup from the static refs.
72
+ const schemaByApiId = Object.fromEntries(Object.values(directConnectionReferences).map(ref => {
73
+ const schema = ref.dataSources.map(ds => schemaMap[ds]).find(Boolean);
74
+ return schema ? [normalizeApiId(ref.id), schema] : null;
75
+ }).filter(entry => entry !== null));
76
+ await Promise.all(Object.values({
77
+ ...directConnectionReferences,
78
+ ...flowConnectionReferences
79
+ }).map(async reference => {
80
+ const apiId = normalizeApiId(reference.id);
81
+ if (schemaByApiId[apiId]) return;
82
+ const metadata = await fetchConnectorMetadata(getConnectorId(reference.id), environmentId, region, token);
83
+ if (!metadata?.primaryRuntimeUrl) return;
84
+ schemaByApiId[apiId] = {
85
+ name: metadata.name,
86
+ properties: {
87
+ primaryRuntimeUrl: metadata.primaryRuntimeUrl,
88
+ scopes: metadata.scopes
89
+ }
90
+ };
91
+ }));
92
+ const base = buildEnvironmentEndpoint(environmentId, region);
93
+ const headers = {
94
+ Authorization: `Bearer ${token}`
95
+ };
96
+
97
+ // Fetch Dataverse instanceUrl and all connection instances in parallel.
98
+ const [instanceUrl, ...connResults] = await Promise.all([fetchInstanceUrl(environmentId, region, acquireToken, token), ...Object.entries(directConnectionReferences).map(async ([localRefId, ref]) => {
99
+ const apiId = normalizeApiId(ref.id);
100
+ const schema = schemaByApiId[apiId];
101
+ if (!schema) {
102
+ return {
103
+ localRefId,
104
+ apiId,
105
+ dataSources: ref.dataSources,
106
+ instances: []
107
+ };
108
+ }
109
+ const url = `${base}/connectivity/connectors/${schema.name}/connections` + `?api-version=${API_VERSION}&$filter=environment eq '${environmentId}'`;
110
+ try {
111
+ const res = await platformHttpClient.request(url, {
112
+ headers
113
+ });
114
+ if (!res.ok) {
115
+ return {
116
+ localRefId,
117
+ apiId,
118
+ dataSources: ref.dataSources,
119
+ instances: []
120
+ };
121
+ }
122
+ const body = await res.json();
123
+ const instances = (body.value ?? []).map(c => {
124
+ const connected = c.properties?.statuses?.[0]?.status === 'Connected';
125
+ return {
126
+ id: extractConnectionId(c.name),
127
+ name: c.name,
128
+ apiId,
129
+ ownerId: c.properties?.createdBy?.id,
130
+ status: connected ? ConnectionStatus.Connected : ConnectionStatus.NotConnected,
131
+ displayName: c.properties?.displayName,
132
+ isAuthenticated: connected
133
+ };
134
+ });
135
+ return {
136
+ localRefId,
137
+ apiId,
138
+ dataSources: ref.dataSources,
139
+ instances
140
+ };
141
+ } catch {
142
+ return {
143
+ localRefId,
144
+ apiId,
145
+ dataSources: ref.dataSources,
146
+ instances: []
147
+ };
148
+ }
149
+ })]);
150
+
151
+ // Pre-disambiguation delegated consent grants (ISV apps only).
152
+ //
153
+ // Connections where the grant fails are marked NeedsAclGrant so
154
+ // disambiguation treats them as unavailable and surfaces the
155
+ // "missing connection" flow, steering the user to create a new one. Fixed
156
+ if (isvClientId) {
157
+ const consentToken = await acquireToken([connectionUserConsentScope(region)]);
158
+ if (consentToken) {
159
+ const attemptedGrants = new Set();
160
+ await Promise.all(connResults.map(async r => {
161
+ const reference = directConnectionReferences[r.localRefId];
162
+ if (!reference || !shouldGrantConnectionUserConsent(reference, true)) return;
163
+ const connId = schemaByApiId[r.apiId]?.name;
164
+ if (!connId) return;
165
+ await Promise.all(r.instances.map(async inst => {
166
+ if (inst.status !== ConnectionStatus.Connected) return;
167
+ if (reference?.sharedConnectionId && !connectionIdsEqual(inst.id, reference.sharedConnectionId)) return;
168
+ const grantKey = `${connId.toLowerCase()}/${inst.id.toLowerCase()}`;
169
+ if (attemptedGrants.has(grantKey)) return;
170
+ attemptedGrants.add(grantKey);
171
+ const ok = await grantConnectionUserConsent(connId, inst.id, environmentId, region, consentToken);
172
+ if (!ok) inst.status = ConnectionStatus.NeedsAclGrant;
173
+ }));
174
+ }));
175
+ } else {
176
+ for (const result of connResults) {
177
+ for (const instance of result.instances) {
178
+ if (instance.status === ConnectionStatus.Connected) {
179
+ instance.status = ConnectionStatus.NeedsAclGrant;
180
+ }
181
+ }
182
+ }
183
+ }
184
+ }
185
+
186
+ // Disambiguation.
187
+ const allConnectionsByApiId = {};
188
+ for (const r of connResults) {
189
+ allConnectionsByApiId[r.apiId] = r.instances;
190
+ }
191
+ const disambiguationResults = disambiguateConnections(directConnectionReferences, allConnectionsByApiId);
192
+ const resolvedIds = {};
193
+ const unresolvedApiIds = [];
194
+ for (const dr of disambiguationResults) {
195
+ if (dr.result.kind === 'resolved') {
196
+ resolvedIds[dr.localRefId] = dr.result.connectionId;
197
+ } else if (dr.result.kind === 'ambiguous') {
198
+ const picked = onDisambiguationNeeded ? await onDisambiguationNeeded({
199
+ localRefId: dr.localRefId,
200
+ apiId: dr.apiId,
201
+ candidates: dr.result.candidates
202
+ }) : null;
203
+ if (picked) {
204
+ resolvedIds[dr.localRefId] = picked;
205
+ } else {
206
+ resolvedIds[dr.localRefId] = '';
207
+ unresolvedApiIds.push(normalizeApiId(dr.apiId));
208
+ }
209
+ } else {
210
+ // kind === 'missing'
211
+ let retried = false;
212
+ if (onMissingConnection) {
213
+ retried = await onMissingConnection({
214
+ localRefId: dr.localRefId,
215
+ apiId: dr.apiId
216
+ });
217
+ }
218
+ if (retried) {
219
+ // One-shot retry for this specific connector.
220
+ const schema = schemaByApiId[dr.apiId];
221
+ if (schema) {
222
+ try {
223
+ const url = `${base}/connectivity/connectors/${schema.name}/connections` + `?api-version=${API_VERSION}&$filter=environment eq '${environmentId}'`;
224
+ const res = await platformHttpClient.request(url, {
225
+ headers
226
+ });
227
+ if (res.ok) {
228
+ const body = await res.json();
229
+ const retryInstances = (body.value ?? []).map(c => {
230
+ const connected = c.properties?.statuses?.[0]?.status === 'Connected';
231
+ return {
232
+ id: extractConnectionId(c.name),
233
+ name: c.name,
234
+ apiId: dr.apiId,
235
+ status: connected ? ConnectionStatus.Connected : ConnectionStatus.NotConnected,
236
+ isAuthenticated: connected
237
+ };
238
+ });
239
+ const connected = retryInstances.find(i => i.status === ConnectionStatus.Connected);
240
+ if (connected) {
241
+ resolvedIds[dr.localRefId] = connected.id;
242
+ continue;
243
+ }
244
+ }
245
+ } catch {
246
+ // Fall through to missing.
247
+ }
248
+ }
249
+ }
250
+ resolvedIds[dr.localRefId] = '';
251
+ unresolvedApiIds.push(normalizeApiId(dr.apiId));
252
+ }
253
+ }
254
+ const dataverseOrgUrl = (instanceUrl ?? '').replace(/\/$/, '');
255
+
256
+ // Build the final AppConnectionReference map.
257
+ const connectionRefs = {};
258
+ for (const [localRefId, ref] of Object.entries(directConnectionReferences)) {
259
+ const normalizedApiId = normalizeApiId(ref.id);
260
+ const schema = schemaByApiId[normalizedApiId];
261
+ const connectionName = resolvedIds[localRefId] ?? '';
262
+ const isDataverse = ref.id.includes('commondataserviceforapps');
263
+ const entry = {
264
+ apiId: normalizedApiId,
265
+ runtimeUrl: schema?.properties.primaryRuntimeUrl ?? '',
266
+ connectionName,
267
+ datasetName: isDataverse ? dataverseOrgUrl : getConnectionDatasetName(ref)
268
+ };
269
+ for (const ds of ref.dataSources) {
270
+ connectionRefs[ds] = entry;
271
+ }
272
+ }
273
+ for (const ref of Object.values(flowConnectionReferences)) {
274
+ const normalizedApiId = normalizeApiId(ref.id);
275
+ const schema = schemaByApiId[normalizedApiId];
276
+ const entry = {
277
+ apiId: normalizedApiId,
278
+ runtimeUrl: schema?.properties.primaryRuntimeUrl ?? '',
279
+ connectionName: getFlowConnectionName(ref),
280
+ datasetName: ''
281
+ };
282
+ for (const dataSource of getFlowDataSourceNames(ref)) {
283
+ connectionRefs[dataSource] = entry;
284
+ }
285
+ }
286
+ const scopesByApiId = Object.fromEntries(Object.entries(schemaByApiId).map(([apiId, schema]) => [apiId, {
287
+ will: schema.properties.scopes?.will ?? [],
288
+ wont: schema.properties.scopes?.wont ?? []
289
+ }]));
290
+ const requirements = buildConnectionRequirements(connectionReferences, scopesByApiId);
291
+ const requirementsByLocalRefId = Object.fromEntries(requirements.map(requirement => [requirement.localRefId, requirement]));
292
+ const missingConnectionReferences = Object.entries(resolvedIds).filter(([, connectionId]) => !connectionId).map(([localRefId]) => requirementsByLocalRefId[localRefId]).filter(requirement => Boolean(requirement));
293
+ const connectionCandidates = Object.fromEntries(connResults.map(result => [result.localRefId, result.instances]));
294
+ return {
295
+ connectionRefs,
296
+ dataverseOrgUrl,
297
+ databaseReferences: {},
298
+ missingConnectors: [...new Set(unresolvedApiIds)],
299
+ missingConnectionReferences,
300
+ connectionRequirements: Object.values(requirementsByLocalRefId),
301
+ connectionCandidates,
302
+ resourceConsents: [],
303
+ configurationErrors: findConnectionDataSourceConflicts(requirements).map(dataSource => `Data source "${dataSource}" is assigned to multiple connection references.`),
304
+ registeredConnectionUsages: [],
305
+ requiresConnectionConsent: requirements.length > 0
306
+ };
307
+ }
308
+
309
+ // ---------------------------------------------------------------------------
310
+ // applyAclGrants
311
+ // ---------------------------------------------------------------------------
312
+
313
+ /**
314
+ * Run idempotent post-resolution delegated consent grants for the calling app.
315
+ *
316
+ * Used by the fast path (production APKs via `fetchAppLaunchData`) where the
317
+ * launch API has already resolved the user's connections. Grants are applied
318
+ * only to user-selected connections; fixed shared connections are already
319
+ * authorized by their owner and must not receive delegated user consent.
320
+ *
321
+ * For the slow path the grants run before disambiguation inside
322
+ * `resolveViaStaticRefs`, where they can still influence which connections
323
+ * are considered available.
324
+ *
325
+ * Returns the connection names whose ACL grants failed so callers can fail closed.
326
+ */
327
+ export async function applyAclGrants(connectionRefs, connectionRequirements, environmentId, region, acquireToken) {
328
+ const connectionNames = getConnectionNamesRequiringAclGrant(connectionRefs, connectionRequirements);
329
+ if (connectionNames.length === 0) return new Set();
330
+ const connectionNamesSet = new Set(connectionNames);
331
+ const consentToken = await acquireToken([connectionUserConsentScope(region)]);
332
+ if (!consentToken) return new Set(connectionNames);
333
+ const seen = new Set();
334
+ const failed = new Set();
335
+ await Promise.all(Object.values(connectionRefs).map(async ref => {
336
+ if (!ref.connectionName || !connectionNamesSet.has(ref.connectionName) || seen.has(ref.connectionName)) return;
337
+ seen.add(ref.connectionName);
338
+ const granted = await grantConnectionUserConsent(ref.apiId, ref.connectionName, environmentId, region, consentToken);
339
+ if (!granted) failed.add(ref.connectionName);
340
+ }));
341
+ return failed;
342
+ }