Skip to content

bedrock provider cleanup #1276

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

narengogi
Copy link
Collaborator

@narengogi narengogi commented Aug 11, 2025

Changes included in this PR:

  • utility for fetching environment variables
  • Bedrock/AWS proxy routes support

Changes not included:
Support for IRSA and IMDS based auth

Copy link

matter-code-review bot commented Aug 11, 2025

Code Quality bug fix new feature

Description

Summary By MatterAI MatterAI logo

🔄 What Changed

This pull request focuses on refining the Bedrock provider's API configuration and utility functions. Key changes include enhancing the getMethod function to dynamically determine HTTP methods for proxy requests, standardizing S3 and Bedrock endpoint URL construction using a shared getAwsEndpointDomain utility, and improving header generation logic to support direct proxying of request headers. Additionally, it corrects usage reporting for Anthropic chat completions by accurately mapping input/output tokens and refines batch processing by ensuring finalizing_at and expires_at are always valid timestamps. Minor cleanup includes adjusting type definitions and removing unused imports.

🔍 Impact of the Change

These changes significantly improve the flexibility and accuracy of the Bedrock provider. The enhanced getMethod and header generation logic allow for more robust proxying capabilities, enabling direct passthrough of headers for non-Bedrock proxy endpoints. Consistent use of getAwsEndpointDomain ensures correct and configurable AWS endpoint resolution across various Bedrock and S3 operations. The fix for Anthropic usage reporting provides accurate token consumption data, crucial for billing and analytics. The batch processing update ensures data consistency for job timestamps. Overall, the PR enhances maintainability, correctness, and extensibility of the Bedrock integration.

📁 Total Files Changed

  • src/providers/bedrock/api.ts: Refactored getMethod for proxy support, updated S3/Bedrock URL construction with getAwsEndpointDomain, and enhanced header generation for proxy requests.
  • src/providers/bedrock/listBatches.ts: Ensured finalizing_at and expires_at are always valid timestamps.
  • src/providers/bedrock/listFinetunes.ts: Minor formatting adjustment.
  • src/providers/bedrock/types.ts: Moved BEDROCK_STOP_REASON enum and added usage property to BedrockChatCompleteStreamChunk while removing message.
  • src/providers/bedrock/uploadFileUtils.ts: Corrected Anthropic chat completion usage mapping and added a comment for Mistral usage.
  • src/providers/bedrock/utils.ts: Updated getAwsEndpointDomain to use Environment(c) for consistency.

🧪 Test Added

N/A

🔒Security Vulnerabilities

N/A

Motivation

This PR aims to improve the robustness, flexibility, and accuracy of the Bedrock provider. Specifically, it addresses the need for better proxy handling, consistent AWS endpoint resolution, and accurate usage reporting for AI model interactions. These changes contribute to a more reliable and maintainable integration with AWS Bedrock services.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactoring (no functional changes)

How Has This Been Tested?

  • Unit Tests
  • Integration Tests
  • Manual Testing

Screenshots (if applicable)

N/A

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Related Issues

N/A

Tip

Quality Recommendations

  1. In src/providers/bedrock/listBatches.ts, the conversion new Date(batch.endTime).getTime() assumes batch.endTime and batch.jobExpirationTime are always valid date strings. Consider adding null/undefined checks or a fallback mechanism to prevent NaN if these fields might be missing or invalid.

  2. The comment // mistral not sending usage. in src/providers/bedrock/uploadFileUtils.ts indicates a known limitation. Implement a more robust strategy for handling missing usage data, such as logging a warning, providing a configurable default, or clearly documenting this behavior, rather than hardcoding 0.

  3. For proxy requests in src/providers/bedrock/api.ts where requestHeaders are used directly, ensure comprehensive validation and sanitization of these headers to prevent potential security vulnerabilities like header injection or unexpected behavior from malicious inputs.

Tanka Poem ♫

Code flows, clean and bright,
Proxy paths now clearly seen,
Usage counts align.
Bedrock's dance, a new rhythm,
Endpoints sing a unified tune. 🧪✨

Copy link

@matter-code-review matter-code-review bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review completed. Found several areas for improvement including missing error handling, potential security issues, and documentation gaps.

Comment on lines +8 to +35
export function getValueOrFileContents(value?: string, ignore?: boolean) {
if (!value || ignore) return value;

try {
// Check if value looks like a file path
if (
value.startsWith('/') ||
value.startsWith('./') ||
value.startsWith('../')
) {
// Resolve the path (handle relative paths)
const resolvedPath = path.resolve(value);

// Check if file exists
if (fs.existsSync(resolvedPath)) {
// File exists, read and return its contents
return fs.readFileSync(resolvedPath, 'utf8').trim();
}
}

// If not a file path or file doesn't exist, return value as is
return value;
} catch (error: any) {
console.log(`Error reading file at ${value}: ${error.message}`);
// Return the original value if there's an error
return value;
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security Issue Fix

Issue: Path traversal vulnerability - the function accepts any file path without validation, potentially allowing access to sensitive files outside intended directories
Fix: Add path validation to restrict file access to safe directories
Impact: Prevents potential security breach through malicious file path injection

Suggested change
export function getValueOrFileContents(value?: string, ignore?: boolean) {
if (!value || ignore) return value;
try {
// Check if value looks like a file path
if (
value.startsWith('/') ||
value.startsWith('./') ||
value.startsWith('../')
) {
// Resolve the path (handle relative paths)
const resolvedPath = path.resolve(value);
// Check if file exists
if (fs.existsSync(resolvedPath)) {
// File exists, read and return its contents
return fs.readFileSync(resolvedPath, 'utf8').trim();
}
}
// If not a file path or file doesn't exist, return value as is
return value;
} catch (error: any) {
console.log(`Error reading file at ${value}: ${error.message}`);
// Return the original value if there's an error
return value;
}
}
export function getValueOrFileContents(value?: string, ignore?: boolean) {
if (!value || ignore) return value;
try {
// Check if value looks like a file path
if (
value.startsWith('/') ||
value.startsWith('./') ||
value.startsWith('../')
) {
// Resolve the path (handle relative paths)
const resolvedPath = path.resolve(value);
// Security: Validate path is within allowed directories
const allowedPaths = [process.cwd(), '/etc/ssl/certs', '/var/secrets'];
const isPathAllowed = allowedPaths.some(allowedPath =>
resolvedPath.startsWith(path.resolve(allowedPath))
);
if (!isPathAllowed) {
console.warn(`Access denied to path outside allowed directories: ${resolvedPath}`);
return value;
}
// Check if file exists
if (fs.existsSync(resolvedPath)) {
// File exists, read and return its contents
return fs.readFileSync(resolvedPath, 'utf8').trim();
}
}
// If not a file path or file doesn't exist, return value as is
return value;
} catch (error: any) {
console.log(`Error reading file at ${value}: ${error.message}`);
// Return the original value if there's an error
return value;
}
}

Comment on lines +37 to +128
const nodeEnv = {
NODE_ENV: getValueOrFileContents(process.env.NODE_ENV, true),
PORT: getValueOrFileContents(process.env.PORT) || 8787,

TLS_KEY_PATH: getValueOrFileContents(process.env.TLS_KEY_PATH, true),
TLS_CERT_PATH: getValueOrFileContents(process.env.TLS_CERT_PATH, true),
TLS_CA_PATH: getValueOrFileContents(process.env.TLS_CA_PATH, true),

AWS_ACCESS_KEY_ID: getValueOrFileContents(process.env.AWS_ACCESS_KEY_ID),
AWS_SECRET_ACCESS_KEY: getValueOrFileContents(
process.env.AWS_SECRET_ACCESS_KEY
),
AWS_SESSION_TOKEN: getValueOrFileContents(process.env.AWS_SESSION_TOKEN),
AWS_ROLE_ARN: getValueOrFileContents(process.env.AWS_ROLE_ARN),
AWS_PROFILE: getValueOrFileContents(process.env.AWS_PROFILE, true),
AWS_WEB_IDENTITY_TOKEN_FILE: getValueOrFileContents(
process.env.AWS_WEB_IDENTITY_TOKEN_FILE,
true
),
AWS_CONTAINER_CREDENTIALS_RELATIVE_URI: getValueOrFileContents(
process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI,
true
),
AWS_ASSUME_ROLE_ACCESS_KEY_ID: getValueOrFileContents(
process.env.AWS_ASSUME_ROLE_ACCESS_KEY_ID
),
AWS_ASSUME_ROLE_SECRET_ACCESS_KEY: getValueOrFileContents(
process.env.AWS_ASSUME_ROLE_SECRET_ACCESS_KEY
),
AWS_ASSUME_ROLE_REGION: getValueOrFileContents(
process.env.AWS_ASSUME_ROLE_REGION
),
AWS_REGION: getValueOrFileContents(process.env.AWS_REGION),
AWS_ENDPOINT_DOMAIN: getValueOrFileContents(process.env.AWS_ENDPOINT_DOMAIN),
AWS_IMDS_V1: getValueOrFileContents(process.env.AWS_IMDS_V1),

AZURE_AUTH_MODE: getValueOrFileContents(process.env.AZURE_AUTH_MODE),
AZURE_ENTRA_CLIENT_ID: getValueOrFileContents(
process.env.AZURE_ENTRA_CLIENT_ID
),
AZURE_ENTRA_CLIENT_SECRET: getValueOrFileContents(
process.env.AZURE_ENTRA_CLIENT_SECRET
),
AZURE_ENTRA_TENANT_ID: getValueOrFileContents(
process.env.AZURE_ENTRA_TENANT_ID
),
AZURE_MANAGED_CLIENT_ID: getValueOrFileContents(
process.env.AZURE_MANAGED_CLIENT_ID
),
AZURE_MANAGED_VERSION: getValueOrFileContents(
process.env.AZURE_MANAGED_VERSION
),
AZURE_IDENTITY_ENDPOINT: getValueOrFileContents(
process.env.IDENTITY_ENDPOINT,
true
),
AZURE_MANAGED_IDENTITY_HEADER: getValueOrFileContents(
process.env.IDENTITY_HEADER
),

SSE_ENCRYPTION_TYPE: getValueOrFileContents(process.env.SSE_ENCRYPTION_TYPE),
KMS_KEY_ID: getValueOrFileContents(process.env.KMS_KEY_ID),
KMS_BUCKET_KEY_ENABLED: getValueOrFileContents(
process.env.KMS_BUCKET_KEY_ENABLED
),
KMS_ENCRYPTION_CONTEXT: getValueOrFileContents(
process.env.KMS_ENCRYPTION_CONTEXT
),
KMS_ENCRYPTION_ALGORITHM: getValueOrFileContents(
process.env.KMS_ENCRYPTION_ALGORITHM
),
KMS_ENCRYPTION_CUSTOMER_KEY: getValueOrFileContents(
process.env.KMS_ENCRYPTION_CUSTOMER_KEY
),
KMS_ENCRYPTION_CUSTOMER_KEY_MD5: getValueOrFileContents(
process.env.KMS_ENCRYPTION_CUSTOMER_KEY_MD5
),
KMS_ROLE_ARN: getValueOrFileContents(process.env.KMS_ROLE_ARN),

HTTP_PROXY: getValueOrFileContents(process.env.HTTP_PROXY),
HTTPS_PROXY: getValueOrFileContents(process.env.HTTPS_PROXY),
};

export const Environment = (c?: Context) => {
if (isNodeInstance) {
return nodeEnv;
}
if (c) {
return env(c);
}
return {};
};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🐛 Bug Fix

Issue: Runtime check will fail in non-Node environments - getRuntimeKey() is called at module level, potentially causing errors in other runtimes
Fix: Move runtime check inside the function to avoid module-level execution issues
Impact: Prevents runtime errors when module is loaded in non-Node environments

Suggested change
const nodeEnv = {
NODE_ENV: getValueOrFileContents(process.env.NODE_ENV, true),
PORT: getValueOrFileContents(process.env.PORT) || 8787,
TLS_KEY_PATH: getValueOrFileContents(process.env.TLS_KEY_PATH, true),
TLS_CERT_PATH: getValueOrFileContents(process.env.TLS_CERT_PATH, true),
TLS_CA_PATH: getValueOrFileContents(process.env.TLS_CA_PATH, true),
AWS_ACCESS_KEY_ID: getValueOrFileContents(process.env.AWS_ACCESS_KEY_ID),
AWS_SECRET_ACCESS_KEY: getValueOrFileContents(
process.env.AWS_SECRET_ACCESS_KEY
),
AWS_SESSION_TOKEN: getValueOrFileContents(process.env.AWS_SESSION_TOKEN),
AWS_ROLE_ARN: getValueOrFileContents(process.env.AWS_ROLE_ARN),
AWS_PROFILE: getValueOrFileContents(process.env.AWS_PROFILE, true),
AWS_WEB_IDENTITY_TOKEN_FILE: getValueOrFileContents(
process.env.AWS_WEB_IDENTITY_TOKEN_FILE,
true
),
AWS_CONTAINER_CREDENTIALS_RELATIVE_URI: getValueOrFileContents(
process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI,
true
),
AWS_ASSUME_ROLE_ACCESS_KEY_ID: getValueOrFileContents(
process.env.AWS_ASSUME_ROLE_ACCESS_KEY_ID
),
AWS_ASSUME_ROLE_SECRET_ACCESS_KEY: getValueOrFileContents(
process.env.AWS_ASSUME_ROLE_SECRET_ACCESS_KEY
),
AWS_ASSUME_ROLE_REGION: getValueOrFileContents(
process.env.AWS_ASSUME_ROLE_REGION
),
AWS_REGION: getValueOrFileContents(process.env.AWS_REGION),
AWS_ENDPOINT_DOMAIN: getValueOrFileContents(process.env.AWS_ENDPOINT_DOMAIN),
AWS_IMDS_V1: getValueOrFileContents(process.env.AWS_IMDS_V1),
AZURE_AUTH_MODE: getValueOrFileContents(process.env.AZURE_AUTH_MODE),
AZURE_ENTRA_CLIENT_ID: getValueOrFileContents(
process.env.AZURE_ENTRA_CLIENT_ID
),
AZURE_ENTRA_CLIENT_SECRET: getValueOrFileContents(
process.env.AZURE_ENTRA_CLIENT_SECRET
),
AZURE_ENTRA_TENANT_ID: getValueOrFileContents(
process.env.AZURE_ENTRA_TENANT_ID
),
AZURE_MANAGED_CLIENT_ID: getValueOrFileContents(
process.env.AZURE_MANAGED_CLIENT_ID
),
AZURE_MANAGED_VERSION: getValueOrFileContents(
process.env.AZURE_MANAGED_VERSION
),
AZURE_IDENTITY_ENDPOINT: getValueOrFileContents(
process.env.IDENTITY_ENDPOINT,
true
),
AZURE_MANAGED_IDENTITY_HEADER: getValueOrFileContents(
process.env.IDENTITY_HEADER
),
SSE_ENCRYPTION_TYPE: getValueOrFileContents(process.env.SSE_ENCRYPTION_TYPE),
KMS_KEY_ID: getValueOrFileContents(process.env.KMS_KEY_ID),
KMS_BUCKET_KEY_ENABLED: getValueOrFileContents(
process.env.KMS_BUCKET_KEY_ENABLED
),
KMS_ENCRYPTION_CONTEXT: getValueOrFileContents(
process.env.KMS_ENCRYPTION_CONTEXT
),
KMS_ENCRYPTION_ALGORITHM: getValueOrFileContents(
process.env.KMS_ENCRYPTION_ALGORITHM
),
KMS_ENCRYPTION_CUSTOMER_KEY: getValueOrFileContents(
process.env.KMS_ENCRYPTION_CUSTOMER_KEY
),
KMS_ENCRYPTION_CUSTOMER_KEY_MD5: getValueOrFileContents(
process.env.KMS_ENCRYPTION_CUSTOMER_KEY_MD5
),
KMS_ROLE_ARN: getValueOrFileContents(process.env.KMS_ROLE_ARN),
HTTP_PROXY: getValueOrFileContents(process.env.HTTP_PROXY),
HTTPS_PROXY: getValueOrFileContents(process.env.HTTPS_PROXY),
};
export const Environment = (c?: Context) => {
if (isNodeInstance) {
return nodeEnv;
}
if (c) {
return env(c);
}
return {};
};
const getNodeEnv = () => ({
NODE_ENV: getValueOrFileContents(process.env.NODE_ENV, true),
PORT: getValueOrFileContents(process.env.PORT) || 8787,
TLS_KEY_PATH: getValueOrFileContents(process.env.TLS_KEY_PATH, true),
TLS_CERT_PATH: getValueOrFileContents(process.env.TLS_CERT_PATH, true),
TLS_CA_PATH: getValueOrFileContents(process.env.TLS_CA_PATH, true),
AWS_ACCESS_KEY_ID: getValueOrFileContents(process.env.AWS_ACCESS_KEY_ID),
AWS_SECRET_ACCESS_KEY: getValueOrFileContents(
process.env.AWS_SECRET_ACCESS_KEY
),
AWS_SESSION_TOKEN: getValueOrFileContents(process.env.AWS_SESSION_TOKEN),
AWS_ROLE_ARN: getValueOrFileContents(process.env.AWS_ROLE_ARN),
AWS_PROFILE: getValueOrFileContents(process.env.AWS_PROFILE, true),
AWS_WEB_IDENTITY_TOKEN_FILE: getValueOrFileContents(
process.env.AWS_WEB_IDENTITY_TOKEN_FILE,
true
),
AWS_CONTAINER_CREDENTIALS_RELATIVE_URI: getValueOrFileContents(
process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI,
true
),
AWS_ASSUME_ROLE_ACCESS_KEY_ID: getValueOrFileContents(
process.env.AWS_ASSUME_ROLE_ACCESS_KEY_ID
),
AWS_ASSUME_ROLE_SECRET_ACCESS_KEY: getValueOrFileContents(
process.env.AWS_ASSUME_ROLE_SECRET_ACCESS_KEY
),
AWS_ASSUME_ROLE_REGION: getValueOrFileContents(
process.env.AWS_ASSUME_ROLE_REGION
),
AWS_REGION: getValueOrFileContents(process.env.AWS_REGION),
AWS_ENDPOINT_DOMAIN: getValueOrFileContents(process.env.AWS_ENDPOINT_DOMAIN),
AWS_IMDS_V1: getValueOrFileContents(process.env.AWS_IMDS_V1),
AZURE_AUTH_MODE: getValueOrFileContents(process.env.AZURE_AUTH_MODE),
AZURE_ENTRA_CLIENT_ID: getValueOrFileContents(
process.env.AZURE_ENTRA_CLIENT_ID
),
AZURE_ENTRA_CLIENT_SECRET: getValueOrFileContents(
process.env.AZURE_ENTRA_CLIENT_SECRET
),
AZURE_ENTRA_TENANT_ID: getValueOrFileContents(
process.env.AZURE_ENTRA_TENANT_ID
),
AZURE_MANAGED_CLIENT_ID: getValueOrFileContents(
process.env.AZURE_MANAGED_CLIENT_ID
),
AZURE_MANAGED_VERSION: getValueOrFileContents(
process.env.AZURE_MANAGED_VERSION
),
AZURE_IDENTITY_ENDPOINT: getValueOrFileContents(
process.env.IDENTITY_ENDPOINT,
true
),
AZURE_MANAGED_IDENTITY_HEADER: getValueOrFileContents(
process.env.IDENTITY_HEADER
),
SSE_ENCRYPTION_TYPE: getValueOrFileContents(process.env.SSE_ENCRYPTION_TYPE),
KMS_KEY_ID: getValueOrFileContents(process.env.KMS_KEY_ID),
KMS_BUCKET_KEY_ENABLED: getValueOrFileContents(
process.env.KMS_BUCKET_KEY_ENABLED
),
KMS_ENCRYPTION_CONTEXT: getValueOrFileContents(
process.env.KMS_ENCRYPTION_CONTEXT
),
KMS_ENCRYPTION_ALGORITHM: getValueOrFileContents(
process.env.KMS_ENCRYPTION_ALGORITHM
),
KMS_ENCRYPTION_CUSTOMER_KEY: getValueOrFileContents(
process.env.KMS_ENCRYPTION_CUSTOMER_KEY
),
KMS_ENCRYPTION_CUSTOMER_KEY_MD5: getValueOrFileContents(
process.env.KMS_ENCRYPTION_CUSTOMER_KEY_MD5
),
KMS_ROLE_ARN: getValueOrFileContents(process.env.KMS_ROLE_ARN),
HTTP_PROXY: getValueOrFileContents(process.env.HTTP_PROXY),
HTTPS_PROXY: getValueOrFileContents(process.env.HTTPS_PROXY),
});
export const Environment = (c?: Context) => {
const isNodeInstance = getRuntimeKey() === 'node';
if (isNodeInstance) {
return getNodeEnv();
}
if (c) {
return env(c);
}
return {};
};

Comment on lines +126 to +127
const model = params.foundationModel || params.model || '';
if (g1EmbedModels.includes(model)) return undefined;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🐛 Bug Fix

Issue: Potential null/undefined access - params.foundationModel and params.model could be undefined, causing the includes() check to fail
Fix: Add proper null/undefined checks before accessing model properties
Impact: Prevents runtime errors when model parameters are missing

Suggested change
const model = params.foundationModel || params.model || '';
if (g1EmbedModels.includes(model)) return undefined;
const model = params?.foundationModel || params?.model || '';
if (model && g1EmbedModels.includes(model)) return undefined;

Comment on lines 39 to 46
headers: async ({ providerOptions, fn }) => {
const {
apiKey,
azureExtraParams,
azureExtraParameters,
azureDeploymentName,
azureAdToken,
azureAuthMode,
} = providerOptions;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Documentation Gap

Issue: Missing JSDoc documentation for the azureExtraParameters field and its purpose
Fix: Add comprehensive documentation explaining the parameter's usage
Impact: Improves code maintainability and developer understanding

Suggested change
headers: async ({ providerOptions, fn }) => {
const {
apiKey,
azureExtraParams,
azureExtraParameters,
azureDeploymentName,
azureAdToken,
azureAuthMode,
} = providerOptions;
headers: async ({ providerOptions, fn }) => {
const {
apiKey,
/**
* Azure extra parameters configuration
* Controls how extra parameters are handled in the request
* @default 'drop' - drops extra parameters not supported by the model
*/
azureExtraParameters,
azureDeploymentName,
azureAdToken,
azureAuthMode,
} = providerOptions;

Copy link

Important

PR Review Skipped

PR review skipped as per the configuration setting. Run a manually review by commenting /matter review

💡Tips to use Matter AI

Command List

  • /matter summary: Generate AI Summary for the PR
  • /matter review: Generate AI Reviews for the latest commit in the PR
  • /matter review-full: Generate AI Reviews for the complete PR
  • /matter release-notes: Generate AI release-notes for the PR
  • /matter : Chat with your PR with Matter AI Agent
  • /matter remember : Generate AI memories for the PR
  • /matter explain: Get an explanation of the PR
  • /matter help: Show the list of available commands and documentation
  • Need help? Join our Discord server: https://discord.gg/fJU5DvanU3

@narengogi narengogi marked this pull request as ready for review August 12, 2025 09:16
@narengogi narengogi requested review from roh26it and VisargD August 12, 2025 09:16
@narengogi narengogi changed the title Chore/bedrock cleanup bedrock provider cleanup Aug 12, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant