@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.
Files changed (154) hide show
  1. package/README.md +21 -10
  2. package/compatibility/current.json +31 -4
  3. package/lib/NOTICE.txt +1325 -24
  4. package/lib/commonjs/auth/AuthContext.js +110 -33
  5. package/lib/commonjs/bridge/NativePowerAppsBridge.js +11 -18
  6. package/lib/commonjs/bridge/PowerPagesBridge.js +27 -8
  7. package/lib/commonjs/bridge/bridgeState.js +40 -0
  8. package/lib/commonjs/config/babelConfig.js +13 -0
  9. package/lib/commonjs/config/expoConfig.js +71 -0
  10. package/lib/commonjs/config/metroConfig.js +70 -0
  11. package/lib/commonjs/config/metroLogger.js +182 -0
  12. package/lib/commonjs/config/tamaguiConfig.js +98 -0
  13. package/lib/commonjs/connections/ConnectionSetupScreen.js +459 -80
  14. package/lib/commonjs/connections/PowerPlatformConnectionApi.js +946 -0
  15. package/lib/commonjs/connections/connectionMetadata.js +127 -0
  16. package/lib/commonjs/connections/connectionState.js +96 -0
  17. package/lib/commonjs/connections/disambiguate.js +19 -11
  18. package/lib/commonjs/connections/resolveConnections.js +348 -0
  19. package/lib/commonjs/connections/types.js +106 -0
  20. package/lib/commonjs/{platform → connections}/useConnectionRefs.js +130 -65
  21. package/lib/commonjs/connections/useConnectionSetup.js +111 -52
  22. package/lib/commonjs/controls/FilePicker.js +6 -0
  23. package/lib/commonjs/controls/ImagePicker.js +8 -0
  24. package/lib/commonjs/index.js +167 -4
  25. package/lib/commonjs/logger.js +9 -6
  26. package/lib/commonjs/offline/createOfflineHostActivationCallbacks.js +61 -0
  27. package/lib/commonjs/offline/useOfflineIntegrationWithStartupGate.js +48 -0
  28. package/lib/commonjs/offline/useOfflineIntegrationWithStartupGate.web.js +13 -0
  29. package/lib/commonjs/platform/DataverseDataSources.js +18 -0
  30. package/lib/commonjs/platform/DataverseGlobalDiscoveryApi.js +94 -0
  31. package/lib/commonjs/platform/GlobalDiscovery.js +51 -0
  32. package/lib/commonjs/platform/PowerPlatformEnvironmentApi.js +6 -106
  33. package/lib/commonjs/provider/PowerAppsHostProvider.js +157 -25
  34. package/lib/commonjs/provider/PowerAppsProvider.js +178 -134
  35. package/lib/commonjs/provider/SessionDetailsGesture.js +208 -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 +110 -33
  40. package/lib/module/bridge/NativePowerAppsBridge.js +12 -19
  41. package/lib/module/bridge/PowerPagesBridge.js +27 -8
  42. package/lib/module/bridge/bridgeState.js +34 -0
  43. package/lib/module/config/babelConfig.js +9 -0
  44. package/lib/module/config/expoConfig.js +67 -0
  45. package/lib/module/config/metroConfig.js +65 -0
  46. package/lib/module/config/metroLogger.js +175 -0
  47. package/lib/module/config/tamaguiConfig.js +93 -0
  48. package/lib/module/connections/ConnectionSetupScreen.js +460 -81
  49. package/lib/module/connections/PowerPlatformConnectionApi.js +932 -0
  50. package/lib/module/connections/connectionMetadata.js +112 -0
  51. package/lib/module/connections/connectionState.js +82 -0
  52. package/lib/module/connections/disambiguate.js +19 -12
  53. package/lib/module/connections/resolveConnections.js +343 -0
  54. package/lib/module/connections/types.js +91 -0
  55. package/lib/module/{platform → connections}/useConnectionRefs.js +131 -66
  56. package/lib/module/connections/useConnectionSetup.js +113 -54
  57. package/lib/module/controls/FilePicker.js +6 -0
  58. package/lib/module/controls/ImagePicker.js +8 -0
  59. package/lib/module/index.js +14 -4
  60. package/lib/module/logger.js +8 -6
  61. package/lib/module/offline/createOfflineHostActivationCallbacks.js +55 -0
  62. package/lib/module/offline/useOfflineIntegrationWithStartupGate.js +44 -0
  63. package/lib/module/offline/useOfflineIntegrationWithStartupGate.web.js +9 -0
  64. package/lib/module/platform/DataverseDataSources.js +13 -0
  65. package/lib/module/platform/DataverseGlobalDiscoveryApi.js +89 -0
  66. package/lib/module/platform/GlobalDiscovery.js +45 -0
  67. package/lib/module/platform/PowerPlatformEnvironmentApi.js +5 -104
  68. package/lib/module/provider/PowerAppsHostProvider.js +160 -28
  69. package/lib/module/provider/PowerAppsProvider.js +182 -138
  70. package/lib/module/provider/SessionDetailsGesture.js +203 -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 +1 -0
  76. package/lib/typescript/commonjs/bridge/bridgeState.d.ts +5 -0
  77. package/lib/typescript/commonjs/config/babelConfig.d.ts +12 -0
  78. package/lib/typescript/commonjs/config/expoConfig.d.ts +21 -0
  79. package/lib/typescript/commonjs/config/metroConfig.d.ts +6 -0
  80. package/lib/typescript/commonjs/config/metroLogger.d.ts +21 -0
  81. package/lib/typescript/commonjs/config/tamaguiConfig.d.ts +1792 -0
  82. package/lib/typescript/commonjs/connections/ConnectionSetupScreen.d.ts +28 -7
  83. package/lib/typescript/commonjs/{platform → connections}/PowerPlatformConnectionApi.d.ts +52 -43
  84. package/lib/typescript/commonjs/connections/connectionMetadata.d.ts +66 -0
  85. package/lib/typescript/commonjs/connections/connectionState.d.ts +16 -0
  86. package/lib/typescript/commonjs/connections/disambiguate.d.ts +4 -2
  87. package/lib/typescript/commonjs/{platform → connections}/resolveConnections.d.ts +22 -20
  88. package/lib/typescript/commonjs/connections/types.d.ts +129 -1
  89. package/lib/typescript/commonjs/{platform → connections}/useConnectionRefs.d.ts +36 -25
  90. package/lib/typescript/commonjs/connections/useConnectionSetup.d.ts +12 -8
  91. package/lib/typescript/commonjs/controls/FilePicker.d.ts +7 -1
  92. package/lib/typescript/commonjs/controls/ImagePicker.d.ts +9 -1
  93. package/lib/typescript/commonjs/index.d.ts +18 -10
  94. package/lib/typescript/commonjs/logger.d.ts +4 -4
  95. package/lib/typescript/commonjs/offline/createOfflineHostActivationCallbacks.d.ts +12 -0
  96. package/lib/typescript/commonjs/offline/useOfflineIntegrationWithStartupGate.d.ts +15 -0
  97. package/lib/typescript/commonjs/offline/useOfflineIntegrationWithStartupGate.web.d.ts +6 -0
  98. package/lib/typescript/commonjs/platform/DataverseDataSources.d.ts +28 -0
  99. package/lib/typescript/commonjs/platform/DataverseGlobalDiscoveryApi.d.ts +3 -0
  100. package/lib/typescript/commonjs/platform/GlobalDiscovery.d.ts +4 -0
  101. package/lib/typescript/commonjs/platform/PowerPlatformEnvironmentApi.d.ts +3 -74
  102. package/lib/typescript/commonjs/provider/PowerAppsHostProvider.d.ts +29 -18
  103. package/lib/typescript/commonjs/provider/PowerAppsProvider.d.ts +54 -9
  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 +1 -0
  110. package/lib/typescript/module/bridge/bridgeState.d.ts +5 -0
  111. package/lib/typescript/module/config/babelConfig.d.ts +12 -0
  112. package/lib/typescript/module/config/expoConfig.d.ts +21 -0
  113. package/lib/typescript/module/config/metroConfig.d.ts +6 -0
  114. package/lib/typescript/module/config/metroLogger.d.ts +21 -0
  115. package/lib/typescript/module/config/tamaguiConfig.d.ts +1792 -0
  116. package/lib/typescript/module/connections/ConnectionSetupScreen.d.ts +28 -7
  117. package/lib/typescript/module/{platform → connections}/PowerPlatformConnectionApi.d.ts +52 -43
  118. package/lib/typescript/module/connections/connectionMetadata.d.ts +66 -0
  119. package/lib/typescript/module/connections/connectionState.d.ts +16 -0
  120. package/lib/typescript/module/connections/disambiguate.d.ts +4 -2
  121. package/lib/typescript/module/{platform → connections}/resolveConnections.d.ts +22 -20
  122. package/lib/typescript/module/connections/types.d.ts +129 -1
  123. package/lib/typescript/module/{platform → connections}/useConnectionRefs.d.ts +36 -25
  124. package/lib/typescript/module/connections/useConnectionSetup.d.ts +12 -8
  125. package/lib/typescript/module/controls/FilePicker.d.ts +7 -1
  126. package/lib/typescript/module/controls/ImagePicker.d.ts +9 -1
  127. package/lib/typescript/module/index.d.ts +18 -10
  128. package/lib/typescript/module/logger.d.ts +4 -4
  129. package/lib/typescript/module/offline/createOfflineHostActivationCallbacks.d.ts +12 -0
  130. package/lib/typescript/module/offline/useOfflineIntegrationWithStartupGate.d.ts +15 -0
  131. package/lib/typescript/module/offline/useOfflineIntegrationWithStartupGate.web.d.ts +6 -0
  132. package/lib/typescript/module/platform/DataverseDataSources.d.ts +28 -0
  133. package/lib/typescript/module/platform/DataverseGlobalDiscoveryApi.d.ts +3 -0
  134. package/lib/typescript/module/platform/GlobalDiscovery.d.ts +4 -0
  135. package/lib/typescript/module/platform/PowerPlatformEnvironmentApi.d.ts +3 -74
  136. package/lib/typescript/module/provider/PowerAppsHostProvider.d.ts +29 -18
  137. package/lib/typescript/module/provider/PowerAppsProvider.d.ts +54 -9
  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 +52 -13
  143. package/scripts/build-codegen-package.js +283 -0
  144. package/scripts/start-web.mjs +9 -5
  145. package/scripts/upgrade/runtime.js +139 -24
  146. package/scripts/upgrade-authoring.js +274 -180
  147. package/scripts/upgrade.js +10 -16
  148. package/src/config/generated-config.d.ts +4 -0
  149. package/src/config/index.d.ts +3 -0
  150. package/src/config/tsconfig.json +26 -0
  151. package/lib/commonjs/platform/PowerPlatformConnectionApi.js +0 -606
  152. package/lib/commonjs/platform/resolveConnections.js +0 -264
  153. package/lib/module/platform/PowerPlatformConnectionApi.js +0 -595
  154. package/lib/module/platform/resolveConnections.js +0 -259
@@ -1,595 +0,0 @@
1
- "use strict";
2
-
3
- /*!
4
- * power-apps-native-host — PowerPlatformConnectionApi
5
- *
6
- * Typed wrappers around the Power Platform environment-scoped connectivity API
7
- * for connection CRUD operations and connector metadata retrieval.
8
- *
9
- * These mirror the endpoints used by ConnectorsApi in @microsoft/power-apps-common
10
- * but are implemented as plain async functions so they have no external dependencies.
11
- */
12
-
13
- import { buildEnvironmentEndpoint, powerPlatformScope } from "./PowerPlatformEnvironmentApi.js";
14
- import { ConnectionStatus } from "../connections/types.js";
15
- import { extractConnectionId } from "../connections/disambiguate.js";
16
- import { getAppLogger, HostOperationNames } from "../telemetry/index.js";
17
- import { debugLog, debugWarn } from "../logger.js";
18
-
19
- // Power Platform connectivity API version used across all calls here.
20
- const API_VERSION = '1';
21
-
22
- // Power Apps RP — used for connection sharing (modifyPermissions).
23
- const POWER_APPS_RP_BASE = 'https://api.powerapps.com';
24
- function getSafeErrorName(error) {
25
- return error instanceof Error ? error.constructor.name : typeof error;
26
- }
27
-
28
- /**
29
- * Token scope for the Power Apps RP (api.powerapps.com).
30
- * Required when calling modifyPermissions to register a service principal
31
- * in the connection ACE so that APIM accepts third-party app tokens at runtime.
32
- */
33
- export const SERVICE_POWERAPPS_SCOPE = 'https://service.powerapps.com/.default';
34
-
35
- // ---------------------------------------------------------------------------
36
- // Internal helpers
37
- // ---------------------------------------------------------------------------
38
-
39
- /**
40
- * Generate a random connection ID as a 32-char lowercase hex string (UUID v4
41
- * without dashes), matching the format Power Platform uses for connection
42
- * resource names (e.g. "326561a0849744d5ae75c9bcaca1e5f1").
43
- */
44
- function generateConnectionId() {
45
- if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
46
- return crypto.randomUUID().replace(/-/g, '');
47
- }
48
- // Fallback: manual UUID v4 bit pattern, returned as 32 hex chars.
49
- return 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'.replace(/[xy]/g, c => {
50
- const r = Math.random() * 16 | 0;
51
- return (c === 'x' ? r : r & 0x3 | 0x8).toString(16);
52
- });
53
- }
54
-
55
- // ---------------------------------------------------------------------------
56
- // Connector metadata
57
- // ---------------------------------------------------------------------------
58
-
59
- /** Subset of connector metadata returned by the Power Platform API. */
60
-
61
- /**
62
- * Fetch metadata for a single connector from the Power Platform API.
63
- *
64
- * @param connectorId - Connector short name (e.g. "shared_office365users")
65
- * or full API path suffix.
66
- * @param environmentId - Environment GUID.
67
- * @param region - Cluster region string (default "prod").
68
- * @param token - Pre-acquired Power Platform API bearer token.
69
- */
70
- export async function fetchConnectorMetadata(connectorId, environmentId, region, token) {
71
- const base = buildEnvironmentEndpoint(environmentId, region);
72
- const url = `${base}/connectivity/connectors/${encodeURIComponent(connectorId)}` + `?$filter=environment eq '${environmentId}'&api-version=${API_VERSION}`;
73
- try {
74
- const res = await fetch(url, {
75
- headers: {
76
- Authorization: `Bearer ${token}`
77
- }
78
- });
79
- if (!res.ok) return null;
80
- const body = await res.json();
81
- return {
82
- name: body.name ?? connectorId,
83
- displayName: body.properties?.displayName ?? connectorId,
84
- iconUri: body.properties?.iconUri ?? '',
85
- description: body.properties?.description ?? '',
86
- isFirstParty: body.properties?.isFirstParty ?? false
87
- };
88
- } catch {
89
- return null;
90
- }
91
- }
92
-
93
- // ---------------------------------------------------------------------------
94
- // Connection listing
95
- // ---------------------------------------------------------------------------
96
-
97
- /**
98
- * Fetch all connections for a given connector in the environment.
99
- * Returns only the `NativeConnectionInstance` shape used by the disambiguation layer.
100
- */
101
- export async function fetchConnectionsForConnector(connectorId, environmentId, region, token) {
102
- const base = buildEnvironmentEndpoint(environmentId, region);
103
- const url = `${base}/connectivity/connectors/${encodeURIComponent(connectorId)}/connections` + `?$filter=environment eq '${environmentId}'&api-version=${API_VERSION}`;
104
- try {
105
- const res = await fetch(url, {
106
- headers: {
107
- Authorization: `Bearer ${token}`
108
- }
109
- });
110
- if (!res.ok) return [];
111
- const body = await res.json();
112
- return (body.value ?? []).map(c => ({
113
- id: extractConnectionId(c.name),
114
- name: c.name,
115
- apiId: `/providers/Microsoft.PowerApps/apis/${connectorId}`,
116
- status: c.properties?.statuses?.[0]?.status === 'Connected' ? ConnectionStatus.Connected : ConnectionStatus.NotConnected,
117
- displayName: c.properties?.displayName
118
- }));
119
- } catch {
120
- return [];
121
- }
122
- }
123
-
124
- // ---------------------------------------------------------------------------
125
- // Connection creation (implicit OAuth)
126
- // ---------------------------------------------------------------------------
127
-
128
- /**
129
- * Result of initiating an OAuth connection creation.
130
- *
131
- * `pending` — the connection has been created but is not yet authenticated.
132
- * The `consentLink` URL must be opened (in a WebView or browser) so the
133
- * user can complete the OAuth consent flow. Poll `pollConnectionStatus`
134
- * until the status changes to "Connected".
135
- *
136
- * `connected` — the connection was created and is already Connected (e.g.,
137
- * first-party SSO connections that do not need explicit OAuth consent).
138
- *
139
- * `error` — creation failed; `message` contains the error description.
140
- */
141
-
142
- /**
143
- * Create a new connection for a connector in the user's environment.
144
- *
145
- * Calls:
146
- * POST {envBase}/connectivity/apis/{connectorId}/connections
147
- *
148
- * If the response contains a `consentLink`, the connection needs OAuth
149
- * completion (caller should open the URL and then poll `pollConnectionStatus`).
150
- * If the connection status is already "Connected", returns `connected`.
151
- *
152
- * @param connectorId - Connector short name (e.g. "shared_office365users").
153
- * @param environmentId - Environment GUID.
154
- * @param region - Cluster region.
155
- * @param token - Power Platform API bearer token.
156
- * @param displayName - Optional display name for the new connection.
157
- * @param oauthRedirectUrl - Redirect URL sent to getConsentLink. The OAuth provider
158
- * will redirect the user here after consent. Defaults to
159
- * the standard native-client redirect accepted by Power Platform.
160
- */
161
- export async function createConnection(connectorId, environmentId, region, token, displayName, oauthRedirectUrl = 'https://login.microsoftonline.com/oauth2/nativeclient') {
162
- const scenario = getAppLogger().trackScenario(HostOperationNames.PlatformCreateConnection, {
163
- connectorId
164
- });
165
- try {
166
- const base = buildEnvironmentEndpoint(environmentId, region);
167
- const connectionId = generateConnectionId();
168
- const url = `${base}/connectivity/connectors/${encodeURIComponent(connectorId)}/connections/${connectionId}` + `?api-version=${API_VERSION}&$filter=environment eq '${environmentId}'`;
169
- const body = {
170
- properties: {
171
- environment: {
172
- name: environmentId
173
- },
174
- connectionParameters: {},
175
- consentInfo: {
176
- redirectUrl: oauthRedirectUrl
177
- },
178
- ...(displayName ? {
179
- displayName
180
- } : {})
181
- }
182
- };
183
- const res = await fetch(url, {
184
- method: 'PUT',
185
- headers: {
186
- Authorization: `Bearer ${token}`,
187
- 'Content-Type': 'application/json'
188
- },
189
- body: JSON.stringify(body)
190
- });
191
- if (!res.ok) {
192
- const errText = await res.text().catch(() => res.statusText);
193
- const result = {
194
- status: 'error',
195
- message: `HTTP ${res.status}: ${errText}`
196
- };
197
- scenario.completeWithError({
198
- httpStatus: res.status
199
- });
200
- return result;
201
- }
202
- const data = await res.json();
203
- const connectionName = data.name ?? connectionId;
204
- const connectionStatus = data.properties?.statuses?.[0]?.status ?? '';
205
- if (connectionStatus === 'Connected') {
206
- scenario.complete({
207
- result: 'connected'
208
- });
209
- return {
210
- status: 'connected',
211
- connectionName
212
- };
213
- }
214
-
215
- // Fetch the OAuth consent link from the dedicated endpoint.
216
- // Use the pre-generated connectionId directly — not the full resource name —
217
- // so the path segment is never double-encoded.
218
- const consentUrl = `${base}/connectivity/connectors/${encodeURIComponent(connectorId)}` + `/connections/${connectionId}/getConsentLink` + `?api-version=${API_VERSION}&$filter=environment eq '${environmentId}'`;
219
- const consentRes = await fetch(consentUrl, {
220
- method: 'POST',
221
- headers: {
222
- Authorization: `Bearer ${token}`,
223
- 'Content-Type': 'application/json'
224
- },
225
- body: JSON.stringify({
226
- redirectUrl: oauthRedirectUrl
227
- })
228
- });
229
- if (consentRes.ok) {
230
- const consentData = await consentRes.json();
231
- const browserFallbackUrl = consentData.consentLink ?? '';
232
-
233
- // For first-party connectors (O365, SharePoint, Dataverse…) attempt silent
234
- // SSO by POSTing the current bearer token to firstPartyLoginUri.
235
- // If the server accepts it the connection becomes Connected without a browser.
236
- if (consentData.firstPartyLoginUri) {
237
- try {
238
- const ssoRes = await fetch(consentData.firstPartyLoginUri, {
239
- method: 'POST',
240
- headers: {
241
- Authorization: `Bearer ${token}`,
242
- 'Content-Type': 'application/json'
243
- },
244
- body: JSON.stringify({
245
- accessToken: token
246
- })
247
- });
248
- if (ssoRes.ok) {
249
- // Silent SSO succeeded — no browser needed.
250
- scenario.complete({
251
- result: 'connected_sso'
252
- });
253
- return {
254
- status: 'connected',
255
- connectionName
256
- };
257
- }
258
- } catch {
259
- // Silent SSO network error — fall through to browser consent.
260
- }
261
- }
262
-
263
- // Fall back to browser-based consent flow.
264
- if (browserFallbackUrl) {
265
- scenario.complete({
266
- result: 'pending_oauth'
267
- });
268
- return {
269
- status: 'pending',
270
- connectionName,
271
- consentLink: browserFallbackUrl
272
- };
273
- }
274
- }
275
-
276
- // No consent link — treat as already connected (e.g. SSO connectors).
277
- scenario.complete({
278
- result: 'connected_no_consent'
279
- });
280
- return {
281
- status: 'connected',
282
- connectionName
283
- };
284
- } catch (e) {
285
- scenario.fail(undefined, {
286
- errorName: getSafeErrorName(e)
287
- });
288
- return {
289
- status: 'error',
290
- message: String(e)
291
- };
292
- }
293
- }
294
-
295
- // ---------------------------------------------------------------------------
296
- // Connection status polling
297
- // ---------------------------------------------------------------------------
298
-
299
- /**
300
- * Poll a connection's status until it is "Connected" or a timeout is reached.
301
- *
302
- * @param connectionName - The connection resource name (from createConnection result).
303
- * @param environmentId - Environment GUID.
304
- * @param region - Cluster region.
305
- * @param token - Power Platform API bearer token.
306
- * @param options.intervalMs - Polling interval in ms (default 2000).
307
- * @param options.timeoutMs - Maximum time to wait in ms (default 60000).
308
- * @returns The resolved `NativeConnectionInstance` if Connected, or `null` if timed out.
309
- */
310
- export async function pollConnectionStatus(connectorId, connectionName, environmentId, region, acquireToken, options = {}) {
311
- const {
312
- intervalMs = 2000,
313
- timeoutMs = 60_000
314
- } = options;
315
- const deadline = Date.now() + timeoutMs;
316
- while (Date.now() < deadline) {
317
- const token = await acquireToken([powerPlatformScope(region)]);
318
- if (!token) return null;
319
- const base = buildEnvironmentEndpoint(environmentId, region);
320
- const connectionId = extractConnectionId(connectionName);
321
- const url = `${base}/connectivity/connectors/${encodeURIComponent(connectorId)}/connections/${connectionId}` + `?api-version=${API_VERSION}&$filter=environment eq '${environmentId}'`;
322
- try {
323
- const res = await fetch(url, {
324
- headers: {
325
- Authorization: `Bearer ${token}`
326
- }
327
- });
328
- if (res.ok) {
329
- const data = await res.json();
330
- const status = data.properties?.statuses?.[0]?.status ?? '';
331
- if (status === 'Connected') {
332
- return {
333
- id: extractConnectionId(data.name ?? connectionName),
334
- name: data.name ?? connectionName,
335
- apiId: data.properties?.apiId ?? '',
336
- status: ConnectionStatus.Connected,
337
- displayName: data.properties?.displayName
338
- };
339
- }
340
- }
341
- } catch {
342
- // Transient network error — continue polling.
343
- }
344
- await new Promise(resolve => {
345
- const timer = setTimeout(resolve, intervalMs);
346
- // Allow GC to clean up the timer reference.
347
- if (typeof timer === 'object' && timer !== null && 'unref' in timer) {
348
- timer.unref();
349
- }
350
- });
351
- }
352
- return null;
353
- }
354
-
355
- // ---------------------------------------------------------------------------
356
- // Consent code confirmation
357
- // ---------------------------------------------------------------------------
358
-
359
- /**
360
- * Confirm a consent code returned by the APIM consent server in the OAuth
361
- * redirect URL (`?code=<commitTokenCode>`).
362
- *
363
- * The consent server embeds a 32-char hex CommitTokenCode in the redirect URL
364
- * when the OAuth exchange completes successfully. POSTing it here causes APIM
365
- * to exchange it for the real OAuth tokens and activate the connection
366
- * (status → Connected).
367
- *
368
- * When the redirect URL uses a custom scheme (e.g. `expo-app://oauth-callback`),
369
- * the consent server may instead return `?error=no+code+returned` and activate
370
- * the connection server-side. In that case this function is not called and the
371
- * caller should poll `pollConnectionStatus` directly.
372
- *
373
- * @param connectorId - Connector short name (e.g. "shared_office365users").
374
- * @param connectionId - Bare connection hex ID (no connector prefix).
375
- * @param environmentId - Environment GUID.
376
- * @param region - Cluster region.
377
- * @param token - Power Platform API bearer token.
378
- * @param code - CommitTokenCode from the redirect URL `?code=` param.
379
- * @returns `true` if the server accepted the code, `false` otherwise.
380
- */
381
- export async function confirmConsentCode(connectorId, connectionId, environmentId, region, token, code) {
382
- const base = buildEnvironmentEndpoint(environmentId, region);
383
- const url = `${base}/connectivity/connectors/${encodeURIComponent(connectorId)}` + `/connections/${connectionId}/confirmConsentCode` + `?api-version=${API_VERSION}&$filter=environment eq '${environmentId}'`;
384
- try {
385
- const res = await fetch(url, {
386
- method: 'POST',
387
- headers: {
388
- Authorization: `Bearer ${token}`,
389
- 'Content-Type': 'application/json'
390
- },
391
- body: JSON.stringify({
392
- code
393
- })
394
- });
395
- return res.ok;
396
- } catch {
397
- return false;
398
- }
399
- }
400
-
401
- // ---------------------------------------------------------------------------
402
- // PowerApps environment — fetch published app launch data
403
- // ---------------------------------------------------------------------------
404
-
405
- /**
406
- * Processed result of the PowerApps app launch API call, ready to be applied
407
- * directly to the NativePowerAppsBridge without further network calls.
408
- */
409
-
410
- /**
411
- * Fetch app launch data from the Power Platform environment-scoped launch
412
- * endpoint and process it into the shape the NativePowerAppsBridge needs.
413
- *
414
- * A single call returns everything required to run the app:
415
- * - `app.properties.connectionReferences` — connectors the app requires
416
- * - `apis[]` — connector metadata (primaryRuntimeUrl)
417
- * - `connections[]` — the current user's resolved connections
418
- * - `app.properties.environment.properties.linkedEnvironmentMetadata.instanceUrl`
419
- * — Dataverse org URL
420
- *
421
- * Endpoint (environment-scoped, same base as all Power Platform API calls):
422
- * GET {envEndpoint}/powerapps/apps/{appId}/launch
423
- * ?api-version=2&lastKnownGoodPublishedVersion=true
424
- *
425
- * Authentication: bearer token for {@link powerPlatformScope}`(region)`.
426
- *
427
- * @param appId - PowerApps app GUID (from `power.config.json` → `appId`).
428
- * @param environmentId - Environment GUID.
429
- * @param region - Cluster region (default "prod").
430
- * @param token - Bearer token for {@link powerPlatformScope}`(region)`.
431
- * @returns Processed launch data, or `null` if the call fails.
432
- */
433
- export async function fetchAppLaunchData(appId, environmentId, region, token) {
434
- const base = buildEnvironmentEndpoint(environmentId, region);
435
- const url = `${base}/powerapps/apps/${encodeURIComponent(appId)}/launch` + `?api-version=2&lastKnownGoodPublishedVersion=true`;
436
- try {
437
- const res = await fetch(url, {
438
- method: 'POST',
439
- headers: {
440
- Authorization: `Bearer ${token}`,
441
- 'Content-Type': 'application/json'
442
- },
443
- body: '{}'
444
- });
445
- if (!res.ok) return null;
446
- const data = await res.json();
447
- const connRefRaw = data.app?.properties?.connectionReferences ?? {};
448
- const apisRaw = data.apis ?? [];
449
- const connectionsRaw = data.connections ?? [];
450
-
451
- // Dataverse org URL from the embedded environment metadata.
452
- const instanceUrl = data.app?.properties?.environment?.properties?.linkedEnvironmentMetadata?.instanceUrl;
453
- const dataverseOrgUrl = (instanceUrl ?? '').replace(/\/$/, '');
454
-
455
- // connectorShortName → primaryRuntimeUrl (from apis[])
456
- const runtimeUrlByConnector = {};
457
- for (const api of apisRaw) {
458
- if (api.name && api.properties?.primaryRuntimeUrl) {
459
- // Normalise the key to lowercase to match the id normalisation below.
460
- runtimeUrlByConnector[api.name.toLowerCase()] = api.properties.primaryRuntimeUrl;
461
- }
462
- }
463
-
464
- // connectorShortName → first Connected connection name (from connections[])
465
- const connectionNameByConnector = {};
466
- for (const conn of connectionsRaw) {
467
- const rawApiId = conn.properties?.apiId ?? '';
468
- const connectorName = (rawApiId.includes('/') ? rawApiId.split('/').pop() ?? rawApiId : rawApiId).toLowerCase();
469
- if (!connectorName || !conn.name) continue;
470
- const isConnected = conn.properties?.statuses?.[0]?.status === 'Connected';
471
- // Keep only the first Connected instance per connector.
472
- if (isConnected && !connectionNameByConnector[connectorName]) {
473
- connectionNameByConnector[connectorName] = conn.name;
474
- }
475
- }
476
- const connectionRefs = {};
477
- const missingConnectors = [];
478
- for (const ref of Object.values(connRefRaw)) {
479
- const rawId = ref.id ?? '';
480
- // Normalise: strip full RP path → short connector name, lowercase.
481
- // e.g. "/providers/microsoft.powerapps/apis/shared_office365users"
482
- // → "shared_office365users"
483
- const connectorName = (rawId.includes('/') ? rawId.split('/').pop() ?? rawId : rawId).toLowerCase();
484
- if (!connectorName) continue;
485
- const runtimeUrl = runtimeUrlByConnector[connectorName] ?? '';
486
- const connectionName = connectionNameByConnector[connectorName] ?? '';
487
- const isDataverse = connectorName.includes('commondataserviceforapps');
488
- const dataSources = ref.dataSources ?? [];
489
- if (!connectionName) {
490
- missingConnectors.push(connectorName);
491
- }
492
- const entry = {
493
- apiId: connectorName,
494
- runtimeUrl,
495
- connectionName,
496
- datasetName: isDataverse ? dataverseOrgUrl : ''
497
- };
498
- for (const ds of dataSources) {
499
- connectionRefs[ds] = entry;
500
- }
501
- }
502
- return {
503
- connectionRefs,
504
- dataverseOrgUrl,
505
- missingConnectors
506
- };
507
- } catch {
508
- return null;
509
- }
510
- }
511
-
512
- // ---------------------------------------------------------------------------
513
- // Connection ACL — add ISV app to connection ACL
514
- // ---------------------------------------------------------------------------
515
-
516
- /**
517
- * Decode the `oid` (object ID) claim from a JWT access token.
518
- *
519
- * The oid claim is in the base64url-encoded payload section (middle part).
520
- * Available in Hermes (React Native) via the global `atob`.
521
- */
522
- export function parseJwtOid(jwtToken) {
523
- try {
524
- const parts = jwtToken.split('.');
525
- if (parts.length !== 3) return null;
526
- const payload = parts[1];
527
- // base64url → base64: replace - with + and _ with /, add padding
528
- const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
529
- const padded = base64 + '==='.slice((base64.length + 3) % 4);
530
- const decoded = atob(padded);
531
- const json = JSON.parse(decoded);
532
- return typeof json['oid'] === 'string' ? json['oid'] : null;
533
- } catch {
534
- return null;
535
- }
536
- }
537
-
538
- /**
539
- * Add the ISV's AAD application (client) ID to the connection's ACL AppIds
540
- * list via the Power Apps RP `addAadAppToConnectionAcl` endpoint.
541
- *
542
- * The RP finds the user's existing ACE (identified by `userOid`) and appends
543
- * `isvClientId` to its `AppIds` list. APIM's stage-2 token check verifies
544
- * that the requesting token's `appid` is present in this list, so without
545
- * this call APIM returns 403 "missing connection ACL" for third-party tokens.
546
- *
547
- * POST https://api.powerapps.com/providers/Microsoft.PowerApps/environments/{environmentId}
548
- * /apis/{connectorId}/connections/{connectionId}/addAadAppToConnectionAcl
549
- * ?api-version=2016-11-01
550
- *
551
- * Body: { "aadApplicationId": "<ISV client ID>", "objectId": "<user OID>" }
552
- *
553
- * @param connectorId - Connector short name (e.g. "shared_office365users").
554
- * @param connectionId - Bare 32-char hex connection ID.
555
- * @param environmentId - Environment GUID.
556
- * @param isvClientId - The ISV's AAD application (client) ID.
557
- * @param userOid - The signed-in user's AAD object ID (oid JWT claim).
558
- * @param token - Bearer token for `https://service.powerapps.com/.default`.
559
- */
560
- export async function addAadAppToConnectionAcl(connectorId, connectionId, environmentId, isvClientId, userOid, token) {
561
- const url = `${POWER_APPS_RP_BASE}/providers/Microsoft.PowerApps/environments/${encodeURIComponent(environmentId)}` + `/apis/${encodeURIComponent(connectorId)}` + `/connections/${encodeURIComponent(connectionId)}/addAadAppToConnectionAcl` + `?api-version=2016-11-01`;
562
- const reqBody = {
563
- aadApplicationId: isvClientId,
564
- objectId: userOid
565
- };
566
- debugLog('[addAadAppToConnectionAcl] POST', url, JSON.stringify(reqBody));
567
- try {
568
- const res = await fetch(url, {
569
- method: 'POST',
570
- headers: {
571
- Authorization: `Bearer ${token}`,
572
- 'Content-Type': 'application/json'
573
- },
574
- body: JSON.stringify(reqBody)
575
- });
576
- const resText = await res.text().catch(() => '');
577
- debugLog(`[addAadAppToConnectionAcl] HTTP ${res.status}`, resText);
578
- if (!res.ok) {
579
- debugWarn(`[addAadAppToConnectionAcl] failed HTTP ${res.status} for connection ${connectionId}: ${resText}`);
580
- getAppLogger().trackErrorEvent(HostOperationNames.PlatformConnectionAclGrant, {
581
- phase: 'addAadAppToConnectionAcl',
582
- reason: 'httpError',
583
- httpStatus: res.status
584
- });
585
- }
586
- return res.ok;
587
- } catch (e) {
588
- debugWarn('[addAadAppToConnectionAcl] error:', e);
589
- getAppLogger().trackErrorEvent(HostOperationNames.PlatformConnectionAclGrant, {
590
- phase: 'addAadAppToConnectionAcl',
591
- reason: 'exception'
592
- });
593
- return false;
594
- }
595
- }