@microsoft/power-apps-native-host 0.2.25 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -10
- package/compatibility/current.json +31 -4
- package/lib/NOTICE.txt +1325 -24
- package/lib/commonjs/auth/AuthContext.js +110 -33
- package/lib/commonjs/bridge/NativePowerAppsBridge.js +11 -18
- package/lib/commonjs/bridge/PowerPagesBridge.js +27 -8
- package/lib/commonjs/bridge/bridgeState.js +40 -0
- package/lib/commonjs/config/babelConfig.js +13 -0
- package/lib/commonjs/config/expoConfig.js +71 -0
- package/lib/commonjs/config/metroConfig.js +70 -0
- package/lib/commonjs/config/metroLogger.js +182 -0
- package/lib/commonjs/config/tamaguiConfig.js +98 -0
- package/lib/commonjs/connections/ConnectionSetupScreen.js +459 -80
- package/lib/commonjs/connections/PowerPlatformConnectionApi.js +946 -0
- package/lib/commonjs/connections/connectionMetadata.js +127 -0
- package/lib/commonjs/connections/connectionState.js +96 -0
- package/lib/commonjs/connections/disambiguate.js +19 -11
- package/lib/commonjs/connections/resolveConnections.js +348 -0
- package/lib/commonjs/connections/types.js +106 -0
- package/lib/commonjs/{platform → connections}/useConnectionRefs.js +130 -65
- package/lib/commonjs/connections/useConnectionSetup.js +111 -52
- package/lib/commonjs/controls/FilePicker.js +6 -0
- package/lib/commonjs/controls/ImagePicker.js +8 -0
- package/lib/commonjs/index.js +167 -4
- package/lib/commonjs/logger.js +9 -6
- package/lib/commonjs/offline/createOfflineHostActivationCallbacks.js +61 -0
- package/lib/commonjs/offline/useOfflineIntegrationWithStartupGate.js +48 -0
- package/lib/commonjs/offline/useOfflineIntegrationWithStartupGate.web.js +13 -0
- package/lib/commonjs/platform/DataverseDataSources.js +18 -0
- package/lib/commonjs/platform/DataverseGlobalDiscoveryApi.js +94 -0
- package/lib/commonjs/platform/GlobalDiscovery.js +51 -0
- package/lib/commonjs/platform/PowerPlatformEnvironmentApi.js +6 -106
- package/lib/commonjs/provider/PowerAppsHostProvider.js +157 -25
- package/lib/commonjs/provider/PowerAppsProvider.js +178 -134
- package/lib/commonjs/provider/SessionDetailsGesture.js +208 -0
- package/lib/commonjs/telemetry/OfflineSdkTelemetry.js +95 -0
- package/lib/commonjs/telemetry/OfflineSdkTelemetry.web.js +9 -0
- package/lib/commonjs/telemetry/index.js +22 -3
- package/lib/module/auth/AuthContext.js +110 -33
- package/lib/module/bridge/NativePowerAppsBridge.js +12 -19
- package/lib/module/bridge/PowerPagesBridge.js +27 -8
- package/lib/module/bridge/bridgeState.js +34 -0
- package/lib/module/config/babelConfig.js +9 -0
- package/lib/module/config/expoConfig.js +67 -0
- package/lib/module/config/metroConfig.js +65 -0
- package/lib/module/config/metroLogger.js +175 -0
- package/lib/module/config/tamaguiConfig.js +93 -0
- package/lib/module/connections/ConnectionSetupScreen.js +460 -81
- package/lib/module/connections/PowerPlatformConnectionApi.js +932 -0
- package/lib/module/connections/connectionMetadata.js +112 -0
- package/lib/module/connections/connectionState.js +82 -0
- package/lib/module/connections/disambiguate.js +19 -12
- package/lib/module/connections/resolveConnections.js +343 -0
- package/lib/module/connections/types.js +91 -0
- package/lib/module/{platform → connections}/useConnectionRefs.js +131 -66
- package/lib/module/connections/useConnectionSetup.js +113 -54
- package/lib/module/controls/FilePicker.js +6 -0
- package/lib/module/controls/ImagePicker.js +8 -0
- package/lib/module/index.js +14 -4
- package/lib/module/logger.js +8 -6
- package/lib/module/offline/createOfflineHostActivationCallbacks.js +55 -0
- package/lib/module/offline/useOfflineIntegrationWithStartupGate.js +44 -0
- package/lib/module/offline/useOfflineIntegrationWithStartupGate.web.js +9 -0
- package/lib/module/platform/DataverseDataSources.js +13 -0
- package/lib/module/platform/DataverseGlobalDiscoveryApi.js +89 -0
- package/lib/module/platform/GlobalDiscovery.js +45 -0
- package/lib/module/platform/PowerPlatformEnvironmentApi.js +5 -104
- package/lib/module/provider/PowerAppsHostProvider.js +160 -28
- package/lib/module/provider/PowerAppsProvider.js +182 -138
- package/lib/module/provider/SessionDetailsGesture.js +203 -0
- package/lib/module/telemetry/OfflineSdkTelemetry.js +91 -0
- package/lib/module/telemetry/OfflineSdkTelemetry.web.js +5 -0
- package/lib/module/telemetry/index.js +15 -4
- package/lib/typescript/commonjs/auth/AuthContext.d.ts +3 -1
- package/lib/typescript/commonjs/auth/AuthContext.web.d.ts +1 -0
- package/lib/typescript/commonjs/bridge/bridgeState.d.ts +5 -0
- package/lib/typescript/commonjs/config/babelConfig.d.ts +12 -0
- package/lib/typescript/commonjs/config/expoConfig.d.ts +21 -0
- package/lib/typescript/commonjs/config/metroConfig.d.ts +6 -0
- package/lib/typescript/commonjs/config/metroLogger.d.ts +21 -0
- package/lib/typescript/commonjs/config/tamaguiConfig.d.ts +1792 -0
- package/lib/typescript/commonjs/connections/ConnectionSetupScreen.d.ts +28 -7
- package/lib/typescript/commonjs/{platform → connections}/PowerPlatformConnectionApi.d.ts +52 -43
- package/lib/typescript/commonjs/connections/connectionMetadata.d.ts +66 -0
- package/lib/typescript/commonjs/connections/connectionState.d.ts +16 -0
- package/lib/typescript/commonjs/connections/disambiguate.d.ts +4 -2
- package/lib/typescript/commonjs/{platform → connections}/resolveConnections.d.ts +22 -20
- package/lib/typescript/commonjs/connections/types.d.ts +129 -1
- package/lib/typescript/commonjs/{platform → connections}/useConnectionRefs.d.ts +36 -25
- package/lib/typescript/commonjs/connections/useConnectionSetup.d.ts +12 -8
- package/lib/typescript/commonjs/controls/FilePicker.d.ts +7 -1
- package/lib/typescript/commonjs/controls/ImagePicker.d.ts +9 -1
- package/lib/typescript/commonjs/index.d.ts +18 -10
- package/lib/typescript/commonjs/logger.d.ts +4 -4
- package/lib/typescript/commonjs/offline/createOfflineHostActivationCallbacks.d.ts +12 -0
- package/lib/typescript/commonjs/offline/useOfflineIntegrationWithStartupGate.d.ts +15 -0
- package/lib/typescript/commonjs/offline/useOfflineIntegrationWithStartupGate.web.d.ts +6 -0
- package/lib/typescript/commonjs/platform/DataverseDataSources.d.ts +28 -0
- package/lib/typescript/commonjs/platform/DataverseGlobalDiscoveryApi.d.ts +3 -0
- package/lib/typescript/commonjs/platform/GlobalDiscovery.d.ts +4 -0
- package/lib/typescript/commonjs/platform/PowerPlatformEnvironmentApi.d.ts +3 -74
- package/lib/typescript/commonjs/provider/PowerAppsHostProvider.d.ts +29 -18
- package/lib/typescript/commonjs/provider/PowerAppsProvider.d.ts +54 -9
- package/lib/typescript/commonjs/provider/SessionDetailsGesture.d.ts +13 -0
- package/lib/typescript/commonjs/telemetry/OfflineSdkTelemetry.d.ts +15 -0
- package/lib/typescript/commonjs/telemetry/OfflineSdkTelemetry.web.d.ts +2 -0
- package/lib/typescript/commonjs/telemetry/index.d.ts +13 -1
- package/lib/typescript/module/auth/AuthContext.d.ts +3 -1
- package/lib/typescript/module/auth/AuthContext.web.d.ts +1 -0
- package/lib/typescript/module/bridge/bridgeState.d.ts +5 -0
- package/lib/typescript/module/config/babelConfig.d.ts +12 -0
- package/lib/typescript/module/config/expoConfig.d.ts +21 -0
- package/lib/typescript/module/config/metroConfig.d.ts +6 -0
- package/lib/typescript/module/config/metroLogger.d.ts +21 -0
- package/lib/typescript/module/config/tamaguiConfig.d.ts +1792 -0
- package/lib/typescript/module/connections/ConnectionSetupScreen.d.ts +28 -7
- package/lib/typescript/module/{platform → connections}/PowerPlatformConnectionApi.d.ts +52 -43
- package/lib/typescript/module/connections/connectionMetadata.d.ts +66 -0
- package/lib/typescript/module/connections/connectionState.d.ts +16 -0
- package/lib/typescript/module/connections/disambiguate.d.ts +4 -2
- package/lib/typescript/module/{platform → connections}/resolveConnections.d.ts +22 -20
- package/lib/typescript/module/connections/types.d.ts +129 -1
- package/lib/typescript/module/{platform → connections}/useConnectionRefs.d.ts +36 -25
- package/lib/typescript/module/connections/useConnectionSetup.d.ts +12 -8
- package/lib/typescript/module/controls/FilePicker.d.ts +7 -1
- package/lib/typescript/module/controls/ImagePicker.d.ts +9 -1
- package/lib/typescript/module/index.d.ts +18 -10
- package/lib/typescript/module/logger.d.ts +4 -4
- package/lib/typescript/module/offline/createOfflineHostActivationCallbacks.d.ts +12 -0
- package/lib/typescript/module/offline/useOfflineIntegrationWithStartupGate.d.ts +15 -0
- package/lib/typescript/module/offline/useOfflineIntegrationWithStartupGate.web.d.ts +6 -0
- package/lib/typescript/module/platform/DataverseDataSources.d.ts +28 -0
- package/lib/typescript/module/platform/DataverseGlobalDiscoveryApi.d.ts +3 -0
- package/lib/typescript/module/platform/GlobalDiscovery.d.ts +4 -0
- package/lib/typescript/module/platform/PowerPlatformEnvironmentApi.d.ts +3 -74
- package/lib/typescript/module/provider/PowerAppsHostProvider.d.ts +29 -18
- package/lib/typescript/module/provider/PowerAppsProvider.d.ts +54 -9
- package/lib/typescript/module/provider/SessionDetailsGesture.d.ts +13 -0
- package/lib/typescript/module/telemetry/OfflineSdkTelemetry.d.ts +15 -0
- package/lib/typescript/module/telemetry/OfflineSdkTelemetry.web.d.ts +2 -0
- package/lib/typescript/module/telemetry/index.d.ts +13 -1
- package/package.json +52 -13
- package/scripts/build-codegen-package.js +283 -0
- package/scripts/start-web.mjs +9 -5
- package/scripts/upgrade/runtime.js +139 -24
- package/scripts/upgrade-authoring.js +274 -180
- package/scripts/upgrade.js +10 -16
- package/src/config/generated-config.d.ts +4 -0
- package/src/config/index.d.ts +3 -0
- package/src/config/tsconfig.json +26 -0
- package/lib/commonjs/platform/PowerPlatformConnectionApi.js +0 -606
- package/lib/commonjs/platform/resolveConnections.js +0 -264
- package/lib/module/platform/PowerPlatformConnectionApi.js +0 -595
- package/lib/module/platform/resolveConnections.js +0 -259
|
@@ -0,0 +1,946 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.AppLaunchAccessDeniedError = void 0;
|
|
7
|
+
exports.confirmConsentCode = confirmConsentCode;
|
|
8
|
+
exports.createConnection = createConnection;
|
|
9
|
+
exports.deleteConnection = deleteConnection;
|
|
10
|
+
exports.fetchAppConnectionUsages = fetchAppConnectionUsages;
|
|
11
|
+
exports.fetchAppLaunchData = fetchAppLaunchData;
|
|
12
|
+
exports.fetchConnectionsForConnector = fetchConnectionsForConnector;
|
|
13
|
+
exports.fetchConnectorMetadata = fetchConnectorMetadata;
|
|
14
|
+
exports.grantConnectionUserConsent = grantConnectionUserConsent;
|
|
15
|
+
exports.pollConnectionStatus = pollConnectionStatus;
|
|
16
|
+
exports.postConnectionUsage = postConnectionUsage;
|
|
17
|
+
exports.repairConnection = repairConnection;
|
|
18
|
+
exports.reportScopeConsents = reportScopeConsents;
|
|
19
|
+
var _PowerPlatformEnvironmentApi = require("../platform/PowerPlatformEnvironmentApi.js");
|
|
20
|
+
var _types = require("../connections/types.js");
|
|
21
|
+
var _types2 = require("./types.js");
|
|
22
|
+
var _connectionMetadata = require("./connectionMetadata.js");
|
|
23
|
+
var _disambiguate = require("../connections/disambiguate.js");
|
|
24
|
+
var _connectionState = require("./connectionState.js");
|
|
25
|
+
var _index = require("../telemetry/index.js");
|
|
26
|
+
var _http = require("@microsoft/power-apps-native-common/http");
|
|
27
|
+
var _logger = require("../logger.js");
|
|
28
|
+
/*!
|
|
29
|
+
* power-apps-native-host — PowerPlatformConnectionApi
|
|
30
|
+
*
|
|
31
|
+
* Typed wrappers around the Power Platform environment-scoped connectivity API
|
|
32
|
+
* for connection CRUD operations and connector metadata retrieval.
|
|
33
|
+
*
|
|
34
|
+
* These mirror the endpoints used by ConnectorsApi in @microsoft/power-apps-common
|
|
35
|
+
* but are implemented as plain async functions so they have no external dependencies.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
// Power Platform connectivity API version used across all calls here.
|
|
39
|
+
const API_VERSION = '1';
|
|
40
|
+
class AppLaunchAccessDeniedError extends Error {
|
|
41
|
+
constructor() {
|
|
42
|
+
super('The signed-in user does not have access to this Power App.');
|
|
43
|
+
this.name = 'AppLaunchAccessDeniedError';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.AppLaunchAccessDeniedError = AppLaunchAccessDeniedError;
|
|
47
|
+
function normalizeResourceConsentId(resourceId) {
|
|
48
|
+
return resourceId.replace(/\/$/, '').toLowerCase();
|
|
49
|
+
}
|
|
50
|
+
function getSafeErrorName(error) {
|
|
51
|
+
return error instanceof Error ? error.constructor.name : typeof error;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Internal helpers
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Generate a random connection ID as a 32-char lowercase hex string (UUID v4
|
|
60
|
+
* without dashes), matching the format Power Platform uses for connection
|
|
61
|
+
* resource names (e.g. "326561a0849744d5ae75c9bcaca1e5f1").
|
|
62
|
+
*/
|
|
63
|
+
function generateConnectionId() {
|
|
64
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
65
|
+
return crypto.randomUUID().replace(/-/g, '');
|
|
66
|
+
}
|
|
67
|
+
// Fallback: manual UUID v4 bit pattern, returned as 32 hex chars.
|
|
68
|
+
return 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
|
69
|
+
const r = Math.random() * 16 | 0;
|
|
70
|
+
return (c === 'x' ? r : r & 0x3 | 0x8).toString(16);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Connector metadata
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
/** Subset of connector metadata returned by the Power Platform API. */
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Fetch metadata for a single connector from the Power Platform API.
|
|
82
|
+
*
|
|
83
|
+
* @param connectorId - Connector short name (e.g. "shared_office365users")
|
|
84
|
+
* or full API path suffix.
|
|
85
|
+
* @param environmentId - Environment GUID.
|
|
86
|
+
* @param region - Cluster region string (default "prod").
|
|
87
|
+
* @param token - Pre-acquired Power Platform API bearer token.
|
|
88
|
+
*/
|
|
89
|
+
async function fetchConnectorMetadata(connectorId, environmentId, region, token) {
|
|
90
|
+
const base = (0, _PowerPlatformEnvironmentApi.buildEnvironmentEndpoint)(environmentId, region);
|
|
91
|
+
const url = `${base}/connectivity/connectors/${encodeURIComponent(connectorId)}` + `?$filter=environment eq '${environmentId}'&api-version=${API_VERSION}`;
|
|
92
|
+
try {
|
|
93
|
+
const res = await _index.platformHttpClient.request(url, {
|
|
94
|
+
headers: {
|
|
95
|
+
Authorization: `Bearer ${token}`
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
if (!res.ok) return null;
|
|
99
|
+
const body = await res.json();
|
|
100
|
+
const parameterSetDefinitions = body.properties?.connectionParameterSets?.values ?? [];
|
|
101
|
+
const parameterSets = (0, _connectionMetadata.buildConnectionParameterSets)(body.properties?.connectionParameters, parameterSetDefinitions);
|
|
102
|
+
return {
|
|
103
|
+
name: body.name ?? connectorId,
|
|
104
|
+
displayName: body.properties?.displayName ?? connectorId,
|
|
105
|
+
iconUri: body.properties?.iconUri ?? '',
|
|
106
|
+
description: body.properties?.description ?? '',
|
|
107
|
+
primaryRuntimeUrl: body.properties?.primaryRuntimeUrl ?? '',
|
|
108
|
+
scopes: {
|
|
109
|
+
will: body.properties?.scopes?.will ?? [],
|
|
110
|
+
wont: body.properties?.scopes?.wont ?? []
|
|
111
|
+
},
|
|
112
|
+
hasOAuthParameter: (0, _connectionMetadata.hasOAuthParameter)(body.properties?.connectionParameters) || parameterSetDefinitions.some(item => (0, _connectionMetadata.hasOAuthParameter)(item.connectionParameters ?? item.parameters)),
|
|
113
|
+
parameterSets
|
|
114
|
+
};
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Connection listing
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Fetch all connections for a given connector in the environment.
|
|
126
|
+
* Returns only the `NativeConnectionInstance` shape used by the disambiguation layer.
|
|
127
|
+
*/
|
|
128
|
+
async function fetchConnectionsForConnector(connectorId, environmentId, region, token) {
|
|
129
|
+
const base = (0, _PowerPlatformEnvironmentApi.buildEnvironmentEndpoint)(environmentId, region);
|
|
130
|
+
const url = `${base}/connectivity/connectors/${encodeURIComponent(connectorId)}/connections` + `?$filter=environment eq '${environmentId}'&api-version=${API_VERSION}`;
|
|
131
|
+
try {
|
|
132
|
+
const res = await _index.platformHttpClient.request(url, {
|
|
133
|
+
headers: {
|
|
134
|
+
Authorization: `Bearer ${token}`
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
if (!res.ok) {
|
|
138
|
+
const errorText = await res.text().catch(() => res.statusText);
|
|
139
|
+
throw new Error(`Unable to list connector connections (HTTP ${res.status}): ${errorText}`);
|
|
140
|
+
}
|
|
141
|
+
const body = await res.json();
|
|
142
|
+
return (body.value ?? []).map(c => {
|
|
143
|
+
const rawStatus = c.properties?.statuses?.[0];
|
|
144
|
+
const connected = rawStatus?.status === 'Connected';
|
|
145
|
+
return {
|
|
146
|
+
id: (0, _disambiguate.extractConnectionId)(c.name),
|
|
147
|
+
name: c.name,
|
|
148
|
+
apiId: `/providers/Microsoft.PowerApps/apis/${connectorId}`,
|
|
149
|
+
ownerId: c.properties?.createdBy?.id,
|
|
150
|
+
status: connected ? _types.ConnectionStatus.Connected : _types.ConnectionStatus.NotConnected,
|
|
151
|
+
displayName: c.properties?.displayName,
|
|
152
|
+
accountName: c.properties?.accountName,
|
|
153
|
+
parameterSetName: c.properties?.connectionParametersSet?.name,
|
|
154
|
+
isAuthenticated: connected,
|
|
155
|
+
statusErrorCode: rawStatus?.error?.code
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (error instanceof Error) throw error;
|
|
160
|
+
throw new Error(`Unable to list connector connections: ${String(error)}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Delete a single connection instance from the user's environment. */
|
|
165
|
+
async function deleteConnection(connectorId, connectionId, environmentId, region, token) {
|
|
166
|
+
const base = (0, _PowerPlatformEnvironmentApi.buildEnvironmentEndpoint)(environmentId, region);
|
|
167
|
+
const url = `${base}/connectivity/connectors/${encodeURIComponent(connectorId)}/connections/${encodeURIComponent(connectionId)}` + `?api-version=${API_VERSION}&$filter=environment eq '${environmentId}'`;
|
|
168
|
+
try {
|
|
169
|
+
const response = await _index.platformHttpClient.request(url, {
|
|
170
|
+
method: 'DELETE',
|
|
171
|
+
headers: {
|
|
172
|
+
Authorization: `Bearer ${token}`
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
return response.ok || response.status === 404;
|
|
176
|
+
} catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Connection creation (implicit OAuth)
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Result of initiating an OAuth connection creation.
|
|
187
|
+
*
|
|
188
|
+
* `pending` — the connection has been created but is not yet authenticated.
|
|
189
|
+
* The `consentLink` URL must be opened (in a WebView or browser) so the
|
|
190
|
+
* user can complete the OAuth consent flow. Poll `pollConnectionStatus`
|
|
191
|
+
* until the status changes to "Connected".
|
|
192
|
+
*
|
|
193
|
+
* `connected` — the connection resource was created in Connected state.
|
|
194
|
+
*
|
|
195
|
+
* `error` — creation failed; `message` contains the error description.
|
|
196
|
+
*/
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Create a new connection for a connector in the user's environment.
|
|
200
|
+
*
|
|
201
|
+
* Calls:
|
|
202
|
+
* PUT {envBase}/connectivity/connectors/{connectorId}/connections/{connectionId}
|
|
203
|
+
*
|
|
204
|
+
* If the response contains a `consentLink`, the connection needs OAuth
|
|
205
|
+
* completion (caller should open the URL and then poll `pollConnectionStatus`).
|
|
206
|
+
* If the connection status is already "Connected", returns `connected`.
|
|
207
|
+
*
|
|
208
|
+
* @param connectorId - Connector short name (e.g. "shared_office365users").
|
|
209
|
+
* @param environmentId - Environment GUID.
|
|
210
|
+
* @param region - Cluster region.
|
|
211
|
+
* @param token - Power Platform API bearer token.
|
|
212
|
+
* @param displayName - Optional display name for the new connection.
|
|
213
|
+
* @param oauthRedirectUrl - Redirect URL sent to getConsentLink. The OAuth provider
|
|
214
|
+
* will redirect the user here after consent. Defaults to
|
|
215
|
+
* the standard native-client redirect accepted by Power Platform.
|
|
216
|
+
*/
|
|
217
|
+
async function createConnection(connectorId, environmentId, region, token, displayName, oauthRedirectUrl = 'https://login.microsoftonline.com/oauth2/nativeclient', input) {
|
|
218
|
+
const scenario = (0, _index.getAppLogger)().trackScenario(_index.HostOperationNames.PlatformCreateConnection, {
|
|
219
|
+
connectorId
|
|
220
|
+
});
|
|
221
|
+
try {
|
|
222
|
+
const base = (0, _PowerPlatformEnvironmentApi.buildEnvironmentEndpoint)(environmentId, region);
|
|
223
|
+
const connectionId = generateConnectionId();
|
|
224
|
+
const url = `${base}/connectivity/connectors/${encodeURIComponent(connectorId)}/connections/${connectionId}` + `?api-version=${API_VERSION}&$filter=environment eq '${environmentId}'`;
|
|
225
|
+
const body = {
|
|
226
|
+
properties: {
|
|
227
|
+
environment: {
|
|
228
|
+
name: environmentId
|
|
229
|
+
},
|
|
230
|
+
...(0, _connectionMetadata.serializeConnectionParameters)(input),
|
|
231
|
+
consentInfo: {
|
|
232
|
+
redirectUrl: oauthRedirectUrl
|
|
233
|
+
},
|
|
234
|
+
...(displayName ? {
|
|
235
|
+
displayName
|
|
236
|
+
} : {})
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
const res = await _index.platformHttpClient.request(url, {
|
|
240
|
+
method: 'PUT',
|
|
241
|
+
headers: {
|
|
242
|
+
Authorization: `Bearer ${token}`,
|
|
243
|
+
'Content-Type': 'application/json'
|
|
244
|
+
},
|
|
245
|
+
body: JSON.stringify(body)
|
|
246
|
+
});
|
|
247
|
+
if (!res.ok) {
|
|
248
|
+
const errText = await res.text().catch(() => res.statusText);
|
|
249
|
+
const result = {
|
|
250
|
+
status: 'error',
|
|
251
|
+
message: `HTTP ${res.status}: ${errText}`
|
|
252
|
+
};
|
|
253
|
+
scenario.completeWithError({
|
|
254
|
+
httpStatus: res.status
|
|
255
|
+
});
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
const data = await res.json();
|
|
259
|
+
const connectionName = data.name ?? connectionId;
|
|
260
|
+
const connectionStatus = data.properties?.statuses?.[0]?.status ?? '';
|
|
261
|
+
if (connectionStatus === 'Connected') {
|
|
262
|
+
scenario.complete({
|
|
263
|
+
result: 'connected'
|
|
264
|
+
});
|
|
265
|
+
return {
|
|
266
|
+
status: 'connected',
|
|
267
|
+
connectionName
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Fetch the OAuth consent link from the dedicated endpoint.
|
|
272
|
+
// Use the pre-generated connectionId directly — not the full resource name —
|
|
273
|
+
// so the path segment is never double-encoded.
|
|
274
|
+
const consentUrl = `${base}/connectivity/connectors/${encodeURIComponent(connectorId)}` + `/connections/${connectionId}/getConsentLink` + `?api-version=${API_VERSION}&$filter=environment eq '${environmentId}'`;
|
|
275
|
+
const consentRes = await _index.platformHttpClient.request(consentUrl, {
|
|
276
|
+
method: 'POST',
|
|
277
|
+
headers: {
|
|
278
|
+
Authorization: `Bearer ${token}`,
|
|
279
|
+
'Content-Type': 'application/json'
|
|
280
|
+
},
|
|
281
|
+
body: JSON.stringify((0, _connectionMetadata.buildConsentLinkDefinition)(oauthRedirectUrl))
|
|
282
|
+
});
|
|
283
|
+
if (!consentRes.ok) {
|
|
284
|
+
const errorText = await consentRes.text().catch(() => consentRes.statusText);
|
|
285
|
+
scenario.completeWithError({
|
|
286
|
+
httpStatus: consentRes.status,
|
|
287
|
+
phase: 'getConsentLink'
|
|
288
|
+
});
|
|
289
|
+
return {
|
|
290
|
+
status: 'error',
|
|
291
|
+
message: `Unable to start connector consent (HTTP ${consentRes.status}): ${errorText}`
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
const consentData = await consentRes.json();
|
|
295
|
+
const browserFallbackUrl = consentData.consentLink ?? '';
|
|
296
|
+
if (browserFallbackUrl) {
|
|
297
|
+
scenario.complete({
|
|
298
|
+
result: 'pending_oauth'
|
|
299
|
+
});
|
|
300
|
+
return {
|
|
301
|
+
status: 'pending',
|
|
302
|
+
connectionName,
|
|
303
|
+
consentLink: browserFallbackUrl
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
scenario.completeWithError({
|
|
307
|
+
phase: 'getConsentLink',
|
|
308
|
+
reason: 'missingConsentPath'
|
|
309
|
+
});
|
|
310
|
+
return {
|
|
311
|
+
status: 'error',
|
|
312
|
+
message: 'The connector did not return a usable sign-in or consent URL.'
|
|
313
|
+
};
|
|
314
|
+
} catch (e) {
|
|
315
|
+
scenario.fail(undefined, {
|
|
316
|
+
errorName: getSafeErrorName(e)
|
|
317
|
+
});
|
|
318
|
+
return {
|
|
319
|
+
status: 'error',
|
|
320
|
+
message: String(e)
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async function repairConnection(connectorId, connectionName, environmentId, region, token, oauthRedirectUrl = 'https://login.microsoftonline.com/oauth2/nativeclient', input, requiresApiConsent = true) {
|
|
325
|
+
const scenario = (0, _index.getAppLogger)().trackScenario(_index.HostOperationNames.PlatformRepairConnection, {
|
|
326
|
+
connectorId,
|
|
327
|
+
hasParameters: Boolean(input),
|
|
328
|
+
requiresApiConsent
|
|
329
|
+
});
|
|
330
|
+
const base = (0, _PowerPlatformEnvironmentApi.buildEnvironmentEndpoint)(environmentId, region);
|
|
331
|
+
const connectionId = (0, _disambiguate.extractConnectionId)(connectionName);
|
|
332
|
+
const connectionUrl = `${base}/connectivity/connectors/${encodeURIComponent(connectorId)}` + `/connections/${encodeURIComponent(connectionId)}` + `?api-version=${API_VERSION}&$filter=environment eq '${environmentId}'`;
|
|
333
|
+
if (input) {
|
|
334
|
+
try {
|
|
335
|
+
const updateResponse = await _index.platformHttpClient.request(connectionUrl, {
|
|
336
|
+
method: 'PATCH',
|
|
337
|
+
headers: {
|
|
338
|
+
Authorization: `Bearer ${token}`,
|
|
339
|
+
'Content-Type': 'application/json'
|
|
340
|
+
},
|
|
341
|
+
body: JSON.stringify({
|
|
342
|
+
properties: {
|
|
343
|
+
...(0, _connectionMetadata.serializeConnectionParameters)(input)
|
|
344
|
+
}
|
|
345
|
+
})
|
|
346
|
+
});
|
|
347
|
+
if (!updateResponse.ok) {
|
|
348
|
+
const errorText = await updateResponse.text().catch(() => updateResponse.statusText);
|
|
349
|
+
scenario.completeWithError({
|
|
350
|
+
httpStatus: updateResponse.status,
|
|
351
|
+
phase: 'update'
|
|
352
|
+
});
|
|
353
|
+
return {
|
|
354
|
+
status: 'error',
|
|
355
|
+
message: `Unable to update the connection (HTTP ${updateResponse.status}): ${errorText}`
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
if (!requiresApiConsent) {
|
|
359
|
+
scenario.complete({
|
|
360
|
+
result: 'updated'
|
|
361
|
+
});
|
|
362
|
+
return {
|
|
363
|
+
status: 'connected',
|
|
364
|
+
connectionName
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
} catch (error) {
|
|
368
|
+
scenario.fail(undefined, {
|
|
369
|
+
phase: 'update',
|
|
370
|
+
errorName: getSafeErrorName(error)
|
|
371
|
+
});
|
|
372
|
+
return {
|
|
373
|
+
status: 'error',
|
|
374
|
+
message: `Unable to update the connection: ${String(error)}`
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (!requiresApiConsent) {
|
|
379
|
+
scenario.completeWithError({
|
|
380
|
+
phase: 'validate',
|
|
381
|
+
reason: 'missingParameters'
|
|
382
|
+
});
|
|
383
|
+
return {
|
|
384
|
+
status: 'error',
|
|
385
|
+
message: 'Connection parameters are required to repair this connection.'
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
const consentUrl = `${base}/connectivity/connectors/${encodeURIComponent(connectorId)}` + `/connections/${encodeURIComponent(connectionId)}/getConsentLink` + `?api-version=${API_VERSION}&$filter=environment eq '${environmentId}'`;
|
|
389
|
+
try {
|
|
390
|
+
const response = await _index.platformHttpClient.request(consentUrl, {
|
|
391
|
+
method: 'POST',
|
|
392
|
+
headers: {
|
|
393
|
+
Authorization: `Bearer ${token}`,
|
|
394
|
+
'Content-Type': 'application/json'
|
|
395
|
+
},
|
|
396
|
+
body: JSON.stringify((0, _connectionMetadata.buildConsentLinkDefinition)(oauthRedirectUrl))
|
|
397
|
+
});
|
|
398
|
+
if (!response.ok) {
|
|
399
|
+
const errorText = await response.text().catch(() => response.statusText);
|
|
400
|
+
scenario.completeWithError({
|
|
401
|
+
httpStatus: response.status,
|
|
402
|
+
phase: 'getConsentLink'
|
|
403
|
+
});
|
|
404
|
+
return {
|
|
405
|
+
status: 'error',
|
|
406
|
+
message: `Unable to repair connector consent (HTTP ${response.status}): ${errorText}`
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
const data = await response.json();
|
|
410
|
+
if (!data.consentLink) {
|
|
411
|
+
scenario.completeWithError({
|
|
412
|
+
phase: 'getConsentLink',
|
|
413
|
+
reason: 'missingConsentPath'
|
|
414
|
+
});
|
|
415
|
+
return {
|
|
416
|
+
status: 'error',
|
|
417
|
+
message: 'The connector did not return a repair consent URL.'
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
scenario.complete({
|
|
421
|
+
result: 'pending_oauth'
|
|
422
|
+
});
|
|
423
|
+
return {
|
|
424
|
+
status: 'pending',
|
|
425
|
+
connectionName,
|
|
426
|
+
consentLink: data.consentLink
|
|
427
|
+
};
|
|
428
|
+
} catch (error) {
|
|
429
|
+
scenario.fail(undefined, {
|
|
430
|
+
phase: 'getConsentLink',
|
|
431
|
+
errorName: getSafeErrorName(error)
|
|
432
|
+
});
|
|
433
|
+
return {
|
|
434
|
+
status: 'error',
|
|
435
|
+
message: `Unable to repair connector consent: ${String(error)}`
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
441
|
+
// Connection status polling
|
|
442
|
+
// ---------------------------------------------------------------------------
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Poll a connection's status until it is "Connected" or a timeout is reached.
|
|
446
|
+
*
|
|
447
|
+
* @param connectionName - The connection resource name (from createConnection result).
|
|
448
|
+
* @param environmentId - Environment GUID.
|
|
449
|
+
* @param region - Cluster region.
|
|
450
|
+
* @param token - Power Platform API bearer token.
|
|
451
|
+
* @param options.intervalMs - Polling interval in ms (default 2000).
|
|
452
|
+
* @param options.timeoutMs - Maximum time to wait in ms (default 60000).
|
|
453
|
+
* @returns The resolved `NativeConnectionInstance` if Connected, or `null` if timed out.
|
|
454
|
+
*/
|
|
455
|
+
async function pollConnectionStatus(connectorId, connectionName, environmentId, region, acquireToken, options = {}) {
|
|
456
|
+
const scenario = (0, _index.getAppLogger)().trackScenario(_index.HostOperationNames.PlatformPollConnectionStatus, {
|
|
457
|
+
connectorId
|
|
458
|
+
});
|
|
459
|
+
const {
|
|
460
|
+
intervalMs = 2000,
|
|
461
|
+
timeoutMs = 60_000,
|
|
462
|
+
signal
|
|
463
|
+
} = options;
|
|
464
|
+
const deadline = Date.now() + timeoutMs;
|
|
465
|
+
try {
|
|
466
|
+
while (Date.now() < deadline) {
|
|
467
|
+
if (signal?.aborted) {
|
|
468
|
+
scenario.complete({
|
|
469
|
+
result: 'aborted'
|
|
470
|
+
});
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
const token = await acquireToken([(0, _PowerPlatformEnvironmentApi.powerPlatformScope)(region)]);
|
|
474
|
+
if (!token) {
|
|
475
|
+
scenario.completeWithError({
|
|
476
|
+
reason: 'tokenUnavailable'
|
|
477
|
+
});
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
const base = (0, _PowerPlatformEnvironmentApi.buildEnvironmentEndpoint)(environmentId, region);
|
|
481
|
+
const connectionId = (0, _disambiguate.extractConnectionId)(connectionName);
|
|
482
|
+
const url = `${base}/connectivity/connectors/${encodeURIComponent(connectorId)}/connections/${connectionId}` + `?api-version=${API_VERSION}&$filter=environment eq '${environmentId}'`;
|
|
483
|
+
try {
|
|
484
|
+
const res = await _index.platformHttpClient.request(url, {
|
|
485
|
+
headers: {
|
|
486
|
+
Authorization: `Bearer ${token}`
|
|
487
|
+
},
|
|
488
|
+
signal
|
|
489
|
+
});
|
|
490
|
+
if (res.ok) {
|
|
491
|
+
const data = await res.json();
|
|
492
|
+
const status = data.properties?.statuses?.[0]?.status ?? '';
|
|
493
|
+
if (status === 'Connected') {
|
|
494
|
+
scenario.complete({
|
|
495
|
+
result: 'connected'
|
|
496
|
+
});
|
|
497
|
+
return {
|
|
498
|
+
id: (0, _disambiguate.extractConnectionId)(data.name ?? connectionName),
|
|
499
|
+
name: data.name ?? connectionName,
|
|
500
|
+
apiId: data.properties?.apiId ?? '',
|
|
501
|
+
status: _types.ConnectionStatus.Connected,
|
|
502
|
+
displayName: data.properties?.displayName,
|
|
503
|
+
isAuthenticated: true
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
} catch {
|
|
508
|
+
// Transient network error — continue polling.
|
|
509
|
+
}
|
|
510
|
+
await new Promise(resolve => {
|
|
511
|
+
const finish = () => {
|
|
512
|
+
signal?.removeEventListener('abort', handleAbort);
|
|
513
|
+
resolve();
|
|
514
|
+
};
|
|
515
|
+
const timer = setTimeout(finish, intervalMs);
|
|
516
|
+
const handleAbort = () => {
|
|
517
|
+
clearTimeout(timer);
|
|
518
|
+
finish();
|
|
519
|
+
};
|
|
520
|
+
signal?.addEventListener('abort', handleAbort, {
|
|
521
|
+
once: true
|
|
522
|
+
});
|
|
523
|
+
// Allow GC to clean up the timer reference.
|
|
524
|
+
if (typeof timer === 'object' && timer !== null && 'unref' in timer) {
|
|
525
|
+
timer.unref();
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
scenario.complete({
|
|
530
|
+
result: 'timeout'
|
|
531
|
+
});
|
|
532
|
+
return null;
|
|
533
|
+
} catch (error) {
|
|
534
|
+
scenario.fail(undefined, {
|
|
535
|
+
errorName: getSafeErrorName(error)
|
|
536
|
+
});
|
|
537
|
+
throw error;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// ---------------------------------------------------------------------------
|
|
542
|
+
// Consent code confirmation
|
|
543
|
+
// ---------------------------------------------------------------------------
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Confirm a consent code returned by the APIM consent server in the OAuth
|
|
547
|
+
* redirect URL (`?code=<commitTokenCode>`).
|
|
548
|
+
*
|
|
549
|
+
* The consent server embeds a 32-char hex CommitTokenCode in the redirect URL
|
|
550
|
+
* when the OAuth exchange completes successfully. POSTing it here causes APIM
|
|
551
|
+
* to exchange it for the real OAuth tokens and activate the connection
|
|
552
|
+
* (status → Connected).
|
|
553
|
+
*
|
|
554
|
+
* When the redirect URL uses a custom scheme (e.g. `expo-app://oauth-callback`),
|
|
555
|
+
* the consent server may instead return `?error=no+code+returned` and activate
|
|
556
|
+
* the connection server-side. In that case this function is not called and the
|
|
557
|
+
* caller should poll `pollConnectionStatus` directly.
|
|
558
|
+
*
|
|
559
|
+
* @param connectorId - Connector short name (e.g. "shared_office365users").
|
|
560
|
+
* @param connectionId - Bare connection hex ID (no connector prefix).
|
|
561
|
+
* @param environmentId - Environment GUID.
|
|
562
|
+
* @param region - Cluster region.
|
|
563
|
+
* @param token - Power Platform API bearer token.
|
|
564
|
+
* @param code - CommitTokenCode from the redirect URL `?code=` param.
|
|
565
|
+
* @returns `true` if the server accepted the code, `false` otherwise.
|
|
566
|
+
*/
|
|
567
|
+
async function confirmConsentCode(connectorId, connectionId, environmentId, region, token, code) {
|
|
568
|
+
const base = (0, _PowerPlatformEnvironmentApi.buildEnvironmentEndpoint)(environmentId, region);
|
|
569
|
+
const url = `${base}/connectivity/connectors/${encodeURIComponent(connectorId)}` + `/connections/${connectionId}/confirmConsentCode` + `?api-version=${API_VERSION}&$filter=environment eq '${environmentId}'`;
|
|
570
|
+
try {
|
|
571
|
+
const res = await _index.platformHttpClient.request(url, {
|
|
572
|
+
method: 'POST',
|
|
573
|
+
headers: {
|
|
574
|
+
Authorization: `Bearer ${token}`,
|
|
575
|
+
'Content-Type': 'application/json'
|
|
576
|
+
},
|
|
577
|
+
body: JSON.stringify({
|
|
578
|
+
code
|
|
579
|
+
})
|
|
580
|
+
});
|
|
581
|
+
return res.ok;
|
|
582
|
+
} catch {
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// ---------------------------------------------------------------------------
|
|
588
|
+
// PowerApps environment — fetch published app launch data
|
|
589
|
+
// ---------------------------------------------------------------------------
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Processed result of the PowerApps app launch API call, ready to be applied
|
|
593
|
+
* directly to the NativePowerAppsBridge without further network calls.
|
|
594
|
+
*/
|
|
595
|
+
|
|
596
|
+
async function fetchAppConnectionUsages(appId, environmentId, region, token) {
|
|
597
|
+
const base = (0, _PowerPlatformEnvironmentApi.buildEnvironmentEndpoint)(environmentId, region);
|
|
598
|
+
const url = `${base}/powerapps/apps/${encodeURIComponent(appId.split('/').pop() ?? appId)}/connections` + `?api-version=${API_VERSION}&$filter=environment eq '${environmentId}'&$expand=api`;
|
|
599
|
+
const response = await _index.platformHttpClient.request(url, {
|
|
600
|
+
headers: {
|
|
601
|
+
Authorization: `Bearer ${token}`
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
if (!response.ok) {
|
|
605
|
+
const errorText = await response.text().catch(() => response.statusText);
|
|
606
|
+
throw new Error(`Unable to load app connection usages (HTTP ${response.status}): ${errorText}`);
|
|
607
|
+
}
|
|
608
|
+
const data = await response.json();
|
|
609
|
+
return (data.value ?? []).filter(usage => Boolean(usage.name && usage.properties?.reference && usage.properties?.apiId)).map(usage => ({
|
|
610
|
+
reference: usage.properties.reference,
|
|
611
|
+
apiId: (0, _types2.normalizeApiId)(usage.properties.apiId),
|
|
612
|
+
connectionId: (0, _disambiguate.extractConnectionId)(usage.name),
|
|
613
|
+
appActions: usage.properties?.appActions ?? []
|
|
614
|
+
}));
|
|
615
|
+
}
|
|
616
|
+
async function postConnectionUsage(appId, usage, environmentId, region, token) {
|
|
617
|
+
const base = (0, _PowerPlatformEnvironmentApi.buildEnvironmentEndpoint)(environmentId, region);
|
|
618
|
+
const url = (0, _connectionMetadata.buildConnectionUsageUrl)(base, environmentId, usage);
|
|
619
|
+
const response = await _index.platformHttpClient.request(url, {
|
|
620
|
+
method: 'POST',
|
|
621
|
+
headers: {
|
|
622
|
+
Authorization: `Bearer ${token}`,
|
|
623
|
+
'Content-Type': 'application/json'
|
|
624
|
+
},
|
|
625
|
+
body: JSON.stringify((0, _connectionMetadata.buildConnectionUsageRequest)(appId, usage))
|
|
626
|
+
});
|
|
627
|
+
if (!response.ok) {
|
|
628
|
+
const errorText = await response.text().catch(() => response.statusText);
|
|
629
|
+
throw new Error(`Unable to register connection usage (HTTP ${response.status}): ${errorText}`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
async function reportScopeConsents(appId, usages, environmentId, region, token) {
|
|
633
|
+
if (usages.length === 0) return;
|
|
634
|
+
const base = (0, _PowerPlatformEnvironmentApi.buildEnvironmentEndpoint)(environmentId, region);
|
|
635
|
+
const response = await _index.platformHttpClient.request((0, _connectionMetadata.buildScopeConsentUrl)(base, appId), {
|
|
636
|
+
method: 'POST',
|
|
637
|
+
headers: {
|
|
638
|
+
Authorization: `Bearer ${token}`,
|
|
639
|
+
'Content-Type': 'application/json'
|
|
640
|
+
},
|
|
641
|
+
body: JSON.stringify((0, _connectionMetadata.buildScopeConsentRequest)(usages))
|
|
642
|
+
});
|
|
643
|
+
if (!response.ok) {
|
|
644
|
+
const errorText = await response.text().catch(() => response.statusText);
|
|
645
|
+
throw new Error(`Unable to register connection scope consent (HTTP ${response.status}): ${errorText}`);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Fetch app launch data from the Power Platform environment-scoped launch
|
|
651
|
+
* endpoint and process it into the shape the NativePowerAppsBridge needs.
|
|
652
|
+
*
|
|
653
|
+
* A single call returns everything required to run the app:
|
|
654
|
+
* - `app.properties.connectionReferences` — connectors the app requires
|
|
655
|
+
* - `apis[]` — connector metadata (primaryRuntimeUrl)
|
|
656
|
+
* - `connections[]` — the current user's resolved connections
|
|
657
|
+
* - `app.properties.environment.properties.linkedEnvironmentMetadata.instanceUrl`
|
|
658
|
+
* — Dataverse org URL
|
|
659
|
+
*
|
|
660
|
+
* Endpoint (environment-scoped, same base as all Power Platform API calls):
|
|
661
|
+
* POST {envEndpoint}/powerapps/apps/{appId}/launch
|
|
662
|
+
* ?api-version=2&lastKnownGoodPublishedVersion=true
|
|
663
|
+
*
|
|
664
|
+
* Authentication: bearer token for {@link powerPlatformScope}`(region)`.
|
|
665
|
+
*
|
|
666
|
+
* @param appId - PowerApps app GUID (from `power.config.json` → `appId`).
|
|
667
|
+
* @param environmentId - Environment GUID.
|
|
668
|
+
* @param region - Cluster region (default "prod").
|
|
669
|
+
* @param token - Bearer token for {@link powerPlatformScope}`(region)`.
|
|
670
|
+
* @returns Processed launch data, or `null` when the app is not found.
|
|
671
|
+
*/
|
|
672
|
+
async function fetchAppLaunchData(appId, environmentId, region, token) {
|
|
673
|
+
const base = (0, _PowerPlatformEnvironmentApi.buildEnvironmentEndpoint)(environmentId, region);
|
|
674
|
+
const url = `${base}/powerapps/apps/${encodeURIComponent(appId)}/launch` + `?api-version=2&lastKnownGoodPublishedVersion=true`;
|
|
675
|
+
let res;
|
|
676
|
+
try {
|
|
677
|
+
res = await _index.platformHttpClient.request(url, {
|
|
678
|
+
method: 'POST',
|
|
679
|
+
headers: {
|
|
680
|
+
Authorization: `Bearer ${token}`,
|
|
681
|
+
'Content-Type': 'application/json'
|
|
682
|
+
},
|
|
683
|
+
body: '{}'
|
|
684
|
+
});
|
|
685
|
+
} catch (error) {
|
|
686
|
+
throw new Error(`Unable to reach the Power Platform app launch endpoint: ${String(error)}`);
|
|
687
|
+
}
|
|
688
|
+
if (!res.ok) {
|
|
689
|
+
const responseText = await res.text().catch(() => res.statusText);
|
|
690
|
+
if ((0, _connectionMetadata.isAppLaunchAccessDenied)(res.status, responseText)) {
|
|
691
|
+
throw new AppLaunchAccessDeniedError();
|
|
692
|
+
}
|
|
693
|
+
const failureMessage = (0, _connectionMetadata.getAppLaunchFailureMessage)(res.status, res.statusText, responseText);
|
|
694
|
+
if (!failureMessage) return null;
|
|
695
|
+
throw new Error(failureMessage);
|
|
696
|
+
}
|
|
697
|
+
try {
|
|
698
|
+
const data = await res.json();
|
|
699
|
+
const connRefRaw = data.app?.properties?.connectionReferences ?? {};
|
|
700
|
+
const apisRaw = data.apis ?? [];
|
|
701
|
+
const connectionsRaw = data.connections ?? [];
|
|
702
|
+
const launchConnectionUsages = (data.appConnections ?? []).filter(usage => Boolean(usage.name && usage.properties?.reference && usage.properties?.apiId)).map(usage => ({
|
|
703
|
+
reference: usage.properties.reference,
|
|
704
|
+
apiId: (0, _types2.normalizeApiId)(usage.properties.apiId),
|
|
705
|
+
connectionId: (0, _disambiguate.extractConnectionId)(usage.name),
|
|
706
|
+
appActions: usage.properties?.appActions ?? []
|
|
707
|
+
}));
|
|
708
|
+
const hasCompleteLaunchUsages = launchConnectionUsages.length > 0 && data.appConnections?.every(usage => Array.isArray(usage.properties?.appActions));
|
|
709
|
+
let registeredConnectionUsages = launchConnectionUsages;
|
|
710
|
+
let fetchedConnectionUsages = false;
|
|
711
|
+
if (!hasCompleteLaunchUsages) {
|
|
712
|
+
try {
|
|
713
|
+
registeredConnectionUsages = await fetchAppConnectionUsages(appId, environmentId, region, token);
|
|
714
|
+
fetchedConnectionUsages = true;
|
|
715
|
+
} catch (usageError) {
|
|
716
|
+
(0, _logger.debugWarn)('[fetchAppLaunchData] appConnections incomplete and usage lookup failed:', usageError);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Dataverse org URL from the embedded environment metadata.
|
|
721
|
+
const instanceUrl = data.app?.properties?.environment?.properties?.linkedEnvironmentMetadata?.instanceUrl;
|
|
722
|
+
const dataverseOrgUrl = (instanceUrl ?? '').replace(/\/$/, '');
|
|
723
|
+
const resourceConsents = new Map();
|
|
724
|
+
const approvedResources = new Set((data.app?.properties?.userAppResourceConsent ?? []).filter(consent => consent.type?.toLowerCase() === 'auth' && consent.consent?.toLowerCase() === 'yes' && Boolean(consent.name)).map(consent => normalizeResourceConsentId(consent.name)));
|
|
725
|
+
for (const authorization of data.app?.properties?.authorizationReferences ?? []) {
|
|
726
|
+
if (authorization.resourceId && !approvedResources.has(normalizeResourceConsentId(authorization.resourceId))) {
|
|
727
|
+
const key = normalizeResourceConsentId(authorization.resourceId);
|
|
728
|
+
resourceConsents.set(key, {
|
|
729
|
+
resourceId: authorization.resourceId,
|
|
730
|
+
displayName: authorization.resourceId
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
for (const reference of Object.values(data.app?.properties?.databaseReferences ?? {})) {
|
|
735
|
+
const metadata = reference.databaseDetails?.linkedEnvironmentMetadata;
|
|
736
|
+
const resourceId = metadata?.instanceUrl;
|
|
737
|
+
if (resourceId && normalizeResourceConsentId(resourceId) !== normalizeResourceConsentId(dataverseOrgUrl) && !approvedResources.has(normalizeResourceConsentId(resourceId))) {
|
|
738
|
+
resourceConsents.set(normalizeResourceConsentId(resourceId), {
|
|
739
|
+
resourceId,
|
|
740
|
+
displayName: metadata?.friendlyName ?? resourceId
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// connectorShortName → primaryRuntimeUrl (from apis[])
|
|
746
|
+
const runtimeUrlByConnector = {};
|
|
747
|
+
const scopesByConnector = {};
|
|
748
|
+
for (const api of apisRaw) {
|
|
749
|
+
const connectorName = (0, _types2.getConnectorId)(api.name ?? '');
|
|
750
|
+
if (connectorName) {
|
|
751
|
+
scopesByConnector[connectorName] = {
|
|
752
|
+
will: api.properties?.scopes?.will ?? [],
|
|
753
|
+
wont: api.properties?.scopes?.wont ?? []
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
if (connectorName && api.properties?.primaryRuntimeUrl) {
|
|
757
|
+
runtimeUrlByConnector[connectorName] = api.properties.primaryRuntimeUrl;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
const requiredConnectorNames = [...new Set(Object.values(connRefRaw).map(reference => reference.id ?? '').filter(apiId => apiId && (0, _types2.getConnectorId)(apiId) !== 'shared_excel').map(_types2.getConnectorId).filter(Boolean))];
|
|
761
|
+
await Promise.all(requiredConnectorNames.map(async connectorName => {
|
|
762
|
+
if (runtimeUrlByConnector[connectorName]) return;
|
|
763
|
+
const metadata = await fetchConnectorMetadata(connectorName, environmentId, region, token);
|
|
764
|
+
if (!metadata) return;
|
|
765
|
+
if (metadata.primaryRuntimeUrl) {
|
|
766
|
+
runtimeUrlByConnector[connectorName] = metadata.primaryRuntimeUrl;
|
|
767
|
+
}
|
|
768
|
+
if (!scopesByConnector[connectorName]) {
|
|
769
|
+
scopesByConnector[connectorName] = metadata.scopes;
|
|
770
|
+
}
|
|
771
|
+
}));
|
|
772
|
+
|
|
773
|
+
// Keep every connected instance. Selection is per local reference, not per
|
|
774
|
+
// connector, because one app can bind the same connector to two accounts.
|
|
775
|
+
const connectedByConnector = {};
|
|
776
|
+
for (const conn of connectionsRaw) {
|
|
777
|
+
const rawApiId = conn.properties?.apiId ?? '';
|
|
778
|
+
const connectorName = (0, _types2.getConnectorId)(rawApiId);
|
|
779
|
+
if (!connectorName || !conn.name) continue;
|
|
780
|
+
const isConnected = conn.properties?.statuses?.[0]?.status === 'Connected';
|
|
781
|
+
if (!isConnected) continue;
|
|
782
|
+
const candidates = connectedByConnector[connectorName] ?? [];
|
|
783
|
+
candidates.push({
|
|
784
|
+
id: (0, _disambiguate.extractConnectionId)(conn.name),
|
|
785
|
+
name: conn.name,
|
|
786
|
+
apiId: (0, _types2.normalizeApiId)(rawApiId),
|
|
787
|
+
ownerId: conn.properties?.createdBy?.id,
|
|
788
|
+
status: _types.ConnectionStatus.Connected,
|
|
789
|
+
displayName: conn.properties?.displayName,
|
|
790
|
+
isAuthenticated: true
|
|
791
|
+
});
|
|
792
|
+
connectedByConnector[connectorName] = candidates;
|
|
793
|
+
}
|
|
794
|
+
const connectionRefs = {};
|
|
795
|
+
const missingConnectors = [];
|
|
796
|
+
const missingConnectionReferences = [];
|
|
797
|
+
const connectionRequirements = [];
|
|
798
|
+
const connectionCandidates = {};
|
|
799
|
+
const missingRuntimeConnectors = new Set();
|
|
800
|
+
const rawConnectionReferences = {};
|
|
801
|
+
for (const [localRefId, reference] of Object.entries(connRefRaw)) {
|
|
802
|
+
if (!reference.id) continue;
|
|
803
|
+
rawConnectionReferences[localRefId] = {
|
|
804
|
+
...reference,
|
|
805
|
+
id: reference.id,
|
|
806
|
+
dataSources: reference.dataSources ?? []
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
const requirementsByLocalRefId = Object.fromEntries((0, _types2.buildConnectionRequirements)(rawConnectionReferences, Object.fromEntries(Object.entries(scopesByConnector).map(([connectorId, scopes]) => [(0, _types2.normalizeApiId)(connectorId), scopes]))).map(requirement => [requirement.localRefId, requirement]));
|
|
810
|
+
for (const [localRefId, ref] of Object.entries(connRefRaw)) {
|
|
811
|
+
const rawId = ref.id ?? '';
|
|
812
|
+
const connectorName = (0, _types2.getConnectorId)(rawId);
|
|
813
|
+
if (!connectorName) continue;
|
|
814
|
+
if (connectorName === 'shared_logicflows') {
|
|
815
|
+
const flowReference = rawConnectionReferences[localRefId];
|
|
816
|
+
const runtimeUrl = runtimeUrlByConnector[connectorName] ?? '';
|
|
817
|
+
const connectionName = (0, _types2.getFlowConnectionName)(flowReference);
|
|
818
|
+
if (!runtimeUrl) missingRuntimeConnectors.add(connectorName);
|
|
819
|
+
if (!connectionName) missingRuntimeConnectors.add(`${connectorName}:workflow`);
|
|
820
|
+
const entry = {
|
|
821
|
+
apiId: (0, _types2.normalizeApiId)(rawId),
|
|
822
|
+
runtimeUrl,
|
|
823
|
+
connectionName,
|
|
824
|
+
datasetName: ''
|
|
825
|
+
};
|
|
826
|
+
for (const dataSource of (0, _types2.getFlowDataSourceNames)(flowReference)) {
|
|
827
|
+
connectionRefs[dataSource] = entry;
|
|
828
|
+
}
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
if ((0, _types2.isCompositeConnectionApiId)(rawId)) continue;
|
|
832
|
+
const runtimeUrl = runtimeUrlByConnector[connectorName] ?? '';
|
|
833
|
+
if (!runtimeUrl) missingRuntimeConnectors.add(connectorName);
|
|
834
|
+
const apiId = (0, _types2.normalizeApiId)(rawId);
|
|
835
|
+
const candidates = connectedByConnector[connectorName] ?? [];
|
|
836
|
+
connectionCandidates[localRefId] = candidates;
|
|
837
|
+
const preferredId = ref.sharedConnectionId ? (0, _disambiguate.extractConnectionId)(String(ref.sharedConnectionId)) : null;
|
|
838
|
+
const selected = preferredId ? candidates.find(candidate => (0, _disambiguate.connectionIdsEqual)(candidate.id, preferredId)) : candidates.length === 1 ? candidates[0] : undefined;
|
|
839
|
+
const connectionName = selected?.id ?? '';
|
|
840
|
+
const isDataverse = connectorName.includes('commondataserviceforapps');
|
|
841
|
+
const dataSources = ref.dataSources ?? [];
|
|
842
|
+
const requirement = requirementsByLocalRefId[localRefId];
|
|
843
|
+
if (!requirement) continue;
|
|
844
|
+
connectionRequirements.push(requirement);
|
|
845
|
+
if (!connectionName) {
|
|
846
|
+
missingConnectors.push(apiId);
|
|
847
|
+
missingConnectionReferences.push(requirement);
|
|
848
|
+
}
|
|
849
|
+
const entry = {
|
|
850
|
+
apiId,
|
|
851
|
+
runtimeUrl,
|
|
852
|
+
connectionName,
|
|
853
|
+
datasetName: isDataverse ? dataverseOrgUrl : (0, _types2.getConnectionDatasetName)(ref)
|
|
854
|
+
};
|
|
855
|
+
for (const ds of dataSources) {
|
|
856
|
+
connectionRefs[ds] = entry;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
const conflictingDataSources = (0, _types2.findConnectionDataSourceConflicts)(connectionRequirements);
|
|
860
|
+
const result = {
|
|
861
|
+
connectionRefs,
|
|
862
|
+
dataverseOrgUrl,
|
|
863
|
+
databaseReferences: data.app?.properties?.databaseReferences ?? {},
|
|
864
|
+
missingConnectors,
|
|
865
|
+
missingConnectionReferences,
|
|
866
|
+
connectionRequirements,
|
|
867
|
+
connectionCandidates,
|
|
868
|
+
resourceConsents: [...resourceConsents.values()],
|
|
869
|
+
configurationErrors: conflictingDataSources.map(dataSource => `Data source "${dataSource}" is assigned to multiple connection references.`).concat([...missingRuntimeConnectors].map(connectorName => connectorName.endsWith(':workflow') ? 'The Flow connection reference is missing its workflow identifier in Power Platform metadata.' : `Connector "${connectorName}" is missing its runtime URL in Power Platform metadata.`)),
|
|
870
|
+
registeredConnectionUsages,
|
|
871
|
+
requiresConnectionConsent: true
|
|
872
|
+
};
|
|
873
|
+
(0, _connectionState.applyRegisteredConnectionUsages)(result, registeredConnectionUsages);
|
|
874
|
+
if (result.requiresConnectionConsent && !fetchedConnectionUsages) {
|
|
875
|
+
try {
|
|
876
|
+
registeredConnectionUsages = await fetchAppConnectionUsages(appId, environmentId, region, token);
|
|
877
|
+
fetchedConnectionUsages = true;
|
|
878
|
+
(0, _connectionState.applyRegisteredConnectionUsages)(result, registeredConnectionUsages);
|
|
879
|
+
} catch (usageError) {
|
|
880
|
+
(0, _logger.debugWarn)('[fetchAppLaunchData] could not refresh unmatched app connections:', usageError);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
const unmatchedRequirements = (0, _connectionState.getUnregisteredConnectionRequirements)(result);
|
|
884
|
+
(0, _logger.debugLog)('[fetchAppLaunchData] consent evaluation', JSON.stringify({
|
|
885
|
+
launchUsageCount: launchConnectionUsages.length,
|
|
886
|
+
persistedUsageCount: registeredConnectionUsages.length,
|
|
887
|
+
fetchedConnectionUsages,
|
|
888
|
+
unmatchedReferences: unmatchedRequirements.map(requirement => ({
|
|
889
|
+
reference: requirement.localRefId,
|
|
890
|
+
connectorId: requirement.connectorId,
|
|
891
|
+
requiredActions: requirement.appActions ?? [],
|
|
892
|
+
persistedActions: registeredConnectionUsages.filter(usage => usage.reference.toLowerCase() === requirement.localRefId.toLowerCase() && (0, _types2.normalizeApiId)(usage.apiId) === requirement.apiId).flatMap(usage => usage.appActions)
|
|
893
|
+
})),
|
|
894
|
+
pendingResourceConsentCount: result.resourceConsents.length
|
|
895
|
+
}));
|
|
896
|
+
return result;
|
|
897
|
+
} catch (error) {
|
|
898
|
+
throw new Error(`The Power Platform app launch response was invalid: ${String(error)}`);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Grant the calling application's delegated access to a connection for the
|
|
904
|
+
* signed-in user. The backend derives both identities from the token and
|
|
905
|
+
* selects the most specific applicable user, group, or tenant ACL.
|
|
906
|
+
*/
|
|
907
|
+
async function grantConnectionUserConsent(connectorId, connectionId, environmentId, region, token) {
|
|
908
|
+
const url = (0, _connectionMetadata.buildConnectionUserConsentUrl)((0, _PowerPlatformEnvironmentApi.buildEnvironmentEndpoint)(environmentId, region), connectorId, connectionId);
|
|
909
|
+
(0, _logger.debugLog)('[grantConnectionUserConsent] POST', url);
|
|
910
|
+
try {
|
|
911
|
+
const maxAttempts = 2;
|
|
912
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
913
|
+
const res = await _index.platformHttpClient.request(url, {
|
|
914
|
+
method: 'POST',
|
|
915
|
+
headers: {
|
|
916
|
+
Authorization: `Bearer ${token}`
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
const resText = await res.text().catch(() => '');
|
|
920
|
+
(0, _logger.debugLog)(`[grantConnectionUserConsent] HTTP ${res.status}`, resText);
|
|
921
|
+
if (res.ok) return true;
|
|
922
|
+
const isTransient = (0, _connectionState.isTransientConnectionConsentStatus)(res.status);
|
|
923
|
+
if (isTransient && attempt < maxAttempts) {
|
|
924
|
+
(0, _logger.debugLog)(`[grantConnectionUserConsent] transient HTTP ${res.status} for connection ${connectionId}; retrying`);
|
|
925
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
(0, _logger.debugWarn)(`[grantConnectionUserConsent] failed HTTP ${res.status} for connection ${connectionId}: ${resText}`);
|
|
929
|
+
(0, _index.getAppLogger)().trackErrorEvent(_index.HostOperationNames.PlatformConnectionAclGrant, {
|
|
930
|
+
phase: 'grantConnectionUserConsent',
|
|
931
|
+
reason: 'httpError',
|
|
932
|
+
httpStatus: res.status,
|
|
933
|
+
...(0, _http.getHttpCorrelationProperties)(res.headers)
|
|
934
|
+
});
|
|
935
|
+
return false;
|
|
936
|
+
}
|
|
937
|
+
return false;
|
|
938
|
+
} catch (error) {
|
|
939
|
+
(0, _logger.debugWarn)('[grantConnectionUserConsent] error:', error);
|
|
940
|
+
(0, _index.getAppLogger)().trackErrorEvent(_index.HostOperationNames.PlatformConnectionAclGrant, {
|
|
941
|
+
phase: 'grantConnectionUserConsent',
|
|
942
|
+
reason: 'exception'
|
|
943
|
+
});
|
|
944
|
+
return false;
|
|
945
|
+
}
|
|
946
|
+
}
|