@pax8-cta/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/LICENSE +198 -0
  2. package/dist/auth/device-code-login.d.ts +40 -0
  3. package/dist/auth/device-code-login.d.ts.map +1 -0
  4. package/dist/auth/device-code-login.js +59 -0
  5. package/dist/auth/device-code-login.js.map +1 -0
  6. package/dist/auth/gdap-client.d.ts +81 -0
  7. package/dist/auth/gdap-client.d.ts.map +1 -0
  8. package/dist/auth/gdap-client.js +128 -0
  9. package/dist/auth/gdap-client.js.map +1 -0
  10. package/dist/auth/index.d.ts +19 -0
  11. package/dist/auth/index.d.ts.map +1 -0
  12. package/dist/auth/index.js +19 -0
  13. package/dist/auth/index.js.map +1 -0
  14. package/dist/auth/token-manager.d.ts +54 -0
  15. package/dist/auth/token-manager.d.ts.map +1 -0
  16. package/dist/auth/token-manager.js +150 -0
  17. package/dist/auth/token-manager.js.map +1 -0
  18. package/dist/client.d.ts +27 -0
  19. package/dist/client.d.ts.map +1 -0
  20. package/dist/client.js +27 -0
  21. package/dist/client.js.map +1 -0
  22. package/dist/config/client.d.ts +24 -0
  23. package/dist/config/client.d.ts.map +1 -0
  24. package/dist/config/client.js +18 -0
  25. package/dist/config/client.js.map +1 -0
  26. package/dist/config/index.d.ts +18 -0
  27. package/dist/config/index.d.ts.map +1 -0
  28. package/dist/config/index.js +18 -0
  29. package/dist/config/index.js.map +1 -0
  30. package/dist/config/loader.d.ts +81 -0
  31. package/dist/config/loader.d.ts.map +1 -0
  32. package/dist/config/loader.js +271 -0
  33. package/dist/config/loader.js.map +1 -0
  34. package/dist/config/schema.d.ts +751 -0
  35. package/dist/config/schema.d.ts.map +1 -0
  36. package/dist/config/schema.js +556 -0
  37. package/dist/config/schema.js.map +1 -0
  38. package/dist/constants.d.ts +116 -0
  39. package/dist/constants.d.ts.map +1 -0
  40. package/dist/constants.js +170 -0
  41. package/dist/constants.js.map +1 -0
  42. package/dist/dataverse/agent-resolver.d.ts +98 -0
  43. package/dist/dataverse/agent-resolver.d.ts.map +1 -0
  44. package/dist/dataverse/agent-resolver.js +185 -0
  45. package/dist/dataverse/agent-resolver.js.map +1 -0
  46. package/dist/dataverse/client.d.ts +104 -0
  47. package/dist/dataverse/client.d.ts.map +1 -0
  48. package/dist/dataverse/client.js +272 -0
  49. package/dist/dataverse/client.js.map +1 -0
  50. package/dist/dataverse/connection-refs.d.ts +115 -0
  51. package/dist/dataverse/connection-refs.d.ts.map +1 -0
  52. package/dist/dataverse/connection-refs.js +203 -0
  53. package/dist/dataverse/connection-refs.js.map +1 -0
  54. package/dist/dataverse/index.d.ts +20 -0
  55. package/dist/dataverse/index.d.ts.map +1 -0
  56. package/dist/dataverse/index.js +20 -0
  57. package/dist/dataverse/index.js.map +1 -0
  58. package/dist/dataverse/solution-ops.d.ts +100 -0
  59. package/dist/dataverse/solution-ops.d.ts.map +1 -0
  60. package/dist/dataverse/solution-ops.js +288 -0
  61. package/dist/dataverse/solution-ops.js.map +1 -0
  62. package/dist/errors.d.ts +171 -0
  63. package/dist/errors.d.ts.map +1 -0
  64. package/dist/errors.js +178 -0
  65. package/dist/errors.js.map +1 -0
  66. package/dist/index.d.ts +27 -0
  67. package/dist/index.d.ts.map +1 -0
  68. package/dist/index.js +40 -0
  69. package/dist/index.js.map +1 -0
  70. package/dist/mock/demo-data.d.ts +213 -0
  71. package/dist/mock/demo-data.d.ts.map +1 -0
  72. package/dist/mock/demo-data.js +1096 -0
  73. package/dist/mock/demo-data.js.map +1 -0
  74. package/dist/mock/demo-deployment-store.d.ts +77 -0
  75. package/dist/mock/demo-deployment-store.d.ts.map +1 -0
  76. package/dist/mock/demo-deployment-store.js +85 -0
  77. package/dist/mock/demo-deployment-store.js.map +1 -0
  78. package/dist/powerplatform/admin-client.d.ts +226 -0
  79. package/dist/powerplatform/admin-client.d.ts.map +1 -0
  80. package/dist/powerplatform/admin-client.js +315 -0
  81. package/dist/powerplatform/admin-client.js.map +1 -0
  82. package/dist/powerplatform/index.d.ts +18 -0
  83. package/dist/powerplatform/index.d.ts.map +1 -0
  84. package/dist/powerplatform/index.js +18 -0
  85. package/dist/powerplatform/index.js.map +1 -0
  86. package/dist/powerplatform/tenant-discovery.d.ts +100 -0
  87. package/dist/powerplatform/tenant-discovery.d.ts.map +1 -0
  88. package/dist/powerplatform/tenant-discovery.js +205 -0
  89. package/dist/powerplatform/tenant-discovery.js.map +1 -0
  90. package/dist/preconditions/check.d.ts +41 -0
  91. package/dist/preconditions/check.d.ts.map +1 -0
  92. package/dist/preconditions/check.js +173 -0
  93. package/dist/preconditions/check.js.map +1 -0
  94. package/dist/preconditions/index.d.ts +20 -0
  95. package/dist/preconditions/index.d.ts.map +1 -0
  96. package/dist/preconditions/index.js +20 -0
  97. package/dist/preconditions/index.js.map +1 -0
  98. package/dist/preconditions/loader.d.ts +33 -0
  99. package/dist/preconditions/loader.d.ts.map +1 -0
  100. package/dist/preconditions/loader.js +65 -0
  101. package/dist/preconditions/loader.js.map +1 -0
  102. package/dist/preconditions/schema.d.ts +103 -0
  103. package/dist/preconditions/schema.d.ts.map +1 -0
  104. package/dist/preconditions/schema.js +93 -0
  105. package/dist/preconditions/schema.js.map +1 -0
  106. package/dist/preconditions/types.d.ts +118 -0
  107. package/dist/preconditions/types.d.ts.map +1 -0
  108. package/dist/preconditions/types.js +17 -0
  109. package/dist/preconditions/types.js.map +1 -0
  110. package/dist/queue/index.d.ts +17 -0
  111. package/dist/queue/index.d.ts.map +1 -0
  112. package/dist/queue/index.js +17 -0
  113. package/dist/queue/index.js.map +1 -0
  114. package/dist/queue/memory-queue.d.ts +86 -0
  115. package/dist/queue/memory-queue.d.ts.map +1 -0
  116. package/dist/queue/memory-queue.js +221 -0
  117. package/dist/queue/memory-queue.js.map +1 -0
  118. package/dist/services/audit-log.d.ts +59 -0
  119. package/dist/services/audit-log.d.ts.map +1 -0
  120. package/dist/services/audit-log.js +193 -0
  121. package/dist/services/audit-log.js.map +1 -0
  122. package/dist/services/auth-error-parser.d.ts +36 -0
  123. package/dist/services/auth-error-parser.d.ts.map +1 -0
  124. package/dist/services/auth-error-parser.js +90 -0
  125. package/dist/services/auth-error-parser.js.map +1 -0
  126. package/dist/services/deployment-doctor.d.ts +109 -0
  127. package/dist/services/deployment-doctor.d.ts.map +1 -0
  128. package/dist/services/deployment-doctor.js +476 -0
  129. package/dist/services/deployment-doctor.js.map +1 -0
  130. package/dist/services/deployment-notifications.d.ts +41 -0
  131. package/dist/services/deployment-notifications.d.ts.map +1 -0
  132. package/dist/services/deployment-notifications.js +161 -0
  133. package/dist/services/deployment-notifications.js.map +1 -0
  134. package/dist/services/deployment-progress.d.ts +89 -0
  135. package/dist/services/deployment-progress.d.ts.map +1 -0
  136. package/dist/services/deployment-progress.js +244 -0
  137. package/dist/services/deployment-progress.js.map +1 -0
  138. package/dist/services/deployment-service.d.ts +97 -0
  139. package/dist/services/deployment-service.d.ts.map +1 -0
  140. package/dist/services/deployment-service.js +375 -0
  141. package/dist/services/deployment-service.js.map +1 -0
  142. package/dist/services/drift-analyzer.d.ts +86 -0
  143. package/dist/services/drift-analyzer.d.ts.map +1 -0
  144. package/dist/services/drift-analyzer.js +273 -0
  145. package/dist/services/drift-analyzer.js.map +1 -0
  146. package/dist/services/environment-setup.d.ts +97 -0
  147. package/dist/services/environment-setup.d.ts.map +1 -0
  148. package/dist/services/environment-setup.js +250 -0
  149. package/dist/services/environment-setup.js.map +1 -0
  150. package/dist/services/health-check.d.ts +168 -0
  151. package/dist/services/health-check.d.ts.map +1 -0
  152. package/dist/services/health-check.js +705 -0
  153. package/dist/services/health-check.js.map +1 -0
  154. package/dist/services/index.d.ts +39 -0
  155. package/dist/services/index.d.ts.map +1 -0
  156. package/dist/services/index.js +39 -0
  157. package/dist/services/index.js.map +1 -0
  158. package/dist/services/logger.d.ts +139 -0
  159. package/dist/services/logger.d.ts.map +1 -0
  160. package/dist/services/logger.js +268 -0
  161. package/dist/services/logger.js.map +1 -0
  162. package/dist/services/notification-service.d.ts +55 -0
  163. package/dist/services/notification-service.d.ts.map +1 -0
  164. package/dist/services/notification-service.js +184 -0
  165. package/dist/services/notification-service.js.map +1 -0
  166. package/dist/services/risk-analyzer.d.ts +252 -0
  167. package/dist/services/risk-analyzer.d.ts.map +1 -0
  168. package/dist/services/risk-analyzer.js +866 -0
  169. package/dist/services/risk-analyzer.js.map +1 -0
  170. package/dist/services/rollback.d.ts +57 -0
  171. package/dist/services/rollback.d.ts.map +1 -0
  172. package/dist/services/rollback.js +270 -0
  173. package/dist/services/rollback.js.map +1 -0
  174. package/dist/services/scheduler.d.ts +80 -0
  175. package/dist/services/scheduler.d.ts.map +1 -0
  176. package/dist/services/scheduler.js +350 -0
  177. package/dist/services/scheduler.js.map +1 -0
  178. package/dist/services/secrets.d.ts +31 -0
  179. package/dist/services/secrets.d.ts.map +1 -0
  180. package/dist/services/secrets.js +206 -0
  181. package/dist/services/secrets.js.map +1 -0
  182. package/dist/services/settings-service.d.ts +132 -0
  183. package/dist/services/settings-service.d.ts.map +1 -0
  184. package/dist/services/settings-service.js +378 -0
  185. package/dist/services/settings-service.js.map +1 -0
  186. package/dist/services/solution-diff.d.ts +127 -0
  187. package/dist/services/solution-diff.d.ts.map +1 -0
  188. package/dist/services/solution-diff.js +260 -0
  189. package/dist/services/solution-diff.js.map +1 -0
  190. package/dist/services/solution-mode-detector.d.ts +35 -0
  191. package/dist/services/solution-mode-detector.d.ts.map +1 -0
  192. package/dist/services/solution-mode-detector.js +84 -0
  193. package/dist/services/solution-mode-detector.js.map +1 -0
  194. package/dist/services/tenant-resolver.d.ts +55 -0
  195. package/dist/services/tenant-resolver.d.ts.map +1 -0
  196. package/dist/services/tenant-resolver.js +126 -0
  197. package/dist/services/tenant-resolver.js.map +1 -0
  198. package/dist/services/unmanaged-customizations.d.ts +104 -0
  199. package/dist/services/unmanaged-customizations.d.ts.map +1 -0
  200. package/dist/services/unmanaged-customizations.js +521 -0
  201. package/dist/services/unmanaged-customizations.js.map +1 -0
  202. package/dist/services/url-templater.d.ts +184 -0
  203. package/dist/services/url-templater.d.ts.map +1 -0
  204. package/dist/services/url-templater.js +327 -0
  205. package/dist/services/url-templater.js.map +1 -0
  206. package/dist/services/version-checker.d.ts +108 -0
  207. package/dist/services/version-checker.d.ts.map +1 -0
  208. package/dist/services/version-checker.js +403 -0
  209. package/dist/services/version-checker.js.map +1 -0
  210. package/dist/services/waves.d.ts +90 -0
  211. package/dist/services/waves.d.ts.map +1 -0
  212. package/dist/services/waves.js +222 -0
  213. package/dist/services/waves.js.map +1 -0
  214. package/dist/services/webhook.d.ts +95 -0
  215. package/dist/services/webhook.d.ts.map +1 -0
  216. package/dist/services/webhook.js +244 -0
  217. package/dist/services/webhook.js.map +1 -0
  218. package/dist/utils/deployment-tools.d.ts +110 -0
  219. package/dist/utils/deployment-tools.d.ts.map +1 -0
  220. package/dist/utils/deployment-tools.js +121 -0
  221. package/dist/utils/deployment-tools.js.map +1 -0
  222. package/dist/utils/index.d.ts +17 -0
  223. package/dist/utils/index.d.ts.map +1 -0
  224. package/dist/utils/index.js +18 -0
  225. package/dist/utils/index.js.map +1 -0
  226. package/package.json +49 -0
@@ -0,0 +1,705 @@
1
+ /**
2
+ * Copyright 2024 Pax8, Inc.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import { parseDuration } from "../config/schema.js";
17
+ import { DEFAULT_HEALTH_CHECK_RETRIES, DEFAULT_HEALTH_CHECK_EXPECTED_STATUS, HEALTH_CHECK_CACHE_DURATION_MS, HEALTH_CHECK_TIMEOUT_MS, } from "../constants.js";
18
+ import { getDemoTenantMetadata } from "../mock/demo-data.js";
19
+ /**
20
+ * Service for performing health checks on tenant environments
21
+ */
22
+ export class HealthCheckService {
23
+ /**
24
+ * Perform all health checks for a tenant
25
+ */
26
+ async checkTenantHealth(tenant, client, settings) {
27
+ const checks = [];
28
+ const startTime = Date.now();
29
+ // Merge with defaults to ensure all properties exist
30
+ const defaultSettings = {
31
+ enabled: true,
32
+ expectedStatus: DEFAULT_HEALTH_CHECK_EXPECTED_STATUS,
33
+ timeout: "30s",
34
+ retries: DEFAULT_HEALTH_CHECK_RETRIES,
35
+ };
36
+ const healthSettings = {
37
+ ...defaultSettings,
38
+ ...tenant.healthCheck,
39
+ ...settings,
40
+ };
41
+ if (!healthSettings.enabled) {
42
+ return {
43
+ healthy: true,
44
+ tenantId: tenant.tenantId,
45
+ tenantName: tenant.name,
46
+ checks: [
47
+ {
48
+ name: "health_check_disabled",
49
+ passed: true,
50
+ message: "Health checks are disabled for this tenant",
51
+ durationMs: 0,
52
+ },
53
+ ],
54
+ totalDurationMs: 0,
55
+ };
56
+ }
57
+ const timeout = healthSettings.timeout
58
+ ? parseDuration(healthSettings.timeout)
59
+ : HEALTH_CHECK_TIMEOUT_MS;
60
+ // Check 1: Dataverse connectivity
61
+ const dataverseCheck = await this.checkDataverseConnectivity(client, timeout);
62
+ checks.push(dataverseCheck);
63
+ // Check 2: Custom endpoint if configured
64
+ if (healthSettings.endpoint) {
65
+ const endpointCheck = await this.checkCustomEndpoint(healthSettings.endpoint, healthSettings.expectedStatus ?? DEFAULT_HEALTH_CHECK_EXPECTED_STATUS, timeout, healthSettings.retries ?? DEFAULT_HEALTH_CHECK_RETRIES);
66
+ checks.push(endpointCheck);
67
+ }
68
+ // Check 3: Solution import capability
69
+ const importCheck = await this.checkSolutionImportCapability(client, timeout);
70
+ checks.push(importCheck);
71
+ const totalDurationMs = Date.now() - startTime;
72
+ const healthy = checks.every((c) => c.passed);
73
+ return {
74
+ healthy,
75
+ tenantId: tenant.tenantId,
76
+ tenantName: tenant.name,
77
+ checks,
78
+ totalDurationMs,
79
+ };
80
+ }
81
+ /**
82
+ * Check basic Dataverse connectivity
83
+ */
84
+ async checkDataverseConnectivity(client, timeout) {
85
+ const startTime = Date.now();
86
+ try {
87
+ // Use AbortController for timeout
88
+ const controller = new AbortController();
89
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
90
+ // Query WhoAmI to verify authentication and connectivity
91
+ const result = await Promise.race([
92
+ client.get("/WhoAmI"),
93
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeout)),
94
+ ]);
95
+ clearTimeout(timeoutId);
96
+ return {
97
+ name: "dataverse_connectivity",
98
+ passed: true,
99
+ message: `Connected successfully (Org: ${result.OrganizationId})`,
100
+ durationMs: Date.now() - startTime,
101
+ };
102
+ }
103
+ catch (error) {
104
+ return {
105
+ name: "dataverse_connectivity",
106
+ passed: false,
107
+ message: `Failed: ${error instanceof Error ? error.message : String(error)}`,
108
+ durationMs: Date.now() - startTime,
109
+ };
110
+ }
111
+ }
112
+ /**
113
+ * Check a custom health endpoint
114
+ */
115
+ async checkCustomEndpoint(endpoint, expectedStatus, timeout, retries) {
116
+ const startTime = Date.now();
117
+ let lastError = "";
118
+ for (let attempt = 0; attempt < retries; attempt++) {
119
+ try {
120
+ const controller = new AbortController();
121
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
122
+ const response = await fetch(endpoint, {
123
+ method: "GET",
124
+ signal: controller.signal,
125
+ });
126
+ clearTimeout(timeoutId);
127
+ if (response.status === expectedStatus) {
128
+ return {
129
+ name: "custom_endpoint",
130
+ passed: true,
131
+ message: `Endpoint returned expected status ${expectedStatus}`,
132
+ durationMs: Date.now() - startTime,
133
+ };
134
+ }
135
+ lastError = `Unexpected status: ${response.status} (expected ${expectedStatus})`;
136
+ }
137
+ catch (error) {
138
+ lastError = error instanceof Error ? error.message : String(error);
139
+ }
140
+ // Wait before retry
141
+ if (attempt < retries - 1) {
142
+ await new Promise((resolve) => setTimeout(resolve, 1000 * (attempt + 1)));
143
+ }
144
+ }
145
+ return {
146
+ name: "custom_endpoint",
147
+ passed: false,
148
+ message: `Failed after ${retries} attempts: ${lastError}`,
149
+ durationMs: Date.now() - startTime,
150
+ };
151
+ }
152
+ /**
153
+ * Check if solution import is possible (verify permissions)
154
+ */
155
+ async checkSolutionImportCapability(client, timeout) {
156
+ const startTime = Date.now();
157
+ try {
158
+ // Query for existing import jobs to verify we have read access
159
+ // This doesn't actually import anything, just checks permissions
160
+ await Promise.race([
161
+ client.get("/importjobs", {
162
+ $top: "1",
163
+ $select: "importjobid",
164
+ }),
165
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeout)),
166
+ ]);
167
+ return {
168
+ name: "solution_import_capability",
169
+ passed: true,
170
+ message: "Import job access verified",
171
+ durationMs: Date.now() - startTime,
172
+ };
173
+ }
174
+ catch (error) {
175
+ return {
176
+ name: "solution_import_capability",
177
+ passed: false,
178
+ message: `Access check failed: ${error instanceof Error ? error.message : String(error)}`,
179
+ durationMs: Date.now() - startTime,
180
+ };
181
+ }
182
+ }
183
+ /**
184
+ * Run health checks for multiple tenants in parallel
185
+ */
186
+ async checkMultipleTenants(tenants, clientFactory, options = {}) {
187
+ const { maxConcurrent = 5, onProgress } = options;
188
+ const results = [];
189
+ let completed = 0;
190
+ // Process in batches
191
+ for (let i = 0; i < tenants.length; i += maxConcurrent) {
192
+ const batch = tenants.slice(i, i + maxConcurrent);
193
+ const batchResults = await Promise.all(batch.map(async (tenant) => {
194
+ try {
195
+ const client = await clientFactory(tenant);
196
+ return this.checkTenantHealth(tenant, client);
197
+ }
198
+ catch (error) {
199
+ return {
200
+ healthy: false,
201
+ tenantId: tenant.tenantId,
202
+ tenantName: tenant.name,
203
+ checks: [
204
+ {
205
+ name: "client_creation",
206
+ passed: false,
207
+ message: `Failed to create client: ${error instanceof Error ? error.message : String(error)}`,
208
+ durationMs: 0,
209
+ },
210
+ ],
211
+ totalDurationMs: 0,
212
+ };
213
+ }
214
+ }));
215
+ results.push(...batchResults);
216
+ completed += batch.length;
217
+ if (onProgress) {
218
+ onProgress(completed, tenants.length);
219
+ }
220
+ }
221
+ return results;
222
+ }
223
+ /**
224
+ * Generate a health report summary
225
+ */
226
+ generateReport(results) {
227
+ const unhealthyDetails = results
228
+ .filter((r) => !r.healthy)
229
+ .map((r) => ({
230
+ tenantName: r.tenantName,
231
+ tenantId: r.tenantId,
232
+ failedChecks: r.checks.filter((c) => !c.passed).map((c) => c.message),
233
+ }));
234
+ return {
235
+ totalTenants: results.length,
236
+ healthyTenants: results.filter((r) => r.healthy).length,
237
+ unhealthyTenants: results.filter((r) => !r.healthy).length,
238
+ unhealthyDetails,
239
+ };
240
+ }
241
+ }
242
+ // Shared singleton for basic tenant connectivity/auth health checks.
243
+ export const healthCheckService = new HealthCheckService();
244
+ export class TenantHealthMonitoringService {
245
+ cache = new Map();
246
+ CACHE_DURATION_MS = HEALTH_CHECK_CACHE_DURATION_MS;
247
+ /**
248
+ * Check health for a single tenant
249
+ */
250
+ async checkTenantHealth(context, skipCache = false) {
251
+ // Check cache first
252
+ if (!skipCache) {
253
+ const cached = this.cache.get(context.tenantId);
254
+ if (cached && Date.now() - cached.timestamp < this.CACHE_DURATION_MS) {
255
+ return cached.data;
256
+ }
257
+ }
258
+ // Run all health checks
259
+ const [gdapStatus, connectionsStatus, deploymentMetrics] = await Promise.all([
260
+ this.checkGDAPHealth(context),
261
+ this.checkConnectionsHealth(context),
262
+ this.analyzeDeploymentHistory(context),
263
+ ]);
264
+ // Collect all issues
265
+ const issues = [
266
+ ...gdapStatus.issues,
267
+ ...connectionsStatus.issues,
268
+ ...deploymentMetrics.issues,
269
+ ];
270
+ // Calculate health score
271
+ const healthScore = this.calculateHealthScore(issues, deploymentMetrics.successRate);
272
+ // Determine overall status
273
+ const status = this.determineStatus(healthScore, issues);
274
+ const health = {
275
+ tenantId: context.tenantId,
276
+ tenantName: context.tenantName,
277
+ healthScore,
278
+ status,
279
+ lastChecked: new Date().toISOString(),
280
+ issues,
281
+ gdapStatus: gdapStatus.status,
282
+ connectionsStatus: connectionsStatus.status,
283
+ recentSuccessRate: deploymentMetrics.successRate,
284
+ recentDeployments: deploymentMetrics.counts,
285
+ };
286
+ // Cache the result
287
+ this.cache.set(context.tenantId, { data: health, timestamp: Date.now() });
288
+ return health;
289
+ }
290
+ /**
291
+ * Check detailed health for a single tenant
292
+ */
293
+ async checkTenantHealthDetail(context, skipCache = false) {
294
+ const basicHealth = await this.checkTenantHealth(context, skipCache);
295
+ // Get detailed GDAP info
296
+ const gdapDetail = await this.getGDAPDetail(context);
297
+ // Get detailed connection info
298
+ const connectionsDetail = await this.getConnectionsDetail(context);
299
+ // Get recent deployment history
300
+ const recentHistory = this.getRecentDeploymentHistory(context);
301
+ // Generate recommendations
302
+ const recommendations = this.generateRecommendations(basicHealth, gdapDetail, connectionsDetail);
303
+ return {
304
+ ...basicHealth,
305
+ gdap: gdapDetail,
306
+ connections: connectionsDetail,
307
+ recentDeploymentHistory: recentHistory,
308
+ recommendations,
309
+ };
310
+ }
311
+ /**
312
+ * Check health for multiple tenants
313
+ */
314
+ async checkMultipleTenants(contexts, skipCache = false) {
315
+ return Promise.all(contexts.map((ctx) => this.checkTenantHealth(ctx, skipCache)));
316
+ }
317
+ /**
318
+ * Clear cache for a tenant or all tenants
319
+ */
320
+ clearCache(tenantId) {
321
+ if (tenantId) {
322
+ this.cache.delete(tenantId);
323
+ }
324
+ else {
325
+ this.cache.clear();
326
+ }
327
+ }
328
+ // ============================================================================
329
+ // Private Health Check Methods
330
+ // ============================================================================
331
+ async checkGDAPHealth(context) {
332
+ const issues = [];
333
+ // In demo mode, use tenant metadata for deterministic GDAP scenarios
334
+ if (process.env.DEMO_MODE === "true") {
335
+ const meta = getDemoTenantMetadata(context.tenantId);
336
+ if (meta) {
337
+ switch (meta.gdapStatus) {
338
+ case "missing_role":
339
+ issues.push({
340
+ severity: "critical",
341
+ category: "permissions",
342
+ message: meta.gdapIssue || "Missing Power Platform Administrator role in GDAP relationship",
343
+ resolution: "Add the Power Platform Administrator role to this tenant's GDAP relationship in Partner Center",
344
+ link: "https://partner.microsoft.com/commerce/granularadminrelationships",
345
+ });
346
+ return { status: "missing_role", issues };
347
+ case "expired":
348
+ issues.push({
349
+ severity: "error",
350
+ category: "permissions",
351
+ message: meta.gdapIssue || "GDAP relationship expired",
352
+ resolution: "Renew the GDAP relationship in Partner Center",
353
+ link: "https://partner.microsoft.com/commerce/granularadminrelationships",
354
+ });
355
+ return { status: "expired", issues };
356
+ case "propagating":
357
+ issues.push({
358
+ severity: "warning",
359
+ category: "permissions",
360
+ message: meta.gdapIssue ||
361
+ "GDAP relationship created recently, permissions may still be propagating",
362
+ resolution: "Wait 24-48 hours for permissions to fully propagate",
363
+ });
364
+ return { status: "propagating", issues };
365
+ case "expiring_soon":
366
+ issues.push({
367
+ severity: "warning",
368
+ category: "permissions",
369
+ message: meta.gdapIssue || "GDAP relationship expiring soon",
370
+ resolution: "Renew GDAP relationship before it expires",
371
+ link: "https://partner.microsoft.com/commerce/granularadminrelationships",
372
+ });
373
+ return { status: "valid", issues };
374
+ case "valid":
375
+ default:
376
+ return { status: "valid", issues: [] };
377
+ }
378
+ }
379
+ return { status: "valid", issues: [] };
380
+ }
381
+ // Live GDAP checks require Microsoft Graph API access, which is not available in all
382
+ // deployment contexts. When Graph credentials are provided, this will query:
383
+ // GET https://graph.microsoft.com/v1.0/tenantRelationships/delegatedAdminRelationships
384
+ // For now, returns "unknown" — the CLI validate command performs real GDAP checks separately.
385
+ return { status: "unknown", issues: [] };
386
+ }
387
+ async checkConnectionsHealth(context) {
388
+ const issues = [];
389
+ // In demo mode, use tenant metadata for deterministic connection scenarios
390
+ if (process.env.DEMO_MODE === "true") {
391
+ const meta = getDemoTenantMetadata(context.tenantId);
392
+ if (meta) {
393
+ switch (meta.connectionStatus) {
394
+ case "expired":
395
+ issues.push({
396
+ severity: "error",
397
+ category: "connections",
398
+ message: meta.connectionIssue || "Connection reference expired: requires reauthentication",
399
+ resolution: "Open the solution in the maker portal and update the connection reference",
400
+ link: `${context.environmentUrl}/main.aspx?forceUCI=1&pagetype=apps`,
401
+ });
402
+ return { status: "expired", issues };
403
+ case "missing":
404
+ issues.push({
405
+ severity: "error",
406
+ category: "connections",
407
+ message: meta.connectionIssue || "Missing required connection: not configured",
408
+ resolution: "Configure the required connection in the maker portal before deploying",
409
+ link: `${context.environmentUrl}/main.aspx?forceUCI=1&pagetype=apps`,
410
+ });
411
+ return { status: "missing", issues };
412
+ case "expiring_certificate":
413
+ issues.push({
414
+ severity: "warning",
415
+ category: "connections",
416
+ message: meta.connectionIssue || "Connection certificate expiring soon",
417
+ resolution: "Rotate the OAuth certificate before it expires",
418
+ });
419
+ return { status: "valid", issues };
420
+ case "valid":
421
+ default:
422
+ return { status: "valid", issues: [] };
423
+ }
424
+ }
425
+ return { status: "valid", issues: [] };
426
+ }
427
+ // Live connection checks require Dataverse API access per tenant. When credentials
428
+ // are available, this will query connectionreferences entity. For now, returns "unknown"
429
+ // — the CLI validate command performs real connection checks separately.
430
+ return { status: "unknown", issues: [] };
431
+ }
432
+ analyzeDeploymentHistory(context) {
433
+ const issues = [];
434
+ if (!context.deploymentHistory || context.deploymentHistory.length === 0) {
435
+ return {
436
+ successRate: 1.0, // Assume healthy if no history
437
+ counts: { total: 0, successful: 0, failed: 0 },
438
+ issues: [],
439
+ };
440
+ }
441
+ const recent = context.deploymentHistory.slice(0, 10);
442
+ const successful = recent.filter((d) => d.status === "success").length;
443
+ const failed = recent.filter((d) => d.status === "failure").length;
444
+ const successRate = successful / recent.length;
445
+ // Check for concerning patterns
446
+ if (successRate < 0.5) {
447
+ issues.push({
448
+ severity: "critical",
449
+ category: "history",
450
+ message: `Low success rate: Only ${Math.round(successRate * 100)}% of recent deployments succeeded`,
451
+ resolution: "Review recent deployment errors and address underlying issues before deploying again",
452
+ });
453
+ }
454
+ else if (successRate < 0.7) {
455
+ issues.push({
456
+ severity: "warning",
457
+ category: "history",
458
+ message: `Moderate success rate: ${Math.round(successRate * 100)}% of recent deployments succeeded`,
459
+ resolution: "Some recent deployments failed, consider investigating before deploying",
460
+ });
461
+ }
462
+ // Check for recent failures
463
+ const lastThree = recent.slice(0, 3);
464
+ const recentFailures = lastThree.filter((d) => d.status === "failure").length;
465
+ if (recentFailures >= 2) {
466
+ issues.push({
467
+ severity: "error",
468
+ category: "history",
469
+ message: `${recentFailures} of last 3 deployments failed`,
470
+ resolution: "Fix underlying issues before attempting another deployment",
471
+ });
472
+ }
473
+ return {
474
+ successRate,
475
+ counts: {
476
+ total: recent.length,
477
+ successful,
478
+ failed,
479
+ },
480
+ issues,
481
+ };
482
+ }
483
+ async getGDAPDetail(context) {
484
+ // In demo mode, return metadata-driven GDAP details
485
+ if (process.env.DEMO_MODE === "true") {
486
+ const meta = getDemoTenantMetadata(context.tenantId);
487
+ if (meta) {
488
+ const baseExpiry = meta.gdapRelationshipExpiry ||
489
+ new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString();
490
+ switch (meta.gdapStatus) {
491
+ case "missing_role":
492
+ return {
493
+ status: "missing_role",
494
+ roles: ["Dynamics 365 Administrator"],
495
+ missingRoles: ["Power Platform Administrator"],
496
+ relationshipExpiry: baseExpiry,
497
+ lastVerified: new Date().toISOString(),
498
+ issue: meta.gdapIssue || "Missing required role",
499
+ };
500
+ case "expired":
501
+ return {
502
+ status: "expired",
503
+ roles: [],
504
+ relationshipExpiry: meta.gdapRelationshipExpiry ||
505
+ new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
506
+ lastVerified: new Date().toISOString(),
507
+ issue: meta.gdapIssue || "GDAP relationship expired",
508
+ };
509
+ case "propagating":
510
+ return {
511
+ status: "propagating",
512
+ roles: ["Power Platform Administrator", "Dynamics 365 Administrator"],
513
+ relationshipExpiry: baseExpiry,
514
+ lastVerified: new Date().toISOString(),
515
+ issue: meta.gdapIssue || "Permissions still propagating",
516
+ };
517
+ case "expiring_soon":
518
+ return {
519
+ status: "valid",
520
+ roles: ["Power Platform Administrator", "Dynamics 365 Administrator"],
521
+ relationshipExpiry: meta.gdapRelationshipExpiry ||
522
+ new Date(Date.now() + 5 * 24 * 60 * 60 * 1000).toISOString(),
523
+ lastVerified: new Date().toISOString(),
524
+ issue: meta.gdapIssue || "GDAP relationship expiring soon",
525
+ };
526
+ }
527
+ }
528
+ return {
529
+ status: "valid",
530
+ roles: ["Power Platform Administrator", "Dynamics 365 Administrator"],
531
+ relationshipExpiry: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(),
532
+ lastVerified: new Date().toISOString(),
533
+ };
534
+ }
535
+ // Live GDAP detail requires Microsoft Graph API access (see checkGDAPHealth above).
536
+ return {
537
+ status: "unknown",
538
+ lastVerified: new Date().toISOString(),
539
+ };
540
+ }
541
+ async getConnectionsDetail(context) {
542
+ // In demo mode, return metadata-driven connection details
543
+ if (process.env.DEMO_MODE === "true") {
544
+ const meta = getDemoTenantMetadata(context.tenantId);
545
+ const connections = [];
546
+ if (meta) {
547
+ switch (meta.connectionStatus) {
548
+ case "expired":
549
+ connections.push({
550
+ name: "shared_commondataserviceforapps",
551
+ displayName: "Dataverse",
552
+ status: "expired",
553
+ expiryDate: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
554
+ issue: meta.connectionIssue || "Connection expired, needs reauthentication",
555
+ });
556
+ break;
557
+ case "missing":
558
+ connections.push({
559
+ name: "shared_commondataserviceforapps",
560
+ displayName: "Dataverse",
561
+ status: "valid",
562
+ expiryDate: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString(),
563
+ });
564
+ connections.push({
565
+ name: "shared_sharepointonline",
566
+ displayName: "SharePoint",
567
+ status: "missing",
568
+ issue: meta.connectionIssue || "Connection never configured",
569
+ });
570
+ break;
571
+ case "expiring_certificate":
572
+ connections.push({
573
+ name: "shared_commondataserviceforapps",
574
+ displayName: "Dataverse",
575
+ status: "valid",
576
+ expiryDate: new Date(Date.now() + 15 * 24 * 60 * 60 * 1000).toISOString(),
577
+ issue: meta.connectionIssue || "OAuth certificate expiring in 15 days",
578
+ });
579
+ break;
580
+ default:
581
+ connections.push({
582
+ name: "shared_commondataserviceforapps",
583
+ displayName: "Dataverse",
584
+ status: "valid",
585
+ expiryDate: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString(),
586
+ });
587
+ break;
588
+ }
589
+ // Add Office 365 connection for tenants that have valid/expiring connections
590
+ if (meta.connectionStatus !== "expired") {
591
+ connections.push({
592
+ name: "shared_office365",
593
+ displayName: "Office 365 Outlook",
594
+ status: "valid",
595
+ expiryDate: new Date(Date.now() + 180 * 24 * 60 * 60 * 1000).toISOString(),
596
+ });
597
+ }
598
+ }
599
+ else {
600
+ // Unknown tenant, return sensible defaults
601
+ connections.push({
602
+ name: "shared_commondataserviceforapps",
603
+ displayName: "Dataverse",
604
+ status: "valid",
605
+ expiryDate: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString(),
606
+ });
607
+ connections.push({
608
+ name: "shared_office365",
609
+ displayName: "Office 365 Outlook",
610
+ status: "valid",
611
+ expiryDate: new Date(Date.now() + 180 * 24 * 60 * 60 * 1000).toISOString(),
612
+ });
613
+ }
614
+ return connections;
615
+ }
616
+ // Live connection detail requires Dataverse API access per tenant (see checkConnectionsHealth above).
617
+ return [];
618
+ }
619
+ getRecentDeploymentHistory(context) {
620
+ if (!context.deploymentHistory)
621
+ return [];
622
+ return context.deploymentHistory.slice(0, 20).map((d, idx) => ({
623
+ id: `deployment-${idx}`,
624
+ timestamp: d.completedAt,
625
+ status: d.status,
626
+ duration: d.durationMinutes,
627
+ error: d.error,
628
+ }));
629
+ }
630
+ generateRecommendations(health, gdap, connections) {
631
+ const recommendations = [];
632
+ // GDAP recommendations
633
+ if (gdap.status === "missing_role") {
634
+ recommendations.push("Add missing GDAP roles in Partner Center before deploying");
635
+ }
636
+ else if (gdap.status === "expired") {
637
+ recommendations.push("Renew GDAP relationship in Partner Center");
638
+ }
639
+ else if (gdap.status === "propagating") {
640
+ recommendations.push("Wait 24-48 hours for GDAP permissions to fully propagate");
641
+ }
642
+ // Connection recommendations
643
+ const expiredConnections = connections.filter((c) => c.status === "expired");
644
+ if (expiredConnections.length > 0) {
645
+ recommendations.push(`Reauthenticate ${expiredConnections.length} expired connection${expiredConnections.length > 1 ? "s" : ""} in maker portal`);
646
+ }
647
+ // History-based recommendations
648
+ if (health.recentSuccessRate < 0.7) {
649
+ recommendations.push("Review and fix recent deployment failures before deploying again");
650
+ }
651
+ // General health recommendations
652
+ if (health.healthScore < 50) {
653
+ recommendations.push("Address critical issues before attempting deployments to this tenant");
654
+ }
655
+ else if (health.healthScore < 70) {
656
+ recommendations.push("Consider fixing warnings to improve deployment success rate");
657
+ }
658
+ return recommendations;
659
+ }
660
+ // ============================================================================
661
+ // Scoring
662
+ // ============================================================================
663
+ calculateHealthScore(issues, successRate) {
664
+ // Start with perfect score
665
+ let score = 100;
666
+ // Deduct points for issues
667
+ for (const issue of issues) {
668
+ switch (issue.severity) {
669
+ case "critical":
670
+ score -= 40;
671
+ break;
672
+ case "error":
673
+ score -= 25;
674
+ break;
675
+ case "warning":
676
+ score -= 10;
677
+ break;
678
+ case "info":
679
+ score -= 2;
680
+ break;
681
+ }
682
+ }
683
+ // Factor in deployment success rate (30% weight)
684
+ const historyScore = successRate * 30;
685
+ score = score * 0.7 + historyScore;
686
+ // Clamp to 0-100
687
+ return Math.max(0, Math.min(100, Math.round(score)));
688
+ }
689
+ determineStatus(score, issues) {
690
+ // Critical if any critical issues or score very low
691
+ if (issues.some((i) => i.severity === "critical") || score < 40) {
692
+ return "critical";
693
+ }
694
+ // Warning if any errors/warnings or medium score
695
+ if (issues.some((i) => i.severity === "error" || i.severity === "warning") || score < 70) {
696
+ return "warning";
697
+ }
698
+ return "healthy";
699
+ }
700
+ }
701
+ // Shared singleton for extended monitoring, recommendations, and scoring.
702
+ export const tenantHealthMonitoringService = new TenantHealthMonitoringService();
703
+ // Backwards-compatible alias for existing consumers.
704
+ export const healthChecker = tenantHealthMonitoringService;
705
+ //# sourceMappingURL=health-check.js.map