@contractspec/integration.providers-impls 2.9.0 → 2.10.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.
package/dist/index.js CHANGED
@@ -14,6 +14,9 @@ export * from "@contractspec/lib.contracts-integrations";
14
14
  // src/embedding.ts
15
15
  export * from "@contractspec/lib.contracts-integrations";
16
16
 
17
+ // src/health.ts
18
+ export * from "@contractspec/lib.contracts-integrations";
19
+
17
20
  // src/impls/elevenlabs-voice.ts
18
21
  import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
19
22
  var FORMAT_MAP = {
@@ -2031,6 +2034,498 @@ async function safeReadError3(response) {
2031
2034
  }
2032
2035
  }
2033
2036
 
2037
+ // src/impls/health/base-health-provider.ts
2038
+ class BaseHealthProvider {
2039
+ providerKey;
2040
+ transport;
2041
+ apiBaseUrl;
2042
+ mcpUrl;
2043
+ apiKey;
2044
+ accessToken;
2045
+ mcpAccessToken;
2046
+ webhookSecret;
2047
+ fetchFn;
2048
+ mcpRequestId = 0;
2049
+ constructor(options) {
2050
+ this.providerKey = options.providerKey;
2051
+ this.transport = options.transport;
2052
+ this.apiBaseUrl = options.apiBaseUrl ?? "https://api.example-health.local";
2053
+ this.mcpUrl = options.mcpUrl;
2054
+ this.apiKey = options.apiKey;
2055
+ this.accessToken = options.accessToken;
2056
+ this.mcpAccessToken = options.mcpAccessToken;
2057
+ this.webhookSecret = options.webhookSecret;
2058
+ this.fetchFn = options.fetchFn ?? fetch;
2059
+ }
2060
+ async listActivities(params) {
2061
+ const result = await this.fetchList("activities", params);
2062
+ return {
2063
+ activities: result.items,
2064
+ nextCursor: result.nextCursor,
2065
+ hasMore: result.hasMore,
2066
+ source: this.currentSource()
2067
+ };
2068
+ }
2069
+ async listWorkouts(params) {
2070
+ const result = await this.fetchList("workouts", params);
2071
+ return {
2072
+ workouts: result.items,
2073
+ nextCursor: result.nextCursor,
2074
+ hasMore: result.hasMore,
2075
+ source: this.currentSource()
2076
+ };
2077
+ }
2078
+ async listSleep(params) {
2079
+ const result = await this.fetchList("sleep", params);
2080
+ return {
2081
+ sleep: result.items,
2082
+ nextCursor: result.nextCursor,
2083
+ hasMore: result.hasMore,
2084
+ source: this.currentSource()
2085
+ };
2086
+ }
2087
+ async listBiometrics(params) {
2088
+ const result = await this.fetchList("biometrics", params);
2089
+ return {
2090
+ biometrics: result.items,
2091
+ nextCursor: result.nextCursor,
2092
+ hasMore: result.hasMore,
2093
+ source: this.currentSource()
2094
+ };
2095
+ }
2096
+ async listNutrition(params) {
2097
+ const result = await this.fetchList("nutrition", params);
2098
+ return {
2099
+ nutrition: result.items,
2100
+ nextCursor: result.nextCursor,
2101
+ hasMore: result.hasMore,
2102
+ source: this.currentSource()
2103
+ };
2104
+ }
2105
+ async getConnectionStatus(params) {
2106
+ const payload = await this.fetchRecord("connection/status", params);
2107
+ const status = readString2(payload, "status") ?? "healthy";
2108
+ return {
2109
+ tenantId: params.tenantId,
2110
+ connectionId: params.connectionId,
2111
+ status: status === "healthy" || status === "degraded" || status === "error" || status === "disconnected" ? status : "healthy",
2112
+ source: this.currentSource(),
2113
+ lastCheckedAt: readString2(payload, "lastCheckedAt") ?? new Date().toISOString(),
2114
+ errorCode: readString2(payload, "errorCode"),
2115
+ errorMessage: readString2(payload, "errorMessage"),
2116
+ metadata: asRecord(payload.metadata)
2117
+ };
2118
+ }
2119
+ async syncActivities(params) {
2120
+ return this.sync("activities", params);
2121
+ }
2122
+ async syncWorkouts(params) {
2123
+ return this.sync("workouts", params);
2124
+ }
2125
+ async syncSleep(params) {
2126
+ return this.sync("sleep", params);
2127
+ }
2128
+ async syncBiometrics(params) {
2129
+ return this.sync("biometrics", params);
2130
+ }
2131
+ async syncNutrition(params) {
2132
+ return this.sync("nutrition", params);
2133
+ }
2134
+ async parseWebhook(request) {
2135
+ const payload = request.parsedBody ?? safeJsonParse(request.rawBody);
2136
+ const body = asRecord(payload);
2137
+ return {
2138
+ providerKey: this.providerKey,
2139
+ eventType: readString2(body, "eventType") ?? readString2(body, "event"),
2140
+ externalEntityId: readString2(body, "externalEntityId") ?? readString2(body, "entityId"),
2141
+ entityType: normalizeEntityType(readString2(body, "entityType") ?? readString2(body, "type")),
2142
+ receivedAt: new Date().toISOString(),
2143
+ verified: await this.verifyWebhook(request),
2144
+ payload
2145
+ };
2146
+ }
2147
+ async verifyWebhook(request) {
2148
+ if (!this.webhookSecret) {
2149
+ return true;
2150
+ }
2151
+ const signature = readHeader(request.headers, "x-webhook-signature");
2152
+ return signature === this.webhookSecret;
2153
+ }
2154
+ async fetchList(resource, params) {
2155
+ const payload = await this.fetchRecord(resource, params);
2156
+ const items = asArray2(payload.items) ?? asArray2(payload[resource]) ?? asArray2(payload.records) ?? [];
2157
+ return {
2158
+ items,
2159
+ nextCursor: readString2(payload, "nextCursor") ?? readString2(payload, "cursor"),
2160
+ hasMore: readBoolean2(payload, "hasMore")
2161
+ };
2162
+ }
2163
+ async sync(resource, params) {
2164
+ const payload = await this.fetchRecord(`sync/${resource}`, params, "POST");
2165
+ return {
2166
+ synced: readNumber(payload, "synced") ?? 0,
2167
+ failed: readNumber(payload, "failed") ?? 0,
2168
+ nextCursor: readString2(payload, "nextCursor"),
2169
+ errors: asArray2(payload.errors)?.map((item) => String(item)),
2170
+ source: this.currentSource()
2171
+ };
2172
+ }
2173
+ async fetchRecord(resource, params, method = "GET") {
2174
+ if (this.transport.endsWith("mcp")) {
2175
+ return this.callMcpTool(resource, params);
2176
+ }
2177
+ const url = new URL(`${this.apiBaseUrl.replace(/\/$/, "")}/${resource}`);
2178
+ if (method === "GET") {
2179
+ for (const [key, value] of Object.entries(params)) {
2180
+ if (value == null)
2181
+ continue;
2182
+ if (Array.isArray(value)) {
2183
+ value.forEach((item) => {
2184
+ url.searchParams.append(key, String(item));
2185
+ });
2186
+ continue;
2187
+ }
2188
+ url.searchParams.set(key, String(value));
2189
+ }
2190
+ }
2191
+ const response = await this.fetchFn(url, {
2192
+ method,
2193
+ headers: {
2194
+ "Content-Type": "application/json",
2195
+ ...this.accessToken || this.apiKey ? { Authorization: `Bearer ${this.accessToken ?? this.apiKey}` } : {}
2196
+ },
2197
+ body: method === "POST" ? JSON.stringify(params) : undefined
2198
+ });
2199
+ if (!response.ok) {
2200
+ const errorBody = await safeResponseText(response);
2201
+ throw new Error(`${this.providerKey} ${resource} failed (${response.status}): ${errorBody}`);
2202
+ }
2203
+ const data = await response.json();
2204
+ return asRecord(data) ?? {};
2205
+ }
2206
+ async callMcpTool(resource, params) {
2207
+ if (!this.mcpUrl) {
2208
+ return {};
2209
+ }
2210
+ const response = await this.fetchFn(this.mcpUrl, {
2211
+ method: "POST",
2212
+ headers: {
2213
+ "Content-Type": "application/json",
2214
+ ...this.mcpAccessToken ? { Authorization: `Bearer ${this.mcpAccessToken}` } : {}
2215
+ },
2216
+ body: JSON.stringify({
2217
+ jsonrpc: "2.0",
2218
+ id: ++this.mcpRequestId,
2219
+ method: "tools/call",
2220
+ params: {
2221
+ name: `${this.providerKey.replace("health.", "")}_${resource.replace(/\//g, "_")}`,
2222
+ arguments: params
2223
+ }
2224
+ })
2225
+ });
2226
+ if (!response.ok) {
2227
+ const errorBody = await safeResponseText(response);
2228
+ throw new Error(`${this.providerKey} MCP ${resource} failed (${response.status}): ${errorBody}`);
2229
+ }
2230
+ const rpcPayload = await response.json();
2231
+ const rpc = asRecord(rpcPayload);
2232
+ const result = asRecord(rpc?.result) ?? {};
2233
+ const structured = asRecord(result.structuredContent);
2234
+ if (structured)
2235
+ return structured;
2236
+ const data = asRecord(result.data);
2237
+ if (data)
2238
+ return data;
2239
+ return result;
2240
+ }
2241
+ currentSource() {
2242
+ return {
2243
+ providerKey: this.providerKey,
2244
+ transport: this.transport,
2245
+ route: "primary"
2246
+ };
2247
+ }
2248
+ }
2249
+ function safeJsonParse(raw) {
2250
+ try {
2251
+ return JSON.parse(raw);
2252
+ } catch {
2253
+ return { rawBody: raw };
2254
+ }
2255
+ }
2256
+ function readHeader(headers, key) {
2257
+ const match = Object.entries(headers).find(([headerKey]) => headerKey.toLowerCase() === key.toLowerCase());
2258
+ if (!match)
2259
+ return;
2260
+ const value = match[1];
2261
+ return Array.isArray(value) ? value[0] : value;
2262
+ }
2263
+ function normalizeEntityType(value) {
2264
+ if (!value)
2265
+ return;
2266
+ if (value === "activity" || value === "workout" || value === "sleep" || value === "biometric" || value === "nutrition") {
2267
+ return value;
2268
+ }
2269
+ return;
2270
+ }
2271
+ function asRecord(value) {
2272
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
2273
+ return;
2274
+ }
2275
+ return value;
2276
+ }
2277
+ function asArray2(value) {
2278
+ return Array.isArray(value) ? value : undefined;
2279
+ }
2280
+ function readString2(record, key) {
2281
+ const value = record?.[key];
2282
+ return typeof value === "string" ? value : undefined;
2283
+ }
2284
+ function readBoolean2(record, key) {
2285
+ const value = record?.[key];
2286
+ return typeof value === "boolean" ? value : undefined;
2287
+ }
2288
+ function readNumber(record, key) {
2289
+ const value = record?.[key];
2290
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
2291
+ }
2292
+ async function safeResponseText(response) {
2293
+ try {
2294
+ return await response.text();
2295
+ } catch {
2296
+ return response.statusText;
2297
+ }
2298
+ }
2299
+
2300
+ // src/impls/health/providers.ts
2301
+ function createProviderOptions(options, fallbackTransport) {
2302
+ return {
2303
+ ...options,
2304
+ transport: options.transport ?? fallbackTransport
2305
+ };
2306
+ }
2307
+
2308
+ class OpenWearablesHealthProvider extends BaseHealthProvider {
2309
+ constructor(options) {
2310
+ super({
2311
+ providerKey: "health.openwearables",
2312
+ ...createProviderOptions(options, "aggregator-api")
2313
+ });
2314
+ }
2315
+ }
2316
+
2317
+ class WhoopHealthProvider extends BaseHealthProvider {
2318
+ constructor(options) {
2319
+ super({
2320
+ providerKey: "health.whoop",
2321
+ ...createProviderOptions(options, "official-api")
2322
+ });
2323
+ }
2324
+ }
2325
+
2326
+ class AppleHealthBridgeProvider extends BaseHealthProvider {
2327
+ constructor(options) {
2328
+ super({
2329
+ providerKey: "health.apple-health",
2330
+ ...createProviderOptions(options, "aggregator-api")
2331
+ });
2332
+ }
2333
+ }
2334
+
2335
+ class OuraHealthProvider extends BaseHealthProvider {
2336
+ constructor(options) {
2337
+ super({
2338
+ providerKey: "health.oura",
2339
+ ...createProviderOptions(options, "official-api")
2340
+ });
2341
+ }
2342
+ }
2343
+
2344
+ class StravaHealthProvider extends BaseHealthProvider {
2345
+ constructor(options) {
2346
+ super({
2347
+ providerKey: "health.strava",
2348
+ ...createProviderOptions(options, "official-api")
2349
+ });
2350
+ }
2351
+ }
2352
+
2353
+ class GarminHealthProvider extends BaseHealthProvider {
2354
+ constructor(options) {
2355
+ super({
2356
+ providerKey: "health.garmin",
2357
+ ...createProviderOptions(options, "official-api")
2358
+ });
2359
+ }
2360
+ }
2361
+
2362
+ class FitbitHealthProvider extends BaseHealthProvider {
2363
+ constructor(options) {
2364
+ super({
2365
+ providerKey: "health.fitbit",
2366
+ ...createProviderOptions(options, "official-api")
2367
+ });
2368
+ }
2369
+ }
2370
+
2371
+ class MyFitnessPalHealthProvider extends BaseHealthProvider {
2372
+ constructor(options) {
2373
+ super({
2374
+ providerKey: "health.myfitnesspal",
2375
+ ...createProviderOptions(options, "official-api")
2376
+ });
2377
+ }
2378
+ }
2379
+
2380
+ class EightSleepHealthProvider extends BaseHealthProvider {
2381
+ constructor(options) {
2382
+ super({
2383
+ providerKey: "health.eightsleep",
2384
+ ...createProviderOptions(options, "official-api")
2385
+ });
2386
+ }
2387
+ }
2388
+
2389
+ class PelotonHealthProvider extends BaseHealthProvider {
2390
+ constructor(options) {
2391
+ super({
2392
+ providerKey: "health.peloton",
2393
+ ...createProviderOptions(options, "official-api")
2394
+ });
2395
+ }
2396
+ }
2397
+
2398
+ class UnofficialHealthAutomationProvider extends BaseHealthProvider {
2399
+ constructor(options) {
2400
+ super({
2401
+ ...createProviderOptions(options, "unofficial"),
2402
+ providerKey: options.providerKey
2403
+ });
2404
+ }
2405
+ }
2406
+
2407
+ // src/impls/health-provider-factory.ts
2408
+ import {
2409
+ isUnofficialHealthProviderAllowed,
2410
+ resolveHealthStrategyOrder
2411
+ } from "@contractspec/integration.runtime/runtime";
2412
+ function createHealthProviderFromContext(context, secrets) {
2413
+ const providerKey = context.spec.meta.key;
2414
+ const config = toFactoryConfig(context.config);
2415
+ const strategyOrder = buildStrategyOrder(config);
2416
+ const errors = [];
2417
+ for (const strategy of strategyOrder) {
2418
+ const provider = createHealthProviderForStrategy(providerKey, strategy, config, secrets);
2419
+ if (provider) {
2420
+ return provider;
2421
+ }
2422
+ errors.push(`${strategy}: not available`);
2423
+ }
2424
+ throw new Error(`Unable to resolve health provider for ${providerKey}. Strategies attempted: ${errors.join(", ")}.`);
2425
+ }
2426
+ function createHealthProviderForStrategy(providerKey, strategy, config, secrets) {
2427
+ const options = {
2428
+ transport: strategy,
2429
+ apiBaseUrl: config.apiBaseUrl,
2430
+ mcpUrl: config.mcpUrl,
2431
+ apiKey: getSecretString(secrets, "apiKey"),
2432
+ accessToken: getSecretString(secrets, "accessToken"),
2433
+ mcpAccessToken: getSecretString(secrets, "mcpAccessToken"),
2434
+ webhookSecret: getSecretString(secrets, "webhookSecret")
2435
+ };
2436
+ if (strategy === "aggregator-api" || strategy === "aggregator-mcp") {
2437
+ return new OpenWearablesHealthProvider(options);
2438
+ }
2439
+ if (strategy === "unofficial") {
2440
+ if (!isUnofficialHealthProviderAllowed(providerKey, config)) {
2441
+ return;
2442
+ }
2443
+ if (providerKey !== "health.myfitnesspal" && providerKey !== "health.eightsleep" && providerKey !== "health.peloton" && providerKey !== "health.garmin") {
2444
+ return;
2445
+ }
2446
+ return new UnofficialHealthAutomationProvider({
2447
+ ...options,
2448
+ providerKey
2449
+ });
2450
+ }
2451
+ if (strategy === "official-mcp") {
2452
+ return createOfficialProvider(providerKey, {
2453
+ ...options,
2454
+ transport: "official-mcp"
2455
+ });
2456
+ }
2457
+ return createOfficialProvider(providerKey, options);
2458
+ }
2459
+ function createOfficialProvider(providerKey, options) {
2460
+ switch (providerKey) {
2461
+ case "health.openwearables":
2462
+ return new OpenWearablesHealthProvider(options);
2463
+ case "health.whoop":
2464
+ return new WhoopHealthProvider(options);
2465
+ case "health.apple-health":
2466
+ return new AppleHealthBridgeProvider(options);
2467
+ case "health.oura":
2468
+ return new OuraHealthProvider(options);
2469
+ case "health.strava":
2470
+ return new StravaHealthProvider(options);
2471
+ case "health.garmin":
2472
+ return new GarminHealthProvider(options);
2473
+ case "health.fitbit":
2474
+ return new FitbitHealthProvider(options);
2475
+ case "health.myfitnesspal":
2476
+ return new MyFitnessPalHealthProvider(options);
2477
+ case "health.eightsleep":
2478
+ return new EightSleepHealthProvider(options);
2479
+ case "health.peloton":
2480
+ return new PelotonHealthProvider(options);
2481
+ default:
2482
+ throw new Error(`Unsupported health provider key: ${providerKey}`);
2483
+ }
2484
+ }
2485
+ function toFactoryConfig(config) {
2486
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
2487
+ return {};
2488
+ }
2489
+ const record = config;
2490
+ return {
2491
+ apiBaseUrl: asString(record.apiBaseUrl),
2492
+ mcpUrl: asString(record.mcpUrl),
2493
+ defaultTransport: normalizeTransport(record.defaultTransport),
2494
+ strategyOrder: normalizeTransportArray(record.strategyOrder),
2495
+ allowUnofficial: typeof record.allowUnofficial === "boolean" ? record.allowUnofficial : false,
2496
+ unofficialAllowList: Array.isArray(record.unofficialAllowList) ? record.unofficialAllowList.map((item) => typeof item === "string" ? item : undefined).filter((item) => Boolean(item)) : undefined
2497
+ };
2498
+ }
2499
+ function buildStrategyOrder(config) {
2500
+ const order = resolveHealthStrategyOrder(config);
2501
+ if (!config.defaultTransport) {
2502
+ return order;
2503
+ }
2504
+ const withoutDefault = order.filter((item) => item !== config.defaultTransport);
2505
+ return [config.defaultTransport, ...withoutDefault];
2506
+ }
2507
+ function normalizeTransport(value) {
2508
+ if (typeof value !== "string")
2509
+ return;
2510
+ if (value === "official-api" || value === "official-mcp" || value === "aggregator-api" || value === "aggregator-mcp" || value === "unofficial") {
2511
+ return value;
2512
+ }
2513
+ return;
2514
+ }
2515
+ function normalizeTransportArray(value) {
2516
+ if (!Array.isArray(value))
2517
+ return;
2518
+ const transports = value.map((item) => normalizeTransport(item)).filter((item) => Boolean(item));
2519
+ return transports.length > 0 ? transports : undefined;
2520
+ }
2521
+ function getSecretString(secrets, key) {
2522
+ const value = secrets[key];
2523
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
2524
+ }
2525
+ function asString(value) {
2526
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
2527
+ }
2528
+
2034
2529
  // src/impls/mistral-llm.ts
2035
2530
  import { Mistral } from "@mistralai/mistralai";
2036
2531
 
@@ -3404,7 +3899,7 @@ function mapStatus(status) {
3404
3899
  }
3405
3900
 
3406
3901
  // src/impls/powens-client.ts
3407
- import { URL } from "url";
3902
+ import { URL as URL2 } from "url";
3408
3903
  var POWENS_BASE_URL = {
3409
3904
  sandbox: "https://api-sandbox.powens.com/v2",
3410
3905
  production: "https://api.powens.com/v2"
@@ -3490,7 +3985,7 @@ class PowensClient {
3490
3985
  });
3491
3986
  }
3492
3987
  async request(options) {
3493
- const url = new URL(options.path, this.baseUrl);
3988
+ const url = new URL2(options.path, this.baseUrl);
3494
3989
  if (options.searchParams) {
3495
3990
  for (const [key, value] of Object.entries(options.searchParams)) {
3496
3991
  if (value === undefined || value === null)
@@ -3560,7 +4055,7 @@ class PowensClient {
3560
4055
  return this.token.accessToken;
3561
4056
  }
3562
4057
  async fetchAccessToken() {
3563
- const url = new URL("/oauth/token", this.baseUrl);
4058
+ const url = new URL2("/oauth/token", this.baseUrl);
3564
4059
  const basicAuth = Buffer.from(`${this.clientId}:${this.clientSecret}`, "utf-8").toString("base64");
3565
4060
  const response = await this.fetchImpl(url, {
3566
4061
  method: "POST",
@@ -4604,6 +5099,10 @@ class IntegrationProviderFactory {
4604
5099
  throw new Error(`Unsupported open banking integration: ${context.spec.meta.key}`);
4605
5100
  }
4606
5101
  }
5102
+ async createHealthProvider(context) {
5103
+ const secrets = await this.loadSecrets(context);
5104
+ return createHealthProviderFromContext(context, secrets);
5105
+ }
4607
5106
  async loadSecrets(context) {
4608
5107
  const cacheKey = context.connection.meta.id;
4609
5108
  if (SECRET_CACHE.has(cacheKey)) {
@@ -4675,11 +5174,15 @@ export * from "@contractspec/lib.contracts-integrations";
4675
5174
  // src/meeting-recorder.ts
4676
5175
  export * from "@contractspec/lib.contracts-integrations";
4677
5176
  export {
5177
+ createHealthProviderFromContext,
5178
+ WhoopHealthProvider,
5179
+ UnofficialHealthAutomationProvider,
4678
5180
  TwilioSmsProvider,
4679
5181
  TldvMeetingRecorderProvider,
4680
5182
  SupabaseVectorProvider,
4681
5183
  SupabasePostgresProvider,
4682
5184
  StripePaymentsProvider,
5185
+ StravaHealthProvider,
4683
5186
  QdrantVectorProvider,
4684
5187
  PowensOpenBankingProvider,
4685
5188
  PowensClientError,
@@ -4687,7 +5190,11 @@ export {
4687
5190
  PostmarkEmailProvider,
4688
5191
  PosthogAnalyticsReader,
4689
5192
  PosthogAnalyticsProvider,
5193
+ PelotonHealthProvider,
5194
+ OuraHealthProvider,
5195
+ OpenWearablesHealthProvider,
4690
5196
  NotionProjectManagementProvider,
5197
+ MyFitnessPalHealthProvider,
4691
5198
  MistralLLMProvider,
4692
5199
  MistralEmbeddingProvider,
4693
5200
  LinearProjectManagementProvider,
@@ -4699,8 +5206,12 @@ export {
4699
5206
  GoogleCalendarProvider,
4700
5207
  GmailOutboundProvider,
4701
5208
  GmailInboundProvider,
5209
+ GarminHealthProvider,
5210
+ FitbitHealthProvider,
4702
5211
  FirefliesMeetingRecorderProvider,
4703
5212
  FathomMeetingRecorderProvider,
4704
5213
  FalVoiceProvider,
4705
- ElevenLabsVoiceProvider
5214
+ ElevenLabsVoiceProvider,
5215
+ EightSleepHealthProvider,
5216
+ AppleHealthBridgeProvider
4706
5217
  };
@@ -0,0 +1,2 @@
1
+ // src/health.ts
2
+ export * from "@contractspec/lib.contracts-integrations";