@microsoft/power-apps-native-host 0.2.22
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/LICENSE +112 -0
- package/README.md +59 -0
- package/app.plugin.js +4 -0
- package/lib/NOTICE.txt +1659 -0
- package/lib/commonjs/ErrorBoundary.js +128 -0
- package/lib/commonjs/auth/AuthContext.js +281 -0
- package/lib/commonjs/bridge/NativePowerAppsBridge.js +279 -0
- package/lib/commonjs/bridge/setupNativeHost.js +65 -0
- package/lib/commonjs/connections/ConnectionSetupScreen.js +531 -0
- package/lib/commonjs/connections/disambiguate.js +88 -0
- package/lib/commonjs/connections/types.js +58 -0
- package/lib/commonjs/connections/useConnectionSetup.js +236 -0
- package/lib/commonjs/controls/FilePicker.js +183 -0
- package/lib/commonjs/controls/ImagePicker.js +236 -0
- package/lib/commonjs/index.js +246 -0
- package/lib/commonjs/logger.js +27 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/platform/PowerPlatformConnectionApi.js +606 -0
- package/lib/commonjs/platform/PowerPlatformEnvironmentApi.js +194 -0
- package/lib/commonjs/platform/resolveConnections.js +264 -0
- package/lib/commonjs/platform/useConnectionRefs.js +270 -0
- package/lib/commonjs/provider/PowerAppsHostProvider.js +336 -0
- package/lib/commonjs/provider/PowerAppsProvider.js +351 -0
- package/lib/commonjs/telemetry/index.js +70 -0
- package/lib/commonjs/theme/ThemeProvider.js +80 -0
- package/lib/commonjs/theme/tokens.js +50 -0
- package/lib/commonjs/utils/dataverseUtils.js +98 -0
- package/lib/commonjs/utils/isDevPlayer.js +27 -0
- package/lib/module/ErrorBoundary.js +122 -0
- package/lib/module/auth/AuthContext.js +276 -0
- package/lib/module/bridge/NativePowerAppsBridge.js +273 -0
- package/lib/module/bridge/setupNativeHost.js +62 -0
- package/lib/module/connections/ConnectionSetupScreen.js +526 -0
- package/lib/module/connections/disambiguate.js +83 -0
- package/lib/module/connections/types.js +60 -0
- package/lib/module/connections/useConnectionSetup.js +233 -0
- package/lib/module/controls/FilePicker.js +178 -0
- package/lib/module/controls/ImagePicker.js +231 -0
- package/lib/module/index.js +41 -0
- package/lib/module/logger.js +22 -0
- package/lib/module/package.json +1 -0
- package/lib/module/platform/PowerPlatformConnectionApi.js +595 -0
- package/lib/module/platform/PowerPlatformEnvironmentApi.js +186 -0
- package/lib/module/platform/resolveConnections.js +259 -0
- package/lib/module/platform/useConnectionRefs.js +267 -0
- package/lib/module/provider/PowerAppsHostProvider.js +332 -0
- package/lib/module/provider/PowerAppsProvider.js +345 -0
- package/lib/module/telemetry/index.js +55 -0
- package/lib/module/theme/ThemeProvider.js +63 -0
- package/lib/module/theme/tokens.js +46 -0
- package/lib/module/utils/dataverseUtils.js +90 -0
- package/lib/module/utils/isDevPlayer.js +23 -0
- package/lib/typescript/commonjs/ErrorBoundary.d.ts +30 -0
- package/lib/typescript/commonjs/auth/AuthContext.d.ts +57 -0
- package/lib/typescript/commonjs/bridge/NativePowerAppsBridge.d.ts +112 -0
- package/lib/typescript/commonjs/bridge/setupNativeHost.d.ts +39 -0
- package/lib/typescript/commonjs/connections/ConnectionSetupScreen.d.ts +107 -0
- package/lib/typescript/commonjs/connections/disambiguate.d.ts +43 -0
- package/lib/typescript/commonjs/connections/types.d.ts +91 -0
- package/lib/typescript/commonjs/connections/useConnectionSetup.d.ts +96 -0
- package/lib/typescript/commonjs/controls/FilePicker.d.ts +65 -0
- package/lib/typescript/commonjs/controls/ImagePicker.d.ts +76 -0
- package/lib/typescript/commonjs/index.d.ts +42 -0
- package/lib/typescript/commonjs/logger.d.ts +11 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/platform/PowerPlatformConnectionApi.d.ts +205 -0
- package/lib/typescript/commonjs/platform/PowerPlatformEnvironmentApi.d.ts +108 -0
- package/lib/typescript/commonjs/platform/resolveConnections.d.ts +86 -0
- package/lib/typescript/commonjs/platform/useConnectionRefs.d.ts +126 -0
- package/lib/typescript/commonjs/provider/PowerAppsHostProvider.d.ts +123 -0
- package/lib/typescript/commonjs/provider/PowerAppsProvider.d.ts +148 -0
- package/lib/typescript/commonjs/telemetry/index.d.ts +48 -0
- package/lib/typescript/commonjs/theme/ThemeProvider.d.ts +30 -0
- package/lib/typescript/commonjs/theme/tokens.d.ts +27 -0
- package/lib/typescript/commonjs/utils/dataverseUtils.d.ts +39 -0
- package/lib/typescript/commonjs/utils/isDevPlayer.d.ts +19 -0
- package/lib/typescript/module/ErrorBoundary.d.ts +30 -0
- package/lib/typescript/module/auth/AuthContext.d.ts +57 -0
- package/lib/typescript/module/bridge/NativePowerAppsBridge.d.ts +112 -0
- package/lib/typescript/module/bridge/setupNativeHost.d.ts +39 -0
- package/lib/typescript/module/connections/ConnectionSetupScreen.d.ts +107 -0
- package/lib/typescript/module/connections/disambiguate.d.ts +43 -0
- package/lib/typescript/module/connections/types.d.ts +91 -0
- package/lib/typescript/module/connections/useConnectionSetup.d.ts +96 -0
- package/lib/typescript/module/controls/FilePicker.d.ts +65 -0
- package/lib/typescript/module/controls/ImagePicker.d.ts +76 -0
- package/lib/typescript/module/index.d.ts +42 -0
- package/lib/typescript/module/logger.d.ts +11 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/platform/PowerPlatformConnectionApi.d.ts +205 -0
- package/lib/typescript/module/platform/PowerPlatformEnvironmentApi.d.ts +108 -0
- package/lib/typescript/module/platform/resolveConnections.d.ts +86 -0
- package/lib/typescript/module/platform/useConnectionRefs.d.ts +126 -0
- package/lib/typescript/module/provider/PowerAppsHostProvider.d.ts +123 -0
- package/lib/typescript/module/provider/PowerAppsProvider.d.ts +148 -0
- package/lib/typescript/module/telemetry/index.d.ts +48 -0
- package/lib/typescript/module/theme/ThemeProvider.d.ts +30 -0
- package/lib/typescript/module/theme/tokens.d.ts +27 -0
- package/lib/typescript/module/utils/dataverseUtils.d.ts +39 -0
- package/lib/typescript/module/utils/isDevPlayer.d.ts +19 -0
- package/package.json +119 -0
- package/scripts/build-ios-base.js +96 -0
- package/scripts/bundle.js +95 -0
- package/scripts/generate-connector-schemas.js +75 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/*!
|
|
4
|
+
* power-apps-native-host — PowerPlatformEnvironmentApi
|
|
5
|
+
*
|
|
6
|
+
* Mirrors the Host SDK's DefaultAppMetadataProvider.fetchAppMetadataFromPowerConfig
|
|
7
|
+
* pattern: resolves the Power Platform environment-scoped API endpoint from the
|
|
8
|
+
* environment GUID and region, then fetches linkedEnvironmentMetadata to obtain
|
|
9
|
+
* the Dataverse instanceUrl.
|
|
10
|
+
*
|
|
11
|
+
* Reference (Host SDK):
|
|
12
|
+
* packages/power-apps-host/src/providers/defaults/DefaultAppMetadataProvider.ts
|
|
13
|
+
* packages/power-apps-common/src/services/PowerPlatformApiDiscovery.ts
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Region → API hostname suffix
|
|
18
|
+
// Mirrors PowerPlatformApiDiscovery.getHostNameSuffix / getHexApiSuffixLength
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
const HOST_NAME_SUFFIX = {
|
|
22
|
+
local: 'api.powerplatform.localhost',
|
|
23
|
+
dev: 'api.dev.powerplatform.com',
|
|
24
|
+
test: 'api.test.powerplatform.com',
|
|
25
|
+
preprod: 'api.preprod.powerplatform.com',
|
|
26
|
+
preview: 'api.powerplatform.com',
|
|
27
|
+
// preview ring → prod endpoints
|
|
28
|
+
firstrelease: 'api.powerplatform.com',
|
|
29
|
+
prod: 'api.powerplatform.com',
|
|
30
|
+
gccmoderate: 'api.gov.powerplatform.microsoft.us',
|
|
31
|
+
gov: 'api.gov.powerplatform.microsoft.us',
|
|
32
|
+
gcchigh: 'api.high.powerplatform.microsoft.us',
|
|
33
|
+
high: 'api.high.powerplatform.microsoft.us',
|
|
34
|
+
dod: 'api.appsplatform.us',
|
|
35
|
+
mooncake: 'api.powerplatform.partner.microsoftonline.cn',
|
|
36
|
+
ex: 'api.powerplatform.eaglex.ic.gov',
|
|
37
|
+
rx: 'api.powerplatform.microsoft.scloud'
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** prod / firstrelease / preview use a 2-char hex suffix; all others use 1. */
|
|
41
|
+
function hexSuffixLength(region) {
|
|
42
|
+
return region === 'prod' || region === 'firstrelease' || region === 'preview' ? 2 : 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build the Power Platform environment-scoped API base URL.
|
|
47
|
+
*
|
|
48
|
+
* Algorithm (mirrors PowerPlatformApiDiscovery.generateDomain):
|
|
49
|
+
* 1. Strip dashes from the environment GUID to get a hex string.
|
|
50
|
+
* 2. Split into prefix (all but last N chars) and suffix (last N chars),
|
|
51
|
+
* where N = 2 for prod/firstrelease, 1 for all other regions.
|
|
52
|
+
* 3. Compose: `{prefix}.{suffix}.environment.{hostNameSuffix}`
|
|
53
|
+
*
|
|
54
|
+
* Example (prod, environmentId = "d909aac0-59d0-e5b1-900d-c082ef5774d7"):
|
|
55
|
+
* hexName = "d909aac059d0e5b1900dc082ef5774d7"
|
|
56
|
+
* suffix = "d7"
|
|
57
|
+
* prefix = "d909aac059d0e5b1900dc082ef5774"
|
|
58
|
+
* → "d909aac059d0e5b1900dc082ef5774.d7.environment.api.powerplatform.com"
|
|
59
|
+
*/
|
|
60
|
+
export function buildEnvironmentEndpoint(environmentId, region) {
|
|
61
|
+
const hostNameSuffix = HOST_NAME_SUFFIX[region] ?? HOST_NAME_SUFFIX['prod'];
|
|
62
|
+
const hexName = environmentId.toLowerCase().replace(/-/g, '');
|
|
63
|
+
const suffixLen = hexSuffixLength(region);
|
|
64
|
+
if (suffixLen >= hexName.length) {
|
|
65
|
+
throw new Error(`Cannot build Power Platform API endpoint: environment identifier too short after normalisation: "${hexName}"`);
|
|
66
|
+
}
|
|
67
|
+
const hexSuffix = hexName.substring(hexName.length - suffixLen);
|
|
68
|
+
const hexPrefix = hexName.substring(0, hexName.length - suffixLen);
|
|
69
|
+
return `https://${hexPrefix}.${hexSuffix}.environment.${hostNameSuffix}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Public API
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Token scope for the Power Platform management API (prod cluster).
|
|
78
|
+
* Non-prod clusters (test, gov, high, dev, mooncake, etc.) host the management
|
|
79
|
+
* API on their own audience, so prod-scope tokens are rejected with 401 by
|
|
80
|
+
* those clusters. Prefer {@link powerPlatformScope} when the region is known.
|
|
81
|
+
*/
|
|
82
|
+
export const POWER_PLATFORM_API_SCOPE = 'https://api.powerplatform.com/.default';
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Region-aware Power Platform management API scope. The audience must match the
|
|
86
|
+
* cluster the request is sent to; reuses the same region → hostname table as
|
|
87
|
+
* {@link buildEnvironmentEndpoint}.
|
|
88
|
+
*/
|
|
89
|
+
export function powerPlatformScope(region) {
|
|
90
|
+
const host = HOST_NAME_SUFFIX[region] ?? HOST_NAME_SUFFIX['prod'];
|
|
91
|
+
return `https://${host}/.default`;
|
|
92
|
+
}
|
|
93
|
+
import { getAppLogger, HostOperationNames } from "../telemetry/index.js";
|
|
94
|
+
function getSafeErrorName(error) {
|
|
95
|
+
return error instanceof Error ? error.constructor.name : typeof error;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Fetch the Dataverse instanceUrl for a Power Platform environment by calling
|
|
100
|
+
* the environment-scoped Power Platform management API — the same call that
|
|
101
|
+
* the Host SDK's DefaultAppMetadataProvider makes to populate
|
|
102
|
+
* `databaseReferences[*].databaseDetails.linkedEnvironmentMetadata`.
|
|
103
|
+
*
|
|
104
|
+
* @param environmentId - Environment GUID (from power.config.json `environmentId`).
|
|
105
|
+
* @param region - Cluster region string (from power.config.json `region`, default "prod").
|
|
106
|
+
* @param acquireToken - Callback to obtain a bearer token for the given scopes.
|
|
107
|
+
* Scope will be {@link powerPlatformScope}`(region)`.
|
|
108
|
+
* @param bearerToken - Optional pre-fetched bearer token for {@link powerPlatformScope}`(region)`.
|
|
109
|
+
* Pass this to avoid a redundant `acquireToken` round-trip when the
|
|
110
|
+
* caller has already obtained a token for the same audience.
|
|
111
|
+
* @returns The Dataverse org URL (e.g. "https://org12345.crm.dynamics.com"), or
|
|
112
|
+
* `undefined` if the environment has no linked CDS database.
|
|
113
|
+
*/
|
|
114
|
+
export async function fetchInstanceUrl(environmentId, region, acquireToken, bearerToken) {
|
|
115
|
+
const scenario = getAppLogger().trackScenario(HostOperationNames.PlatformFetchInstanceUrl, {
|
|
116
|
+
environmentId,
|
|
117
|
+
region
|
|
118
|
+
});
|
|
119
|
+
try {
|
|
120
|
+
const baseUrl = buildEnvironmentEndpoint(environmentId, region);
|
|
121
|
+
const url = `${baseUrl}/powerapps/environment?api-version=1&%24filter=name+eq+%27${encodeURIComponent(environmentId)}%27`;
|
|
122
|
+
const token = bearerToken ?? (await acquireToken([powerPlatformScope(region)]));
|
|
123
|
+
if (!token) {
|
|
124
|
+
throw new Error('fetchInstanceUrl: failed to acquire Power Platform API token');
|
|
125
|
+
}
|
|
126
|
+
const response = await fetch(url, {
|
|
127
|
+
method: 'GET',
|
|
128
|
+
headers: {
|
|
129
|
+
Authorization: `Bearer ${token}`,
|
|
130
|
+
'Content-Type': 'application/json'
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
throw new Error(`fetchInstanceUrl: environment API returned ${response.status} ${response.statusText}`);
|
|
135
|
+
}
|
|
136
|
+
const env = await response.json();
|
|
137
|
+
const instanceUrl = env.properties?.linkedEnvironmentMetadata?.instanceUrl;
|
|
138
|
+
scenario.complete({
|
|
139
|
+
hasInstanceUrl: instanceUrl !== undefined
|
|
140
|
+
});
|
|
141
|
+
return instanceUrl;
|
|
142
|
+
} catch (e) {
|
|
143
|
+
scenario.fail(undefined, {
|
|
144
|
+
errorName: getSafeErrorName(e)
|
|
145
|
+
});
|
|
146
|
+
throw e;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// buildCdsDataSources
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Shape of a single Dataverse table entry inside power.config.json
|
|
156
|
+
* `databaseReferences[*].dataSources[*]`.
|
|
157
|
+
*/
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Shape of a single database reference entry inside power.config.json
|
|
161
|
+
* `databaseReferences`.
|
|
162
|
+
*/
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Build the CDS data source config array that the SDK reads via
|
|
166
|
+
* AppPowerAppsClientPlugin.getAppCdsDataSourceConfigsAsync.
|
|
167
|
+
*
|
|
168
|
+
* Pass the resolved Dataverse org URL (from useConnectionRefs) and the
|
|
169
|
+
* databaseReferences map from power.config.json. The resulting array is
|
|
170
|
+
* passed directly to <PowerAppsHostProvider dataSources={...} />.
|
|
171
|
+
*
|
|
172
|
+
* @param databaseReferences - The `databaseReferences` map from power.config.json.
|
|
173
|
+
* @param dataverseOrgUrl - The resolved Dataverse org URL
|
|
174
|
+
* (connectionRefs['commondataserviceforapps']?.datasetName).
|
|
175
|
+
* Pass an empty string before the org URL is known;
|
|
176
|
+
* the host provider will receive empty runtimeUrl entries
|
|
177
|
+
* and the SDK will not attempt Dataverse calls.
|
|
178
|
+
*/
|
|
179
|
+
export function buildCdsDataSources(databaseReferences, dataverseOrgUrl) {
|
|
180
|
+
return Object.values(databaseReferences).flatMap(dbRef => Object.values(dbRef.dataSources ?? {}).map(ds => ({
|
|
181
|
+
runtimeUrl: dataverseOrgUrl ? `${dataverseOrgUrl}/api/data/v9.0/` : '',
|
|
182
|
+
entitySetName: ds.entitySetName ?? '',
|
|
183
|
+
logicalName: ds.logicalName ?? '',
|
|
184
|
+
version: '9.2'
|
|
185
|
+
})));
|
|
186
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
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 by the Dev Player
|
|
11
|
+
* and during local development (no appId) or when the launch API is
|
|
12
|
+
* unavailable. Handles pre-disambiguation ACL grants so inaccessible
|
|
13
|
+
* connections are excluded before the user is asked to pick.
|
|
14
|
+
*
|
|
15
|
+
* • applyAclGrants — post-resolution ACL grants used by the fast path
|
|
16
|
+
* (production APKs via fetchAppLaunchData).
|
|
17
|
+
*
|
|
18
|
+
* Both helpers return / consume the shared AppLaunchResult shape so the
|
|
19
|
+
* hook orchestrator can handle them uniformly.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { addAadAppToConnectionAcl, parseJwtOid, SERVICE_POWERAPPS_SCOPE } from "./PowerPlatformConnectionApi.js";
|
|
23
|
+
import { fetchInstanceUrl, buildEnvironmentEndpoint } from "./PowerPlatformEnvironmentApi.js";
|
|
24
|
+
import { disambiguateConnections, extractConnectionId } from "../connections/disambiguate.js";
|
|
25
|
+
import { ConnectionStatus } from "../connections/types.js";
|
|
26
|
+
const API_VERSION = '1';
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// ConnectorSchema
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Connector schema entry generated by `generate-connector-schemas`.
|
|
34
|
+
* Keyed by data source name in the `schemaMap` passed to `useConnectionRefs`.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// resolveViaStaticRefs
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve connections from the static `connectionReferences` map.
|
|
43
|
+
*
|
|
44
|
+
* Steps:
|
|
45
|
+
* 1. Build an apiId → schema lookup.
|
|
46
|
+
* 2. In parallel: fetch the Dataverse instanceUrl and all connection instances
|
|
47
|
+
* for every connector.
|
|
48
|
+
* 3. If `isvClientId` is set, run pre-disambiguation ACL grants — connections
|
|
49
|
+
* where the grant fails are marked `NeedsAclGrant` and excluded from
|
|
50
|
+
* resolution, prompting the user to create a new connection.
|
|
51
|
+
* 4. Disambiguate: resolve single matches, invoke callbacks for ambiguous /
|
|
52
|
+
* missing cases, perform one-shot retries when `onMissingConnection` returns
|
|
53
|
+
* `true`.
|
|
54
|
+
* 5. Build and return an `AppLaunchResult`-shaped object.
|
|
55
|
+
*/
|
|
56
|
+
export async function resolveViaStaticRefs({
|
|
57
|
+
connectionReferences,
|
|
58
|
+
schemaMap,
|
|
59
|
+
environmentId,
|
|
60
|
+
region,
|
|
61
|
+
acquireToken,
|
|
62
|
+
token,
|
|
63
|
+
isvClientId,
|
|
64
|
+
onDisambiguationNeeded,
|
|
65
|
+
onMissingConnection
|
|
66
|
+
}) {
|
|
67
|
+
// Build apiId → schema lookup from the static refs.
|
|
68
|
+
const schemaByApiId = Object.fromEntries(Object.values(connectionReferences).map(ref => {
|
|
69
|
+
const schema = ref.dataSources.map(ds => schemaMap[ds]).find(Boolean);
|
|
70
|
+
return schema ? [ref.id, schema] : null;
|
|
71
|
+
}).filter(entry => entry !== null));
|
|
72
|
+
const base = buildEnvironmentEndpoint(environmentId, region);
|
|
73
|
+
const headers = {
|
|
74
|
+
Authorization: `Bearer ${token}`
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Fetch Dataverse instanceUrl and all connection instances in parallel.
|
|
78
|
+
const [instanceUrl, ...connResults] = await Promise.all([fetchInstanceUrl(environmentId, region, acquireToken, token), ...Object.entries(connectionReferences).map(async ([localRefId, ref]) => {
|
|
79
|
+
const schema = schemaByApiId[ref.id];
|
|
80
|
+
if (!schema) {
|
|
81
|
+
return {
|
|
82
|
+
localRefId,
|
|
83
|
+
apiId: ref.id,
|
|
84
|
+
dataSources: ref.dataSources,
|
|
85
|
+
instances: []
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const url = `${base}/connectivity/apis/${schema.name}/connections` + `?api-version=${API_VERSION}&$filter=environment eq '${environmentId}'`;
|
|
89
|
+
try {
|
|
90
|
+
const res = await fetch(url, {
|
|
91
|
+
headers
|
|
92
|
+
});
|
|
93
|
+
if (!res.ok) {
|
|
94
|
+
return {
|
|
95
|
+
localRefId,
|
|
96
|
+
apiId: ref.id,
|
|
97
|
+
dataSources: ref.dataSources,
|
|
98
|
+
instances: []
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const body = await res.json();
|
|
102
|
+
const instances = (body.value ?? []).map(c => ({
|
|
103
|
+
id: extractConnectionId(c.name),
|
|
104
|
+
name: c.name,
|
|
105
|
+
apiId: ref.id,
|
|
106
|
+
status: c.properties?.statuses?.[0]?.status === 'Connected' ? ConnectionStatus.Connected : ConnectionStatus.NotConnected,
|
|
107
|
+
displayName: c.properties?.displayName
|
|
108
|
+
}));
|
|
109
|
+
return {
|
|
110
|
+
localRefId,
|
|
111
|
+
apiId: ref.id,
|
|
112
|
+
dataSources: ref.dataSources,
|
|
113
|
+
instances
|
|
114
|
+
};
|
|
115
|
+
} catch {
|
|
116
|
+
return {
|
|
117
|
+
localRefId,
|
|
118
|
+
apiId: ref.id,
|
|
119
|
+
dataSources: ref.dataSources,
|
|
120
|
+
instances: []
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
})]);
|
|
124
|
+
|
|
125
|
+
// Pre-disambiguation ACL grants (ISV apps only).
|
|
126
|
+
//
|
|
127
|
+
// Connections where the grant fails are marked NeedsAclGrant so
|
|
128
|
+
// disambiguation treats them as unavailable and surfaces the
|
|
129
|
+
// "missing connection" flow, steering the user to create a new one.
|
|
130
|
+
if (isvClientId) {
|
|
131
|
+
const paToken = await acquireToken([SERVICE_POWERAPPS_SCOPE]);
|
|
132
|
+
const paOid = paToken ? parseJwtOid(paToken) : null;
|
|
133
|
+
if (paToken && paOid) {
|
|
134
|
+
await Promise.all(connResults.map(async r => {
|
|
135
|
+
const connId = schemaByApiId[r.apiId]?.name;
|
|
136
|
+
if (!connId) return;
|
|
137
|
+
await Promise.all(r.instances.map(async inst => {
|
|
138
|
+
if (inst.status !== ConnectionStatus.Connected) return;
|
|
139
|
+
const ok = await addAadAppToConnectionAcl(connId, inst.id, environmentId, isvClientId, paOid, paToken);
|
|
140
|
+
if (!ok) inst.status = ConnectionStatus.NeedsAclGrant;
|
|
141
|
+
}));
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Disambiguation.
|
|
147
|
+
const allConnectionsByApiId = {};
|
|
148
|
+
for (const r of connResults) {
|
|
149
|
+
allConnectionsByApiId[r.apiId] = r.instances;
|
|
150
|
+
}
|
|
151
|
+
const disambiguationResults = disambiguateConnections(connectionReferences, allConnectionsByApiId);
|
|
152
|
+
const resolvedIds = {};
|
|
153
|
+
const unresolvedApiIds = [];
|
|
154
|
+
for (const dr of disambiguationResults) {
|
|
155
|
+
if (dr.result.kind === 'resolved') {
|
|
156
|
+
resolvedIds[dr.localRefId] = dr.result.connectionId;
|
|
157
|
+
} else if (dr.result.kind === 'ambiguous') {
|
|
158
|
+
const picked = onDisambiguationNeeded ? await onDisambiguationNeeded({
|
|
159
|
+
localRefId: dr.localRefId,
|
|
160
|
+
apiId: dr.apiId,
|
|
161
|
+
candidates: dr.result.candidates
|
|
162
|
+
}) : null;
|
|
163
|
+
// Fall back to the first Connected candidate so the app can still start.
|
|
164
|
+
resolvedIds[dr.localRefId] = picked ?? dr.result.candidates[0]?.id ?? '';
|
|
165
|
+
} else {
|
|
166
|
+
// kind === 'missing'
|
|
167
|
+
let retried = false;
|
|
168
|
+
if (onMissingConnection) {
|
|
169
|
+
retried = await onMissingConnection({
|
|
170
|
+
localRefId: dr.localRefId,
|
|
171
|
+
apiId: dr.apiId
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
if (retried) {
|
|
175
|
+
// One-shot retry for this specific connector.
|
|
176
|
+
const schema = schemaByApiId[dr.apiId];
|
|
177
|
+
if (schema) {
|
|
178
|
+
try {
|
|
179
|
+
const url = `${base}/connectivity/apis/${schema.name}/connections` + `?api-version=${API_VERSION}&$filter=environment eq '${environmentId}'`;
|
|
180
|
+
const res = await fetch(url, {
|
|
181
|
+
headers
|
|
182
|
+
});
|
|
183
|
+
if (res.ok) {
|
|
184
|
+
const body = await res.json();
|
|
185
|
+
const retryInstances = (body.value ?? []).map(c => ({
|
|
186
|
+
id: extractConnectionId(c.name),
|
|
187
|
+
name: c.name,
|
|
188
|
+
apiId: dr.apiId,
|
|
189
|
+
status: c.properties?.statuses?.[0]?.status === 'Connected' ? ConnectionStatus.Connected : ConnectionStatus.NotConnected
|
|
190
|
+
}));
|
|
191
|
+
const connected = retryInstances.find(i => i.status === ConnectionStatus.Connected);
|
|
192
|
+
if (connected) {
|
|
193
|
+
resolvedIds[dr.localRefId] = connected.id;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} catch {
|
|
198
|
+
// Fall through to missing.
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
resolvedIds[dr.localRefId] = '';
|
|
203
|
+
unresolvedApiIds.push(dr.apiId);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const dataverseOrgUrl = (instanceUrl ?? '').replace(/\/$/, '');
|
|
207
|
+
|
|
208
|
+
// Build the final AppConnectionReference map.
|
|
209
|
+
const connectionRefs = {};
|
|
210
|
+
for (const [localRefId, ref] of Object.entries(connectionReferences)) {
|
|
211
|
+
const schema = schemaByApiId[ref.id];
|
|
212
|
+
const connectionName = resolvedIds[localRefId] ?? '';
|
|
213
|
+
const isDataverse = ref.id.includes('commondataserviceforapps');
|
|
214
|
+
const entry = {
|
|
215
|
+
apiId: ref.id,
|
|
216
|
+
runtimeUrl: schema?.properties.primaryRuntimeUrl ?? '',
|
|
217
|
+
connectionName,
|
|
218
|
+
datasetName: isDataverse ? dataverseOrgUrl : ''
|
|
219
|
+
};
|
|
220
|
+
for (const ds of ref.dataSources) {
|
|
221
|
+
connectionRefs[ds] = entry;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
connectionRefs,
|
|
226
|
+
dataverseOrgUrl,
|
|
227
|
+
missingConnectors: unresolvedApiIds
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
// applyAclGrants
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Run idempotent post-resolution ACL grants on behalf of the ISV app.
|
|
237
|
+
*
|
|
238
|
+
* Used by the fast path (production APKs via `fetchAppLaunchData`) where the
|
|
239
|
+
* launch API has already resolved the user's connections — grants are applied
|
|
240
|
+
* after the fact so APIM accepts tokens issued to the ISV's AAD app.
|
|
241
|
+
*
|
|
242
|
+
* For the slow path the grants run before disambiguation inside
|
|
243
|
+
* `resolveViaStaticRefs`, where they can still influence which connections
|
|
244
|
+
* are considered available.
|
|
245
|
+
*
|
|
246
|
+
* Failures are swallowed — a failed grant will surface as a 403 at runtime
|
|
247
|
+
* when the connector is called.
|
|
248
|
+
*/
|
|
249
|
+
export async function applyAclGrants(connectionRefs, environmentId, isvClientId, acquireToken) {
|
|
250
|
+
const paToken = await acquireToken([SERVICE_POWERAPPS_SCOPE]);
|
|
251
|
+
const paOid = paToken ? parseJwtOid(paToken) : null;
|
|
252
|
+
if (!paToken || !paOid) return;
|
|
253
|
+
const seen = new Set();
|
|
254
|
+
await Promise.all(Object.values(connectionRefs).map(async ref => {
|
|
255
|
+
if (!ref.connectionName || seen.has(ref.connectionName)) return;
|
|
256
|
+
seen.add(ref.connectionName);
|
|
257
|
+
await addAadAppToConnectionAcl(ref.apiId, ref.connectionName, environmentId, isvClientId, paOid, paToken);
|
|
258
|
+
}));
|
|
259
|
+
}
|