diff --git a/.changeset/heavy-readers-accept.md b/.changeset/heavy-readers-accept.md new file mode 100644 index 0000000000..ffd66e56a0 --- /dev/null +++ b/.changeset/heavy-readers-accept.md @@ -0,0 +1,5 @@ +--- +'@chainlink/infralabs-adapter': major +--- + +First build with support for API authentication, signature validation, and KMS integration diff --git a/.yarn/cache/@aws-sdk-client-kms-npm-3.1071.0-c9a5e077ad-f5a3670815.zip b/.yarn/cache/@aws-sdk-client-kms-npm-3.1071.0-c9a5e077ad-f5a3670815.zip new file mode 100644 index 0000000000..d141c9bf42 Binary files /dev/null and b/.yarn/cache/@aws-sdk-client-kms-npm-3.1071.0-c9a5e077ad-f5a3670815.zip differ diff --git a/.yarn/cache/@aws-sdk-core-npm-3.974.22-f3d714718c-71449cdcf5.zip b/.yarn/cache/@aws-sdk-core-npm-3.974.22-f3d714718c-71449cdcf5.zip new file mode 100644 index 0000000000..edc284362f Binary files /dev/null and b/.yarn/cache/@aws-sdk-core-npm-3.974.22-f3d714718c-71449cdcf5.zip differ diff --git a/.yarn/cache/@aws-sdk-credential-provider-env-npm-3.972.48-6ed63e95d8-6e0c5491d8.zip b/.yarn/cache/@aws-sdk-credential-provider-env-npm-3.972.48-6ed63e95d8-6e0c5491d8.zip new file mode 100644 index 0000000000..90ce992b7c Binary files /dev/null and b/.yarn/cache/@aws-sdk-credential-provider-env-npm-3.972.48-6ed63e95d8-6e0c5491d8.zip differ diff --git a/.yarn/cache/@aws-sdk-credential-provider-http-npm-3.972.50-7061d7b515-b87a873b87.zip b/.yarn/cache/@aws-sdk-credential-provider-http-npm-3.972.50-7061d7b515-b87a873b87.zip new file mode 100644 index 0000000000..9ad315395e Binary files /dev/null and b/.yarn/cache/@aws-sdk-credential-provider-http-npm-3.972.50-7061d7b515-b87a873b87.zip differ diff --git a/.yarn/cache/@aws-sdk-credential-provider-ini-npm-3.972.55-38443928ef-bfafed3c8b.zip b/.yarn/cache/@aws-sdk-credential-provider-ini-npm-3.972.55-38443928ef-bfafed3c8b.zip new file mode 100644 index 0000000000..0c16116f13 Binary files /dev/null and b/.yarn/cache/@aws-sdk-credential-provider-ini-npm-3.972.55-38443928ef-bfafed3c8b.zip differ diff --git a/.yarn/cache/@aws-sdk-credential-provider-login-npm-3.972.54-99c5309c4d-e83ca8ef77.zip b/.yarn/cache/@aws-sdk-credential-provider-login-npm-3.972.54-99c5309c4d-e83ca8ef77.zip new file mode 100644 index 0000000000..e9d0ac307b Binary files /dev/null and b/.yarn/cache/@aws-sdk-credential-provider-login-npm-3.972.54-99c5309c4d-e83ca8ef77.zip differ diff --git a/.yarn/cache/@aws-sdk-credential-provider-node-npm-3.972.57-8221cf817e-02cd5767f2.zip b/.yarn/cache/@aws-sdk-credential-provider-node-npm-3.972.57-8221cf817e-02cd5767f2.zip new file mode 100644 index 0000000000..b0691620e6 Binary files /dev/null and b/.yarn/cache/@aws-sdk-credential-provider-node-npm-3.972.57-8221cf817e-02cd5767f2.zip differ diff --git a/.yarn/cache/@aws-sdk-credential-provider-process-npm-3.972.48-da60b257dd-7a48a1d4c0.zip b/.yarn/cache/@aws-sdk-credential-provider-process-npm-3.972.48-da60b257dd-7a48a1d4c0.zip new file mode 100644 index 0000000000..31ba792a14 Binary files /dev/null and b/.yarn/cache/@aws-sdk-credential-provider-process-npm-3.972.48-da60b257dd-7a48a1d4c0.zip differ diff --git a/.yarn/cache/@aws-sdk-credential-provider-sso-npm-3.972.54-14bc44903d-a313362308.zip b/.yarn/cache/@aws-sdk-credential-provider-sso-npm-3.972.54-14bc44903d-a313362308.zip new file mode 100644 index 0000000000..9412686377 Binary files /dev/null and b/.yarn/cache/@aws-sdk-credential-provider-sso-npm-3.972.54-14bc44903d-a313362308.zip differ diff --git a/.yarn/cache/@aws-sdk-credential-provider-web-identity-npm-3.972.54-63c0f2f18d-b876cde1b5.zip b/.yarn/cache/@aws-sdk-credential-provider-web-identity-npm-3.972.54-63c0f2f18d-b876cde1b5.zip new file mode 100644 index 0000000000..a5fe6f4ed6 Binary files /dev/null and b/.yarn/cache/@aws-sdk-credential-provider-web-identity-npm-3.972.54-63c0f2f18d-b876cde1b5.zip differ diff --git a/.yarn/cache/@aws-sdk-nested-clients-npm-3.997.22-4b246aca16-138ea0da37.zip b/.yarn/cache/@aws-sdk-nested-clients-npm-3.997.22-4b246aca16-138ea0da37.zip new file mode 100644 index 0000000000..49e52c1c14 Binary files /dev/null and b/.yarn/cache/@aws-sdk-nested-clients-npm-3.997.22-4b246aca16-138ea0da37.zip differ diff --git a/.yarn/cache/@aws-sdk-token-providers-npm-3.1071.0-4a0661daa2-846b924f1b.zip b/.yarn/cache/@aws-sdk-token-providers-npm-3.1071.0-4a0661daa2-846b924f1b.zip new file mode 100644 index 0000000000..e2a8ba7cf1 Binary files /dev/null and b/.yarn/cache/@aws-sdk-token-providers-npm-3.1071.0-4a0661daa2-846b924f1b.zip differ diff --git a/packages/sources/infralabs/README.md b/packages/sources/infralabs/README.md new file mode 100644 index 0000000000..3013f4dea6 --- /dev/null +++ b/packages/sources/infralabs/README.md @@ -0,0 +1,57 @@ +# INFRALABS + +![1.0.0](https://img.shields.io/github/package-json/v/smartcontractkit/external-adapters-js?filename=packages/sources/infralabs/package.json) ![v3](https://img.shields.io/badge/framework%20version-v3-blueviolet) + +This document was generated automatically. Please see [README Generator](../../scripts#readme-generator) for more info. + +## Environment Variables + +| Required? | Name | Description | Type | Options | Default | +| :-------: | :-----------------------: | :---------------------------------------------------------------------------: | :-----: | :-----: | :--------------------------------------------------------: | +| ✅ | API_KEY | Infralabs API key (shared across all endpoints) | string | | | +| | USHP_API_ENDPOINT | Infralabs USHP index API URL | string | | `https://ushp-index-interface.staging.infralabs.xyz/index` | +| | USHP_MAX_STALENESS_SECS | Maximum age in seconds for the USHP index value before it is considered stale | number | | `3600000` | +| | BACKGROUND_EXECUTE_MS | Milliseconds between background data refreshes | number | | `10000` | +| | KMS_KEY_TTL_MS | Milliseconds before a cached KMS public key is considered expired | number | | `60000` | +| | KMS_REGION | AWS region where the Infralabs KMS key is hosted | string | | `us-east-1` | +| ✅ | AWS_ACCESS_KEY_ID | AWS access key ID for KMS authentication | string | | | +| ✅ | AWS_SECRET_ACCESS_KEY | AWS secret access key for KMS authentication | string | | | +| | KMS_VERIFICATION_DISABLED | Disable KMS signature verification | boolean | | `true` | + +--- + +## Data Provider Rate Limits + +There are no rate limits for this adapter. + +--- + +## Input Parameters + +| Required? | Name | Description | Type | Options | Default | +| :-------: | :------: | :-----------------: | :----: | :--------------------: | :-----: | +| | endpoint | The endpoint to use | string | [ushp](#ushp-endpoint) | `ushp` | + +## Ushp Endpoint + +`ushp` is the only supported name for this endpoint. + +### Input Params + +There are no input parameters for this endpoint. + +### Example + +Request: + +```json +{ + "data": { + "endpoint": "ushp" + } +} +``` + +--- + +MIT License diff --git a/packages/sources/infralabs/package.json b/packages/sources/infralabs/package.json new file mode 100644 index 0000000000..b9d493721a --- /dev/null +++ b/packages/sources/infralabs/package.json @@ -0,0 +1,41 @@ +{ + "name": "@chainlink/infralabs-adapter", + "version": "1.0.0", + "description": "Chainlink external adapter for Infralabs indices", + "keywords": [ + "Chainlink", + "LINK", + "blockchain", + "oracle", + "infralabs" + ], + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "repository": { + "url": "https://github.com/smartcontractkit/external-adapters-js", + "type": "git" + }, + "license": "MIT", + "scripts": { + "clean": "rm -rf dist && rm -f tsconfig.tsbuildinfo", + "prepack": "yarn build", + "build": "tsc -b", + "server": "node -e 'require(\"./index.js\").server()'", + "server:dist": "node -e 'require(\"./dist/index.js\").server()'", + "start": "yarn server:dist" + }, + "devDependencies": { + "@types/jest": "29.5.14", + "@types/node": "22.14.1", + "nock": "13.5.6", + "typescript": "5.8.3" + }, + "dependencies": { + "@aws-sdk/client-kms": "^3.1071.0", + "@chainlink/external-adapter-framework": "2.17.1", + "tslib": "2.6.3" + } +} diff --git a/packages/sources/infralabs/src/config/index.ts b/packages/sources/infralabs/src/config/index.ts new file mode 100644 index 0000000000..eb9fcf6af1 --- /dev/null +++ b/packages/sources/infralabs/src/config/index.ts @@ -0,0 +1,55 @@ +import { AdapterConfig } from '@chainlink/external-adapter-framework/config' + +// TODO change to prod default once ready +export const STAGING_USHP_API_ENDPOINT = 'https://ushp-index-interface.staging.infralabs.xyz/index' + +export const config = new AdapterConfig({ + API_KEY: { + description: 'Infralabs API key (shared across all endpoints)', + type: 'string', + required: true, + sensitive: true, + }, + USHP_API_ENDPOINT: { + description: 'Infralabs USHP index API URL', + type: 'string', + default: STAGING_USHP_API_ENDPOINT, + }, + USHP_MAX_STALENESS_SECS: { + description: 'Maximum age in seconds for the USHP index value before it is considered stale', + type: 'number', + default: 3_600_000, + }, + BACKGROUND_EXECUTE_MS: { + description: 'Milliseconds between background data refreshes', + type: 'number', + default: 10_000, + }, + KMS_KEY_TTL_MS: { + description: 'Milliseconds before a cached KMS public key is considered expired', + type: 'number', + default: 60_000, + }, + KMS_REGION: { + description: 'AWS region where the Infralabs KMS key is hosted', + type: 'string', + default: 'us-east-1', + }, + AWS_ACCESS_KEY_ID: { + description: 'AWS access key ID for KMS authentication', + type: 'string', + required: true, + sensitive: true, + }, + AWS_SECRET_ACCESS_KEY: { + description: 'AWS secret access key for KMS authentication', + type: 'string', + required: true, + sensitive: true, + }, + KMS_VERIFICATION_DISABLED: { + description: 'Disable KMS signature verification', + type: 'boolean', + default: true, + }, +}) diff --git a/packages/sources/infralabs/src/endpoint/index.ts b/packages/sources/infralabs/src/endpoint/index.ts new file mode 100644 index 0000000000..5e38bf42ff --- /dev/null +++ b/packages/sources/infralabs/src/endpoint/index.ts @@ -0,0 +1 @@ +export { ushpEndpoint as ushp } from './ushp' diff --git a/packages/sources/infralabs/src/endpoint/types.ts b/packages/sources/infralabs/src/endpoint/types.ts new file mode 100644 index 0000000000..1290be90da --- /dev/null +++ b/packages/sources/infralabs/src/endpoint/types.ts @@ -0,0 +1,19 @@ +import { InputParameters } from '@chainlink/external-adapter-framework/validation' +import { config } from '../config' + +export const inputParameters = new InputParameters({}) + +export type BaseEndpointTypes = { + Parameters: typeof inputParameters.definition + Settings: typeof config.settings + Response: { + Result: string + Data: { + price: number + rawValue: string + scale: number + lastUpdatedAt: number + signature: string + } + } +} diff --git a/packages/sources/infralabs/src/endpoint/ushp.ts b/packages/sources/infralabs/src/endpoint/ushp.ts new file mode 100644 index 0000000000..a15fa04659 --- /dev/null +++ b/packages/sources/infralabs/src/endpoint/ushp.ts @@ -0,0 +1,9 @@ +import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter' +import { ushpTransport } from '../transport/ushp' +import { BaseEndpointTypes, inputParameters } from './types' + +export const ushpEndpoint = new AdapterEndpoint({ + name: 'ushp', + transport: ushpTransport, + inputParameters, +}) diff --git a/packages/sources/infralabs/src/index.ts b/packages/sources/infralabs/src/index.ts new file mode 100644 index 0000000000..f1b62287d3 --- /dev/null +++ b/packages/sources/infralabs/src/index.ts @@ -0,0 +1,13 @@ +import { expose, ServerInstance } from '@chainlink/external-adapter-framework' +import { Adapter } from '@chainlink/external-adapter-framework/adapter' +import { config } from './config' +import { ushp } from './endpoint' + +export const adapter = new Adapter({ + defaultEndpoint: ushp.name, + name: 'INFRALABS', + config, + endpoints: [ushp], +}) + +export const server = (): Promise => expose(adapter) diff --git a/packages/sources/infralabs/src/transport/infralabs.ts b/packages/sources/infralabs/src/transport/infralabs.ts new file mode 100644 index 0000000000..61060c179f --- /dev/null +++ b/packages/sources/infralabs/src/transport/infralabs.ts @@ -0,0 +1,197 @@ +import { GetPublicKeyCommand, KMSClient } from '@aws-sdk/client-kms' +import { EndpointContext } from '@chainlink/external-adapter-framework/adapter' +import { TransportDependencies } from '@chainlink/external-adapter-framework/transports' +import { SubscriptionTransport } from '@chainlink/external-adapter-framework/transports/abstract/subscription' +import { AdapterResponse, makeLogger, sleep } from '@chainlink/external-adapter-framework/util' +import { Requester } from '@chainlink/external-adapter-framework/util/requester' +import * as crypto from 'crypto' +import { BaseEndpointTypes, inputParameters } from '../endpoint/types' +import { isFresh, isSaneSignature, rescale } from './utils' + +const logger = makeLogger('InfralabsTransport') + +type RequestParams = typeof inputParameters.validated + +interface InfralabsResponse { + value: string + scale: string + timestamp: string + signature: string + key_id: string +} + +interface KmsCacheEntry { + key: crypto.KeyObject + derBytes: Buffer + fetchedAt: number +} + +export class InfralabsTransport extends SubscriptionTransport { + settings!: BaseEndpointTypes['Settings'] + requester!: Requester + kmsClient!: KMSClient + private apiEndpoint!: string + private maxStaleness!: number + private kmsKeyCache = new Map() + private kmsKeyTtlMs!: number + + constructor( + private readonly apiEndpointFn: (s: BaseEndpointTypes['Settings']) => string, + private readonly maxStalenessFn: (s: BaseEndpointTypes['Settings']) => number, + ) { + super() + } + + async initialize( + dependencies: TransportDependencies, + adapterSettings: BaseEndpointTypes['Settings'], + endpointName: string, + transportName: string, + ): Promise { + await super.initialize(dependencies, adapterSettings, endpointName, transportName) + this.settings = adapterSettings + this.requester = dependencies.requester + this.kmsClient = new KMSClient({ + region: adapterSettings.KMS_REGION, + credentials: { + accessKeyId: adapterSettings.AWS_ACCESS_KEY_ID, + secretAccessKey: adapterSettings.AWS_SECRET_ACCESS_KEY, + }, + }) + this.kmsKeyTtlMs = adapterSettings.KMS_KEY_TTL_MS + this.apiEndpoint = this.apiEndpointFn(adapterSettings) + this.maxStaleness = this.maxStalenessFn(adapterSettings) + } + + async backgroundHandler( + context: EndpointContext, + entries: RequestParams[], + ): Promise { + await Promise.all(entries.map((param) => this.handleRequest(param))) + await sleep(context.adapterSettings.BACKGROUND_EXECUTE_MS) + } + + async handleRequest(param: RequestParams): Promise { + let response: AdapterResponse + try { + response = await this._handleRequest() + } catch (e) { + const errorMessage = e instanceof Error ? e.message : 'Unknown error occurred' + logger.error(e, errorMessage) + response = { + statusCode: 502, + errorMessage, + timestamps: { + providerDataRequestedUnixMs: 0, + providerDataReceivedUnixMs: 0, + providerIndicatedTimeUnixMs: undefined, + }, + } + } + await this.responseCache.write(this.name, [{ params: param, response }]) + } + + async _handleRequest(): Promise> { + const providerDataRequestedUnixMs = Date.now() + + // Fetch raw response body as text to preserve exact bytes for sig verification + const requestConfig = { + url: this.apiEndpoint, + method: 'GET' as const, + headers: { Authorization: `ApiKey ${this.settings.API_KEY}` }, + responseType: 'text' as const, + } + const sourceResponse = await this.requester.request( + JSON.stringify(requestConfig), + requestConfig, + ) + + const rawResponseBody = sourceResponse.response.data + const responseBody = JSON.parse(rawResponseBody) as InfralabsResponse + const signature = responseBody.signature + + if (!this.settings.KMS_VERIFICATION_DISABLED) { + const publicKey = await this.getKmsPublicKey(responseBody.key_id) + + if (!isSaneSignature(rawResponseBody, publicKey, signature)) { + // Verification failed with the cached key - the provider may have rotated their KMS key. + // Fetch the latest key from AWS: if it's the same key, the signature is genuinely invalid; + // if it's a new key, try verification once more with the rotated key before giving up. + const { key: refreshedKey, isNewKey } = await this.tryRefreshKmsKey(responseBody.key_id) + if (!isNewKey) throw new Error('Signature verification failed') + + if (!isSaneSignature(rawResponseBody, refreshedKey, signature)) + throw new Error('Signature verification failed') + } + } + + if (!isFresh(responseBody.timestamp, this.maxStaleness, Date.now())) + throw new Error('Price is stale') + + const scale = parseInt(responseBody.scale) + const result = rescale(responseBody.value, scale) + + return { + result: result.toString(), + data: { + price: Number(result) / 10 ** 8, + rawValue: responseBody.value, + scale, + lastUpdatedAt: parseInt(responseBody.timestamp), + signature: signature, + }, + statusCode: 200, + timestamps: { + providerDataRequestedUnixMs, + providerDataReceivedUnixMs: Date.now(), + providerIndicatedTimeUnixMs: parseInt(responseBody.timestamp) * 1000, + }, + } + } + + /** Fetches the public key for `keyId` directly from AWS KMS and returns it alongside its raw DER + * bytes, which are used for cache-equality checks in `tryRefreshKmsKey`. */ + private async fetchKmsKeyFromAws( + keyId: string, + ): Promise<{ key: crypto.KeyObject; derBytes: Buffer }> { + const kmsResponse = await this.kmsClient.send(new GetPublicKeyCommand({ KeyId: keyId })) + if (!kmsResponse.PublicKey) { + throw new Error(`KMS did not return a public key for key ID: ${keyId}`) + } + + const derBytes = Buffer.from(kmsResponse.PublicKey) + const key = crypto.createPublicKey({ key: derBytes, format: 'der', type: 'spki' }) + return { key, derBytes } + } + + /** Returns the cached public key for `keyId` if it was fetched within `kmsKeyTtlMs`; otherwise + * fetches a fresh copy from AWS and updates the cache. */ + private async getKmsPublicKey(keyId: string): Promise { + const cached = this.kmsKeyCache.get(keyId) + if (cached && Date.now() - cached.fetchedAt < this.kmsKeyTtlMs) return cached.key + + // Always re-populate cache if key is expired + const { key, derBytes } = await this.fetchKmsKeyFromAws(keyId) + this.kmsKeyCache.set(keyId, { key, derBytes, fetchedAt: Date.now() }) + + return key + } + + /** Fetches the current key from AWS and compares it to the cached copy. Returns the fresh key and + * whether it differs from what was cached — callers use `isNewKey` to distinguish a genuine + * signature failure from a key rotation that invalidated the cache. */ + private async tryRefreshKmsKey( + keyId: string, + ): Promise<{ key: crypto.KeyObject; isNewKey: boolean }> { + const cached = this.kmsKeyCache.get(keyId) + const { key, derBytes } = await this.fetchKmsKeyFromAws(keyId) + if (cached && cached.derBytes.equals(derBytes)) return { key: cached.key, isNewKey: false } + this.kmsKeyCache.set(keyId, { key, derBytes, fetchedAt: Date.now() }) + + return { key, isNewKey: true } + } + + getSubscriptionTtlFromConfig(adapterSettings: BaseEndpointTypes['Settings']): number { + return adapterSettings.WARMUP_SUBSCRIPTION_TTL + } +} diff --git a/packages/sources/infralabs/src/transport/ushp.ts b/packages/sources/infralabs/src/transport/ushp.ts new file mode 100644 index 0000000000..f6ed90a69a --- /dev/null +++ b/packages/sources/infralabs/src/transport/ushp.ts @@ -0,0 +1,6 @@ +import { InfralabsTransport } from './infralabs' + +export const ushpTransport = new InfralabsTransport( + (s) => s.USHP_API_ENDPOINT, + (s) => s.USHP_MAX_STALENESS_SECS, +) diff --git a/packages/sources/infralabs/src/transport/utils.ts b/packages/sources/infralabs/src/transport/utils.ts new file mode 100644 index 0000000000..79ef157ffb --- /dev/null +++ b/packages/sources/infralabs/src/transport/utils.ts @@ -0,0 +1,50 @@ +import * as crypto from 'crypto' + +const OUTPUT_DECIMALS = 8 + +/** + * Strips the "signature" field from the raw JSON response body, preserving the + * exact remaining bytes. Using string manipulation rather than parse→stringify + * avoids any serialisation mismatch between Python's json.dumps (which they sign with) and JS JSON.stringify (compact). + */ +export function buildUnsignedMessage(rawBody: string): string { + return rawBody.replace(/,?\s*"signature"\s*:\s*"[^"]*"/, '') +} + +/** Verifies that `signature` (base64) over the unsigned body (raw JSON minus the "signature" field) matches `publicKey` using SHA-256. */ +export function isSaneSignature( + rawResponseBody: string, + publicKey: crypto.KeyObject, + signature: string, +): boolean { + const unsignedResponseBody = buildUnsignedMessage(rawResponseBody) + + let isSane = crypto + .createVerify('SHA256') + .update(Buffer.from(unsignedResponseBody, 'utf-8')) + .verify(publicKey, Buffer.from(signature, 'base64')) + + return isSane +} + +/** Returns true if the provider timestamp is within `maxAgeSecs` of `nowMs`. */ +export function isFresh(timestamp: string, maxAgeSecs: number, nowMs: number): boolean { + const ageSecs = Math.floor(nowMs / 1000) - parseInt(timestamp) + return ageSecs <= maxAgeSecs +} + +/** + * Rescales an integer string from `fromScale` implied decimal places to + * OUTPUT_DECIMALS (8) using BigInt arithmetic to avoid floating-point loss. + * + * Examples: + * rescale('1003968325', 9) → 100396832n (9 → 8: divide by 10) + * rescale('10039683', 8) → 10039683n (8 → 8: identity) + * rescale('1003968', 7) → 100396800n (7 → 8: multiply by 10) + */ +export function rescale(value: string, fromScale: number): bigint { + const diff = fromScale - OUTPUT_DECIMALS + if (diff > 0) return BigInt(value) / 10n ** BigInt(diff) + if (diff < 0) return BigInt(value) * 10n ** BigInt(-diff) + return BigInt(value) +} diff --git a/packages/sources/infralabs/test-payload.json b/packages/sources/infralabs/test-payload.json new file mode 100644 index 0000000000..050a3f7dd1 --- /dev/null +++ b/packages/sources/infralabs/test-payload.json @@ -0,0 +1,7 @@ +{ + "requests": [ + { + "endpoint": "ushp" + } + ] +} diff --git a/packages/sources/infralabs/test/integration/__snapshots__/adapter.test.ts.snap b/packages/sources/infralabs/test/integration/__snapshots__/adapter.test.ts.snap new file mode 100644 index 0000000000..7d9e9ca9d8 --- /dev/null +++ b/packages/sources/infralabs/test/integration/__snapshots__/adapter.test.ts.snap @@ -0,0 +1,63 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`execute KMS key caching should return 502 and call KMS twice when signature is invalid and AWS returns the same key 1`] = ` +{ + "errorMessage": "Signature verification failed", + "statusCode": 502, + "timestamps": { + "providerDataReceivedUnixMs": 0, + "providerDataRequestedUnixMs": 0, + }, +} +`; + +exports[`execute ushp endpoint should return 502 when KMS cannot return the public key 1`] = ` +{ + "errorMessage": "KMS unavailable for key: arn:aws:kms:[KMS_REGION REDACTED]:123456789012:key/unavailable-key-id", + "statusCode": 502, + "timestamps": { + "providerDataReceivedUnixMs": 0, + "providerDataRequestedUnixMs": 0, + }, +} +`; + +exports[`execute ushp endpoint should return 502 when the price is stale 1`] = ` +{ + "errorMessage": "Price is stale", + "statusCode": 502, + "timestamps": { + "providerDataReceivedUnixMs": 0, + "providerDataRequestedUnixMs": 0, + }, +} +`; + +exports[`execute ushp endpoint should return 502 when the signature does not match the response body 1`] = ` +{ + "errorMessage": "Signature verification failed", + "statusCode": 502, + "timestamps": { + "providerDataReceivedUnixMs": 0, + "providerDataRequestedUnixMs": 0, + }, +} +`; + +exports[`execute ushp endpoint should return a rescaled result for a valid response 1`] = ` +{ + "data": { + "lastUpdatedAt": 1704103871, + "price": 1.00396832, + "rawValue": "1003968325", + "scale": 9, + }, + "result": "100396832", + "statusCode": 200, + "timestamps": { + "providerDataReceivedUnixMs": 1704107471111, + "providerDataRequestedUnixMs": 1704107471111, + "providerIndicatedTimeUnixMs": 1704103871000, + }, +} +`; diff --git a/packages/sources/infralabs/test/integration/adapter.test.ts b/packages/sources/infralabs/test/integration/adapter.test.ts new file mode 100644 index 0000000000..1c1e3cdaf2 --- /dev/null +++ b/packages/sources/infralabs/test/integration/adapter.test.ts @@ -0,0 +1,213 @@ +import { + setEnvVariables, + TestAdapter, +} from '@chainlink/external-adapter-framework/util/testing-utils' +import * as crypto from 'crypto' +import nock from 'nock' +import { createFixtures, MOCK_DATE, TEST_KEY_ID } from './fixtures' + +const TEST_API_HOST = 'http://test-infralabs.local' +const TEST_API_PATH = '/index' + +const mockKmsSend = jest.fn() + +jest.mock('@aws-sdk/client-kms', () => ({ + ...jest.requireActual('@aws-sdk/client-kms'), + KMSClient: jest.fn().mockImplementation(() => ({ + send: mockKmsSend, + })), +})) + +describe('execute', () => { + let spy: jest.SpyInstance + let testAdapter: TestAdapter + let oldEnv: NodeJS.ProcessEnv + let fixtures: ReturnType + let publicKeyDer: Buffer + + jest.setTimeout(10000) + + beforeAll(async () => { + oldEnv = JSON.parse(JSON.stringify(process.env)) + + const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }) + publicKeyDer = publicKey.export({ format: 'der', type: 'spki' }) as Buffer + + fixtures = createFixtures(privateKey) + + mockKmsSend.mockImplementation( + async (command: { constructor: { name: string }; input: { KeyId: string } }) => { + if (command.constructor.name === 'GetPublicKeyCommand') { + if (command.input.KeyId === TEST_KEY_ID) { + return { PublicKey: new Uint8Array(publicKeyDer) } + } + throw new Error(`KMS unavailable for key: ${command.input.KeyId}`) + } + }, + ) + + process.env['API_KEY'] = 'test-api-key' + process.env['USHP_API_ENDPOINT'] = `${TEST_API_HOST}${TEST_API_PATH}` + process.env['USHP_MAX_STALENESS_SECS'] = '90000' + process.env['BACKGROUND_EXECUTE_MS'] = '1000' + process.env['KMS_KEY_TTL_MS'] = '60000' + process.env['KMS_REGION'] = 'us-east-1' + process.env['AWS_ACCESS_KEY_ID'] = 'test-access-key-id' + process.env['AWS_SECRET_ACCESS_KEY'] = 'test-secret-access-key' + process.env['KMS_VERIFICATION_DISABLED'] = 'false' + process.env['METRICS_ENABLED'] = 'false' + + spy = jest.spyOn(Date, 'now').mockReturnValue(MOCK_DATE.getTime()) + + const adapter = (await import('./../../src')).adapter + adapter.rateLimiting = undefined + testAdapter = await TestAdapter.startWithMockedCache(adapter, { + testAdapter: {} as TestAdapter, + }) + }) + + afterAll(async () => { + setEnvVariables(oldEnv) + await testAdapter.api.close() + spy.mockRestore() + nock.cleanAll() + jest.clearAllMocks() + }) + + beforeEach(() => { + nock.cleanAll() + testAdapter.mockCache?.cache.clear() + }) + + describe('ushp endpoint', () => { + it('should return a rescaled result for a valid response', async () => { + nock(TEST_API_HOST) + .get(TEST_API_PATH) + .matchHeader('authorization', 'ApiKey test-api-key') + .reply(200, fixtures.success) + + const response = await testAdapter.request({ endpoint: 'ushp' }) + const json = response.json() + + expect(response.statusCode).toBe(200) + expect(typeof json.data?.signature).toBe('string') + expect(json.data?.signature.length).toBeGreaterThan(0) + const { signature: _sig, ...dataWithoutSignature } = json.data + expect({ ...json, data: dataWithoutSignature }).toMatchSnapshot() + }) + + it('should return 502 when the price is stale', async () => { + nock(TEST_API_HOST).get(TEST_API_PATH).reply(200, fixtures.stale) + + const response = await testAdapter.request({ endpoint: 'ushp' }) + + expect(response.statusCode).toBe(502) + expect(response.json()).toMatchSnapshot() + }) + + it('should return 502 when the signature does not match the response body', async () => { + nock(TEST_API_HOST).get(TEST_API_PATH).reply(200, fixtures.badSig) + + const response = await testAdapter.request({ endpoint: 'ushp' }) + + expect(response.statusCode).toBe(502) + expect(response.json()).toMatchSnapshot() + }) + + it('should return 502 when KMS cannot return the public key', async () => { + nock(TEST_API_HOST).get(TEST_API_PATH).reply(200, fixtures.kmsUnavailable) + + const response = await testAdapter.request({ endpoint: 'ushp' }) + + expect(response.statusCode).toBe(502) + expect(response.json()).toMatchSnapshot() + }) + }) + + describe('KMS key caching', () => { + // Each test advances the clock by 300 s from MOCK_DATE so any key cached by a previous + // test (fetchedAt < current - 60 s) is always considered expired at the start of a new test. + let timeOffset = 0 + + beforeEach(() => { + timeOffset += 300_000 + spy.mockReturnValue(MOCK_DATE.getTime() + timeOffset) + mockKmsSend.mockClear() + // Clear the response cache AFTER updating the clock so any response pre-populated + // by the background handler at the old clock time is discarded. + testAdapter.mockCache?.cache.clear() + }) + + it('should fetch the KMS key from AWS when the cache is cold or expired', async () => { + nock(TEST_API_HOST).get(TEST_API_PATH).reply(200, fixtures.success) + + const response = await testAdapter.request({ endpoint: 'ushp' }) + + expect(response.statusCode).toBe(200) + expect(mockKmsSend).toHaveBeenCalledTimes(1) + }) + + it('should reuse the cached KMS key for subsequent requests within the TTL', async () => { + nock(TEST_API_HOST).get(TEST_API_PATH).reply(200, fixtures.success) + await testAdapter.request({ endpoint: 'ushp' }) + mockKmsSend.mockClear() + + // Same clock value — key was just cached, still within 60 s TTL + nock(TEST_API_HOST).get(TEST_API_PATH).reply(200, fixtures.success) + const response = await testAdapter.request({ endpoint: 'ushp' }) + + expect(response.statusCode).toBe(200) + expect(mockKmsSend).not.toHaveBeenCalled() + }) + + it('should re-fetch the KMS key after the 60 s TTL expires', async () => { + nock(TEST_API_HOST).get(TEST_API_PATH).reply(200, fixtures.success) + await testAdapter.request({ endpoint: 'ushp' }) + expect(mockKmsSend).toHaveBeenCalledTimes(1) + mockKmsSend.mockClear() + + // Advance another 61 s past the TTL from the current test's base time, then clear the + // response cache so the next request re-triggers the background handler and re-checks the KMS TTL. + spy.mockReturnValue(MOCK_DATE.getTime() + timeOffset + 61_000) + testAdapter.mockCache?.cache.clear() + + nock(TEST_API_HOST).get(TEST_API_PATH).reply(200, fixtures.success) + await testAdapter.request({ endpoint: 'ushp' }) + + expect(mockKmsSend).toHaveBeenCalledTimes(1) + }) + + it('should return 502 and call KMS twice when signature is invalid and AWS returns the same key', async () => { + nock(TEST_API_HOST).get(TEST_API_PATH).reply(200, fixtures.badSig) + + const response = await testAdapter.request({ endpoint: 'ushp' }) + + expect(response.statusCode).toBe(502) + // One call for getKmsPublicKey (cache expired) + one call for tryRefreshKmsKey + expect(mockKmsSend).toHaveBeenCalledTimes(2) + expect(response.json()).toMatchSnapshot() + }) + + it('should succeed after a key rotation when the refreshed key validates the signature', async () => { + const rotatedKeys = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }) + const rotatedPublicKeyDer = rotatedKeys.publicKey.export({ + type: 'spki', + format: 'der', + }) as Buffer + const rotatedFixtures = createFixtures(rotatedKeys.privateKey) + + // First KMS call (getKmsPublicKey, cache expired) returns old key → sig check fails + // Second KMS call (tryRefreshKmsKey) returns rotated key → cache updated → sig check passes + mockKmsSend + .mockResolvedValueOnce({ PublicKey: new Uint8Array(publicKeyDer) }) + .mockResolvedValueOnce({ PublicKey: new Uint8Array(rotatedPublicKeyDer) }) + + nock(TEST_API_HOST).get(TEST_API_PATH).reply(200, rotatedFixtures.success) + + const response = await testAdapter.request({ endpoint: 'ushp' }) + + expect(response.statusCode).toBe(200) + expect(mockKmsSend).toHaveBeenCalledTimes(2) + }) + }) +}) diff --git a/packages/sources/infralabs/test/integration/fixtures.ts b/packages/sources/infralabs/test/integration/fixtures.ts new file mode 100644 index 0000000000..c28d32aa3f --- /dev/null +++ b/packages/sources/infralabs/test/integration/fixtures.ts @@ -0,0 +1,66 @@ +import * as crypto from 'crypto' + +export const TEST_KEY_ID = 'arn:aws:kms:us-east-1:123456789012:key/test-key-id' +export const UNAVAILABLE_KEY_ID = 'arn:aws:kms:us-east-1:123456789012:key/unavailable-key-id' + +// Fixed mock date: tests use jest.spyOn(Date, 'now') to return this value. +// Must be after 2018-01-01 so that providerIndicatedTimeUnixMs passes framework validation. +export const MOCK_DATE = new Date('2024-01-01T11:11:11.111Z') +const MOCK_TIMESTAMP_SECS = Math.floor(MOCK_DATE.getTime() / 1000) + +// 1 hour before mock now — well within any reasonable USHP_MAX_STALENESS_SECS +export const FRESH_TIMESTAMP = String(MOCK_TIMESTAMP_SECS - 3600) +// Epoch — will always be stale +export const STALE_TIMESTAMP = '1' + +export const VALUE = '1003968325' +export const SCALE = '9' + +// Expected rescaled result: 1003968325 / 10^(9-8) = 100396832 +export const EXPECTED_RESULT = '100396832' +export const EXPECTED_PRICE = 1.00396832 + +function signResponseBody(body: Record, privateKey: crypto.KeyObject): string { + const message = JSON.stringify(body) + const signer = crypto.createSign('SHA256') + signer.update(Buffer.from(message, 'utf-8')) + // Default ECDSA output is DER-encoded, matching what the transport verifies + return signer.sign(privateKey, 'base64') +} + +function makeResponse( + opts: { timestamp: string; keyId?: string; tamperedValue?: string }, + privateKey: crypto.KeyObject, +): string { + const keyId = opts.keyId ?? TEST_KEY_ID + // The server signs the body WITHOUT the signature field + const unsignedBody = { value: VALUE, scale: SCALE, timestamp: opts.timestamp, key_id: keyId } + const signature = signResponseBody(unsignedBody, privateKey) + // Return the full body including signature; value may be tampered AFTER signing + const responseBody = opts.tamperedValue + ? { + value: opts.tamperedValue, + scale: SCALE, + timestamp: opts.timestamp, + key_id: keyId, + signature, + } + : { ...unsignedBody, signature } + return JSON.stringify(responseBody) +} + +export function createFixtures(privateKey: crypto.KeyObject) { + return { + // Valid response, recent timestamp + success: makeResponse({ timestamp: FRESH_TIMESTAMP }, privateKey), + // Valid signature but timestamp is far in the past + stale: makeResponse({ timestamp: STALE_TIMESTAMP }, privateKey), + // Signature is valid for original value, but value has been tampered after signing + badSig: makeResponse({ timestamp: FRESH_TIMESTAMP, tamperedValue: '9999999999' }, privateKey), + // Valid response but key_id points to a KMS key the mock will reject + kmsUnavailable: makeResponse( + { timestamp: FRESH_TIMESTAMP, keyId: UNAVAILABLE_KEY_ID }, + privateKey, + ), + } +} diff --git a/packages/sources/infralabs/test/unit/utils.test.ts b/packages/sources/infralabs/test/unit/utils.test.ts new file mode 100644 index 0000000000..08e7a83068 --- /dev/null +++ b/packages/sources/infralabs/test/unit/utils.test.ts @@ -0,0 +1,131 @@ +import * as crypto from 'crypto' +import { buildUnsignedMessage, isFresh, isSaneSignature, rescale } from '../../src/transport/utils' + +describe('buildUnsignedMessage', () => { + it('strips the signature field from compact JSON (signature last)', () => { + const raw = '{"value":"1","scale":"9","timestamp":"1","key_id":"arn:k","signature":"abc123"}' + const expected = '{"value":"1","scale":"9","timestamp":"1","key_id":"arn:k"}' + expect(buildUnsignedMessage(raw)).toBe(expected) + }) + + it('strips the signature field from compact JSON (signature in the middle)', () => { + const raw = '{"value":"1","signature":"abc123","key_id":"arn:k"}' + const expected = '{"value":"1","key_id":"arn:k"}' + expect(buildUnsignedMessage(raw)).toBe(expected) + }) + + it('strips the signature field from Python-style JSON (space after colon and comma)', () => { + const raw = + '{"value": "1", "scale": "9", "timestamp": "1", "key_id": "arn:k", "signature": "abc123"}' + const expected = '{"value": "1", "scale": "9", "timestamp": "1", "key_id": "arn:k"}' + expect(buildUnsignedMessage(raw)).toBe(expected) + }) + + it('handles a long base64 signature value', () => { + const sig = 'MEUCIQDmJz2+/abc123def456ghi789jklmno=='.repeat(2) + const raw = `{"value":"1","signature":"${sig}"}` + expect(buildUnsignedMessage(raw)).toBe('{"value":"1"}') + }) + + it('returns the input unchanged when no signature field is present', () => { + const raw = '{"value":"1","scale":"9"}' + expect(buildUnsignedMessage(raw)).toBe(raw) + }) +}) + +describe('rescale', () => { + it('divides when fromScale > 8 (the typical case: scale=9)', () => { + // 1003968325 / 10^(9-8) = 100396832 + expect(rescale('1003968325', 9).toString()).toBe('100396832') + }) + + it('returns the value unchanged when fromScale equals 8', () => { + expect(rescale('100396832', 8).toString()).toBe('100396832') + }) + + it('multiplies when fromScale < 8', () => { + // 1003968 * 10^(8-7) = 10039680 + expect(rescale('1003968', 7).toString()).toBe('10039680') + }) + + it('handles a large scale difference (e.g. scale=18)', () => { + // 10^17 / 10^(18-8) = 10^17 / 10^10 = 10^7 + expect(rescale('100000000000000000', 18).toString()).toBe('10000000') + }) + + it('preserves BigInt precision — no floating-point rounding', () => { + // 99999999999999999 / 10 = 9999999999999999 (safe with BigInt, lossy with float64) + expect(rescale('99999999999999999', 9).toString()).toBe('9999999999999999') + }) +}) + +describe('isFresh', () => { + const nowMs = 1_704_107_471_000 // 2024-01-01T11:11:11.000Z + const maxAgeSecs = 90_000 // 25 hours + + it('returns true when the value is fresh', () => { + const timestamp = String(Math.floor(nowMs / 1000) - 3_600) // 1 hour ago + expect(isFresh(timestamp, maxAgeSecs, nowMs)).toBe(true) + }) + + it('returns true when the value is exactly at the staleness boundary', () => { + const timestamp = String(Math.floor(nowMs / 1000) - maxAgeSecs) + expect(isFresh(timestamp, maxAgeSecs, nowMs)).toBe(true) + }) + + it('returns false when the value is one second past the boundary', () => { + const timestamp = String(Math.floor(nowMs / 1000) - maxAgeSecs - 1) + expect(isFresh(timestamp, maxAgeSecs, nowMs)).toBe(false) + }) + + it('returns false for a very old timestamp', () => { + expect(isFresh('1', maxAgeSecs, nowMs)).toBe(false) + }) +}) + +describe('isSaneSignature', () => { + const rawBody = + '{"value":"1003968325","scale":"9","timestamp":"1704103871","key_id":"arn:k","signature":"placeholder"}' + let publicKey: crypto.KeyObject + let privateKey: crypto.KeyObject + + beforeAll(() => { + const pair = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }) + publicKey = pair.publicKey + privateKey = pair.privateKey + }) + + function sign(body: string): string { + const unsigned = buildUnsignedMessage(body) + return crypto + .createSign('SHA256') + .update(Buffer.from(unsigned, 'utf-8')) + .sign(privateKey, 'base64') + } + + it('returns true for a valid signature', () => { + const sig = sign(rawBody) + expect(isSaneSignature(rawBody, publicKey, sig)).toBe(true) + }) + + it('returns false when the body has been tampered with', () => { + const sig = sign(rawBody) + const tamperedBody = rawBody.replace('"value":"1003968325"', '"value":"9999999999"') + expect(isSaneSignature(tamperedBody, publicKey, sig)).toBe(false) + }) + + it('returns false when the signature is for a different key', () => { + const { privateKey: otherPrivateKey } = crypto.generateKeyPairSync('ec', { + namedCurve: 'P-256', + }) + const sig = crypto + .createSign('SHA256') + .update(Buffer.from(buildUnsignedMessage(rawBody), 'utf-8')) + .sign(otherPrivateKey, 'base64') + expect(isSaneSignature(rawBody, publicKey, sig)).toBe(false) + }) + + it('returns false for a corrupted signature', () => { + expect(isSaneSignature(rawBody, publicKey, 'bm90YXZhbGlkc2lnbmF0dXJl')).toBe(false) + }) +}) diff --git a/packages/sources/infralabs/tsconfig.json b/packages/sources/infralabs/tsconfig.json new file mode 100644 index 0000000000..f59363fd76 --- /dev/null +++ b/packages/sources/infralabs/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*", "src/**/*.json"], + "exclude": ["dist", "**/*.spec.ts", "**/*.test.ts"] +} diff --git a/packages/sources/infralabs/tsconfig.test.json b/packages/sources/infralabs/tsconfig.test.json new file mode 100644 index 0000000000..e3de28cb5c --- /dev/null +++ b/packages/sources/infralabs/tsconfig.test.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src/**/*", "**/test", "src/**/*.json"], + "compilerOptions": { + "noEmit": true + } +} diff --git a/packages/tsconfig.json b/packages/tsconfig.json index 1e049d4524..72af31f933 100644 --- a/packages/tsconfig.json +++ b/packages/tsconfig.json @@ -347,6 +347,9 @@ { "path": "./sources/lo-tech" }, + { + "path": "./sources/infralabs" + }, { "path": "./sources/lotus" }, diff --git a/yarn.lock b/yarn.lock index b7a49ee992..e91d7d3ff0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -236,6 +236,24 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/client-kms@npm:^3.1071.0": + version: 3.1071.0 + resolution: "@aws-sdk/client-kms@npm:3.1071.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:5.2.0" + "@aws-crypto/sha256-js": "npm:5.2.0" + "@aws-sdk/core": "npm:^3.974.22" + "@aws-sdk/credential-provider-node": "npm:^3.972.57" + "@aws-sdk/types": "npm:^3.973.13" + "@smithy/core": "npm:^3.24.6" + "@smithy/fetch-http-handler": "npm:^5.4.6" + "@smithy/node-http-handler": "npm:^4.7.6" + "@smithy/types": "npm:^4.14.3" + tslib: "npm:^2.6.2" + checksum: 10/f5a36708156f64ece15d88709cb93dc643eeda49d14b708c48787671d39c2114ec7410c40077970149b90f4e5073d025b0edd3cf26138bdc420945174cf544de + languageName: node + linkType: hard + "@aws-sdk/client-s3@npm:^3.600.0": version: 3.1070.0 resolution: "@aws-sdk/client-s3@npm:3.1070.0" @@ -274,6 +292,22 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/core@npm:^3.974.22": + version: 3.974.22 + resolution: "@aws-sdk/core@npm:3.974.22" + dependencies: + "@aws-sdk/types": "npm:^3.973.13" + "@aws-sdk/xml-builder": "npm:^3.972.30" + "@aws/lambda-invoke-store": "npm:^0.2.2" + "@smithy/core": "npm:^3.24.6" + "@smithy/signature-v4": "npm:^5.4.6" + "@smithy/types": "npm:^4.14.3" + bowser: "npm:^2.11.0" + tslib: "npm:^2.6.2" + checksum: 10/71449cdcf585e6ff6710d8291efa04ab53a254deebc579ed504c2a0edf4d8e4758c144a4151bd6878fff4647174fa0ba030e2339180f19e4d2d82a9d5ce7dd56 + languageName: node + linkType: hard + "@aws-sdk/credential-provider-env@npm:^3.972.47": version: 3.972.47 resolution: "@aws-sdk/credential-provider-env@npm:3.972.47" @@ -287,6 +321,19 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-env@npm:^3.972.48": + version: 3.972.48 + resolution: "@aws-sdk/credential-provider-env@npm:3.972.48" + dependencies: + "@aws-sdk/core": "npm:^3.974.22" + "@aws-sdk/types": "npm:^3.973.13" + "@smithy/core": "npm:^3.24.6" + "@smithy/types": "npm:^4.14.3" + tslib: "npm:^2.6.2" + checksum: 10/6e0c5491d872deedaf38f234f010d4daa2d5dd46e9c1b58c8f3d894c6766a530b96af0e99734967141c90dae360f5a15ea85f029e45d434d0e20aa85ca07642a + languageName: node + linkType: hard + "@aws-sdk/credential-provider-http@npm:^3.972.49": version: 3.972.49 resolution: "@aws-sdk/credential-provider-http@npm:3.972.49" @@ -302,6 +349,21 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-http@npm:^3.972.50": + version: 3.972.50 + resolution: "@aws-sdk/credential-provider-http@npm:3.972.50" + dependencies: + "@aws-sdk/core": "npm:^3.974.22" + "@aws-sdk/types": "npm:^3.973.13" + "@smithy/core": "npm:^3.24.6" + "@smithy/fetch-http-handler": "npm:^5.4.6" + "@smithy/node-http-handler": "npm:^4.7.6" + "@smithy/types": "npm:^4.14.3" + tslib: "npm:^2.6.2" + checksum: 10/b87a873b8751001b31712bb777e92ed3c5d7678a3448428b45fc087cb645176be0f431cc4b1da708034ebd9820b7c7ad8cd3be76180e4d278c4beb37f568fd81 + languageName: node + linkType: hard + "@aws-sdk/credential-provider-ini@npm:^3.972.54": version: 3.972.54 resolution: "@aws-sdk/credential-provider-ini@npm:3.972.54" @@ -323,6 +385,27 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-ini@npm:^3.972.55": + version: 3.972.55 + resolution: "@aws-sdk/credential-provider-ini@npm:3.972.55" + dependencies: + "@aws-sdk/core": "npm:^3.974.22" + "@aws-sdk/credential-provider-env": "npm:^3.972.48" + "@aws-sdk/credential-provider-http": "npm:^3.972.50" + "@aws-sdk/credential-provider-login": "npm:^3.972.54" + "@aws-sdk/credential-provider-process": "npm:^3.972.48" + "@aws-sdk/credential-provider-sso": "npm:^3.972.54" + "@aws-sdk/credential-provider-web-identity": "npm:^3.972.54" + "@aws-sdk/nested-clients": "npm:^3.997.22" + "@aws-sdk/types": "npm:^3.973.13" + "@smithy/core": "npm:^3.24.6" + "@smithy/credential-provider-imds": "npm:^4.3.7" + "@smithy/types": "npm:^4.14.3" + tslib: "npm:^2.6.2" + checksum: 10/bfafed3c8b5463609e46199b5f3c0f61644e8a132a1348a6829887ea71556a291ff1b37156c443140beda588a9e84b870a4fd97e7bc375691f4616809901399b + languageName: node + linkType: hard + "@aws-sdk/credential-provider-login@npm:^3.972.53": version: 3.972.53 resolution: "@aws-sdk/credential-provider-login@npm:3.972.53" @@ -337,6 +420,20 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-login@npm:^3.972.54": + version: 3.972.54 + resolution: "@aws-sdk/credential-provider-login@npm:3.972.54" + dependencies: + "@aws-sdk/core": "npm:^3.974.22" + "@aws-sdk/nested-clients": "npm:^3.997.22" + "@aws-sdk/types": "npm:^3.973.13" + "@smithy/core": "npm:^3.24.6" + "@smithy/types": "npm:^4.14.3" + tslib: "npm:^2.6.2" + checksum: 10/e83ca8ef772e4e369d632add9b3c3adbe0c05311f26d85ffc4353364af2a07b3bb19a60f69168d631934c65394c38aa9673df160b7b7679d7f5a3872e82779fb + languageName: node + linkType: hard + "@aws-sdk/credential-provider-node@npm:^3.972.56": version: 3.972.56 resolution: "@aws-sdk/credential-provider-node@npm:3.972.56" @@ -356,6 +453,25 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-node@npm:^3.972.57": + version: 3.972.57 + resolution: "@aws-sdk/credential-provider-node@npm:3.972.57" + dependencies: + "@aws-sdk/credential-provider-env": "npm:^3.972.48" + "@aws-sdk/credential-provider-http": "npm:^3.972.50" + "@aws-sdk/credential-provider-ini": "npm:^3.972.55" + "@aws-sdk/credential-provider-process": "npm:^3.972.48" + "@aws-sdk/credential-provider-sso": "npm:^3.972.54" + "@aws-sdk/credential-provider-web-identity": "npm:^3.972.54" + "@aws-sdk/types": "npm:^3.973.13" + "@smithy/core": "npm:^3.24.6" + "@smithy/credential-provider-imds": "npm:^4.3.7" + "@smithy/types": "npm:^4.14.3" + tslib: "npm:^2.6.2" + checksum: 10/02cd5767f294b289fe1940009688c178db2e49dd4f6b4fb5ac44244256e8e46899878b3e6adf97c9b40e3f75885b8903906ad908c18381c5fb7c0f0fdb9a479f + languageName: node + linkType: hard + "@aws-sdk/credential-provider-process@npm:^3.972.47": version: 3.972.47 resolution: "@aws-sdk/credential-provider-process@npm:3.972.47" @@ -369,6 +485,19 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-process@npm:^3.972.48": + version: 3.972.48 + resolution: "@aws-sdk/credential-provider-process@npm:3.972.48" + dependencies: + "@aws-sdk/core": "npm:^3.974.22" + "@aws-sdk/types": "npm:^3.973.13" + "@smithy/core": "npm:^3.24.6" + "@smithy/types": "npm:^4.14.3" + tslib: "npm:^2.6.2" + checksum: 10/7a48a1d4c007f213348426c210caa149610889bebe08c9bb8318b56acb826cf1d18d5686ecf01c2cfa10ab9279ba8dad4448cff01d199cfb709bc4b9a275d5db + languageName: node + linkType: hard + "@aws-sdk/credential-provider-sso@npm:^3.972.53": version: 3.972.53 resolution: "@aws-sdk/credential-provider-sso@npm:3.972.53" @@ -384,6 +513,21 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-sso@npm:^3.972.54": + version: 3.972.54 + resolution: "@aws-sdk/credential-provider-sso@npm:3.972.54" + dependencies: + "@aws-sdk/core": "npm:^3.974.22" + "@aws-sdk/nested-clients": "npm:^3.997.22" + "@aws-sdk/token-providers": "npm:3.1071.0" + "@aws-sdk/types": "npm:^3.973.13" + "@smithy/core": "npm:^3.24.6" + "@smithy/types": "npm:^4.14.3" + tslib: "npm:^2.6.2" + checksum: 10/a313362308ce8ce774528c45c8cb462dcbf405d2ae11223f6e4749925f3b991590bfff8d578e8cbb1c1808034c7611ad70bf379611a0291ee920c18787e19b3f + languageName: node + linkType: hard + "@aws-sdk/credential-provider-web-identity@npm:^3.972.53": version: 3.972.53 resolution: "@aws-sdk/credential-provider-web-identity@npm:3.972.53" @@ -398,6 +542,20 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-web-identity@npm:^3.972.54": + version: 3.972.54 + resolution: "@aws-sdk/credential-provider-web-identity@npm:3.972.54" + dependencies: + "@aws-sdk/core": "npm:^3.974.22" + "@aws-sdk/nested-clients": "npm:^3.997.22" + "@aws-sdk/types": "npm:^3.973.13" + "@smithy/core": "npm:^3.24.6" + "@smithy/types": "npm:^4.14.3" + tslib: "npm:^2.6.2" + checksum: 10/b876cde1b530b231ad2d89c65af5e3196864f513d0f6be9d3c5bad507b95647007f38024097010ac08f903510813f9f9744aef40f634c936b01b5e99b8ec8e06 + languageName: node + linkType: hard + "@aws-sdk/middleware-flexible-checksums@npm:^3.974.31": version: 3.974.31 resolution: "@aws-sdk/middleware-flexible-checksums@npm:3.974.31" @@ -440,6 +598,24 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/nested-clients@npm:^3.997.22": + version: 3.997.22 + resolution: "@aws-sdk/nested-clients@npm:3.997.22" + dependencies: + "@aws-crypto/sha256-browser": "npm:5.2.0" + "@aws-crypto/sha256-js": "npm:5.2.0" + "@aws-sdk/core": "npm:^3.974.22" + "@aws-sdk/signature-v4-multi-region": "npm:^3.996.35" + "@aws-sdk/types": "npm:^3.973.13" + "@smithy/core": "npm:^3.24.6" + "@smithy/fetch-http-handler": "npm:^5.4.6" + "@smithy/node-http-handler": "npm:^4.7.6" + "@smithy/types": "npm:^4.14.3" + tslib: "npm:^2.6.2" + checksum: 10/138ea0da3722e67892106ea1cc896f1cce8d53ecec0e0e743e6e1d5455129ec845c84f51229b52d249d53a78e26b0164f5bbe713ad818f36ac5576c9fcbbb546 + languageName: node + linkType: hard + "@aws-sdk/signature-v4-multi-region@npm:^3.996.35": version: 3.996.35 resolution: "@aws-sdk/signature-v4-multi-region@npm:3.996.35" @@ -466,6 +642,20 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/token-providers@npm:3.1071.0": + version: 3.1071.0 + resolution: "@aws-sdk/token-providers@npm:3.1071.0" + dependencies: + "@aws-sdk/core": "npm:^3.974.22" + "@aws-sdk/nested-clients": "npm:^3.997.22" + "@aws-sdk/types": "npm:^3.973.13" + "@smithy/core": "npm:^3.24.6" + "@smithy/types": "npm:^4.14.3" + tslib: "npm:^2.6.2" + checksum: 10/846b924f1bd3f91fa748ec55b9cc83dbddfcd43dbf3e5e323edb0a0d13a4dd5dda1545f811941f9468362aad648326719ca8fa0455c0d48a18c74e4037895c2c + languageName: node + linkType: hard + "@aws-sdk/types@npm:^3.222.0": version: 3.696.0 resolution: "@aws-sdk/types@npm:3.696.0" @@ -3948,6 +4138,34 @@ __metadata: languageName: unknown linkType: soft +"@chainlink/infralabs-adapter@workspace:packages/sources/infralabs": + version: 0.0.0-use.local + resolution: "@chainlink/infralabs-adapter@workspace:packages/sources/infralabs" + dependencies: + "@aws-sdk/client-kms": "npm:^3.1071.0" + "@chainlink/external-adapter-framework": "npm:2.17.1" + "@types/jest": "npm:29.5.14" + "@types/node": "npm:22.14.1" + nock: "npm:13.5.6" + tslib: "npm:2.6.3" + typescript: "npm:5.8.3" + languageName: unknown + linkType: soft + +"@chainlink/longmark-ushp-adapter@workspace:packages/sources/longmark-ushp": + version: 0.0.0-use.local + resolution: "@chainlink/longmark-ushp-adapter@workspace:packages/sources/longmark-ushp" + dependencies: + "@aws-sdk/client-kms": "npm:^3.1071.0" + "@chainlink/external-adapter-framework": "npm:2.17.1" + "@types/jest": "npm:29.5.14" + "@types/node": "npm:22.14.1" + nock: "npm:13.5.6" + tslib: "npm:2.6.3" + typescript: "npm:5.8.3" + languageName: unknown + linkType: soft + "@chainlink/lotus-adapter@workspace:*, @chainlink/lotus-adapter@workspace:packages/sources/lotus": version: 0.0.0-use.local resolution: "@chainlink/lotus-adapter@workspace:packages/sources/lotus"