├── .npmrc ├── .github ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── license-check.yaml │ ├── new_pr.yml │ ├── runtimes-ci.yaml │ └── release-please.yaml ├── types ├── .gitignore ├── .npmignore ├── lsp.ts ├── tsconfig.json ├── index.ts ├── README.md ├── auth.ts ├── package.json ├── didChangeDependencyPaths.ts ├── workspace.ts ├── window.ts └── inlineCompletionWithReferences.ts ├── runtimes ├── runtimes │ ├── auth │ │ ├── index.ts │ │ └── standalone │ │ │ └── encryption.test.ts │ ├── index.ts │ ├── util │ │ ├── index.ts │ │ ├── lspCacheUtil.ts │ │ ├── sharedConfigFile.ts │ │ ├── standalone │ │ │ ├── proxyUtil.ts │ │ │ ├── getProxySettings │ │ │ │ ├── getWindowsProxySettings.test.ts │ │ │ │ ├── getMacProxySettings.test.ts │ │ │ │ ├── parseScutil.ts │ │ │ │ ├── getMacProxySettings.ts │ │ │ │ └── getWindowsProxySettings.ts │ │ │ └── certificatesReaders.ts │ │ ├── lspCacheUtil.test.ts │ │ ├── loggingUtil.ts │ │ ├── serverDataDirPath.ts │ │ ├── pathUtil.ts │ │ ├── loggingUtil.test.ts │ │ ├── telemetryLspServer.ts │ │ ├── pathUtil.test.ts │ │ └── shared.ts │ ├── lsp │ │ ├── index.ts │ │ ├── router │ │ │ ├── constants.ts │ │ │ ├── initializeUtils.ts │ │ │ ├── loggingServer.ts │ │ │ ├── util.ts │ │ │ ├── routerByServerName.ts │ │ │ ├── routerByServerName.test.ts │ │ │ └── initializeUtils.test.ts │ │ └── textDocuments │ │ │ └── textDocumentConnection.ts │ ├── versioning.ts │ ├── webworker.ts │ ├── runtime.ts │ ├── encoding.ts │ ├── encoding.test.ts │ ├── README.md │ ├── versioning.test.ts │ ├── webworker.test.ts │ ├── operational-telemetry │ │ ├── operational-telemetry.ts │ │ ├── resource-metrics.test.json │ │ ├── telemetry-schemas │ │ │ └── telemetry-schema.json │ │ ├── README.md │ │ └── resource-logs.test.json │ ├── chat │ │ ├── encryptedChat.test.ts │ │ └── encryptedChat.ts │ └── agent.ts ├── testing │ └── index.ts ├── .prettierignore ├── flow-diagram.png ├── protocol │ ├── telemetry.ts │ ├── README.md │ ├── didChangeDependencyPaths.ts │ ├── getConfigurationFromServer.ts │ ├── index.ts │ ├── editCompletions.ts │ ├── inlineCompletions.ts │ ├── auth.ts │ ├── window.ts │ ├── configuration.ts │ ├── workspace.ts │ ├── inlineCompletionWithReferences.ts │ └── notification.ts ├── server-interface │ ├── logging.ts │ ├── index.ts │ ├── README.md │ ├── notification.ts │ ├── auth.ts │ ├── telemetry.ts │ ├── sdk-initializator.ts │ ├── runtime.ts │ ├── server.ts │ ├── identity-management.ts │ ├── workspace.ts │ ├── chat.ts │ └── agent.ts ├── tsconfig.json ├── script │ └── generate-types.ts ├── docs │ └── proxy.md └── package.json ├── .gitignore ├── chat-client-ui-types ├── .gitignore ├── src │ ├── index.ts │ └── uiContracts.ts ├── .npmignore ├── tsconfig.json ├── package.json └── README.md ├── NOTICE ├── .prettierignore ├── .husky ├── commit-msg ├── pre-commit └── pre-push ├── .release-please-manifest.json ├── .npmignore ├── .prettierrc ├── SECURITY.md ├── CODE_OF_CONDUCT.md ├── .commitlintrc ├── tsconfig.packages.json ├── .vscode ├── settings.json ├── tasks.json └── launch.json ├── tsconfig.json ├── release-please-config.json ├── README.md ├── package.json ├── script └── clean.ts └── CONTRIBUTING.md /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @aws/flare 2 | -------------------------------------------------------------------------------- /types/.gitignore: -------------------------------------------------------------------------------- 1 | LICENSE 2 | NOTICE 3 | SECURITY.md -------------------------------------------------------------------------------- /runtimes/runtimes/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth' 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | out/ 3 | **/.DS_Store 4 | **/*.d.ts -------------------------------------------------------------------------------- /chat-client-ui-types/.gitignore: -------------------------------------------------------------------------------- 1 | LICENSE 2 | NOTICE 3 | SECURITY.md -------------------------------------------------------------------------------- /chat-client-ui-types/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './uiContracts' 2 | -------------------------------------------------------------------------------- /runtimes/runtimes/index.ts: -------------------------------------------------------------------------------- 1 | export { standalone } from './standalone' 2 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | -------------------------------------------------------------------------------- /runtimes/testing/index.ts: -------------------------------------------------------------------------------- 1 | export { TestFeatures } from './TestFeatures' 2 | -------------------------------------------------------------------------------- /runtimes/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | out/ 3 | **/*.md 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /runtimes/runtimes/util/index.ts: -------------------------------------------------------------------------------- 1 | export { getTelemetryReasonDesc } from './shared' 2 | -------------------------------------------------------------------------------- /types/.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | node_modules/ 3 | package-lock.json 4 | .* 5 | /*.ts 6 | tsconfig.* -------------------------------------------------------------------------------- /chat-client-ui-types/.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | node_modules/ 3 | package-lock.json 4 | .* 5 | tsconfig.* -------------------------------------------------------------------------------- /runtimes/runtimes/lsp/index.ts: -------------------------------------------------------------------------------- 1 | export { observe } from './textDocuments/textDocumentConnection' 2 | -------------------------------------------------------------------------------- /runtimes/flow-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws/language-server-runtimes/HEAD/runtimes/flow-diagram.png -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | out/ 3 | **/*.md 4 | package-lock.json 5 | .prettierrc 6 | .vscode 7 | .github/workflows/** -------------------------------------------------------------------------------- /runtimes/runtimes/lsp/router/constants.ts: -------------------------------------------------------------------------------- 1 | export const SERVER_CAPABILITES_CONFIGURATION_SECTION = 'aws.serverCapabilities' 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | echo [pre-commit] linting commit message... 2 | npm run commitlint ${1} 3 | echo [pre-commit] done linting commit message -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "chat-client-ui-types": "0.1.69", 3 | "runtimes": "0.3.11", 4 | "types": "0.1.63" 5 | } 6 | -------------------------------------------------------------------------------- /types/lsp.ts: -------------------------------------------------------------------------------- 1 | export type { TextDocument } from 'vscode-languageserver-textdocument' 2 | 3 | export * from 'vscode-languageserver-types' 4 | -------------------------------------------------------------------------------- /runtimes/protocol/telemetry.ts: -------------------------------------------------------------------------------- 1 | import { TelemetryEventNotification } from './lsp' 2 | 3 | export const telemetryNotificationType = TelemetryEventNotification.type 4 | -------------------------------------------------------------------------------- /types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.packages.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "./out" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | CODE_OF_CONDUCT.md 3 | CONTRIBUTING.md 4 | node_modules/ 5 | package-lock.json 6 | .* 7 | tsconfig.json 8 | tsconfig.tsbuildinfo 9 | *.test.js 10 | *.test.json 11 | *.test.d.ts 12 | *.test.js.map 13 | -------------------------------------------------------------------------------- /runtimes/runtimes/versioning.ts: -------------------------------------------------------------------------------- 1 | export function handleVersionArgument(version?: string) { 2 | if (process.argv.some(arg => arg === '--version' || arg === '-v')) { 3 | console.log(version) 4 | process.exit(0) 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "es5", 4 | "tabWidth": 4, 5 | "singleQuote": true, 6 | "semi": false, 7 | "bracketSpacing": true, 8 | "arrowParens": "avoid", 9 | "endOfLine": "lf" 10 | } -------------------------------------------------------------------------------- /chat-client-ui-types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.packages.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./out", 6 | "tsBuildInfoFile": "./out/tsconfig.tsbuildinfo" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth' 2 | export * from './chat' 3 | export * from './didChangeDependencyPaths' 4 | export * from './inlineCompletionWithReferences' 5 | export * from './lsp' 6 | export * from './window' 7 | export * from './workspace' 8 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security issue notifications 2 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | echo [pre-commit] formatting staged files... 2 | npm run format-staged 3 | echo [pre-commit] done formatting staged files 4 | 5 | git secrets --register-aws || (echo 'Please install git-secrets https://github.com/awslabs/git-secrets to check for accidentally commited secrets!' && exit 1) 6 | git secrets --scan 7 | -------------------------------------------------------------------------------- /runtimes/runtimes/util/lspCacheUtil.ts: -------------------------------------------------------------------------------- 1 | import { InitializeParams } from '../../protocol' 2 | import { LspRouter } from '../lsp/router/lspRouter' 3 | 4 | export const getClientInitializeParamsHandlerFactory = (lspRouter: LspRouter): (() => InitializeParams | undefined) => { 5 | return () => lspRouter.clientInitializeParams 6 | } 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /.commitlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"], 3 | "rules": { 4 | "scope-enum": [ 5 | 2, 6 | "always", 7 | [ 8 | "chat-client-ui-types", 9 | "runtimes", 10 | "types" 11 | ] 12 | ] 13 | } 14 | } -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | branch="$(git symbolic-ref HEAD 2>/dev/null)" 2 | 3 | if [ "$branch" = "refs/heads/main" ]; then 4 | echo "Direct push to the main branch is not allowed. Please create a new branch and open Pull Request." 5 | exit 1 6 | fi 7 | 8 | echo "[pre-push] linting commit message..." 9 | npm run commitlint 10 | echo "[pre-commit] done linting commit message" -------------------------------------------------------------------------------- /runtimes/server-interface/logging.ts: -------------------------------------------------------------------------------- 1 | import { LogLevel } from '../runtimes/util/loggingUtil' 2 | 3 | /** 4 | * The logging feature interface 5 | */ 6 | export type Logging = { 7 | error: (message: string) => void 8 | warn: (message: string) => void 9 | info: (message: string) => void 10 | log: (message: string) => void 11 | debug: (message: string) => void 12 | } 13 | -------------------------------------------------------------------------------- /runtimes/runtimes/webworker.ts: -------------------------------------------------------------------------------- 1 | import { BrowserMessageReader, BrowserMessageWriter } from 'vscode-languageserver/browser' 2 | 3 | import { RuntimeProps } from './runtime' 4 | import { baseRuntime } from './base-runtime' 5 | 6 | declare const self: WindowOrWorkerGlobalScope 7 | 8 | export const webworker = baseRuntime({ reader: new BrowserMessageReader(self), writer: new BrowserMessageWriter(self) }) 9 | -------------------------------------------------------------------------------- /runtimes/protocol/README.md: -------------------------------------------------------------------------------- 1 | ## AWS Language Servers Runtimes Protocol 2 | 3 | Implementation of AWS Language Servers Runtimes Protocol, which defines a protocol for communication between Servers and Clients. 4 | It is modelled after LSP, and provides several custom extensions to enable custom AWS Language Server Runtimes features. 5 | 6 | Protocol specification can be found in main [README](../README.md). 7 | -------------------------------------------------------------------------------- /runtimes/protocol/didChangeDependencyPaths.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DID_CHANGE_DEPENDENCY_PATHS_NOTIFICATION_METHOD, 3 | DidChangeDependencyPathsParams, 4 | ProtocolNotificationType, 5 | } from './lsp' 6 | 7 | export const didChangeDependencyPathsNotificationType = new ProtocolNotificationType< 8 | DidChangeDependencyPathsParams, 9 | void 10 | >(DID_CHANGE_DEPENDENCY_PATHS_NOTIFICATION_METHOD) 11 | -------------------------------------------------------------------------------- /tsconfig.packages.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "target": "ES2021", 5 | "declaration": true, 6 | "sourceMap": true, 7 | "skipLibCheck": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "esModuleInterop": true 11 | }, 12 | "exclude": ["**/node_modules", "**/out"] 13 | } 14 | -------------------------------------------------------------------------------- /runtimes/server-interface/index.ts: -------------------------------------------------------------------------------- 1 | export { Server } from './server' 2 | export * from './auth' 3 | export { Logging } from './logging' 4 | export * from './lsp' 5 | export * from './telemetry' 6 | export { Workspace } from './workspace' 7 | export { Chat } from './chat' 8 | export * from './runtime' 9 | export * from './identity-management' 10 | export * from './notification' 11 | export * from './sdk-initializator' 12 | export * from './agent' 13 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Problem 2 | 3 | ## Solution 4 | 5 | 13 | 14 | ## License 15 | 16 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. 17 | -------------------------------------------------------------------------------- /runtimes/protocol/getConfigurationFromServer.ts: -------------------------------------------------------------------------------- 1 | import { AutoParameterStructuresProtocolRequestType, LSPAny } from './lsp' 2 | 3 | export const getConfigurationFromServerRequestType = new AutoParameterStructuresProtocolRequestType< 4 | GetConfigurationFromServerParams, 5 | LSPAny, 6 | never, 7 | void, 8 | void 9 | >('aws/getConfigurationFromServer') 10 | 11 | export interface GetConfigurationFromServerParams { 12 | section: string 13 | } 14 | -------------------------------------------------------------------------------- /runtimes/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.packages.json", 3 | "compilerOptions": { 4 | "esModuleInterop": true, 5 | "resolveJsonModule": true, 6 | "rootDir": ".", 7 | "outDir": "./out" 8 | }, 9 | "include": [ 10 | "protocol", 11 | "runtimes", 12 | "server-interface", 13 | "testing", 14 | "runtimes/operational-telemetry/telemetry-schemas/telemetry-schema.json" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: 'npm' 7 | directory: '/' 8 | schedule: 9 | interval: 'weekly' 10 | commit-message: 11 | prefix: 'chore' 12 | ignore: 13 | - dependency-name: 'jose' 14 | versions: ['>=6.0.0'] 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.preferences.quoteStyle": "single", 3 | "typescript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": false, 4 | "[typescript]": { 5 | "editor.insertSpaces": true, 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll.eslint": "explicit", 8 | "source.organizeImports": "never", 9 | "source.sortMembers": "never" 10 | }, 11 | "editor.formatOnSave": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /runtimes/server-interface/README.md: -------------------------------------------------------------------------------- 1 | ## AWS Language Server types 2 | 3 | This module contains types, that define interfaces of features available to Server implementors and used by AWS Runtimes to provide it's features. 4 | 5 | Server capabilities uses on Runtimes Protocol as a source of truth for types that used in both Server interface and Runtime Protocol. 6 | Some features in Server interface pass payload received from Client unmodified, e.g. in LSP feature. 7 | 8 | See main [README](../README.md) for more details. -------------------------------------------------------------------------------- /runtimes/protocol/index.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | export * from './auth' 7 | export * from './chat' 8 | export * from './didChangeDependencyPaths' 9 | export * from './getConfigurationFromServer' 10 | export * from './identity-management' 11 | export * from './lsp' 12 | export * from './notification' 13 | export * from './telemetry' 14 | export * from './configuration' 15 | export * from './window' 16 | export * from './workspace' 17 | -------------------------------------------------------------------------------- /runtimes/runtimes/runtime.ts: -------------------------------------------------------------------------------- 1 | import { Server } from '../server-interface' 2 | 3 | /** 4 | * Properties used for runtime initialisation 5 | */ 6 | export type RuntimeProps = { 7 | /** 8 | * Version of the build artifact resulting from initialising the runtime with the list of servers 9 | */ 10 | version?: string 11 | /** 12 | * The list of servers to initialize and run 13 | */ 14 | servers: Server[] 15 | /** 16 | * Name of the server used inside the runtime 17 | */ 18 | name: string 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "rootDir": "./src", 10 | "outDir": "./out", 11 | "declaration": true 12 | }, 13 | "files": [], 14 | "references": [ 15 | { 16 | "path": "./types" 17 | }, 18 | { 19 | "path": "./runtimes" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /runtimes/protocol/editCompletions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InlineCompletionItem, 3 | InlineCompletionList, 4 | InlineCompletionParams, 5 | InlineCompletionRegistrationOptions, 6 | } from './lsp' 7 | 8 | import { ProtocolRequestType } from 'vscode-languageserver-protocol' 9 | 10 | export const editCompletionRequestType = new ProtocolRequestType< 11 | InlineCompletionParams, 12 | InlineCompletionList | InlineCompletionItem[] | null, 13 | InlineCompletionItem[], 14 | void, 15 | InlineCompletionRegistrationOptions 16 | >('aws/textDocument/editCompletion') 17 | -------------------------------------------------------------------------------- /runtimes/server-interface/notification.ts: -------------------------------------------------------------------------------- 1 | import { NotificationParams, NotificationFollowupParams, NotificationHandler } from '../protocol' 2 | 3 | /* 4 | * The notification feature interface. To use the feature: 5 | * - Server must define "serverInfo" in initialize result of @type {PartialInitializeResult}. 6 | * - Notifications must contain id in @type {NotificationParams} 7 | */ 8 | export type Notification = { 9 | showNotification: (params: NotificationParams) => void 10 | onNotificationFollowup: (handler: NotificationHandler) => void 11 | } 12 | -------------------------------------------------------------------------------- /types/README.md: -------------------------------------------------------------------------------- 1 | # Language Server Runtimes Types 2 | 3 | Language Server Runtimes Types is a package containing types used by the [Language Server Runtimes](../runtimes/) package. 4 | 5 | This package is independent of [VSCode Protocol](https://github.com/microsoft/vscode-languageserver-node/tree/main/protocol) type definitions. 6 | Interfaces defined here must contain only type definitions with no implementations or side effects. For example, it should not re-export classes from `vscode-languageserver-protocol` or `vscode-jsonrpc` packages. 7 | 8 | ## License 9 | 10 | This project is licensed under the Apache-2.0 License. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for the server runtimes 4 | labels: feature-request 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | 9 | 10 | 11 | **Describe the solution you'd like** 12 | 13 | 14 | 15 | **Describe alternatives you've considered** 16 | 17 | 18 | 19 | **Additional context** 20 | 21 | 22 | -------------------------------------------------------------------------------- /runtimes/protocol/inlineCompletions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InlineCompletionItem, 3 | InlineCompletionList, 4 | InlineCompletionParams, 5 | InlineCompletionRegistrationOptions, 6 | } from './lsp' 7 | 8 | import { ProtocolRequestType } from 'vscode-languageserver-protocol' 9 | 10 | /** 11 | * inlineCompletionRequestType defines the custom method that the language client 12 | * requests from the server to provide inline completion recommendations. 13 | */ 14 | export const inlineCompletionRequestType = new ProtocolRequestType< 15 | InlineCompletionParams, 16 | InlineCompletionList | InlineCompletionItem[] | null, 17 | InlineCompletionItem[], 18 | void, 19 | InlineCompletionRegistrationOptions 20 | >('aws/textDocument/inlineCompletion') 21 | -------------------------------------------------------------------------------- /.github/workflows/license-check.yaml: -------------------------------------------------------------------------------- 1 | name: License Check 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | jobs: 8 | license-check: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: Set up Node.js 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: '24' 18 | 19 | - name: Install dependencies 20 | run: | 21 | npm install -g license-checker 22 | 23 | - name: Run license check 24 | run: | 25 | #!/bin/bash 26 | set -euo pipefail 27 | 28 | EXCLUDED_LICENSES="MIT,Apache-2.0,BSD-2-Clause,BSD-3-Clause,ISC,0BSD,Python-2.0,BlueOak-1.0.0" 29 | 30 | license-checker --production --exclude "$EXCLUDED_LICENSES" --excludePackages "*" 31 | -------------------------------------------------------------------------------- /types/auth.ts: -------------------------------------------------------------------------------- 1 | export type IamCredentials = { 2 | readonly accessKeyId: string 3 | readonly secretAccessKey: string 4 | readonly sessionToken?: string 5 | readonly expiration?: Date 6 | } 7 | 8 | export type BearerCredentials = { 9 | readonly token: string 10 | } 11 | 12 | export interface SsoProfileData { 13 | startUrl?: string 14 | } 15 | 16 | export interface ConnectionMetadata { 17 | sso?: SsoProfileData 18 | } 19 | 20 | export interface UpdateCredentialsParams { 21 | // Plaintext Credentials (for browser based environments) or encrypted JWT token 22 | data: IamCredentials | BearerCredentials | string 23 | metadata?: ConnectionMetadata 24 | // If the payload is encrypted 25 | // Defaults to false if undefined or null 26 | encrypted?: boolean 27 | } 28 | -------------------------------------------------------------------------------- /runtimes/server-interface/auth.ts: -------------------------------------------------------------------------------- 1 | import { IamCredentials, BearerCredentials, ConnectionMetadata } from '../protocol' 2 | 3 | // Exports for Capability implementor 4 | export { IamCredentials, BearerCredentials, ConnectionMetadata } 5 | 6 | export type CredentialsType = 'iam' | 'bearer' 7 | export type Credentials = IamCredentials | BearerCredentials 8 | export type SsoConnectionType = 'builderId' | 'identityCenter' | 'external_idp' | 'none' 9 | 10 | export interface CredentialsProvider { 11 | hasCredentials: (type: CredentialsType) => boolean 12 | getCredentials: (type: CredentialsType) => Credentials | undefined 13 | getConnectionMetadata: () => ConnectionMetadata | undefined 14 | getConnectionType: () => SsoConnectionType 15 | onCredentialsDeleted: (handler: (type: CredentialsType) => void) => void 16 | } 17 | -------------------------------------------------------------------------------- /runtimes/server-interface/telemetry.ts: -------------------------------------------------------------------------------- 1 | import { NotificationHandler } from 'vscode-languageserver-protocol' 2 | 3 | export type Metric = { 4 | name: string 5 | } 6 | 7 | export type MetricEvent = Metric & { 8 | data?: any 9 | result?: ResultType 10 | errorData?: ErrorData 11 | } 12 | 13 | export type BusinessMetricEvent = Metric & { 14 | // TODO: define more 15 | } 16 | 17 | type ResultType = 'Succeeded' | 'Failed' | 'Cancelled' 18 | 19 | type ErrorData = { 20 | reason: string 21 | errorCode?: string 22 | httpStatusCode?: number 23 | } 24 | 25 | /** 26 | * The telemetry feature interface. 27 | */ 28 | export type Telemetry = { 29 | emitMetric: (metric: MetricEvent) => void 30 | // Handles telemetry events sent from clients 31 | onClientTelemetry: (handler: NotificationHandler) => void 32 | } 33 | -------------------------------------------------------------------------------- /runtimes/runtimes/util/sharedConfigFile.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * The implementation is inspired by https://github.com/aws/aws-toolkit-vscode/blob/2c8d6667ec45f747db25f456a524e50242ff454b/packages/core/src/auth/credentials/sharedCredentialsFile.ts#L44 6 | */ 7 | 8 | import * as fs from 'fs' 9 | import * as path from 'path' 10 | import * as os from 'os' 11 | 12 | export function checkAWSConfigFile(): boolean { 13 | const awsConfigFile = process.env.AWS_CONFIG_FILE 14 | if (awsConfigFile) { 15 | return fs.existsSync(awsConfigFile) 16 | } else { 17 | const homedir = os.homedir() 18 | const defaultConfigPath = path.join(homedir, '.aws', 'config') 19 | return fs.existsSync(defaultConfigPath) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /runtimes/runtimes/util/standalone/proxyUtil.ts: -------------------------------------------------------------------------------- 1 | import { Workspace } from '../../../server-interface' 2 | 3 | import { HttpsProxyAgent } from 'hpagent' 4 | import { NodeHttpHandler } from '@smithy/node-http-handler' 5 | 6 | // proxy configuration for sdk v3 clients 7 | export const makeProxyConfigv3Standalone = (workspace: Workspace): NodeHttpHandler | undefined => { 8 | const proxyUrl = process.env.HTTPS_PROXY ?? process.env.https_proxy 9 | const certs = process.env.AWS_CA_BUNDLE ? [workspace.fs.readFileSync(process.env.AWS_CA_BUNDLE)] : undefined 10 | 11 | if (proxyUrl) { 12 | const agent = new HttpsProxyAgent({ 13 | proxy: proxyUrl, 14 | ca: certs, 15 | }) 16 | 17 | return new NodeHttpHandler({ 18 | httpAgent: agent, 19 | httpsAgent: agent, 20 | }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "compile", 7 | "group": "build", 8 | "presentation": { 9 | "panel": "dedicated", 10 | "reveal": "never" 11 | }, 12 | "problemMatcher": ["$tsc"] 13 | }, 14 | { 15 | "label": "watch", 16 | "type": "npm", 17 | "script": "watch", 18 | "isBackground": true, 19 | "group": { 20 | "kind": "build", 21 | "isDefault": true 22 | }, 23 | "presentation": { 24 | "panel": "dedicated", 25 | "reveal": "never" 26 | }, 27 | "problemMatcher": ["$tsc-watch"] 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /chat-client-ui-types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@aws/chat-client-ui-types", 3 | "version": "0.1.69", 4 | "description": "Type definitions for Chat UIs in Language Servers and Runtimes for AWS", 5 | "main": "./out/index.js", 6 | "scripts": { 7 | "clean": "rm -rf out/", 8 | "compile": "tsc --build", 9 | "prepub:copyFiles": "shx cp ../LICENSE ../NOTICE ../SECURITY.md .", 10 | "prepub": "npm run clean && npm run compile && npm run prepub:copyFiles", 11 | "pub": "npm publish" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/aws/language-server-runtimes", 16 | "directory": "chat-client-ui-types" 17 | }, 18 | "author": "Amazon Web Services", 19 | "license": "Apache-2.0", 20 | "dependencies": { 21 | "@aws/language-server-runtimes-types": "^0.1.63" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /runtimes/script/generate-types.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process' 2 | import * as path from 'path' 3 | import { promisify } from 'util' 4 | 5 | const execAsync = promisify(exec) 6 | 7 | async function generateTypes() { 8 | try { 9 | const schemaDir = path.resolve(__dirname, '../runtimes/operational-telemetry/telemetry-schemas') 10 | const input = path.join(schemaDir, 'telemetry-schema.json') 11 | const output = path.join(schemaDir, '../types/generated/telemetry.d.ts') 12 | 13 | console.log('Generating TypeScript types from json schemas...') 14 | await execAsync(`json2ts -i "${input}" -o "${output}" --unreachableDefinitions`, { cwd: schemaDir }) 15 | console.log('Types generated successfully') 16 | } catch (error) { 17 | console.error('Error generating types:', error) 18 | process.exit(1) 19 | } 20 | } 21 | 22 | generateTypes() 23 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bootstrap-sha": "9f719b9fce8b79e75b8f0021527cb09b221368a5", 3 | "bump-minor-pre-major": true, 4 | "bump-patch-for-minor-pre-major": true, 5 | "always-link-local": false, 6 | "separate-pull-requests": false, 7 | "group-pull-request-title-pattern": "chore(release): release packages from branch ${branch}", 8 | "plugins": ["node-workspace"], 9 | "tag-separator": "/", 10 | "include-v-in-tag": true, 11 | "packages": { 12 | "chat-client-ui-types": { 13 | "component": "chat-client-ui-types" 14 | }, 15 | "runtimes": { 16 | "component": "language-server-runtimes" 17 | }, 18 | "types": { 19 | "component": "language-server-runtimes-types" 20 | } 21 | }, 22 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" 23 | } 24 | -------------------------------------------------------------------------------- /types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@aws/language-server-runtimes-types", 3 | "version": "0.1.63", 4 | "description": "Type definitions in Language Servers and Runtimes for AWS", 5 | "main": "out/index.js", 6 | "scripts": { 7 | "clean": "rm -rf out/", 8 | "compile": "tsc --build", 9 | "prepub:copyFiles": "shx cp ../LICENSE ../NOTICE ../SECURITY.md .", 10 | "prepub": "npm run clean && npm run compile && npm run prepub:copyFiles", 11 | "pub": "npm publish" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/aws/language-server-runtimes", 16 | "directory": "types" 17 | }, 18 | "author": "Amazon Web Services", 19 | "license": "Apache-2.0", 20 | "dependencies": { 21 | "vscode-languageserver-textdocument": "^1.0.12", 22 | "vscode-languageserver-types": "^3.17.5" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /runtimes/runtimes/util/lspCacheUtil.test.ts: -------------------------------------------------------------------------------- 1 | import { StubbedInstance, stubInterface } from 'ts-sinon' 2 | import assert from 'assert' 3 | import { LspRouter } from '../lsp/router/lspRouter' 4 | import { InitializeParams } from '../../protocol' 5 | import { getClientInitializeParamsHandlerFactory } from './lspCacheUtil' 6 | 7 | describe('getClientInitializeParamsFactory', () => { 8 | let lspRouterStub: StubbedInstance 9 | 10 | beforeEach(() => { 11 | lspRouterStub = stubInterface() 12 | }) 13 | 14 | it('returns the client params of the passed lsp router', () => { 15 | const expected: InitializeParams = { processId: 0, rootUri: 'some-root-uri', capabilities: {} } 16 | lspRouterStub.clientInitializeParams = expected 17 | 18 | const handler = getClientInitializeParamsHandlerFactory(lspRouterStub) 19 | 20 | assert.deepStrictEqual(handler(), expected) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /runtimes/server-interface/sdk-initializator.ts: -------------------------------------------------------------------------------- 1 | // aws sdk v3 clients constructor type 2 | export type SDKClientConstructorV3 = new (...[configuration]: [P] | []) => T 3 | 4 | /** 5 | * SDKInitializator type initializes AWS SDK v3 clients whose constructor and initial configurations are passed as input. 6 | * This type serves as a client factory that wraps SDK client constructors, and instantiates them at runtime, allowing additional runtime/environment-specific 7 | * configurations (e.g. proxy settings) to be injected at runtime. 8 | * 9 | * @template T - Type parameter for the aws sdk v3 client 10 | * @template P - Type parameter for the configurations options 11 | * 12 | * @example 13 | * const v3Client = sdkInitializator(MySdkV3Client, { 14 | * region: 'example_region', 15 | * endpoint: 'example_endpoint' 16 | * }); 17 | */ 18 | export type SDKInitializator = (Ctor: SDKClientConstructorV3, current_config: P) => T 19 | -------------------------------------------------------------------------------- /types/didChangeDependencyPaths.ts: -------------------------------------------------------------------------------- 1 | export const DID_CHANGE_DEPENDENCY_PATHS_NOTIFICATION_METHOD = 'aws/didChangeDependencyPaths' 2 | 3 | /** 4 | * Parameters for notifying AWS language server about dependency paths 5 | */ 6 | export interface DidChangeDependencyPathsParams { 7 | /** Name of the module being processed */ 8 | moduleName: string 9 | /** Programming language runtime (e.g., 'javascript', 'python', 'java') */ 10 | runtimeLanguage: string 11 | /** Absolute paths to dependency files and directories*/ 12 | paths: string[] 13 | /** 14 | * Glob patterns to include specific files/directories 15 | * Patterns should conform to https://github.com/isaacs/node-glob 16 | * @optional 17 | */ 18 | includePatterns?: string[] 19 | /** 20 | * Glob patterns to exclude specific files/directories 21 | * Patterns should conform to https://github.com/isaacs/node-glob 22 | * @optional 23 | */ 24 | excludePatterns?: string[] 25 | } 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a bug report to help us improve the server runtimes 4 | --- 5 | 6 | **Describe the bug** 7 | 8 | 9 | 10 | **To reproduce** 11 | 12 | 13 | 14 | 15 | 16 | 17 | **Expected behavior** 18 | 19 | 20 | 21 | **Screenshots** 22 | 23 | 24 | 25 | **Your Environment** 26 | 27 | 28 | 29 | - OS: 30 | 31 | **Additional context** 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /runtimes/server-interface/runtime.ts: -------------------------------------------------------------------------------- 1 | export type ServerInfo = { 2 | name: string 3 | version?: string 4 | } 5 | 6 | export type Platform = NodeJS.Platform | 'browser' 7 | 8 | /** 9 | * The Runtime feature interface. 10 | */ 11 | export interface Runtime { 12 | /** 13 | * Information about runtime server, set in runtime props at build time. 14 | */ 15 | serverInfo: ServerInfo 16 | 17 | /** 18 | * Platform where the runtime is running. 19 | * Set to NodeJS.Platform for standalone and 'browser' for webworker runtime. 20 | */ 21 | platform: Platform 22 | 23 | /** 24 | * Get a runtime configuration value. 25 | * @param key The configuration key to retrieve. 26 | * @returns The configuration value or undefined if the key is not set. 27 | */ 28 | getConfiguration(key: string): string | undefined 29 | 30 | /** 31 | * Get the ATX credentials provider. 32 | * @returns The ATX credentials provider. 33 | */ 34 | getAtxCredentialsProvider(): any 35 | } 36 | -------------------------------------------------------------------------------- /runtimes/runtimes/encoding.ts: -------------------------------------------------------------------------------- 1 | export interface Encoding { 2 | decode(value: string): string 3 | encode(value: string): string 4 | } 5 | 6 | const HEX_PAD: string = '00' 7 | const HEX_REGEX: RegExp = /%([0-9A-F]{2})/g 8 | export class WebBase64Encoding implements Encoding { 9 | constructor(private window: WindowOrWorkerGlobalScope) {} 10 | 11 | decode(value: string): string { 12 | const decoded = this.window.atob(value) 13 | // to support Unicode chars 14 | return decodeURIComponent( 15 | Array.from(decoded) 16 | .map(char => { 17 | return '%' + (HEX_PAD + (char as string).charCodeAt(0).toString(16)).slice(-2) 18 | }) 19 | .join('') 20 | ) 21 | } 22 | 23 | encode(value: string): string { 24 | // to support Unicode chars 25 | const converted = encodeURIComponent(value).replace(HEX_REGEX, (_, arg) => { 26 | return String.fromCharCode(parseInt(arg, 16)) 27 | }) 28 | return this.window.btoa(converted) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/new_pr.yml: -------------------------------------------------------------------------------- 1 | name: Publish a notification to Slack 2 | on: 3 | pull_request: 4 | branches: [main] 5 | types: [opened, reopened, ready_for_review, synchronize] 6 | jobs: 7 | notify: 8 | name: Slack notification 9 | if: github.event.pull_request.user.login != 'dependabot[bot]' && github.event.pull_request.draft == false && github.event.pull_request.head.repo.fork == false 10 | runs-on: [ubuntu-latest] 11 | steps: 12 | - name: Post message 13 | uses: slackapi/slack-github-action@v1.25.0 14 | env: 15 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} 16 | with: 17 | payload: | 18 | { 19 | "title": ${{ toJson(github.event.pull_request.title) }}, 20 | "author": ${{ toJson(github.event.pull_request.user.login) }}, 21 | "link": ${{ toJson(github.event.pull_request.html_url) }}, 22 | "repository": "Language-server-runtimes" 23 | } 24 | -------------------------------------------------------------------------------- /runtimes/protocol/auth.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AutoParameterStructuresProtocolRequestType, 3 | ConnectionMetadata, 4 | ProtocolNotificationType0, 5 | ProtocolRequestType0, 6 | UpdateCredentialsParams, 7 | } from './lsp' 8 | 9 | export const iamCredentialsUpdateRequestType = new AutoParameterStructuresProtocolRequestType< 10 | UpdateCredentialsParams, 11 | null, 12 | void, 13 | void, 14 | void 15 | >('aws/credentials/iam/update') 16 | 17 | export const iamCredentialsDeleteNotificationType = new ProtocolNotificationType0('aws/credentials/iam/delete') 18 | 19 | export const bearerCredentialsUpdateRequestType = new AutoParameterStructuresProtocolRequestType< 20 | UpdateCredentialsParams, 21 | null, 22 | void, 23 | void, 24 | void 25 | >('aws/credentials/token/update') 26 | 27 | export const bearerCredentialsDeleteNotificationType = new ProtocolNotificationType0( 28 | 'aws/credentials/token/delete' 29 | ) 30 | 31 | export const getConnectionMetadataRequestType = new ProtocolRequestType0( 32 | 'aws/credentials/getConnectionMetadata' 33 | ) 34 | -------------------------------------------------------------------------------- /chat-client-ui-types/README.md: -------------------------------------------------------------------------------- 1 | # Chat Client UI Types for AWS Language Server Runtimes 2 | 3 | This package provides type definitions for Chat UIs in Language Servers and Runtimes for AWS. 4 | 5 | The `@aws/chat-client-ui-types` package is a component of the AWS Language Server Runtimes ecosystem. It defines the type interfaces and contracts for chat client UI components, enabling seamless integration between the UI layer and the backend services of AWS language servers and runtimes. 6 | 7 | This package serves as a bridge between the chat client UI and the underlying language server functionality, ensuring type safety and consistency across the application. It provides a set of well-defined interfaces for various UI interactions, such as sending messages to the prompt, handling authentication follow-ups, executing generic commands, and managing chat options. 8 | 9 | ## Usage Instructions 10 | 11 | ### Installation 12 | 13 | To install the package, run the following command in your project directory: 14 | 15 | ```bash 16 | npm install @aws/chat-client-ui-types 17 | ``` 18 | 19 | ## License 20 | 21 | This project is licensed under the Apache-2.0 License. -------------------------------------------------------------------------------- /runtimes/runtimes/encoding.test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import { WebBase64Encoding } from './encoding' 3 | import sinon, { StubbedInstance, stubInterface } from 'ts-sinon' 4 | 5 | describe('WebBase64Encoding', () => { 6 | const wdw = {} as WindowOrWorkerGlobalScope 7 | const encoding = new WebBase64Encoding(wdw) 8 | 9 | it('encodes and decodes string', () => { 10 | const val = 'server1__1' 11 | const encodedVal = 'xxxx' 12 | wdw.btoa = sinon.stub().withArgs(val).returns(encodedVal) 13 | wdw.atob = sinon.stub().withArgs(encodedVal).returns(val) 14 | const valAfterEncoding = encoding.decode(encoding.encode(val)) 15 | assert.equal(valAfterEncoding, val) 16 | }) 17 | 18 | it('encodes and decodes unicode', () => { 19 | const val = 'Σ' 20 | const convertedVal = 'Σ' 21 | const encodedVal = 'xxxx' 22 | wdw.btoa = sinon.stub().withArgs(convertedVal).returns(encodedVal) 23 | wdw.atob = sinon.stub().withArgs(encodedVal).returns(convertedVal) 24 | const valAfterEncoding = encoding.decode(encoding.encode(val)) 25 | assert.equal(valAfterEncoding, val) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /runtimes/runtimes/README.md: -------------------------------------------------------------------------------- 1 | ## AWS Language Server Runtimes Implementation 2 | 3 | Implementation of AWS Language Server runtimes and Runtimes features. 4 | 5 | Each runtime implements a LSP server and opens communication channel with client over LSP connection. Runtime initialised all registered Capabilities. 6 | 7 | Runtime sets up message passing, that translates Runtimes Protocol messages to a function calls to Capabilities features, defined in Server and server features interfaces. 8 | 9 | Runtime implementation acts as a intermediate layer between Runtime Client and a Runtime Servers, injected into runtime at build time. 10 | The runtime implements message passing between Client application and injected Servers, and interface with both by predefined APIs: 11 | 12 | - **Runtime Protocol**: a protocol to define communication between Runtime and Client application (e.g. Runtime<->AWS Toolkit extension). It uses LSP (and JSON-RPC) connection as a transport. 13 | - **Runtime Server Interface**: defines an interface of the Server and features exposed to Runtime Server developers (e.g. Runtime<->AWS CodeWhisperer server). 14 | 15 | See main project [README](../README.md) for more detailed explanation of the architecture. 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Attach to AWS Documents Language Server", 5 | "type": "node", 6 | "request": "attach", 7 | "port": 6012, // Hard defined in the vscode client activation.ts 8 | "outFiles": ["${workspaceFolder}/**/out/**/*.js"], 9 | "restart": { 10 | "maxAttempts": 10, 11 | "delay": 1000 12 | } 13 | }, 14 | { 15 | "type": "node", 16 | "request": "launch", 17 | "preLaunchTask": "watch", 18 | "name": "Unit Tests", 19 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", 20 | "args": ["${workspaceRoot}/**/out/**/*.test.js"], 21 | "cwd": "${workspaceRoot}" 22 | }, 23 | { 24 | "type": "node", 25 | "request": "launch", 26 | "preLaunchTask": "watch", 27 | "name": "Unit Tests (Current File)", 28 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", 29 | "args": ["${workspaceRoot}/**/out/**/${fileBasenameNoExtension}.js"], 30 | "cwd": "${workspaceRoot}" 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /.github/workflows/runtimes-ci.yaml: -------------------------------------------------------------------------------- 1 | name: Language Server Runtime CI 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | 8 | jobs: 9 | test: 10 | name: Test 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Sync Code 14 | uses: actions/checkout@v4 15 | - name: Set up Node 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: 24 19 | - name: Build 20 | run: | 21 | npm ci 22 | npm run check:formatting 23 | npm run compile 24 | - name: Test 25 | run: | 26 | npm run test 27 | test-windows: 28 | name: Test (Windows) 29 | runs-on: windows-latest 30 | steps: 31 | - name: Sync Code 32 | uses: actions/checkout@v4 33 | - name: Set up Node 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version: 24 37 | - name: Build 38 | run: | 39 | npm ci 40 | npm run compile 41 | - name: Test 42 | run: | 43 | npm run test 44 | -------------------------------------------------------------------------------- /runtimes/protocol/window.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ProtocolRequestType, 3 | SHOW_SAVE_FILE_DIALOG_REQUEST_METHOD, 4 | ShowSaveFileDialogParams, 5 | ShowSaveFileDialogResult, 6 | ShowOpenDialogResult, 7 | ShowOpenDialogParams, 8 | SHOW_OPEN_FILE_DIALOG_REQUEST_METHOD, 9 | CHECK_DIAGNOSTICS_REQUEST_METHOD, 10 | CheckDiagnosticsParams, 11 | CheckDiagnosticsResult, 12 | } from './lsp' 13 | 14 | /** 15 | * The show message notification is sent from a server to a client to ask the client to display "Save File" dialog. 16 | * Server may indicate list of filetypes and default save path to show in the dialog. 17 | */ 18 | export const ShowSaveFileDialogRequestType = new ProtocolRequestType< 19 | ShowSaveFileDialogParams, 20 | ShowSaveFileDialogResult, 21 | never, 22 | void, 23 | void 24 | >(SHOW_SAVE_FILE_DIALOG_REQUEST_METHOD) 25 | 26 | export const ShowOpenDialogRequestType = new ProtocolRequestType< 27 | ShowOpenDialogParams, 28 | ShowOpenDialogResult, 29 | never, 30 | void, 31 | void 32 | >(SHOW_OPEN_FILE_DIALOG_REQUEST_METHOD) 33 | 34 | export const CheckDiagnosticsRequestType = new ProtocolRequestType< 35 | CheckDiagnosticsParams, 36 | CheckDiagnosticsResult, 37 | never, 38 | void, 39 | void 40 | >(CHECK_DIAGNOSTICS_REQUEST_METHOD) 41 | -------------------------------------------------------------------------------- /runtimes/runtimes/lsp/router/initializeUtils.ts: -------------------------------------------------------------------------------- 1 | import { InitializeParams, WorkspaceFolder } from 'vscode-languageserver-protocol' 2 | import { basenamePath } from '../../util/pathUtil' 3 | import { URI } from 'vscode-uri' 4 | import { RemoteConsole } from 'vscode-languageserver' 5 | 6 | export function getWorkspaceFoldersFromInit(console: RemoteConsole, params?: InitializeParams): WorkspaceFolder[] { 7 | if (!params) { 8 | return [] 9 | } 10 | 11 | if (params.workspaceFolders && params.workspaceFolders.length > 0) { 12 | return params.workspaceFolders 13 | } 14 | try { 15 | const getFolderName = (parsedUri: URI) => basenamePath(parsedUri.fsPath) || parsedUri.toString() 16 | 17 | if (params.rootUri) { 18 | const parsedUri = URI.parse(params.rootUri) 19 | const folderName = getFolderName(parsedUri) 20 | return [{ name: folderName, uri: params.rootUri }] 21 | } 22 | if (params.rootPath) { 23 | const parsedUri = URI.parse(params.rootPath) 24 | const folderName = getFolderName(parsedUri) 25 | return [{ name: folderName, uri: parsedUri.toString() }] 26 | } 27 | return [] 28 | } catch (error) { 29 | console.error(`Error occurred when determining workspace folders: ${error}`) 30 | return [] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /runtimes/runtimes/util/standalone/getProxySettings/getWindowsProxySettings.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Based on windows-system-proxy 1.0.0 (Apache-2.0). 3 | * https://github.com/httptoolkit/windows-system-proxy/blob/main/test/index.spec.ts 4 | */ 5 | import * as assert from 'assert' 6 | import { getWindowsSystemProxy } from './getWindowsProxySettings' 7 | 8 | describe('getWindowsSystemProxy', () => { 9 | it('can get the Windows system proxy', async function () { 10 | if (process.platform !== 'win32') return this.skip() 11 | 12 | const result = await getWindowsSystemProxy() 13 | assert.ok(result === undefined || (typeof result === 'object' && result !== null)) 14 | 15 | if (result) { 16 | assert.strictEqual(typeof result.proxyUrl, 'string') 17 | assert.ok(Array.isArray(result.noProxy)) 18 | } 19 | }) 20 | 21 | it('returns undefined on non-Windows platforms', async function () { 22 | if (process.platform === 'win32') return this.skip() 23 | 24 | const result = await getWindowsSystemProxy() 25 | assert.strictEqual(result, undefined) 26 | }) 27 | 28 | it('handles registry access failure gracefully', async function () { 29 | // This test verifies the function doesn't throw 30 | assert.doesNotThrow(async () => await getWindowsSystemProxy()) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /types/workspace.ts: -------------------------------------------------------------------------------- 1 | import { URI } from './lsp' 2 | 3 | export const SELECT_WORKSPACE_ITEM_REQUEST_METHOD = 'aws/selectWorkspaceItem' 4 | export const OPEN_FILE_DIFF_NOTIFICATION_METHOD = 'aws/openFileDiff' 5 | 6 | export const DID_COPY_FILE_NOTIFICATION_METHOD = 'aws/didCopyFile' 7 | export const DID_WRITE_FILE_NOTIFICATION_METHOD = 'aws/didWriteFile' 8 | export const DID_APPEND_FILE_NOTIFICATION_METHOD = 'aws/didAppendFile' 9 | export const DID_REMOVE_FILE_OR_DIRECTORY_NOTIFICATION_METHOD = 'aws/didRemoveFileOrDirectory' 10 | export const DID_CREATE_DIRECTORY_NOTIFICATION_METHOD = 'aws/didCreateDirectory' 11 | export const OPEN_WORKSPACE_FILE_REQUEST_METHOD = 'aws/openWorkspaceFile' 12 | 13 | export interface SelectWorkspaceItemParams { 14 | canSelectFolders: boolean 15 | canSelectFiles: boolean 16 | canSelectMany: boolean 17 | title?: string 18 | } 19 | export interface WorkspaceItem { 20 | uri: URI 21 | name?: string 22 | } 23 | export interface SelectWorkspaceItemResult { 24 | items: WorkspaceItem[] 25 | } 26 | 27 | export interface OpenFileDiffParams { 28 | originalFileUri: URI 29 | originalFileContent?: string 30 | isDeleted: boolean 31 | fileContent?: string 32 | } 33 | 34 | export interface CopyFileParams { 35 | oldPath: string 36 | newPath: string 37 | } 38 | 39 | export interface FileParams { 40 | path: string 41 | } 42 | 43 | export interface OpenWorkspaceFileParams { 44 | filePath: string 45 | makeActive?: boolean 46 | } 47 | 48 | export interface OpenWorkspaceFileResult { 49 | success: boolean 50 | } 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Language Server Runtimes 2 | 3 | This project (the "runtime") defines the runtime library for creating a fully working [language server](https://github.com/aws/language-servers/tree/main). The runtime provides the interface of the servers, and expose the features of the protocol defined through them. All AWS Language Servers created using this implementation are stored in the [language-servers repo](https://github.com/aws/language-servers/tree/main). 4 | 5 | ## Where things go 6 | 7 | - To create a new protocol or feature for all language servers: contribute to the [runtimes/](runtimes) package in this repo. 8 | - To create a new "capability" for a particular language, contribute to the [language-servers](https://github.com/aws/language-servers/tree/main) repo. 9 | 10 | ## Structure 11 | 12 | Monorepo 13 | 14 | - [runtimes/](runtimes) - library for creating fully working runtimes for language servers 15 | - [protocol/](runtimes/protocol) - LSP based protocol for communications between language servers and clients 16 | - [runtimes/](runtimes/runtimes) - implementation of runtime features for language servers 17 | - [server-interface/](runtimes/server-interface) - server type definition to create language servers 18 | - [types/](types) - type definitions for the runtimes 19 | 20 | ## Contributing 21 | 22 | - [How to contribute](CONTRIBUTING.md#contributing) 23 | 24 | ## Security 25 | 26 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 27 | 28 | ## License 29 | 30 | This project is licensed under the Apache-2.0 License. 31 | -------------------------------------------------------------------------------- /types/window.ts: -------------------------------------------------------------------------------- 1 | import { URI } from 'vscode-languageserver-types' 2 | 3 | export const SHOW_SAVE_FILE_DIALOG_REQUEST_METHOD = 'aws/showSaveFileDialog' 4 | export const SHOW_OPEN_FILE_DIALOG_REQUEST_METHOD = 'aws/showOpenFileDialog' 5 | export const CHECK_DIAGNOSTICS_REQUEST_METHOD = 'aws/checkDiagnostics' 6 | export interface ShowSaveFileDialogParams { 7 | // Using untyped string to avoid locking this too strictly. 8 | // TODO: Migrate to LanguageKind when it is released in 3.18.0 9 | // https://github.com/microsoft/vscode-languageserver-node/blob/main/types/src/main.ts#L1890-L1895 10 | supportedFormats?: string[] 11 | defaultUri?: URI 12 | } 13 | 14 | export interface ShowSaveFileDialogResult { 15 | targetUri: URI 16 | } 17 | 18 | // bridge to consume ide's api 19 | export interface ShowOpenDialogParams { 20 | canSelectFiles?: boolean 21 | canSelectFolders?: boolean 22 | canSelectMany?: boolean 23 | filters?: { [key: string]: string[] } 24 | defaultUri?: URI 25 | title?: string 26 | } 27 | 28 | export interface ShowOpenDialogResult { 29 | uris: URI[] 30 | } 31 | 32 | export interface DiagnosticInfo { 33 | range: { 34 | start: { line: number; character: number } 35 | end: { line: number; character: number } 36 | } 37 | severity?: number 38 | message: string 39 | source?: string 40 | code?: string | number 41 | } 42 | 43 | export interface CheckDiagnosticsParams { 44 | fileDiagnostics: Record 45 | } 46 | 47 | export interface CheckDiagnosticsResult { 48 | fileDiagnostics: Record 49 | } 50 | -------------------------------------------------------------------------------- /runtimes/runtimes/util/loggingUtil.ts: -------------------------------------------------------------------------------- 1 | import { Logging } from '../../server-interface' 2 | import { Connection, MessageType } from 'vscode-languageserver/node' 3 | 4 | const LOG_LEVELS = { 5 | error: MessageType.Error, 6 | warn: MessageType.Warning, 7 | info: MessageType.Info, 8 | log: MessageType.Log, 9 | debug: MessageType.Debug, 10 | } as const 11 | 12 | export type LogLevel = keyof typeof LOG_LEVELS 13 | 14 | export const isLogLevelEnabled = (l1: LogLevel, l2: LogLevel): boolean => { 15 | return LOG_LEVELS[l1] >= LOG_LEVELS[l2] 16 | } 17 | 18 | export const isValidLogLevel = (level: LogLevel): boolean => { 19 | return Object.keys(LOG_LEVELS).includes(level) 20 | } 21 | 22 | export const DEFAULT_LOG_LEVEL: LogLevel = 'log' 23 | 24 | export class DefaultLogger implements Logging { 25 | public level: LogLevel 26 | private lspConnection: Connection 27 | 28 | constructor(level: LogLevel, connection: Connection) { 29 | this.level = level 30 | this.lspConnection = connection 31 | } 32 | 33 | sendToLog(logLevel: LogLevel, message: string): void { 34 | if (isLogLevelEnabled(this.level, logLevel)) { 35 | this.lspConnection.console[logLevel](`[${new Date().toISOString()}] lserver: ${message}`) 36 | } 37 | } 38 | 39 | error = (message: string) => this.sendToLog('error', message) 40 | warn = (message: string) => this.sendToLog('warn', message) 41 | info = (message: string) => this.sendToLog('info', message) 42 | log = (message: string) => this.sendToLog('log', message) 43 | debug = (message: string) => this.sendToLog('debug', message) 44 | } 45 | -------------------------------------------------------------------------------- /runtimes/runtimes/util/standalone/getProxySettings/getMacProxySettings.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Based on mac-system-proxy 1.0.2 (Apache-2.0). Modified for synchronous use 3 | * https://github.com/httptoolkit/mac-system-proxy/blob/main/test/index.spec.ts 4 | */ 5 | import * as assert from 'assert' 6 | import { getMacSystemProxy } from './getMacProxySettings' 7 | 8 | describe('getMacSystemProxy', () => { 9 | it('can get the Mac system proxy', function () { 10 | if (process.platform !== 'darwin') return this.skip() 11 | 12 | const result = getMacSystemProxy() 13 | assert.ok(result === undefined || (typeof result === 'object' && result !== null)) 14 | 15 | if (result) { 16 | assert.strictEqual(typeof result.proxyUrl, 'string') 17 | assert.ok(Array.isArray(result.noProxy)) 18 | } 19 | }) 20 | 21 | it('returns undefined on non-Mac platforms', function () { 22 | if (process.platform === 'darwin') return this.skip() 23 | 24 | const result = getMacSystemProxy() 25 | assert.strictEqual(result, undefined) 26 | }) 27 | 28 | it('handles scutil command failure gracefully', function () { 29 | // This test verifies the function doesn't throw 30 | assert.doesNotThrow(() => getMacSystemProxy()) 31 | }) 32 | 33 | it('returns undefined when no proxy is configured', function () { 34 | if (process.platform !== 'darwin') return this.skip() 35 | 36 | const result = getMacSystemProxy() 37 | if (result) { 38 | assert.strictEqual(typeof result.proxyUrl, 'string') 39 | assert.ok(Array.isArray(result.noProxy)) 40 | } 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /runtimes/runtimes/versioning.test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import sinon, { SinonStub } from 'sinon' 3 | import { handleVersionArgument } from './versioning' 4 | 5 | describe('handleVersionArgument', () => { 6 | let processExitStub: SinonStub 7 | let consoleLogStub: SinonStub 8 | const version = '1.0.0' 9 | 10 | beforeEach(() => { 11 | processExitStub = sinon.stub(process, 'exit') 12 | consoleLogStub = sinon.stub(console, 'log') 13 | }) 14 | 15 | afterEach(() => { 16 | processExitStub.restore() 17 | consoleLogStub.restore() 18 | }) 19 | 20 | it('should log the version and exit when --version is in process.argv', () => { 21 | process.argv = ['node', 'script.js', '--version'] 22 | 23 | handleVersionArgument(version) 24 | 25 | assert.strictEqual(consoleLogStub.calledOnceWithExactly(version), true) 26 | assert.strictEqual(processExitStub.calledOnceWithExactly(0), true) 27 | }) 28 | 29 | it('should log the version and exit when -v is in process.argv', () => { 30 | process.argv = ['node', 'script.js', '-v'] 31 | 32 | handleVersionArgument(version) 33 | 34 | assert.strictEqual(consoleLogStub.calledOnceWithExactly(version), true) 35 | assert.strictEqual(processExitStub.calledOnceWithExactly(0), true) 36 | }) 37 | 38 | it('should do nothing when neither --version nor -v is in process.argv', () => { 39 | process.argv = ['node', 'script.js', '--stdio'] 40 | 41 | handleVersionArgument(version) 42 | 43 | assert.strictEqual(consoleLogStub.called, false) 44 | assert.strictEqual(processExitStub.called, false) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /runtimes/protocol/configuration.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { LSPAny, ProtocolRequestType, ResponseError } from './lsp' 7 | 8 | /** 9 | * LSP Extension: Configuration Update Support 10 | * 11 | * This module implements an LSP extension that provides a push-based configuration 12 | * update mechanism with synchronous server response. This extends the standard LSP 13 | * configuration model to support scenarios requiring immediate confirmation of 14 | * configuration changes. 15 | * 16 | * The extension defines: 17 | * - UpdateConfigurationParams interface for specifying section and settings to update 18 | * - Protocol request type for configuration updates 19 | * 20 | * @remarks 21 | * This extension should only be used in limited scenarios where immediate server 22 | * response is required to maintain stable UX flow. For routine configuration updates, 23 | * the standard LSP configuration model using workspace/didChangeConfiguration should 24 | * be used instead. 25 | * 26 | * Example usage: 27 | * ```typescript 28 | * // Client-side request 29 | * const params: UpdateConfigurationParams = { 30 | * section: "aws.amazonq", 31 | * settings: { setting1: "value1" } 32 | * }; 33 | * await client.sendRequest(updateConfigurationRequestType, params); 34 | * ``` 35 | */ 36 | export interface UpdateConfigurationParams { 37 | section: string 38 | settings: LSPAny 39 | } 40 | 41 | export const updateConfigurationRequestType = new ProtocolRequestType< 42 | UpdateConfigurationParams, 43 | null, 44 | never, 45 | ResponseError, 46 | void 47 | >('aws/updateConfiguration') 48 | -------------------------------------------------------------------------------- /runtimes/server-interface/server.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Logging, 3 | Lsp, 4 | Telemetry, 5 | Workspace, 6 | CredentialsProvider, 7 | Chat, 8 | Runtime, 9 | Notification, 10 | SDKInitializator, 11 | Agent, 12 | } from '.' 13 | import { IdentityManagement } from './identity-management' 14 | 15 | /** 16 | * Servers are used to provide features to the client. 17 | * 18 | * Servers can make use of the {CredentialsProvider}, {Lsp}, {Workspace}, {Logging}, {Telemetry} and other features 19 | * to implement their functionality. Servers are notexpected to perform actions when their method 20 | * is called, but instead to set up listeners, event handlers, etc to handle. 21 | * 22 | * {CredentialsProvider}}, {Lsp}, and {Workspace} features may be initialized asynchronously, and can be empty until initialization 23 | * is completed and the client or user provides the necessary information. It's up to Server implementations to 24 | * either wait for the content to become available, or to gracefully handle cases where content is not yet available. 25 | * 26 | * The main use case for Servers is to listen to {Lsp} events and respond to these appropriately. 27 | * 28 | * @returns A function that will be called when the client exits, used to dispose of any held resources. 29 | */ 30 | export type Server = (features: Features) => () => void 31 | 32 | export type Features = { 33 | chat: Chat 34 | credentialsProvider: CredentialsProvider 35 | lsp: Lsp 36 | workspace: Workspace 37 | logging: Logging 38 | telemetry: Telemetry 39 | runtime: Runtime 40 | identityManagement: IdentityManagement 41 | notification: Notification 42 | sdkInitializator: SDKInitializator 43 | agent: Agent 44 | atxCredentialsProvider?: CredentialsProvider 45 | } 46 | -------------------------------------------------------------------------------- /runtimes/runtimes/lsp/router/loggingServer.ts: -------------------------------------------------------------------------------- 1 | import { InitializeParams, InitializeResult, Logging } from '../../../server-interface' 2 | import { LspServer } from './lspServer' 3 | import { Connection } from 'vscode-languageserver/node' 4 | import { Encoding } from '../../encoding' 5 | import { DEFAULT_LOG_LEVEL, isValidLogLevel, DefaultLogger, LogLevel } from '../../util/loggingUtil' 6 | 7 | export class LoggingServer { 8 | private logger: DefaultLogger 9 | private lspServer: LspServer 10 | 11 | constructor( 12 | private lspConnection: Connection, 13 | private encoding: Encoding 14 | ) { 15 | this.logger = new DefaultLogger(DEFAULT_LOG_LEVEL as LogLevel, this.lspConnection) 16 | this.lspServer = new LspServer(this.lspConnection, this.encoding, this.logger) 17 | this.lspServer.setInitializeHandler(async (params: InitializeParams): Promise => { 18 | this.updateLoggingLevel(params.initializationOptions?.logLevel ?? ('log' as LogLevel)) 19 | return { 20 | capabilities: {}, 21 | } 22 | }) 23 | this.lspServer.setDidChangeConfigurationHandler(async params => { 24 | const logLevelConfig = await lspConnection.workspace.getConfiguration({ 25 | section: 'aws.logLevel', 26 | }) 27 | if (isValidLogLevel(logLevelConfig)) { 28 | this.updateLoggingLevel(logLevelConfig as LogLevel) 29 | } 30 | }) 31 | } 32 | 33 | private updateLoggingLevel(logLevel: LogLevel) { 34 | this.logger.level = logLevel 35 | this.logger.info(`Logging level changed to ${logLevel}`) 36 | } 37 | 38 | public getLoggingObject(): Logging { 39 | return this.logger 40 | } 41 | 42 | public getLspServer(): LspServer { 43 | return this.lspServer 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /runtimes/protocol/workspace.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CopyFileParams, 3 | DID_APPEND_FILE_NOTIFICATION_METHOD, 4 | DID_COPY_FILE_NOTIFICATION_METHOD, 5 | DID_CREATE_DIRECTORY_NOTIFICATION_METHOD, 6 | DID_REMOVE_FILE_OR_DIRECTORY_NOTIFICATION_METHOD, 7 | DID_WRITE_FILE_NOTIFICATION_METHOD, 8 | FileParams, 9 | OPEN_FILE_DIFF_NOTIFICATION_METHOD, 10 | OpenFileDiffParams, 11 | OPEN_WORKSPACE_FILE_REQUEST_METHOD, 12 | OpenWorkspaceFileParams, 13 | OpenWorkspaceFileResult, 14 | ProtocolNotificationType, 15 | ProtocolRequestType, 16 | SELECT_WORKSPACE_ITEM_REQUEST_METHOD, 17 | SelectWorkspaceItemParams, 18 | SelectWorkspaceItemResult, 19 | } from './lsp' 20 | 21 | export const selectWorkspaceItemRequestType = new ProtocolRequestType< 22 | SelectWorkspaceItemParams, 23 | SelectWorkspaceItemResult, 24 | never, 25 | void, 26 | void 27 | >(SELECT_WORKSPACE_ITEM_REQUEST_METHOD) 28 | 29 | export const openFileDiffNotificationType = new ProtocolNotificationType( 30 | OPEN_FILE_DIFF_NOTIFICATION_METHOD 31 | ) 32 | 33 | export const didCopyFileNotificationType = new ProtocolNotificationType( 34 | DID_COPY_FILE_NOTIFICATION_METHOD 35 | ) 36 | 37 | export const didRemoveFileOrDirNotificationType = new ProtocolNotificationType( 38 | DID_REMOVE_FILE_OR_DIRECTORY_NOTIFICATION_METHOD 39 | ) 40 | 41 | export const didWriteFileNotificationType = new ProtocolNotificationType( 42 | DID_WRITE_FILE_NOTIFICATION_METHOD 43 | ) 44 | 45 | export const didAppendFileNotificationType = new ProtocolNotificationType( 46 | DID_APPEND_FILE_NOTIFICATION_METHOD 47 | ) 48 | 49 | export const didCreateDirectoryNotificationType = new ProtocolNotificationType( 50 | DID_CREATE_DIRECTORY_NOTIFICATION_METHOD 51 | ) 52 | 53 | export const openWorkspaceFileRequestType = new ProtocolRequestType< 54 | OpenWorkspaceFileParams, 55 | OpenWorkspaceFileResult, 56 | never, 57 | void, 58 | void 59 | >(OPEN_WORKSPACE_FILE_REQUEST_METHOD) 60 | -------------------------------------------------------------------------------- /runtimes/server-interface/identity-management.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AwsResponseError, 3 | GetIamCredentialParams, 4 | GetIamCredentialResult, 5 | GetSsoTokenParams, 6 | GetSsoTokenResult, 7 | InvalidateSsoTokenParams, 8 | InvalidateSsoTokenResult, 9 | InvalidateStsCredentialParams, 10 | InvalidateStsCredentialResult, 11 | ListProfilesParams, 12 | ListProfilesResult, 13 | GetMfaCodeParams, 14 | SsoTokenChangedParams, 15 | StsCredentialChangedParams, 16 | UpdateProfileParams, 17 | UpdateProfileResult, 18 | GetMfaCodeResult, 19 | } from '../protocol/identity-management' 20 | import { RequestHandler } from '../protocol' 21 | 22 | export * from '../protocol/identity-management' 23 | 24 | export type IdentityManagement = { 25 | onListProfiles: ( 26 | handler: RequestHandler 27 | ) => void 28 | 29 | onUpdateProfile: ( 30 | handler: RequestHandler 31 | ) => void 32 | 33 | onGetSsoToken: ( 34 | handler: RequestHandler 35 | ) => void 36 | 37 | onGetIamCredential: ( 38 | handler: RequestHandler 39 | ) => void 40 | 41 | onInvalidateSsoToken: ( 42 | handler: RequestHandler 43 | ) => void 44 | 45 | onInvalidateStsCredential: ( 46 | handler: RequestHandler< 47 | InvalidateStsCredentialParams, 48 | InvalidateStsCredentialResult | undefined | null, 49 | AwsResponseError 50 | > 51 | ) => void 52 | 53 | sendSsoTokenChanged: (params: SsoTokenChangedParams) => void 54 | 55 | sendStsCredentialChanged: (params: StsCredentialChangedParams) => void 56 | 57 | sendGetMfaCode: (params: GetMfaCodeParams) => Promise 58 | } 59 | -------------------------------------------------------------------------------- /runtimes/protocol/inlineCompletionWithReferences.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InlineCompletionListWithReferences, 3 | InlineCompletionItemWithReferences, 4 | LogInlineCompletionSessionResultsParams, 5 | InlineCompletionRegistrationOptions, 6 | InlineCompletionParams, 7 | PartialResultParams, 8 | } from './lsp' 9 | 10 | import { 11 | DidChangeTextDocumentParams, 12 | ProtocolNotificationType, 13 | ProtocolRequestType, 14 | } from 'vscode-languageserver-protocol' 15 | 16 | interface DocumentChangeParams { 17 | documentChangeParams?: DidChangeTextDocumentParams 18 | } 19 | 20 | interface OpenTabParams { 21 | openTabFilepaths?: string[] 22 | } 23 | 24 | interface FileContext { 25 | leftFileContent: string 26 | rightFileContent: string 27 | filename: string 28 | fileUri?: string 29 | programmingLanguage: string 30 | } 31 | 32 | interface FileContextParams { 33 | fileContextOverride?: FileContext 34 | } 35 | 36 | export interface SupplementalContextItem { 37 | content: string 38 | filePath: string 39 | score?: number 40 | } 41 | 42 | export interface GetSupplementalContextParams { 43 | filePath: string 44 | } 45 | 46 | export type InlineCompletionWithReferencesParams = InlineCompletionParams & 47 | PartialResultParams & 48 | DocumentChangeParams & 49 | OpenTabParams & 50 | FileContextParams 51 | 52 | export const inlineCompletionWithReferencesRequestType = new ProtocolRequestType< 53 | InlineCompletionWithReferencesParams, 54 | InlineCompletionListWithReferences | InlineCompletionItemWithReferences[] | null, 55 | InlineCompletionItemWithReferences[], 56 | void, 57 | InlineCompletionRegistrationOptions 58 | >('aws/textDocument/inlineCompletionWithReferences') 59 | 60 | export const logInlineCompletionSessionResultsNotificationType = new ProtocolNotificationType< 61 | LogInlineCompletionSessionResultsParams, 62 | void 63 | >('aws/logInlineCompletionSessionResults') 64 | 65 | export const getSupplementalContextRequestType = new ProtocolRequestType< 66 | GetSupplementalContextParams, 67 | SupplementalContextItem[], 68 | SupplementalContextItem[], 69 | void, 70 | void 71 | >('aws/textDocument/getProjectContext') 72 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yaml: -------------------------------------------------------------------------------- 1 | name: release-please 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | id-token: write # Required for OIDC authentication with npm 10 | contents: write # to create release commit (google-github-actions/release-please-action) 11 | pull-requests: write # to create release PR (google-github-actions/release-please-action) 12 | 13 | jobs: 14 | release-please: 15 | runs-on: ubuntu-latest 16 | steps: 17 | # The "release" step invokes on every merge to 'main' branch 18 | # and collects pending changes in pending release PR. 19 | - uses: googleapis/release-please-action@v4 20 | id: release 21 | with: 22 | token: ${{ secrets.GITHUB_TOKEN }} 23 | config-file: release-please-config.json 24 | manifest-file: .release-please-manifest.json 25 | 26 | # Steps below publish released packages to npm. 27 | # They only trigger after release-please PR from previous step is merged to main. 28 | - uses: actions/checkout@v4 29 | with: 30 | fetch-depth: 0 31 | persist-credentials: false 32 | if: ${{ fromJson(steps.release.outputs.releases_created) }} 33 | 34 | - name: Setup Nodejs 35 | uses: actions/setup-node@v4 36 | with: 37 | node-version: '24.x' 38 | registry-url: 'https://registry.npmjs.org' 39 | scope: '@aws' 40 | if: ${{ fromJson(steps.release.outputs.releases_created) }} 41 | 42 | - name: Compile and test packages 43 | run: | 44 | npm clean-install 45 | npm run compile 46 | npm run test 47 | if: ${{ fromJson(steps.release.outputs.releases_created) }} 48 | 49 | - name: Publish Chat Client UI Types to npm 50 | run: npm run pub --workspace chat-client-ui-types 51 | if: ${{ steps.release.outputs['chat-client-ui-types--release_created'] }} 52 | 53 | - name: Publish Runtimes to npm 54 | run: npm run pub --workspace runtimes 55 | if: ${{ steps.release.outputs['runtimes--release_created'] }} 56 | 57 | - name: Publish Types to npm 58 | run: npm run pub --workspace types 59 | if: ${{ steps.release.outputs['types--release_created'] }} -------------------------------------------------------------------------------- /runtimes/runtimes/lsp/router/util.ts: -------------------------------------------------------------------------------- 1 | function func(value: any): value is Function { 2 | return typeof value === 'function' 3 | } 4 | 5 | function thenable(value: any): value is Thenable { 6 | return value && func(value.then) 7 | } 8 | 9 | export function asPromise(value: Promise): Promise 10 | export function asPromise(value: Thenable): Promise 11 | export function asPromise(value: T): Promise 12 | export function asPromise(value: any): Promise { 13 | if (value instanceof Promise) { 14 | return value 15 | } else if (thenable(value)) { 16 | return new Promise((resolve, reject) => { 17 | value.then( 18 | resolved => resolve(resolved), 19 | error => reject(error) 20 | ) 21 | }) 22 | } else { 23 | return Promise.resolve(value) 24 | } 25 | } 26 | 27 | export function mergeObjects(obj1: any, obj2: any) { 28 | let merged: any = {} 29 | 30 | for (let key in obj1) { 31 | if (obj1.hasOwnProperty(key)) { 32 | if (Array.isArray(obj1) && Array.isArray(obj2)) { 33 | merged = [...new Set([...obj1, ...obj2])] 34 | } else if (typeof obj1[key] === 'object' && typeof obj2[key] === 'object') { 35 | merged[key] = mergeObjects(obj1[key], obj2[key]) 36 | } else { 37 | merged[key] = obj1[key] 38 | if (obj2.hasOwnProperty(key)) { 39 | merged[key] = obj2[key] 40 | } 41 | } 42 | } 43 | } 44 | 45 | for (let key in obj2) { 46 | if (Array.isArray(obj1) && Array.isArray(obj2)) { 47 | continue 48 | } else if (obj2.hasOwnProperty(key) && !obj1.hasOwnProperty(key)) { 49 | merged[key] = obj2[key] 50 | } 51 | } 52 | return merged 53 | } 54 | 55 | export function findDuplicates(array: T[]): T[] | undefined { 56 | const seen = new Set() 57 | const dups = array 58 | .filter(a => a !== undefined) 59 | .filter(function (a) { 60 | if (seen.has(a)) { 61 | return true 62 | } 63 | seen.add(a) 64 | return false 65 | }) 66 | return dups.length > 0 ? dups : undefined 67 | } 68 | -------------------------------------------------------------------------------- /runtimes/runtimes/lsp/router/routerByServerName.ts: -------------------------------------------------------------------------------- 1 | import { EventIdentifier, FollowupIdentifier, NotificationHandler } from '../../../protocol' 2 | import { Encoding } from '../../encoding' 3 | import { OperationalTelemetryProvider, TELEMETRY_SCOPES } from '../../operational-telemetry/operational-telemetry' 4 | 5 | type NotificationId = { 6 | serverName: string 7 | id: string 8 | } 9 | 10 | export class RouterByServerName

, F extends FollowupIdentifier> { 11 | constructor( 12 | private serverName: string, 13 | private encoding: Encoding 14 | ) {} 15 | 16 | send(sendHandler: (params: P) => Promise, params: P) { 17 | const attachServerName = (): P => { 18 | const idObject = { 19 | serverName: this.serverName, 20 | id: params.id!, 21 | } 22 | const id = this.encoding.encode(JSON.stringify(idObject)) 23 | return { 24 | ...params, 25 | id, 26 | } 27 | } 28 | 29 | const sendParams = params.id ? attachServerName() : params 30 | sendHandler(sendParams) 31 | } 32 | 33 | processFollowup(followupHandler: NotificationHandler, params: F) { 34 | if (!params.source.id) { 35 | return 36 | } 37 | 38 | const sourceId = this.encoding.decode(params.source.id) 39 | const id = this.parseServerName(sourceId) 40 | if (id?.serverName === this.serverName) { 41 | params = { 42 | ...params, 43 | source: { 44 | id: id.id, 45 | }, 46 | } 47 | followupHandler(params) 48 | } 49 | } 50 | 51 | private parseServerName(idJson: string): NotificationId | null { 52 | try { 53 | return JSON.parse(idJson) as NotificationId 54 | } catch (error: any) { 55 | OperationalTelemetryProvider.getTelemetryForScope(TELEMETRY_SCOPES.RUNTIMES).emitEvent({ 56 | errorOrigin: 'caughtError', 57 | errorType: 'routerServerNameParse', 58 | errorName: error?.name ?? 'unknown', 59 | errorCode: error?.code ?? '', 60 | }) 61 | return null 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /runtimes/runtimes/webworker.test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import sinon, { stubInterface } from 'ts-sinon' 3 | import { RuntimeProps } from './runtime' 4 | import { Features } from '../server-interface/server' 5 | 6 | describe('webworker', () => { 7 | let stubServer: sinon.SinonStub 8 | let props: RuntimeProps 9 | 10 | beforeEach(() => { 11 | stubServer = sinon.stub() 12 | props = { 13 | version: '0.1.0', 14 | servers: [stubServer], 15 | name: 'Test', 16 | } 17 | }) 18 | 19 | afterEach(() => { 20 | sinon.restore() 21 | }) 22 | 23 | it('should initialize lsp connection and start listening', async () => { 24 | try { 25 | const { webworker } = await import('./webworker') 26 | // If webworker loads successfully, it should be a function 27 | assert.strictEqual(typeof webworker, 'function') 28 | } catch (error) { 29 | // Expected: webworker fails to load in Node.js due to browser dependencies 30 | assert.ok(error instanceof Error) 31 | } 32 | }) 33 | 34 | describe('features', () => { 35 | describe('Runtime', () => { 36 | it('should set params from runtime properties', () => { 37 | // Since webworker can't run in Node.js, simulate its expected behavior 38 | const mockFeatures: Features = { 39 | runtime: { 40 | serverInfo: { 41 | name: props.name, 42 | version: props.version, 43 | }, 44 | platform: 'browser', 45 | }, 46 | } as Features 47 | 48 | // Simulate webworker calling the server 49 | props.servers.forEach(server => server(mockFeatures)) 50 | 51 | // Verify the server received correct runtime properties 52 | const features = stubServer.getCall(0).args[0] as Features 53 | assert.strictEqual(features.runtime.serverInfo.name, props.name) 54 | assert.strictEqual(features.runtime.serverInfo.version, props.version) 55 | assert.strictEqual(features.runtime.platform, 'browser') 56 | }) 57 | }) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /runtimes/runtimes/util/serverDataDirPath.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as os from 'os' 3 | import { InitializeParams } from '../../protocol' 4 | 5 | export function getServerDataDirPath(serverName: string, initializeParams: InitializeParams | undefined): string { 6 | const clientSpecifiedLocation = initializeParams?.initializationOptions?.aws?.clientDataFolder 7 | if (clientSpecifiedLocation) { 8 | return path.join(clientSpecifiedLocation, serverName) 9 | } 10 | 11 | const clientFolderName = getClientNameFromParams(initializeParams) 12 | const standardizedClientFolderName = standardizeFolderName(clientFolderName) 13 | 14 | const appDataFolder = getPlatformAppDataFolder() 15 | return appDataFolder === os.homedir() 16 | ? path.join( 17 | appDataFolder, 18 | `.${standardizedClientFolderName}`, 19 | standardizedClientFolderName ? serverName : `.${serverName}` 20 | ) 21 | : path.join(appDataFolder, standardizedClientFolderName, serverName) 22 | } 23 | 24 | function getPlatformAppDataFolder(): string { 25 | switch (process.platform) { 26 | case 'win32': 27 | return process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming') 28 | 29 | case 'darwin': 30 | return path.join(os.homedir(), 'Library', 'Application Support') 31 | 32 | case 'linux': 33 | return process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share') 34 | 35 | default: 36 | return os.homedir() 37 | } 38 | } 39 | 40 | function getClientNameFromParams(initializeParams: InitializeParams | undefined): string { 41 | const clientInfo = initializeParams?.clientInfo 42 | const awsClientInfo = initializeParams?.initializationOptions?.aws?.clientInfo 43 | 44 | return [awsClientInfo?.name || clientInfo?.name || '', awsClientInfo?.extension.name || ''] 45 | .filter(Boolean) 46 | .join('_') 47 | } 48 | 49 | function standardizeFolderName(clientFolderName: string): string { 50 | return clientFolderName 51 | .toLowerCase() 52 | .replace(/[^a-zA-Z0-9]/g, '_') // Replace non-alphanumeric characters with an underscore 53 | .replace(/_+/g, '_') // Replace multiple underscore characters with a single one 54 | .replace(/^_+|_+$/g, '') // Trim underscore characters 55 | .slice(0, 100) // Reduce the filename to avoid exceeding filesystem limits 56 | } 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@amzn/monorepo-language-server-runtimes", 3 | "version": "1.0.0", 4 | "description": "A monorepo for Language Servers Runtimes for AWS", 5 | "files": [ 6 | "out", 7 | "protocol", 8 | "runtimes", 9 | "server-interface", 10 | "testing" 11 | ], 12 | "workspaces": [ 13 | "types", 14 | "runtimes", 15 | "chat-client-ui-types" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/aws/language-server-runtimes" 20 | }, 21 | "author": "Amazon Web Services", 22 | "license": "Apache-2.0", 23 | "engines": { 24 | "node": ">=24.0.0", 25 | "npm": ">=7.0.0 <12.0.0" 26 | }, 27 | "scripts": { 28 | "clean": "ts-node ./script/clean.ts", 29 | "commitlint": "commitlint --edit", 30 | "precompile": "npm run generate-types --workspaces --if-present", 31 | "compile": "tsc --build && npm run compile --workspaces --if-present", 32 | "check:formatting": "prettier --check .", 33 | "format": "prettier . --write", 34 | "format-staged": "npx pretty-quick --staged", 35 | "prepare": "husky .husky", 36 | "test": "npm run test --workspaces --if-present", 37 | "preversion": "npm run test", 38 | "version": "npm run compile && git add -A .", 39 | "watch": "tsc --build --watch" 40 | }, 41 | "devDependencies": { 42 | "@commitlint/cli": "^19.8.1", 43 | "@commitlint/config-conventional": "^19.8.0", 44 | "@types/mocha": "^10.0.9", 45 | "@types/node": "^22.15.17", 46 | "assert": "^2.0.0", 47 | "conventional-changelog-conventionalcommits": "^8.0.0", 48 | "husky": "^9.1.7", 49 | "prettier": "3.5.3", 50 | "pretty-quick": "^4.1.1", 51 | "shx": "^0.4.0", 52 | "sinon": "^20.0.0", 53 | "ts-mocha": "^11.1.0", 54 | "ts-sinon": "^2.0.2", 55 | "typescript": "^5.8.3" 56 | }, 57 | "overrides": { 58 | "cross-spawn": "^7.0.6" 59 | }, 60 | "typesVersions": { 61 | "*": { 62 | "browser": [ 63 | "./out/runtimes/webworker.d.ts" 64 | ] 65 | } 66 | }, 67 | "prettier": { 68 | "printWidth": 120, 69 | "trailingComma": "es5", 70 | "tabWidth": 4, 71 | "singleQuote": true, 72 | "semi": false, 73 | "bracketSpacing": true, 74 | "arrowParens": "avoid", 75 | "endOfLine": "lf" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /runtimes/runtimes/util/pathUtil.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Simplified version of path.join that can be safely used on web 3 | * @param segments 4 | * @returns 5 | */ 6 | export function joinUnixPaths(...segments: string[]): string { 7 | // Filter out empty segments and normalize each segment 8 | const normalizedSegments = segments.filter(Boolean).map(segment => segment.replace(/^\/+|\/+$/g, '')) 9 | 10 | // Join segments with a single slash and then split to handle internal slashes 11 | const parts = normalizedSegments 12 | .join('/') 13 | .replace(/\/+/g, '/') // Replace multiple consecutive slashes with a single slash 14 | .split('/') 15 | 16 | const result = [] 17 | 18 | for (const part of parts) { 19 | if (part === '..') { 20 | result.pop() 21 | } else if (part !== '.' && part !== '') { 22 | // Skip empty parts and current directory markers 23 | result.push(part) 24 | } 25 | } 26 | 27 | return result.join('/') 28 | } 29 | 30 | /** 31 | * Simplified version of path.basename that can be safely used on web 32 | * It should match the behaviour of the original 33 | * @param path The path to extract the basename from 34 | * @param ext Optional extension to remove from the result 35 | * @returns The last portion of the path, optionally with extension removed 36 | */ 37 | export function basenamePath(path: string, ext?: string): string { 38 | if (!path || typeof path !== 'string' || path === '') { 39 | return '' 40 | } 41 | 42 | // Normalize path separators and remove trailing slashes 43 | const normalizedPath = path.replace(/\\/g, '/').replace(/\/+$/, '') 44 | 45 | if (!normalizedPath || normalizedPath === '/') { 46 | return '' 47 | } 48 | 49 | // Find the last segment 50 | const lastSlashIndex = normalizedPath.lastIndexOf('/') 51 | const basename = lastSlashIndex === -1 ? normalizedPath : normalizedPath.slice(lastSlashIndex + 1) 52 | 53 | if (!basename || !ext) { 54 | return basename 55 | } 56 | 57 | // Remove extension if it matches 58 | if (ext.startsWith('.')) { 59 | return basename.endsWith(ext) && basename !== ext ? basename.slice(0, -ext.length) : basename 60 | } else { 61 | // For extensions without dot, check both with and without dot 62 | if (basename.endsWith(ext) && basename !== ext) { 63 | return basename.slice(0, -ext.length) 64 | } 65 | const dotExt = '.' + ext 66 | return basename.endsWith(dotExt) && basename !== dotExt ? basename.slice(0, -dotExt.length) : basename 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /runtimes/runtimes/util/standalone/getProxySettings/parseScutil.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Based on mac-system-proxy 1.0.2 (Apache-2.0). 6 | * https://github.com/httptoolkit/mac-system-proxy/blob/main/src/parse-scutil.ts 7 | */ 8 | const TYPE_KEY = '__scutil__type__' 9 | 10 | // Quick hacky parser, which translates output into valid JSON: 11 | export function parseScutilOutput(output: string): {} { 12 | try { 13 | // Unclear how this happens, but it seems that it can in some cases: 14 | if (output === '') return {} 15 | 16 | const unquotedJsonString = output 17 | // Reduce type markers to just an inline __scutil__type__ marker on array objects: 18 | .replace(/ /g, '') 19 | .replace(/ {/g, `{\n${TYPE_KEY} : array`) 20 | .trim() 21 | 22 | // Turn unquoted key/value string into a string of quote key values (but with no commas). 23 | // We effectively parse by splitting on the first " : " in each line, if present. 24 | const jsonKeyValues = unquotedJsonString.split('\n').map(line => { 25 | const [key, value] = line.split(/ : (.*)/) 26 | 27 | if (value === undefined) return key.trim() 28 | else if (value === '{') return `"${key.trim()}": {` 29 | else return `"${key.trim()}": ${JSON.stringify(value.trim())}` 30 | }) 31 | 32 | // Insert commas everywhere they're needed 33 | const jsonFormattedString = jsonKeyValues.reduce((jsonString, nextValue) => { 34 | if (!jsonString || jsonString.endsWith('{') || nextValue === '}') { 35 | // JSON has no commas after/before object {} tokens or 36 | // at the very start of the string: 37 | return jsonString + nextValue 38 | } else { 39 | return jsonString + ', ' + nextValue 40 | } 41 | }, '') 42 | 43 | const data = JSON.parse(jsonFormattedString, (key, value) => { 44 | // Convert array-tagged objects back into arrays: 45 | if (value[TYPE_KEY] === 'array') { 46 | delete value[TYPE_KEY] 47 | return Object.values(value) 48 | } else { 49 | return value 50 | } 51 | }) 52 | 53 | return data 54 | } catch (e) { 55 | throw Object.assign( 56 | new Error('Unexpected scutil proxy output format'), 57 | { scutilOutput: output } // Attach output for debugging elsewhere 58 | ) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /runtimes/docs/proxy.md: -------------------------------------------------------------------------------- 1 | # Proxy Configuration for AWS SDK 2 | 3 | ## Overview 4 | The proxy configuration feature enables AWS SDK to work in environments with various HTTP proxy setups, including both explicitly configured proxies and transparent network proxies. 5 | 6 | There are two versions of proxy configuration available: 7 | - Old mechanism (default) 8 | - New experimental mechanism that can be enabled by setting `EXPERIMENTAL_HTTP_PROXY_SUPPORT=true` in environmental variables 9 | 10 | The following documentation details the new experimental mechanism. 11 | 12 | ## Certificate Management 13 | The system aggregates SSL/TLS certificates from multiple sources: 14 | 1. Operating System certificates (automatically loaded) 15 | 2. Custom CA bundle passed in environmental variables 16 | 17 | ### Certificate Configuration 18 | SSL/TLS connections are automatically secured using certificates from the operating system's trust store. Follow your operating system documentation for adding required certificates to system's trust store. 19 | 20 | Custom certificates can be passed using next environmental variables: 21 | - `AWS_CA_BUNDLE` - Path to custom CA bundle 22 | - `NODE_EXTRA_CA_CERTS` - Path to additional CA certificates 23 | 24 | ## Supported Proxy Configurations 25 | 26 | ### 1. Explicit HTTP Proxy 27 | The following environment variables are supported for proxy configuration (in order of precedence): 28 | 1. `HTTPS_PROXY` 29 | 2. `https_proxy` 30 | 3. `HTTP_PROXY` 31 | 4. `http_proxy` 32 | 33 | #### Note on Support of `https://` Proxy 34 | If proxy is using SSL and URL starts with `https://`, then path to proxy server root certificate must be set in `NODE_EXTRA_CA_CERTS`. At the moment, other methods of passing certificate are not supported and won't work. 35 | 36 | ### 2. Transparent Network Proxy 37 | A transparent proxy (also known as an intercepting proxy) is a server that sits between computer and the internet, routing all network traffic through it without requiring explicit configuration in end user applications. This is typically set up by organization's network administrators to monitor and secure network traffic. 38 | 39 | Runtimes Proxy feature supports Transparent Proxy by reading and configuring SDK clients with certificates from #Certificate Configuration section. 40 | 41 | #### Troubleshooting Transparent Proxy 42 | In case of connection issues: 43 | 1. Verify that organization's certificates are properly installed 44 | 2. Check with network administrator if additional certificates are required 45 | 3. Ensure your system's certificate store is up to date or path to certificate is set in `AWS_CA_BUNDLE` or `NODE_EXTRA_CA_CERTS` environment variables. 46 | 4. Examine LSP Client connection logs for any errors. 47 | -------------------------------------------------------------------------------- /runtimes/runtimes/operational-telemetry/operational-telemetry.ts: -------------------------------------------------------------------------------- 1 | import { ErrorEventAttributes, MetricName } from './types/generated/telemetry' 2 | 3 | export type OperationalEventAttributes = ErrorEventAttributes 4 | 5 | export type ErrorOrigin = ErrorEventAttributes['errorOrigin'] 6 | 7 | export interface OperationalTelemetry { 8 | registerGaugeProvider(metricName: MetricName, valueProvider: () => number, unit?: string, scopeName?: string): void 9 | emitEvent(eventAttr: OperationalEventAttributes, scopeName?: string): void 10 | toggleOptOut(telemetryOptOut: boolean): void 11 | } 12 | 13 | class NoopOperationalTelemetry implements OperationalTelemetry { 14 | toggleOptOut(telemetryOptOut: boolean): void {} 15 | 16 | registerGaugeProvider( 17 | _metricName: MetricName, 18 | _valueProvider: () => number, 19 | _unit: string, 20 | _scopeName?: string 21 | ): void {} 22 | 23 | emitEvent(_eventAttr: OperationalEventAttributes, _scopeName?: string): void {} 24 | } 25 | 26 | class ScopedTelemetryService implements OperationalTelemetry { 27 | private telemetryService: OperationalTelemetry 28 | private defaultScopeName: string 29 | 30 | constructor(scope: string, telemetryService: OperationalTelemetry) { 31 | this.telemetryService = telemetryService 32 | this.defaultScopeName = scope 33 | } 34 | 35 | toggleOptOut(telemetryOptOut: boolean): void { 36 | this.telemetryService.toggleOptOut(telemetryOptOut) 37 | } 38 | 39 | emitEvent(eventAttr: OperationalEventAttributes, scopeName?: string): void { 40 | this.telemetryService.emitEvent(eventAttr, scopeName ?? this.defaultScopeName) 41 | } 42 | 43 | registerGaugeProvider( 44 | metricName: MetricName, 45 | valueProvider: () => number, 46 | unit?: string, 47 | scopeName?: string 48 | ): void { 49 | this.telemetryService.registerGaugeProvider( 50 | metricName, 51 | valueProvider, 52 | unit, 53 | scopeName ? scopeName : this.defaultScopeName 54 | ) 55 | } 56 | } 57 | 58 | export class OperationalTelemetryProvider { 59 | private static telemetryInstance: OperationalTelemetry = new NoopOperationalTelemetry() 60 | 61 | static setTelemetryInstance(telemetryInstance: OperationalTelemetry): void { 62 | OperationalTelemetryProvider.telemetryInstance = telemetryInstance 63 | } 64 | 65 | static getTelemetryForScope(scopeName: string): OperationalTelemetry { 66 | return new ScopedTelemetryService(scopeName, OperationalTelemetryProvider.telemetryInstance) 67 | } 68 | } 69 | 70 | export const TELEMETRY_SCOPES = { 71 | RUNTIMES: 'language-server-runtimes', 72 | } as const 73 | -------------------------------------------------------------------------------- /runtimes/runtimes/lsp/router/routerByServerName.test.ts: -------------------------------------------------------------------------------- 1 | import sinon, { assert } from 'sinon' 2 | import { Encoding } from '../../encoding' 3 | import { EventIdentifier, FollowupIdentifier } from '../../../protocol' 4 | import { RouterByServerName } from './routerByServerName' 5 | 6 | describe('RouterByServerName', () => { 7 | const encoding = { 8 | encode: value => Buffer.from(value).toString('base64'), 9 | decode: value => Buffer.from(value, 'base64').toString('utf-8'), 10 | } as Encoding 11 | const serverName = 'Server_XXX' 12 | 13 | let router: RouterByServerName, FollowupIdentifier> 14 | 15 | beforeEach(() => { 16 | router = new RouterByServerName, FollowupIdentifier>(serverName, encoding) 17 | }) 18 | 19 | describe('send', () => { 20 | it('attaches serverName to id if id is defined', () => { 21 | const sendSpy = sinon.spy() 22 | router.send(sendSpy, { id: '123' }) 23 | 24 | const expectedEncodedId = encoding.encode('{"serverName":"Server_XXX","id":"123"}') 25 | assert.calledWithMatch(sendSpy, { id: expectedEncodedId }) 26 | }) 27 | it('uses original params if id is not defined', () => { 28 | const sendSpy = sinon.spy() 29 | router.send(sendSpy, {}) 30 | assert.calledWith(sendSpy, {}) 31 | }) 32 | }) 33 | describe('processFollowup', () => { 34 | it('calls followup handler if id contains serverName and removes serverName from id', () => { 35 | const followupHandlerSpy = sinon.spy() 36 | const params = { 37 | source: { 38 | id: encoding.encode('{"serverName":"Server_XXX", "id":"123"}'), 39 | }, 40 | } 41 | router.processFollowup(followupHandlerSpy, params) 42 | assert.calledOnceWithMatch(followupHandlerSpy, { source: { id: '123' } }) 43 | }) 44 | it('does not call followup handler if id does not contain serverName', () => { 45 | const followupHandlerSpy = sinon.spy() 46 | const params = { source: { id: 'A' } } 47 | router.processFollowup(followupHandlerSpy, params) 48 | assert.notCalled(followupHandlerSpy) 49 | }) 50 | it('does not call followup handler if id contains different serverName', () => { 51 | const followupHandlerSpy = sinon.spy() 52 | const params = { 53 | source: { 54 | id: encoding.encode('{"serverName":"Fake", "id":"123"}'), 55 | }, 56 | } 57 | router.processFollowup(followupHandlerSpy, params) 58 | assert.notCalled(followupHandlerSpy) 59 | }) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /runtimes/protocol/notification.ts: -------------------------------------------------------------------------------- 1 | import { MessageType, ProtocolNotificationType } from './lsp' 2 | 3 | export interface EventIdentifier { 4 | readonly id: string 5 | } 6 | 7 | export interface FollowupIdentifier { 8 | readonly source: EventIdentifier 9 | } 10 | 11 | export interface NotificationContent { 12 | readonly text: string 13 | readonly title?: string 14 | } 15 | 16 | export namespace FollowupNotificationActionType { 17 | export const Acknowledge = 'Acknowledge' 18 | } 19 | 20 | export type FollowupNotificationActionType = typeof FollowupNotificationActionType.Acknowledge 21 | 22 | export namespace NotificationActionType { 23 | export const Url = 'Url' 24 | export const Marketplace = 'Marketplace' 25 | } 26 | 27 | export type NotificationActionType = 28 | | typeof NotificationActionType.Url 29 | | typeof NotificationActionType.Marketplace 30 | | typeof FollowupNotificationActionType.Acknowledge 31 | 32 | export interface NotificationAction { 33 | readonly text: string 34 | readonly type: NotificationActionType 35 | } 36 | 37 | export interface UrlAction extends NotificationAction { 38 | readonly type: typeof NotificationActionType.Url 39 | readonly url: string 40 | } 41 | 42 | export interface MarketplaceAction extends NotificationAction { 43 | readonly type: typeof NotificationActionType.Marketplace 44 | } 45 | 46 | export interface AcknowledgeRequestAction extends NotificationAction { 47 | readonly type: typeof FollowupNotificationActionType.Acknowledge 48 | } 49 | 50 | export interface NotificationParams extends Partial { 51 | readonly type: MessageType 52 | readonly content: NotificationContent 53 | readonly actions?: NotificationAction[] 54 | } 55 | 56 | export interface NotificationFollowupParams extends FollowupIdentifier { 57 | readonly action: FollowupNotificationActionType 58 | } 59 | 60 | /** 61 | * showNotificationRequestType defines the custom method that the language server 62 | * sends to the client to provide notifications to show to customers. 63 | */ 64 | export const showNotificationRequestType = new ProtocolNotificationType( 65 | 'aws/window/showNotification' 66 | ) 67 | 68 | /** 69 | * notificationFollowupRequestType defines the custom method that the language client 70 | * sends to the server to provide asynchronous customer followup to notification shown. 71 | * This method is expected to be used only for notification that require followup. 72 | * 73 | * Client is responsible for passing `id` of source notification that triggered the followup notification 74 | * in the parameters. 75 | */ 76 | export const notificationFollowupRequestType = new ProtocolNotificationType( 77 | 'aws/window/notificationFollowup' 78 | ) 79 | -------------------------------------------------------------------------------- /runtimes/runtimes/util/standalone/getProxySettings/getMacProxySettings.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Based on mac-system-proxy 1.0.2 (Apache-2.0). Modified for synchronous use 6 | * https://github.com/httptoolkit/mac-system-proxy/blob/main/src/index.ts 7 | */ 8 | import { spawnSync } from 'child_process' 9 | import { parseScutilOutput } from './parseScutil' 10 | 11 | export interface ProxyConfig { 12 | proxyUrl: string 13 | noProxy: string[] 14 | } 15 | 16 | export function getMacSystemProxy(): ProxyConfig | undefined { 17 | // Invoke `scutil --proxy` synchronously 18 | console.debug('Executing scutil --proxy to retrieve Mac system proxy settings') 19 | const result = spawnSync('scutil', ['--proxy'], { encoding: 'utf8' }) 20 | if (result.error || result.status !== 0) { 21 | console.warn(`scutil --proxy failed: ${result.error?.message ?? 'exit code ' + result.status}`) 22 | return undefined 23 | } 24 | console.debug('Successfully retrieved scutil output') 25 | 26 | let settings: Record 27 | try { 28 | settings = parseScutilOutput(result.stdout) 29 | } catch (e: any) { 30 | console.warn(`Failed to parse scutil output: ${e.message}`) 31 | return undefined 32 | } 33 | 34 | const noProxy = settings.ExceptionsList ?? [] 35 | 36 | // Honor PAC URL first if configured 37 | if (settings.ProxyAutoConfigEnable === '1' && settings.ProxyAutoConfigURLString) { 38 | console.debug(`PAC URL detected: ${settings.ProxyAutoConfigURLString}`) 39 | // TODO: Parse PAC file to get actual proxy 40 | // For now, skip PAC and fall through to manual proxy settings 41 | console.warn('PAC file support not yet implemented, falling back to manual proxy settings') 42 | } 43 | 44 | // Otherwise pick the first enabled protocol 45 | console.debug('Checking for enabled proxy protocols') 46 | if (settings.HTTPEnable === '1' && settings.HTTPProxy && settings.HTTPPort) { 47 | console.debug(`Using HTTP proxy: ${settings.HTTPProxy}:${settings.HTTPPort}`) 48 | return { proxyUrl: `http://${settings.HTTPProxy}:${settings.HTTPPort}`, noProxy } 49 | } 50 | if (settings.HTTPSEnable === '1' && settings.HTTPSProxy && settings.HTTPSPort) { 51 | console.debug(`Using HTTPS proxy: ${settings.HTTPSProxy}:${settings.HTTPSPort}`) 52 | return { proxyUrl: `http://${settings.HTTPSProxy}:${settings.HTTPSPort}`, noProxy } 53 | } 54 | // TODO: Enable support for SOCKS Proxy 55 | // if (settings.SOCKSEnable === '1' && settings.SOCKSProxy && settings.SOCKSPort) { 56 | // console.debug(`Using SOCKS proxy: ${settings.SOCKSProxy}:${settings.SOCKSPort}`) 57 | // return { proxyUrl: `socks://${settings.SOCKSProxy}:${settings.SOCKSPort}`, noProxy } 58 | // } 59 | 60 | return undefined 61 | } 62 | -------------------------------------------------------------------------------- /runtimes/server-interface/workspace.ts: -------------------------------------------------------------------------------- 1 | import { TextDocument, WorkspaceFolder } from '../protocol' 2 | 3 | // Minimal version of fs.Dirent 4 | interface Dirent { 5 | isFile(): boolean 6 | isDirectory(): boolean 7 | isSymbolicLink(): boolean 8 | name: string 9 | parentPath: string 10 | } 11 | 12 | /** 13 | * The Workspace feature interface. Provides access to currently 14 | * open files in the workspace. May not provide full filesystem 15 | * access to files that are not currently open or outside the 16 | * workspace root. 17 | */ 18 | export type Workspace = { 19 | getTextDocument: (uri: string) => Promise 20 | getAllTextDocuments: () => Promise 21 | /** Gets workspace folder associated with the given file uri */ 22 | getWorkspaceFolder: (uri: string) => WorkspaceFolder | null | undefined 23 | /** Gets all workspace folders */ 24 | getAllWorkspaceFolders: () => WorkspaceFolder[] 25 | fs: { 26 | /** 27 | * Copies a file from src to dest. Dest is overwritten if it already exists. 28 | * @param {string} src - The source path. 29 | * @param {string} dest - The destination path. 30 | * @param {boolean} [options.ensureDir] - Whether to create the destination directory if it doesn't exist, defaults to false. 31 | * @returns A promise that resolves when the copy operation is complete. 32 | */ 33 | copyFile: (src: string, dest: string, options?: { ensureDir?: boolean }) => Promise 34 | exists: (path: string) => Promise 35 | getFileSize: (path: string) => Promise<{ size: number }> 36 | getServerDataDirPath: (serverName: string) => string 37 | getTempDirPath: () => string 38 | getUserHomeDir: () => string 39 | /** 40 | * Reads the contents of a directory. 41 | * @param {string} path - The path to the directory. 42 | * @returns A promise that resolves to an array of Dirent objects. 43 | */ 44 | readdir: (path: string) => Promise 45 | /** 46 | * Reads the entire contents of a file. 47 | * @param {string} path - The path to the file. 48 | * @param {string} [options.encoding] - The encoding to use when reading the file, defaults to 'utf-8'. 49 | * @returns A promise that resolves to the contents of the file as a string. 50 | */ 51 | readFile: (path: string, options?: { encoding?: string }) => Promise 52 | isFile: (path: string) => Promise 53 | rm: (dir: string, options?: { recursive?: boolean; force?: boolean }) => Promise 54 | writeFile: (path: string, data: string, options?: { mode?: number | string }) => Promise 55 | appendFile: (path: string, data: string) => Promise 56 | mkdir: (path: string, options?: { recursive?: boolean }) => Promise 57 | /** 58 | * Reads the entire contents of a file. 59 | * @param {string} path - The path to the file. 60 | * @param {string} [options.encoding] - The encoding to use when reading the file, defaults to 'utf-8'. 61 | * @returns A string referring to the contents of the file. 62 | */ 63 | readFileSync: (path: string, options?: { encoding?: string }) => string 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /runtimes/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@aws/language-server-runtimes", 3 | "version": "0.3.11", 4 | "description": "Runtimes to host Language Servers for AWS", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/aws/language-server-runtimes", 8 | "directory": "runtimes" 9 | }, 10 | "author": "Amazon Web Services", 11 | "license": "Apache-2.0", 12 | "engines": { 13 | "node": ">=24.0.0" 14 | }, 15 | "scripts": { 16 | "clean": "rm -rf out/", 17 | "precompile": "npm run generate-types", 18 | "compile": "tsc --build && npm run copy-files", 19 | "fix:prettier": "prettier . --write", 20 | "format": "npm run fix:prettier", 21 | "prepare": "husky install", 22 | "prepub:copyFiles": "shx cp ../.npmignore CHANGELOG.md ../LICENSE ../NOTICE README.md ../SECURITY.md package.json out/", 23 | "prepub": "npm run clean && npm run test && npm run compile && npm run prepub:copyFiles", 24 | "pub": "cd out && npm publish", 25 | "test:unit": "ts-mocha -b './**/*.test.ts'", 26 | "test": "npm run test:unit", 27 | "preversion": "npm run test", 28 | "version": "npm run compile && git add -A .", 29 | "copy-files": "copyfiles runtimes/operational-telemetry/types/generated/* out", 30 | "generate-types": "ts-node ./script/generate-types.ts" 31 | }, 32 | "dependencies": { 33 | "@aws/language-server-runtimes-types": "^0.1.63", 34 | "@opentelemetry/api": "^1.9.0", 35 | "@opentelemetry/api-logs": "^0.200.0", 36 | "@opentelemetry/core": "^2.0.0", 37 | "@opentelemetry/exporter-logs-otlp-http": "^0.200.0", 38 | "@opentelemetry/exporter-metrics-otlp-http": "^0.200.0", 39 | "@opentelemetry/resources": "^2.0.1", 40 | "@opentelemetry/sdk-logs": "^0.200.0", 41 | "@opentelemetry/sdk-metrics": "^2.0.1", 42 | "@smithy/node-http-handler": "^4.0.4", 43 | "ajv": "^8.17.1", 44 | "hpagent": "^1.2.0", 45 | "jose": "^5.9.6", 46 | "mac-ca": "^3.1.1", 47 | "rxjs": "^7.8.2", 48 | "vscode-languageserver": "^9.0.1", 49 | "vscode-languageserver-protocol": "^3.17.5", 50 | "vscode-uri": "^3.1.0", 51 | "win-ca": "^3.5.1", 52 | "winreg": "^1.2.5" 53 | }, 54 | "devDependencies": { 55 | "@types/mocha": "^10.0.9", 56 | "@types/mock-fs": "^4.13.4", 57 | "@types/node": "^22.15.17", 58 | "@types/node-forge": "^1.3.11", 59 | "@types/winreg": "^1.2.36", 60 | "assert": "^2.0.0", 61 | "copyfiles": "^2.4.1", 62 | "husky": "^9.1.7", 63 | "json-schema-to-typescript": "^15.0.4", 64 | "mock-fs": "^5.5.0", 65 | "node-forge": "^1.3.1", 66 | "prettier": "3.5.3", 67 | "sinon": "^20.0.0", 68 | "ts-mocha": "^11.1.0", 69 | "ts-sinon": "^2.0.2", 70 | "typescript": "^5.8.3" 71 | }, 72 | "typesVersions": { 73 | "*": { 74 | "browser": [ 75 | "./out/runtimes/webworker.d.ts" 76 | ] 77 | } 78 | }, 79 | "prettier": { 80 | "printWidth": 120, 81 | "trailingComma": "es5", 82 | "tabWidth": 4, 83 | "singleQuote": true, 84 | "semi": false, 85 | "bracketSpacing": true, 86 | "arrowParens": "avoid", 87 | "endOfLine": "lf" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /runtimes/runtimes/util/standalone/certificatesReaders.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { readdirSync, readFileSync } from 'node:fs' 7 | import * as path from 'node:path' 8 | import { OperationalTelemetryProvider, TELEMETRY_SCOPES } from '../../operational-telemetry/operational-telemetry' 9 | 10 | const UNIX_CERT_FILES = [ 11 | '/etc/ssl/certs/ca-certificates.crt', 12 | '/etc/pki/tls/certs/ca-bundle.crt', 13 | '/etc/ssl/ca-bundle.pem', 14 | '/etc/pki/tls/cacert.pem', 15 | '/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem', 16 | '/etc/ssl/cert.pem', 17 | ] 18 | 19 | const UNIX_CERT_DIRS = ['/etc/ssl/certs', '/etc/pki/tls/certs', '/system/etc/security/cacerts'] 20 | 21 | const PEM_CERT_REGEXP = /-----BEGIN\s+CERTIFICATE-----[\s\S]+?-----END\s+CERTIFICATE-----$/gm 22 | 23 | export function readLinuxCertificates(): string[] { 24 | const allFiles = [...UNIX_CERT_FILES] 25 | const certificates: string[] = [] 26 | let firstError: Error | undefined 27 | let hasSeenCertificate = false 28 | 29 | // Step 1: Collect all certificate files from directories 30 | for (const dir of UNIX_CERT_DIRS) { 31 | try { 32 | const dirFiles = readdirSync(dir).map(file => path.join(dir, file)) 33 | allFiles.push(...dirFiles) 34 | } catch (error: any) { 35 | firstError ??= error 36 | } 37 | } 38 | 39 | // Step 2: Extract certificates from all collected files 40 | for (const file of allFiles) { 41 | try { 42 | const content = readFileSync(file, 'utf8') 43 | const matches = content.match(PEM_CERT_REGEXP) 44 | 45 | // Skip if no certificates found in this file 46 | if (!matches) continue 47 | 48 | // Track if we've found any valid certificates 49 | hasSeenCertificate = hasSeenCertificate || matches.length > 0 50 | 51 | // Add trimmed certificates to our collection 52 | const validCertificates = matches.map(cert => cert.trim()) 53 | certificates.push(...validCertificates) 54 | } catch (error: any) { 55 | firstError ??= error 56 | } 57 | } 58 | 59 | // Step 3: Handle errors and return results 60 | if (!hasSeenCertificate && firstError) { 61 | const errorMessage = 'Error when reading Linux certificates' 62 | console.log(errorMessage) 63 | OperationalTelemetryProvider.getTelemetryForScope(TELEMETRY_SCOPES.RUNTIMES).emitEvent({ 64 | errorOrigin: 'caughtError', 65 | errorName: firstError?.name ?? 'unknown', 66 | errorType: 'linuxCertificateReader', 67 | errorMessage: errorMessage, 68 | }) 69 | console.error(firstError) 70 | return [] 71 | } 72 | 73 | return certificates 74 | } 75 | 76 | export function readWindowsCertificates(): string[] { 77 | const winCaReader = require('win-ca/api') 78 | const certs: string[] = [] 79 | 80 | winCaReader({ 81 | store: ['root', 'ca'], 82 | format: winCaReader.der2.pem, 83 | ondata: (crt: string) => certs.push(crt), 84 | }) 85 | 86 | return certs 87 | } 88 | 89 | export function readMacosCertificates(): string[] { 90 | const macCertsReader = require('mac-ca') 91 | const certs = macCertsReader.get({ 92 | excludeBundled: false, 93 | }) 94 | 95 | return certs 96 | } 97 | -------------------------------------------------------------------------------- /runtimes/runtimes/chat/encryptedChat.test.ts: -------------------------------------------------------------------------------- 1 | import { EncryptedChat } from './encryptedChat' 2 | import { SinonStub, stub, spy, assert as sinonAssert } from 'sinon' 3 | import { encryptObjectWithKey } from '../auth/standalone/encryption' 4 | import { CancellationToken } from 'vscode-languageserver-protocol' 5 | import assert from 'assert' 6 | import { chatRequestType, quickActionRequestType } from '../../protocol' 7 | 8 | class ConnectionMock { 9 | public onRequest: SinonStub 10 | public onNotification: SinonStub 11 | 12 | constructor() { 13 | this.onRequest = stub() 14 | this.onNotification = stub() 15 | } 16 | 17 | public triggerRequest(method: string, params: any, cancellationToken: any) { 18 | const handler = this.onRequest.getCall(0).args[1] 19 | return handler(params, cancellationToken) 20 | } 21 | } 22 | 23 | const testKey = Buffer.from('a'.repeat(32)).toString('base64') // Key has to be 256 bit long in our JWE configuration 24 | 25 | describe('EncryptedChat', () => { 26 | let connection: ConnectionMock 27 | let encryptedChat: EncryptedChat 28 | 29 | beforeEach(() => { 30 | connection = new ConnectionMock() 31 | encryptedChat = new EncryptedChat(connection as any, testKey, 'JWT') 32 | }) 33 | 34 | it('should reject unencrypted onChatPrompt requests', async () => { 35 | const handler = spy() 36 | encryptedChat.onChatPrompt(handler) 37 | 38 | const result = await connection.triggerRequest( 39 | chatRequestType.method, 40 | { message: 'unencryptedMessage' }, 41 | CancellationToken.None 42 | ) 43 | assert(result instanceof Error) 44 | assert.strictEqual(result.message, 'The request was not encrypted correctly') 45 | sinonAssert.notCalled(handler) 46 | }) 47 | 48 | it('should handle encrypted onChatPrompt requests', async () => { 49 | const handler = spy(params => params) 50 | encryptedChat.onChatPrompt(handler) 51 | 52 | const encryptedRequest = { 53 | message: await encryptObjectWithKey({ body: 'something' }, testKey, 'dir', 'A256GCM'), 54 | } 55 | 56 | const result = await connection.triggerRequest(chatRequestType.method, encryptedRequest, CancellationToken.None) 57 | assert(!(result instanceof Error)) 58 | sinonAssert.calledOnce(handler) 59 | }) 60 | 61 | it('should reject unencrypted onQuickAction requests', async () => { 62 | const handler = spy() 63 | encryptedChat.onQuickAction(handler) 64 | 65 | const result = await connection.triggerRequest( 66 | quickActionRequestType.method, 67 | { message: 'unencryptedMessage' }, 68 | CancellationToken.None 69 | ) 70 | assert(result instanceof Error) 71 | assert.strictEqual(result.message, 'The request was not encrypted correctly') 72 | sinonAssert.notCalled(handler) 73 | }) 74 | 75 | it('should handle encrypted onQuickAction requests', async () => { 76 | const handler = spy(params => params) 77 | encryptedChat.onQuickAction(handler) 78 | 79 | const encryptedRequest = { 80 | message: await encryptObjectWithKey({ tabId: 'tab-1', quickAction: '/help' }, testKey, 'dir', 'A256GCM'), 81 | } 82 | 83 | const result = await connection.triggerRequest( 84 | quickActionRequestType.method, 85 | encryptedRequest, 86 | CancellationToken.None 87 | ) 88 | assert(!(result instanceof Error)) 89 | sinonAssert.calledOnce(handler) 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /runtimes/runtimes/util/loggingUtil.test.ts: -------------------------------------------------------------------------------- 1 | import { Connection, RemoteConsole } from 'vscode-languageserver/node' 2 | import { isLogLevelEnabled, isValidLogLevel, DefaultLogger, LogLevel } from './loggingUtil' 3 | import sinon from 'sinon' 4 | import assert from 'assert' 5 | 6 | describe('LoggingUtil', () => { 7 | let mockConnection: Partial 8 | let consoleMock: Pick 9 | beforeEach(() => { 10 | consoleMock = { 11 | log: sinon.stub(), 12 | error: sinon.stub(), 13 | warn: sinon.stub(), 14 | info: sinon.stub(), 15 | debug: sinon.stub(), 16 | } 17 | mockConnection = { 18 | console: consoleMock as any, 19 | } 20 | }) 21 | 22 | afterEach(() => { 23 | sinon.restore() 24 | }) 25 | 26 | describe('isValidLogLevel', () => { 27 | it('should return true for valid log levels', () => { 28 | const validLevels: LogLevel[] = ['error', 'warn', 'info', 'log', 'debug'] 29 | validLevels.forEach(level => { 30 | assert.strictEqual(true, isValidLogLevel(level)) 31 | }) 32 | }) 33 | 34 | it('should return false for invalid log levels', () => { 35 | const invalidLevels = ['trace', 'verbose', undefined, null] 36 | invalidLevels.forEach(level => { 37 | assert.strictEqual(false, isValidLogLevel(level as LogLevel)) 38 | }) 39 | }) 40 | }) 41 | 42 | describe('isLogLevelEnabled', () => { 43 | it('should correctly compare log levels based on hierarchy', () => { 44 | const testCases = [ 45 | { level1: 'debug', level2: 'error', expected: true }, 46 | { level1: 'error', level2: 'debug', expected: false }, 47 | { level1: 'info', level2: 'warn', expected: true }, 48 | { level1: 'warn', level2: 'error', expected: true }, 49 | { level1: 'error', level2: 'error', expected: true }, 50 | ] 51 | testCases.forEach(({ level1, level2, expected }) => { 52 | assert.strictEqual(expected, isLogLevelEnabled(level1 as LogLevel, level2 as LogLevel)) 53 | }) 54 | }) 55 | }) 56 | 57 | describe('DefaultLogger implementation', () => { 58 | it('should check logging method of configured log level are called', async () => { 59 | const logging = new DefaultLogger('warn', mockConnection as Connection) 60 | assert.strictEqual('warn', logging.level) 61 | assert.strictEqual('function', typeof logging.error) 62 | assert.strictEqual('function', typeof logging.warn) 63 | assert.strictEqual('function', typeof logging.info) 64 | assert.strictEqual('function', typeof logging.log) 65 | assert.strictEqual('function', typeof logging.debug) 66 | logging.error('test error message') 67 | sinon.assert.calledOnceWithMatch(mockConnection.console?.error as sinon.SinonStub, 'test error message') 68 | logging.warn('test warn message') 69 | sinon.assert.calledOnceWithMatch(mockConnection.console?.warn as sinon.SinonStub, 'test warn message') 70 | logging.info('test info message') 71 | sinon.assert.notCalled(mockConnection.console?.info as sinon.SinonStub) 72 | logging.log('test log message') 73 | sinon.assert.notCalled(mockConnection.console?.log as sinon.SinonStub) 74 | logging.debug('test debug message') 75 | sinon.assert.notCalled(mockConnection.console?.debug as sinon.SinonStub) 76 | }) 77 | }) 78 | }) 79 | -------------------------------------------------------------------------------- /runtimes/runtimes/operational-telemetry/resource-metrics.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceMetrics": [ 3 | { 4 | "resource": { 5 | "attributes": [ 6 | { 7 | "key": "server.name", 8 | "value": { 9 | "stringValue": "test-telemetry-service" 10 | } 11 | }, 12 | { 13 | "key": "server.version", 14 | "value": { 15 | "stringValue": "1.0.0" 16 | } 17 | }, 18 | { 19 | "key": "clientInfo.name", 20 | "value": { 21 | "stringValue": "test-client" 22 | } 23 | }, 24 | { 25 | "key": "clientInfo.version", 26 | "value": { 27 | "stringValue": "1.0.0" 28 | } 29 | }, 30 | { 31 | "key": "clientInfo.clientId", 32 | "value": { 33 | "stringValue": "test-client-id" 34 | } 35 | }, 36 | { 37 | "key": "clientInfo.extension.name", 38 | "value": { 39 | "stringValue": "test-extension" 40 | } 41 | }, 42 | { 43 | "key": "clientInfo.extension.version", 44 | "value": { 45 | "stringValue": "1.0.0" 46 | } 47 | }, 48 | { 49 | "key": "operational.telemetry.schema.version", 50 | "value": { 51 | "stringValue": "1.0.0" 52 | } 53 | }, 54 | { 55 | "key": "telemetry.sdk.version", 56 | "value": { 57 | "stringValue": "2.0.0" 58 | } 59 | }, 60 | { 61 | "key": "sessionId", 62 | "value": { 63 | "stringValue": "80fd44e9-55e5-4b80-a08a-4f2bcaf2e1b9" 64 | } 65 | } 66 | ], 67 | "droppedAttributesCount": 0 68 | }, 69 | "scopeMetrics": [ 70 | { 71 | "scope": { 72 | "name": "language-server-runtimes", 73 | "version": "" 74 | }, 75 | "metrics": [ 76 | { 77 | "name": "heapUsed", 78 | "description": "", 79 | "unit": "byte", 80 | "gauge": { 81 | "dataPoints": [ 82 | { 83 | "attributes": [], 84 | "startTimeUnixNano": "1746710710801000000", 85 | "timeUnixNano": "1746710710801000000", 86 | "asDouble": 12345 87 | } 88 | ] 89 | } 90 | } 91 | ] 92 | } 93 | ] 94 | } 95 | ] 96 | } 97 | -------------------------------------------------------------------------------- /runtimes/runtimes/operational-telemetry/telemetry-schemas/telemetry-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "definitions": { 4 | "MetricName": { 5 | "title": "MetricName", 6 | "type": "string", 7 | "enum": ["userCpuUsage", "systemCpuUsage", "heapUsed", "heapTotal", "rss", "memoryUsage"] 8 | }, 9 | "OperationalTelemetryResource": { 10 | "title": "OperationalTelemetryResource", 11 | "type": "object", 12 | "required": ["operational.telemetry.schema.version", "sessionId", "server.name", "telemetry.sdk.version"], 13 | "additionalProperties": false, 14 | "properties": { 15 | "operational.telemetry.schema.version": { 16 | "type": "string", 17 | "description": "The version of the operational telemetry schema that the telemetry data is compliant with", 18 | "const": "1.0.0" 19 | }, 20 | "sessionId": { 21 | "type": "string", 22 | "description": "A unique identifier for the runtime session that persists through the lifetime of the language server" 23 | }, 24 | "server.name": { 25 | "type": "string", 26 | "description": "The name of the language server as reported during the LSP initialize request" 27 | }, 28 | "server.version": { 29 | "type": "string", 30 | "description": "The version of the language server as reported during the LSP initialize request" 31 | }, 32 | "clientInfo.name": { 33 | "type": "string", 34 | "description": "The name of the client (IDE/editor) that connects to the server as reported during the LSP initialize request" 35 | }, 36 | "clientInfo.version": { 37 | "type": "string", 38 | "description": "The version of the client as reported during the LSP initialize request" 39 | }, 40 | "clientInfo.clientId": { 41 | "type": "string", 42 | "description": "Unique identifier for the client as reported during the LSP initialize request" 43 | }, 44 | "clientInfo.extension.name": { 45 | "type": "string", 46 | "description": "The name of the client extension as reported during the LSP initialize request" 47 | }, 48 | "clientInfo.extension.version": { 49 | "type": "string", 50 | "description": "The version of the client extension as reported during the LSP initialize request" 51 | }, 52 | "telemetry.sdk.version": { 53 | "type": "string", 54 | "description": "The version string of the OpenTelemetry SDK." 55 | } 56 | } 57 | }, 58 | "ErrorEventAttributes": { 59 | "title": "ErrorEventAttributes", 60 | "type": "object", 61 | "additionalProperties": false, 62 | "required": ["errorName", "errorOrigin", "errorType"], 63 | "properties": { 64 | "errorName": { 65 | "type": "string" 66 | }, 67 | "errorOrigin": { 68 | "type": "string", 69 | "enum": ["caughtError", "uncaughtException", "unhandledRejection", "other"] 70 | }, 71 | "errorType": { 72 | "type": "string" 73 | }, 74 | "errorCode": { 75 | "type": "string" 76 | }, 77 | "errorMessage": { 78 | "type": "string" 79 | } 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /runtimes/runtimes/operational-telemetry/README.md: -------------------------------------------------------------------------------- 1 | # Operational Telemetry for Language Server Runtimes 2 | 3 | The Operational Telemetry Service collects, processes and exports operational telemetry data from language server runtimes. This service integrates with OpenTelemetry to gather various types of operational data. Based on the [telemetry-schemas](./telemetry-schemas/), currently it can collect: 4 | * resource usage metrics, 5 | * caught/uncaught errors. 6 | 7 | Json telemetry schemas can be extended in the future to send other type of operational data. These schemas have a strictly defined format that allow us to benefit from TypeScript type checks and validation on the backend. 8 | 9 | The collected data is then securely sent in batches via https to an AWS API Gateway endpoint. Collected data is sent on a best effort basis, and any telemetry related error should not impact customer experience using standalone runtime. 10 | 11 | 12 | ## Telemetry Opt-Out 13 | 14 | The OperationalTelemetryService implements a telemetry opt-out mechanism that respects user privacy preferences and allows changes during the session without restarting the IDE. When telemetry is opted out, all data collection and exports are disabled through OpenTelemetry SDK shutdown. The opt-out state can be toggled at any time using the toggleOptOut method. 15 | 16 | For the standalone runtime, the OperationalTelemetryService instance is initialized during the initialize handshake. It checks for telemetry preferences in the initialization options, defaulting to opted-out (true) if no preference is specified. It then instantiates the OperationalTelemetryService with these preferences along with service information and client details. A didChangeConfigurationHandler is added to listen for updates to the 'aws.optOutTelemetry' workspace setting, allowing users to dynamically toggle telemetry collection through their IDE settings. When a configuration change occurs, the handler updates the telemetry opt-out state accordingly through the OperationalTelemetryProvider. 17 | 18 | 19 | ## Usage Instructions 20 | 21 | 1. Initialize the OperationalTelemetryService and set it in the OperationalTelemetryProvider: 22 | 23 | ```typescript 24 | const telemetryService = OperationalTelemetryService.getInstance({serviceName: 'language-server-runtimes', serviceVersion: '1.0.0', lspConsole: lspConnection.console, endpoint: 'example.com', telemetryOptOut: false}); 25 | OperationalTelemetryProvider.setTelemetryInstance(telemetryService) 26 | ``` 27 | 28 | 2. Retrieve the telemetry instance from the provider and register gauge providers for resource usage metrics: 29 | 30 | ```typescript 31 | const telemetryService = OperationalTelemetryProvider.getTelemetryForScope('myScope'); 32 | telemetryService.registerGaugeProvider('heapTotal', () => process.memoryUsage().heapTotal, 'byte') 33 | telemetryService.registerGaugeProvider('heapUsed', () => process.memoryUsage().heapUsed, 'byte') 34 | telemetryService.registerGaugeProvider('rss', () => process.memoryUsage().rss, 'byte') 35 | ``` 36 | 37 | 3. Record errors or server crashes: 38 | 39 | ```typescript 40 | telemetryService.emitEvent({ 41 | errorOrigin: 'caughtError', 42 | errorType: 'proxyCertificateRead', 43 | errorName: error?.name ?? 'unknown', 44 | errorCode: error?.code ?? '', 45 | errorMessage: 'Failed to parse server name', 46 | }) 47 | ``` 48 | 49 | ## Configuration 50 | 51 | The service requires the following configuration: 52 | - AWS API Gateway Endpoint 53 | 54 | This can be configured using the following environment variables: 55 | - `TELEMETRY_GATEWAY_ENDPOINT` - The endpoint URL for the telemetry gateway 56 | 57 | Default values for these configurations can be found in `language-server-runtimes/runtimes/runtimes/util/telemetryLspServer.ts`. 58 | 59 | ## Data Flow 60 | 61 | 1. Telemetry signals are collected by OpenTelemetry SDK. 62 | 2. OTLP HTTP exporters transform and export collected data. 63 | 3. Telemetry data reaches AWS API Gateway endpoint. 64 | 65 | ``` 66 | [Application] -> [OpenTelemetry SDK] -> [OTLP HTTP request] -> [AWS API Gateway] 67 | ``` -------------------------------------------------------------------------------- /script/clean.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | /* 7 | * This script removes compilation and packaging related files/folders. 8 | * Used to perform a clean compile, which is useful for things like: 9 | * - flushing out stale test files. 10 | * - updating dependencies after changing branches 11 | */ 12 | 13 | /* 14 | * File copied over from https://github.com/aws/language-servers/blob/main/script/clean.ts 15 | */ 16 | 17 | import * as fs from 'fs' 18 | import * as path from 'path' 19 | import * as util from 'util' 20 | 21 | const readFile = util.promisify(fs.readFile) 22 | const readdir = util.promisify(fs.readdir) 23 | const rmdir = util.promisify(fs.rmdir) 24 | const stat = util.promisify(fs.stat) 25 | const unlink = util.promisify(fs.unlink) 26 | 27 | // Recursive delete without requiring a third-party library. This allows the script 28 | // to be run before `npm install`. 29 | async function rdelete(p: string) { 30 | const stats = await stat(p) 31 | if (stats.isFile()) { 32 | await unlink(p) 33 | } else if (stats.isDirectory()) { 34 | const promises = (await readdir(p)).map(child => rdelete(path.join(p, child))) 35 | 36 | await Promise.all(promises) 37 | await rmdir(p) 38 | } else { 39 | throw new Error(`Could not delete '${p}' because it is neither a file nor directory`) 40 | } 41 | } 42 | 43 | async function tryDelete(target: string) { 44 | try { 45 | if (!exists(target)) { 46 | console.log( 47 | `Could not access '${target}', probably because it does not exist. Skipping clean for this path.` 48 | ) 49 | return 50 | } 51 | 52 | await rdelete(target) 53 | } catch (e) { 54 | console.error(`Could not clean '${target}': ${String(e)}`) 55 | } 56 | } 57 | 58 | function exists(p: string): boolean { 59 | try { 60 | fs.accessSync(p) 61 | return true 62 | } catch { 63 | return false 64 | } 65 | } 66 | 67 | function getPathsToDelete(): string[] { 68 | const subfolders = ['runtimes', 'types'] 69 | 70 | const paths: string[] = [] 71 | 72 | for (const subfolder of subfolders) { 73 | const fullPath = path.join(process.cwd(), subfolder) 74 | paths.push(...rFileFind(fullPath, 'tsconfig.tsbuildinfo')) 75 | paths.push(...rDirectoryFind(fullPath, 'out')) 76 | } 77 | 78 | return paths 79 | } 80 | 81 | function rFileFind(parentPath: string, fileName: string): string[] { 82 | if (!fs.existsSync(parentPath) || !fs.lstatSync(parentPath).isDirectory()) { 83 | return [] 84 | } 85 | 86 | const files: string[] = [] 87 | 88 | const childFiles = fs.readdirSync(parentPath) 89 | for (const childFile of childFiles) { 90 | const filePath = path.join(parentPath, childFile) 91 | const fileStat = fs.lstatSync(filePath) 92 | 93 | if (fileStat.isDirectory()) { 94 | files.push(...rFileFind(filePath, fileName)) 95 | } else if (childFile === fileName) { 96 | files.push(filePath) 97 | } 98 | } 99 | 100 | return files 101 | } 102 | 103 | function rDirectoryFind(parentPath: string, directoryName: string): string[] { 104 | if (!fs.existsSync(parentPath) || !fs.lstatSync(parentPath).isDirectory()) { 105 | return [] 106 | } 107 | 108 | const directories: string[] = [] 109 | 110 | const childFiles = fs.readdirSync(parentPath) 111 | for (const childFile of childFiles) { 112 | const fullPath = path.join(parentPath, childFile) 113 | const fileStat = fs.lstatSync(fullPath) 114 | 115 | if (fileStat.isDirectory()) { 116 | if (childFile === directoryName) { 117 | directories.push(fullPath) 118 | } else { 119 | directories.push(...rDirectoryFind(fullPath, directoryName)) 120 | } 121 | } 122 | } 123 | return directories 124 | } 125 | 126 | ;(async () => { 127 | const pathsToDelete = getPathsToDelete() 128 | 129 | await Promise.all(pathsToDelete.map(tryDelete)) 130 | })() 131 | -------------------------------------------------------------------------------- /runtimes/runtimes/util/telemetryLspServer.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from 'vscode-languageserver/node' 2 | import { Encoding } from '../encoding' 3 | import { Logging } from '../../server-interface/logging' 4 | import { LspServer } from '../lsp/router/lspServer' 5 | import { OperationalTelemetryProvider, TELEMETRY_SCOPES } from '../operational-telemetry/operational-telemetry' 6 | import { RuntimeProps } from '../runtime' 7 | import { InitializeParams, InitializeResult } from '../../protocol' 8 | import { Runtime } from '../../server-interface' 9 | import { totalmem } from 'os' 10 | import { OperationalTelemetryService } from '../operational-telemetry/operational-telemetry-service' 11 | 12 | const DEFAULT_TELEMETRY_ENDPOINT = 'https://telemetry.aws-language-servers.us-east-1.amazonaws.com' 13 | 14 | function setMemoryUsageTelemetry() { 15 | const optel = OperationalTelemetryProvider.getTelemetryForScope(TELEMETRY_SCOPES.RUNTIMES) 16 | optel.registerGaugeProvider('heapTotal', () => process.memoryUsage().heapTotal, 'byte') 17 | optel.registerGaugeProvider('heapUsed', () => process.memoryUsage().heapUsed, 'byte') 18 | optel.registerGaugeProvider('rss', () => process.memoryUsage().rss, 'byte') 19 | optel.registerGaugeProvider('userCpuUsage', () => process.cpuUsage().user, 'second') 20 | optel.registerGaugeProvider('systemCpuUsage', () => process.cpuUsage().system, 'second') 21 | optel.registerGaugeProvider('memoryUsage', () => (process.memoryUsage().rss / totalmem()) * 100, 'percent') 22 | } 23 | 24 | function setServerCrashTelemetryListeners() { 25 | const optel = OperationalTelemetryProvider.getTelemetryForScope(TELEMETRY_SCOPES.RUNTIMES) 26 | 27 | // Handles both 'uncaughtException' and 'unhandledRejection' 28 | process.on('uncaughtExceptionMonitor', async (err, origin) => { 29 | optel.emitEvent({ 30 | errorOrigin: origin, 31 | errorType: 'unknownServerCrash', 32 | errorName: err?.name ?? 'unknown', 33 | }) 34 | }) 35 | } 36 | 37 | export function getTelemetryLspServer( 38 | lspConnection: Connection, 39 | encoding: Encoding, 40 | logging: Logging, 41 | props: RuntimeProps, 42 | runtime: Runtime 43 | ): LspServer { 44 | const lspServer = new LspServer(lspConnection, encoding, logging) 45 | 46 | lspServer.setInitializeHandler(async (params: InitializeParams): Promise => { 47 | const optOut = params.initializationOptions?.telemetryOptOut ?? true // telemetry disabled if option not provided 48 | const endpoint = runtime.getConfiguration('TELEMETRY_GATEWAY_ENDPOINT') ?? DEFAULT_TELEMETRY_ENDPOINT 49 | 50 | // Initialize telemetry asynchronously without blocking 51 | setImmediate(() => { 52 | try { 53 | logging.debug(`Configuring Runtimes OperationalTelemetry with endpoint: ${endpoint}`) 54 | 55 | const optel = OperationalTelemetryService.getInstance({ 56 | serviceName: props.name, 57 | serviceVersion: props.version, 58 | extendedClientInfo: params.initializationOptions?.aws?.clientInfo, 59 | logging: logging, 60 | endpoint: endpoint, 61 | telemetryOptOut: optOut, 62 | }) 63 | 64 | OperationalTelemetryProvider.setTelemetryInstance(optel) 65 | 66 | logging.info(`Initialized Runtimes OperationalTelemetry with optOut=${optOut}`) 67 | 68 | setServerCrashTelemetryListeners() 69 | setMemoryUsageTelemetry() 70 | } catch (error) { 71 | logging.warn(`Failed to initialize telemetry: ${error}`) 72 | } 73 | }) 74 | 75 | return { 76 | capabilities: {}, 77 | } 78 | }) 79 | 80 | lspServer.setDidChangeConfigurationHandler(async params => { 81 | const optOut = await lspConnection.workspace.getConfiguration({ 82 | section: 'aws.optOutTelemetry', 83 | }) 84 | 85 | if (typeof optOut === 'boolean') { 86 | logging.info(`Updating Runtimes OperationalTelemetry with optOut=${optOut}`) 87 | OperationalTelemetryProvider.getTelemetryForScope('').toggleOptOut(optOut) 88 | setMemoryUsageTelemetry() 89 | } 90 | }) 91 | 92 | return lspServer 93 | } 94 | -------------------------------------------------------------------------------- /runtimes/runtimes/operational-telemetry/resource-logs.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceLogs": [ 3 | { 4 | "resource": { 5 | "attributes": [ 6 | { 7 | "key": "server.name", 8 | "value": { 9 | "stringValue": "test-telemetry-service" 10 | } 11 | }, 12 | { 13 | "key": "server.version", 14 | "value": { 15 | "stringValue": "1.0.0" 16 | } 17 | }, 18 | { 19 | "key": "clientInfo.name", 20 | "value": { 21 | "stringValue": "test-client" 22 | } 23 | }, 24 | { 25 | "key": "clientInfo.version", 26 | "value": { 27 | "stringValue": "1.0.0" 28 | } 29 | }, 30 | { 31 | "key": "clientInfo.clientId", 32 | "value": { 33 | "stringValue": "test-client-id" 34 | } 35 | }, 36 | { 37 | "key": "clientInfo.extension.name", 38 | "value": { 39 | "stringValue": "test-extension" 40 | } 41 | }, 42 | { 43 | "key": "clientInfo.extension.version", 44 | "value": { 45 | "stringValue": "1.0.0" 46 | } 47 | }, 48 | { 49 | "key": "operational.telemetry.schema.version", 50 | "value": { 51 | "stringValue": "1.0.0" 52 | } 53 | }, 54 | { 55 | "key": "telemetry.sdk.version", 56 | "value": { 57 | "stringValue": "2.0.0" 58 | } 59 | }, 60 | { 61 | "key": "sessionId", 62 | "value": { 63 | "stringValue": "80fd44e9-55e5-4b80-a08a-4f2bcaf2e1b9" 64 | } 65 | } 66 | ], 67 | "droppedAttributesCount": 0 68 | }, 69 | "scopeLogs": [ 70 | { 71 | "scope": { 72 | "name": "language-server-runtimes" 73 | }, 74 | "logRecords": [ 75 | { 76 | "timeUnixNano": "1746710710801000000", 77 | "observedTimeUnixNano": "1746710710801000000", 78 | "body": {}, 79 | "attributes": [ 80 | { 81 | "key": "errorName", 82 | "value": { 83 | "stringValue": "TestError" 84 | } 85 | }, 86 | { 87 | "key": "errorOrigin", 88 | "value": { 89 | "stringValue": "caughtError" 90 | } 91 | }, 92 | { 93 | "key": "errorType", 94 | "value": { 95 | "stringValue": "Error" 96 | } 97 | } 98 | ], 99 | "droppedAttributesCount": 0 100 | } 101 | ] 102 | } 103 | ] 104 | } 105 | ] 106 | } 107 | -------------------------------------------------------------------------------- /runtimes/runtimes/util/standalone/getProxySettings/getWindowsProxySettings.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Based on windows-system-proxy 1.0.0 (Apache-2.0). Modified for synchronous use 6 | * https://github.com/httptoolkit/windows-system-proxy/blob/main/src/index.ts 7 | */ 8 | import winreg from 'winreg' 9 | 10 | export interface ProxyConfig { 11 | proxyUrl: string 12 | noProxy: string[] 13 | } 14 | 15 | const KEY_PROXY_ENABLE = 'ProxyEnable' 16 | const KEY_PROXY_SERVER = 'ProxyServer' 17 | const KEY_PROXY_OVERRIDE = 'ProxyOverride' 18 | 19 | type WindowsProxyRegistryKeys = { 20 | proxyEnable: string | undefined 21 | proxyServer: string | undefined 22 | proxyOverride: string | undefined 23 | } 24 | 25 | function readWindowsRegistry(): Promise { 26 | return new Promise((resolve, reject) => { 27 | const regKey = new winreg({ 28 | hive: winreg.HKCU, 29 | key: '\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings', 30 | }) 31 | 32 | regKey.values((err: Error, items: winreg.RegistryItem[]) => { 33 | if (err) { 34 | console.warn('', err.message) 35 | resolve({ 36 | proxyEnable: undefined, 37 | proxyServer: undefined, 38 | proxyOverride: undefined, 39 | }) 40 | return 41 | } 42 | 43 | const results: Record = {} 44 | 45 | items.forEach((item: winreg.RegistryItem) => { 46 | results[item.name] = item.value as string 47 | }) 48 | 49 | resolve({ 50 | proxyEnable: results[KEY_PROXY_ENABLE], 51 | proxyServer: results[KEY_PROXY_SERVER], 52 | proxyOverride: results[KEY_PROXY_OVERRIDE], 53 | }) 54 | }) 55 | }) 56 | } 57 | 58 | export async function getWindowsSystemProxy(): Promise { 59 | const registryValues = await readWindowsRegistry() 60 | const proxyEnabled = registryValues.proxyEnable 61 | const proxyServer = registryValues.proxyServer 62 | const proxyOverride = registryValues.proxyOverride 63 | 64 | if (!proxyEnabled || !proxyServer) { 65 | console.debug('Proxy not enabled or server not configured') 66 | return undefined 67 | } 68 | 69 | const noProxy = (proxyOverride ? (proxyOverride as string).split(';') : []).flatMap(host => 70 | host === '' ? ['localhost', '127.0.0.1', '::1'] : [host] 71 | ) 72 | 73 | // Parse proxy configuration which can be in multiple formats 74 | const proxyConfigString = proxyServer 75 | 76 | if (proxyConfigString.startsWith('http://') || proxyConfigString.startsWith('https://')) { 77 | console.debug('Using full URL format proxy configuration') 78 | // Handle full URL format (documented in Microsoft registry configuration guide) 79 | // https://docs.microsoft.com/en-us/troubleshoot/windows-client/networking/configure-client-proxy-server-settings-by-registry-file 80 | return { 81 | proxyUrl: proxyConfigString, 82 | noProxy, 83 | } 84 | } else if (proxyConfigString.includes('=')) { 85 | console.debug('Using protocol-specific format proxy configuration') 86 | // Handle protocol-specific format: protocol=host;protocol=host pairs 87 | // Prefer HTTPS, then HTTP, then SOCKS proxy 88 | const proxies = Object.fromEntries( 89 | proxyConfigString.split(';').map(proxyPair => proxyPair.split('=') as [string, string]) 90 | ) 91 | 92 | const proxyUrl = proxies['https'] 93 | ? `https://${proxies['https']}` 94 | : proxies['http'] 95 | ? `http://${proxies['http']}` 96 | : // TODO: Enable support for SOCKS Proxy 97 | // proxies['socks'] ? `socks://${proxies['socks']}`: 98 | undefined 99 | 100 | if (!proxyUrl) { 101 | throw new Error(`Could not get usable proxy URL from ${proxyConfigString}`) 102 | } 103 | console.debug(`Selected proxy URL: ${proxyUrl}`) 104 | 105 | return { 106 | proxyUrl, 107 | noProxy, 108 | } 109 | } else { 110 | console.debug('Using bare hostname format, defaulting to HTTP') 111 | // Handle bare hostname format, default to HTTP 112 | return { 113 | proxyUrl: `http://${proxyConfigString}`, 114 | noProxy, 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /runtimes/runtimes/lsp/textDocuments/textDocumentConnection.ts: -------------------------------------------------------------------------------- 1 | import { Observable, fromEventPattern, share } from 'rxjs' 2 | import { 3 | DidChangeTextDocumentParams, 4 | DidCloseTextDocumentParams, 5 | DidOpenTextDocumentParams, 6 | DidSaveTextDocumentParams, 7 | WillSaveTextDocumentParams, 8 | NotificationHandler, 9 | RequestHandler, 10 | TextEdit, 11 | } from '../../../protocol' 12 | import { TextDocumentConnection } from 'vscode-languageserver/lib/common/textDocuments' 13 | 14 | // Filter out only the handlers that return void to avoid the request/response format, 15 | // which would not support multiple handlers since it requires disambiguating the handler 16 | // responsible for responding. 17 | type TextDocumentNotifications = { 18 | [key in keyof TextDocumentConnection]: ReturnType[0]> extends void 19 | ? key 20 | : never 21 | }[keyof TextDocumentConnection] 22 | 23 | type TextDocumentObservable = { 24 | [key in TextDocumentNotifications]: Observable[0]>[0]> 25 | } 26 | 27 | /** 28 | * Wrap a standard LSP {TextDocumentConnection}, that only supports a single callback for each operation, 29 | * with observables for each operation. 30 | * 31 | * This is useful for integrating mutliple Servers that each want to handle events one or more times. The {Observable} 32 | * interface allows for low-level control of subscribe, resubscribe, and notification handling. The callback interface 33 | * mimics the {TextDocumentConnection}, but does not overwrite each callback with the next. 34 | * 35 | * **Note:** {onWillSaveTextDocumentWaitUntil} will NOT support multiple handlers. The last registrered is the one 36 | * providing the return value. This is due to multiple handlers would have to disabiguate which one gets to 37 | * return the actual response, without some sort of smart merging of the desired edits. 38 | * 39 | * @param connection A {TextDocumentConnection} to wrap with observables 40 | * @returns A wrapper around the {TextDocumentConnection} providing both a callback and an observable interface 41 | */ 42 | export const observe = ( 43 | connection: TextDocumentConnection 44 | ): { callbacks: TextDocumentConnection } & TextDocumentObservable => { 45 | const onDidChangeTextDocument = fromEventPattern( 46 | connection.onDidChangeTextDocument 47 | ).pipe(share()) 48 | const onDidOpenTextDocument = fromEventPattern(connection.onDidOpenTextDocument).pipe( 49 | share() 50 | ) 51 | const onDidCloseTextDocument = fromEventPattern(connection.onDidCloseTextDocument).pipe( 52 | share() 53 | ) 54 | const onWillSaveTextDocument = fromEventPattern(connection.onWillSaveTextDocument).pipe( 55 | share() 56 | ) 57 | const onDidSaveTextDocument = fromEventPattern(connection.onDidSaveTextDocument).pipe( 58 | share() 59 | ) 60 | 61 | return { 62 | callbacks: { 63 | onDidChangeTextDocument: (handler: NotificationHandler) => { 64 | const subscription = onDidChangeTextDocument.subscribe(handler) 65 | return { dispose: () => subscription.unsubscribe() } 66 | }, 67 | onDidOpenTextDocument: (handler: NotificationHandler) => { 68 | const subscription = onDidOpenTextDocument.subscribe(handler) 69 | return { dispose: () => subscription.unsubscribe() } 70 | }, 71 | onDidCloseTextDocument: (handler: NotificationHandler) => { 72 | const subscription = onDidCloseTextDocument.subscribe(handler) 73 | return { dispose: () => subscription.unsubscribe() } 74 | }, 75 | onWillSaveTextDocument: (handler: NotificationHandler) => { 76 | const subscription = onWillSaveTextDocument.subscribe(handler) 77 | return { dispose: () => subscription.unsubscribe() } 78 | }, 79 | onDidSaveTextDocument: (handler: NotificationHandler) => { 80 | const subscription = onDidSaveTextDocument.subscribe(handler) 81 | return { dispose: () => subscription.unsubscribe() } 82 | }, 83 | onWillSaveTextDocumentWaitUntil: ( 84 | handler: RequestHandler 85 | ) => { 86 | return connection.onWillSaveTextDocumentWaitUntil(handler) 87 | }, 88 | }, 89 | 90 | onDidChangeTextDocument, 91 | onDidOpenTextDocument, 92 | onDidCloseTextDocument, 93 | onWillSaveTextDocument, 94 | onDidSaveTextDocument, 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /runtimes/runtimes/agent.ts: -------------------------------------------------------------------------------- 1 | import Ajv, { ErrorObject, ValidateFunction } from 'ajv' 2 | import { 3 | Agent, 4 | BedrockTools, 5 | CancellationToken, 6 | GetToolsOptions, 7 | InferSchema, 8 | ObjectSchema, 9 | ToolClassification, 10 | Tools, 11 | ToolSpec, 12 | } from '../server-interface' 13 | 14 | type Tool = { 15 | name: string 16 | description: string 17 | inputSchema: ObjectSchema 18 | validate: (input: T, token?: CancellationToken) => boolean | ValidateFunction['errors'] 19 | invoke: (input: T, token?: CancellationToken, updates?: WritableStream) => Promise 20 | } 21 | 22 | export const newAgent = (): Agent => { 23 | const tools: Record> = {} 24 | const ajv = new Ajv({ strictSchema: false }) 25 | const builtInToolNames: string[] = [] 26 | const builtInWriteToolNames: string[] = [] 27 | 28 | return { 29 | addTool: , S extends ToolSpec>( 30 | spec: S, 31 | handler: (input: T, token?: CancellationToken) => Promise, 32 | toolClassification?: ToolClassification 33 | ) => { 34 | const validator = ajv.compile(spec.inputSchema) 35 | const tool = { 36 | validate: (input: InferSchema) => { 37 | const isValid = validator(input) 38 | return validator.errors ?? isValid 39 | }, 40 | invoke: handler, 41 | name: spec.name, 42 | description: spec.description, 43 | inputSchema: spec.inputSchema, 44 | } 45 | 46 | tools[spec.name] = tool 47 | if ( 48 | toolClassification === ToolClassification.BuiltIn || 49 | toolClassification === ToolClassification.BuiltInCanWrite 50 | ) { 51 | builtInToolNames.push(spec.name) 52 | if (toolClassification === ToolClassification.BuiltInCanWrite) { 53 | builtInWriteToolNames.push(spec.name) 54 | } 55 | } 56 | }, 57 | 58 | runTool: async (toolName: string, input: any, token?: CancellationToken, updates?: WritableStream) => { 59 | const tool = tools[toolName] 60 | if (!tool) { 61 | throw new Error(`Tool ${toolName} not found`) 62 | } 63 | 64 | const validateResult = tool.validate(input, token) 65 | if (validateResult !== true) { 66 | const errors = (validateResult as ValidateFunction['errors']) || [] 67 | const errorDetails = 68 | errors.map((err: ErrorObject) => `${err.instancePath || 'root'}: ${err.message}`).join('\n') || 69 | `\nReceived: ${input}` 70 | if ( 71 | errors.length === 1 && 72 | errors.at(0)?.instancePath === '/diffs' && 73 | errors.at(0)?.message === 'must be array' 74 | ) { 75 | throw new Error( 76 | `${toolName} tool input validation failed: ${errorDetails} in JSON format without quotation marks` 77 | ) 78 | } 79 | throw new Error(`${toolName} tool input validation failed: ${errorDetails}`) 80 | } 81 | 82 | return tool.invoke(input, token, updates) 83 | }, 84 | 85 | getTools: (options?: T) => { 86 | // we have to manually assert the type since 87 | // Typescript won't be able to infer the return type 88 | // from the if statement. 89 | if (options?.format === 'bedrock') { 90 | return Object.values(tools).map((tool: Tool) => { 91 | return { 92 | toolSpecification: { 93 | name: tool.name, 94 | description: tool.description, 95 | inputSchema: { 96 | json: tool.inputSchema, 97 | }, 98 | }, 99 | } 100 | }) as T extends { format: 'bedrock' } ? BedrockTools : never 101 | } 102 | 103 | return Object.values(tools).map((tool: Tool) => { 104 | return { 105 | name: tool.name, 106 | description: tool.description, 107 | inputSchema: tool.inputSchema, 108 | } 109 | }) as T extends { format: 'bedrock' } ? never : Tools 110 | }, 111 | 112 | removeTool: (name: string) => { 113 | delete tools[name] 114 | }, 115 | 116 | getBuiltInToolNames: () => { 117 | return builtInToolNames 118 | }, 119 | 120 | getBuiltInWriteToolNames: () => { 121 | return builtInWriteToolNames 122 | }, 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /types/inlineCompletionWithReferences.ts: -------------------------------------------------------------------------------- 1 | import { InlineCompletionItem } from 'vscode-languageserver-types' 2 | 3 | /** 4 | * Extend InlineCompletionItem to include optional references and imports. 5 | */ 6 | export type InlineCompletionItemWithReferences = InlineCompletionItem & { 7 | /** 8 | * Identifier for the the recommendation returned by server. 9 | */ 10 | itemId: string 11 | 12 | /** 13 | * Flag to indicate this is an edit suggestion rather than a standard inline completion. 14 | */ 15 | isInlineEdit?: boolean 16 | 17 | /** 18 | * Specifies where the next edit suggestion should appear for tab-tab-tab workflow navigation. 19 | */ 20 | displayLocation?: { 21 | range: { 22 | start: { line: number; character: number } 23 | end: { line: number; character: number } 24 | } 25 | label: string 26 | } 27 | 28 | references?: { 29 | referenceName?: string 30 | referenceUrl?: string 31 | licenseName?: string 32 | position?: { 33 | startCharacter?: number 34 | endCharacter?: number 35 | } 36 | }[] 37 | 38 | mostRelevantMissingImports?: { 39 | statement?: string 40 | }[] 41 | } 42 | /** 43 | * Extend InlineCompletionList to include optional references. This is not inheriting from `InlineCompletionList` 44 | * since the `items` arrays are incompatible. 45 | */ 46 | 47 | export type InlineCompletionListWithReferences = { 48 | /** 49 | * Server returns a session ID for current recommendation session. 50 | * Client need to attach this session ID in the request when sending 51 | * a completion session results. 52 | */ 53 | sessionId: string 54 | /** 55 | * The inline completion items with optional references 56 | */ 57 | items: InlineCompletionItemWithReferences[] 58 | /** 59 | * Server returns partialResultToken for client to request next set of results 60 | */ 61 | partialResultToken?: number | string 62 | } 63 | 64 | export interface InlineCompletionStates { 65 | /** 66 | * Completion item was displayed in the client application UI. 67 | */ 68 | seen: boolean 69 | /** 70 | * Completion item was accepted. 71 | */ 72 | accepted: boolean 73 | /** 74 | * Recommendation was filtered out on the client-side and marked as discarded. 75 | */ 76 | discarded: boolean 77 | } 78 | 79 | export interface IdeDiagnostic { 80 | /** 81 | * The range at which the message applies. 82 | */ 83 | range?: { 84 | start: { line: number; character: number } 85 | end: { line: number; character: number } 86 | } 87 | /** 88 | * A human-readable string describing the source of the diagnostic 89 | */ 90 | source?: string 91 | /** 92 | * Diagnostic Error type 93 | */ 94 | severity?: string 95 | /** 96 | * Type of the diagnostic 97 | */ 98 | ideDiagnosticType: string 99 | } 100 | 101 | export interface LogInlineCompletionSessionResultsParams { 102 | /** 103 | * Session Id attached to get completion items response. 104 | * This value must match to the one that server returned in InlineCompletionListWithReferences response. 105 | */ 106 | sessionId: string 107 | /** 108 | * Map with results of interaction with completion items in the client UI. 109 | * This list contain a state of each recommendation items from the recommendation session. 110 | */ 111 | completionSessionResult: { 112 | [itemId: string /* Completion itemId */]: InlineCompletionStates 113 | } 114 | /** 115 | * Time from completion request invocation start to rendering of the first recommendation in the UI. 116 | */ 117 | firstCompletionDisplayLatency?: number 118 | /** 119 | * Total time when items from this completion session were visible in UI 120 | */ 121 | totalSessionDisplayTime?: number 122 | /** 123 | * Length of additional characters inputed by user from when the trigger happens to when the user decision was made 124 | */ 125 | typeaheadLength?: number 126 | /** 127 | * The number of new characters of code that will be added by the suggestion if accepted, excluding any characters 128 | * from the beginning of the suggestion that the user had typed in after the trigger. 129 | */ 130 | addedCharacterCount?: number 131 | /** 132 | * The number of characters of existing code that will be removed by the suggestion if accepted. 133 | */ 134 | deletedCharacterCount?: number 135 | /** 136 | * Flag to indicate this is an edit suggestion rather than a standard inline completion. 137 | */ 138 | isInlineEdit?: boolean 139 | /** 140 | * List of diagnostic added after inline completion completion acceptence. 141 | */ 142 | addedDiagnostics?: IdeDiagnostic[] 143 | /** 144 | * List of diagnostic removed after inline completion completion acceptence. 145 | */ 146 | removedDiagnostics?: IdeDiagnostic[] 147 | /** 148 | * Generic logging reason 149 | */ 150 | reason?: string 151 | } 152 | -------------------------------------------------------------------------------- /runtimes/server-interface/chat.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NotificationHandler, 3 | RequestHandler, 4 | ChatParams, 5 | ChatResult, 6 | EndChatParams, 7 | EndChatResult, 8 | FeedbackParams, 9 | FollowUpClickParams, 10 | InfoLinkClickParams, 11 | InsertToCursorPositionParams, 12 | LinkClickParams, 13 | QuickActionParams, 14 | QuickActionResult, 15 | SourceLinkClickParams, 16 | TabChangeParams, 17 | TabAddParams, 18 | TabRemoveParams, 19 | OpenTabParams, 20 | OpenTabResult, 21 | ChatUpdateParams, 22 | FileClickParams, 23 | InlineChatParams, 24 | InlineChatResult, 25 | ContextCommandParams, 26 | CreatePromptParams, 27 | InlineChatResultParams, 28 | ListConversationsParams, 29 | ListConversationsResult, 30 | ConversationClickParams, 31 | ConversationClickResult, 32 | GetSerializedChatResult, 33 | GetSerializedChatParams, 34 | TabBarActionParams, 35 | TabBarActionResult, 36 | ChatOptionsUpdateParams, 37 | PromptInputOptionChangeParams, 38 | ButtonClickParams, 39 | ButtonClickResult, 40 | ListMcpServersParams, 41 | ListMcpServersResult, 42 | McpServerClickResult, 43 | McpServerClickParams, 44 | OpenFileDialogParams, 45 | OpenFileDialogResult, 46 | RuleClickParams, 47 | ListRulesParams, 48 | ListRulesResult, 49 | RuleClickResult, 50 | PinnedContextParams, 51 | ActiveEditorChangedParams, 52 | ListAvailableModelsParams, 53 | ListAvailableModelsResult, 54 | SubscriptionDetailsParams, 55 | SubscriptionUpgradeParams, 56 | } from '../protocol' 57 | 58 | /** 59 | * The Chat feature interface. Provides access to chat features 60 | */ 61 | export type Chat = { 62 | // Requests 63 | onChatPrompt: (handler: RequestHandler) => void 64 | onInlineChatPrompt: ( 65 | handler: RequestHandler 66 | ) => void 67 | onEndChat: (handler: RequestHandler) => void 68 | onQuickAction: (handler: RequestHandler) => void 69 | openTab: (params: OpenTabParams) => Promise 70 | onButtonClick: (handler: RequestHandler) => void 71 | onListConversations: (handler: RequestHandler) => void 72 | onListMcpServers: (handler: RequestHandler) => void 73 | onMcpServerClick: (handler: RequestHandler) => void 74 | onConversationClick: (handler: RequestHandler) => void 75 | onTabBarAction: (handler: RequestHandler) => void 76 | getSerializedChat: (params: GetSerializedChatParams) => Promise 77 | onListRules: (handler: RequestHandler) => void 78 | onRuleClick: (handler: RequestHandler) => void 79 | onOpenFileDialog: ( 80 | handler: RequestHandler 81 | ) => void 82 | onListAvailableModels: (handler: RequestHandler) => void 83 | // Notifications 84 | onSendFeedback: (handler: NotificationHandler) => void 85 | onReady: (handler: NotificationHandler) => void 86 | onTabAdd: (handler: NotificationHandler) => void 87 | onTabChange: (handler: NotificationHandler) => void 88 | onTabRemove: (handler: NotificationHandler) => void 89 | onCodeInsertToCursorPosition: (handler: NotificationHandler) => void 90 | onLinkClick: (handler: NotificationHandler) => void 91 | onInfoLinkClick: (handler: NotificationHandler) => void 92 | onSourceLinkClick: (handler: NotificationHandler) => void 93 | onFollowUpClicked: (handler: NotificationHandler) => void 94 | sendChatUpdate: (params: ChatUpdateParams) => void 95 | onFileClicked: (handler: NotificationHandler) => void 96 | chatOptionsUpdate: (params: ChatOptionsUpdateParams) => void 97 | sendContextCommands: (params: ContextCommandParams) => void 98 | sendPinnedContext: (params: PinnedContextParams) => void 99 | onPinnedContextAdd: (handler: NotificationHandler) => void 100 | onPinnedContextRemove: (handler: NotificationHandler) => void 101 | onActiveEditorChanged: (handler: NotificationHandler) => void 102 | onCreatePrompt: (handler: NotificationHandler) => void 103 | onInlineChatResult: (handler: NotificationHandler) => void 104 | onPromptInputOptionChange: (handler: NotificationHandler) => void 105 | sendSubscriptionDetails: (params: SubscriptionDetailsParams) => void 106 | onSubscriptionUpgrade: (handler: NotificationHandler) => void 107 | } 108 | -------------------------------------------------------------------------------- /runtimes/runtimes/lsp/router/initializeUtils.test.ts: -------------------------------------------------------------------------------- 1 | import { URI } from 'vscode-uri' 2 | import * as path from 'path' 3 | import os from 'os' 4 | import * as assert from 'assert' 5 | import * as sinon from 'sinon' 6 | import { InitializeParams, WorkspaceFolder } from 'vscode-languageserver-protocol' 7 | import { getWorkspaceFoldersFromInit } from './initializeUtils' 8 | import { RemoteConsole } from 'vscode-languageserver' 9 | 10 | describe('initializeUtils', () => { 11 | let consoleStub: sinon.SinonStubbedInstance 12 | 13 | beforeEach(() => { 14 | consoleStub = { 15 | log: sinon.stub(), 16 | error: sinon.stub(), 17 | warn: sinon.stub(), 18 | info: sinon.stub(), 19 | } as sinon.SinonStubbedInstance 20 | }) 21 | 22 | describe('getWorkspaceFolders', () => { 23 | const sampleWorkspaceUri = 'file:///path/to/folder' 24 | const sampleWorkspaceName = 'folder' 25 | const sampleWorkspaceFolder: WorkspaceFolder = { 26 | name: sampleWorkspaceName, 27 | uri: sampleWorkspaceUri, 28 | } 29 | 30 | const createParams = (params: Partial) => params as InitializeParams 31 | 32 | it('should return workspaceFolders when provided', () => { 33 | const workspaceFolders: WorkspaceFolder[] = [ 34 | sampleWorkspaceFolder, 35 | { name: 'folder2', uri: 'file:///path/to/folder2' }, 36 | ] 37 | const params = createParams({ workspaceFolders }) 38 | const result = getWorkspaceFoldersFromInit(consoleStub, params) 39 | 40 | assert.deepStrictEqual(result, workspaceFolders) 41 | }) 42 | 43 | describe('should create workspace folder from rootUri when workspaceFolders is not provided', () => { 44 | const invalidWorkspaceFolderCases = [ 45 | ['no workspaceFolder param', { rootUri: sampleWorkspaceUri }], 46 | ['empty workspaceFolder param params', { WorkspaceFolders: [], rootUri: sampleWorkspaceUri }], 47 | ] as const 48 | 49 | invalidWorkspaceFolderCases.forEach(([name, input]) => { 50 | it(`should return root uri for ${name}`, () => { 51 | const params = createParams(input) 52 | const result = getWorkspaceFoldersFromInit(consoleStub, params) 53 | assert.deepStrictEqual(result, [sampleWorkspaceFolder]) 54 | }) 55 | }) 56 | const params = createParams({ rootUri: sampleWorkspaceUri }) 57 | const result = getWorkspaceFoldersFromInit(consoleStub, params) 58 | 59 | assert.deepStrictEqual(result, [sampleWorkspaceFolder]) 60 | }) 61 | 62 | it('should create workspace folder from rootPath when neither workspaceFolders nor rootUri is provided', () => { 63 | const rootPath = '/path/to/folder' 64 | const params = createParams({ rootPath: rootPath }) 65 | const result = getWorkspaceFoldersFromInit(consoleStub, params) 66 | 67 | assert.deepStrictEqual(result, [sampleWorkspaceFolder]) 68 | }) 69 | 70 | it('should use uri as folder name when URI basename is empty', () => { 71 | const rootUri = 'file:///' 72 | const params = createParams({ rootUri }) 73 | const result = getWorkspaceFoldersFromInit(consoleStub, params) 74 | 75 | assert.deepStrictEqual(result, [{ name: rootUri, uri: rootUri }]) 76 | }) 77 | 78 | it('should handle Windows paths correctly', () => { 79 | const rootPath = 'C:\\Users\\test\\folder' 80 | const pathUri = URI.parse(rootPath).toString() 81 | const params = createParams({ rootPath }) 82 | 83 | const result = getWorkspaceFoldersFromInit(consoleStub, params) 84 | let expectedName 85 | if (os.platform() === 'win32') { 86 | expectedName = path.basename(URI.parse(pathUri).fsPath) 87 | } else { 88 | // using path.basename on unix with a windows path 89 | // will cause it to return \\Users\\test\\folder instead 90 | expectedName = 'folder' 91 | } 92 | 93 | assert.deepStrictEqual(result, [{ name: expectedName, uri: pathUri }]) 94 | }) 95 | 96 | it('should handle rootUri with special characters', () => { 97 | const rootUri = 'file:///path/to/special%20project' 98 | const decodedPath = URI.parse(rootUri).path 99 | const folderName = path.basename(decodedPath) 100 | 101 | const params = createParams({ rootUri }) 102 | const result = getWorkspaceFoldersFromInit(consoleStub, params) 103 | 104 | assert.deepStrictEqual(result, [{ name: folderName, uri: rootUri }]) 105 | assert.equal('special project', result[0].name) 106 | }) 107 | 108 | describe('should return empty workspaceFolder array', () => { 109 | const emptyArrayCases = [ 110 | ['no params', {} as InitializeParams], 111 | ['undefined params', undefined as unknown as InitializeParams], 112 | ['null params', null as unknown as InitializeParams], 113 | ['empty workspaceFolders', { workspaceFolders: [] } as unknown as InitializeParams], 114 | ] as const 115 | 116 | emptyArrayCases.forEach(([name, input]) => { 117 | it(`should return empty array for ${name}`, () => { 118 | const result = getWorkspaceFoldersFromInit(consoleStub, input) 119 | assert.equal(result.length, 0) 120 | }) 121 | }) 122 | }) 123 | }) 124 | }) 125 | -------------------------------------------------------------------------------- /runtimes/server-interface/agent.ts: -------------------------------------------------------------------------------- 1 | import { CancellationToken } from '../protocol' 2 | 3 | interface BaseSchema { 4 | title?: string 5 | description?: string 6 | default?: any 7 | examples?: any[] 8 | $id?: string 9 | $schema?: string 10 | definitions?: Record 11 | [extensionKeywords: string]: any 12 | } 13 | 14 | interface StringSchema extends BaseSchema { 15 | type: 'string' 16 | minLength?: number 17 | maxLength?: number 18 | pattern?: string 19 | format?: string 20 | enum?: string[] 21 | } 22 | 23 | interface NumberSchema extends BaseSchema { 24 | type: 'number' | 'integer' 25 | minimum?: number 26 | maximum?: number 27 | exclusiveMinimum?: number 28 | exclusiveMaximum?: number 29 | multipleOf?: number 30 | enum?: number[] 31 | } 32 | 33 | interface BooleanSchema extends BaseSchema { 34 | type: 'boolean' 35 | enum?: boolean[] 36 | } 37 | 38 | interface ArraySchema extends BaseSchema { 39 | type: 'array' 40 | items: JSONSchema | JSONSchema[] 41 | minItems?: number 42 | maxItems?: number 43 | uniqueItems?: boolean 44 | additionalItems?: boolean | JSONSchema 45 | } 46 | 47 | export interface ObjectSchema extends BaseSchema { 48 | type: 'object' 49 | properties?: Record 50 | required?: readonly string[] 51 | additionalProperties?: boolean | JSONSchema 52 | minProperties?: number 53 | maxProperties?: number 54 | patternProperties?: Record 55 | dependencies?: Record 56 | } 57 | 58 | type JSONSchema = (StringSchema | NumberSchema | BooleanSchema | ArraySchema | ObjectSchema) & { 59 | $ref?: string 60 | allOf?: JSONSchema[] 61 | anyOf?: JSONSchema[] 62 | oneOf?: JSONSchema[] 63 | not?: JSONSchema 64 | } 65 | 66 | type Primitive = T['type'] extends 'string' 67 | ? string 68 | : T['type'] extends 'number' 69 | ? number 70 | : T['type'] extends 'boolean' 71 | ? boolean 72 | : T['type'] extends 'null' 73 | ? null 74 | : never 75 | 76 | type InferArray = T['items'] extends { type: string } 77 | ? InferSchema[] 78 | : never 79 | 80 | type InferObject }> = T extends { 81 | required: readonly string[] 82 | } 83 | ? { 84 | [K in keyof T['properties'] as K extends T['required'][number] ? K : never]: InferSchema 85 | } & { 86 | [K in keyof T['properties'] as K extends T['required'][number] ? never : K]?: InferSchema 87 | } 88 | : { 89 | [K in keyof T['properties']]?: InferSchema 90 | } 91 | 92 | export type InferSchema = T extends { type: 'array'; items: any } 93 | ? InferArray 94 | : T extends { type: 'object'; properties: Record } 95 | ? InferObject 96 | : T extends { type: string } 97 | ? Primitive 98 | : never 99 | 100 | export type ToolSpec = { 101 | name: string 102 | description: string 103 | inputSchema: ObjectSchema 104 | } 105 | 106 | export type GetToolsOptions = { 107 | format: 'bedrock' | 'mcp' 108 | } 109 | 110 | export type Tools = ToolSpec[] 111 | export type BedrockTools = { 112 | toolSpecification: Omit & { inputSchema: { json: ToolSpec['inputSchema'] } } 113 | }[] 114 | 115 | export enum ToolClassification { 116 | BuiltIn = 'builtIn', 117 | BuiltInCanWrite = 'builtInCanWrite', 118 | MCP = 'mcp', 119 | } 120 | 121 | export type Agent = { 122 | /** 123 | * Add a tool to the local tool repository. Tools with the same name will be overwritten. 124 | * 125 | * Tools should be called using `runTool`. 126 | * 127 | * @param spec Tool Specification 128 | * @param handler The async method to execute when the tool is called 129 | */ 130 | addTool: , S extends ToolSpec, R>( 131 | spec: S, 132 | handler: (input: T, token?: CancellationToken, updates?: WritableStream) => Promise, 133 | toolClassification?: ToolClassification 134 | ) => void 135 | 136 | /** 137 | * Run a tool by name. This method will lookup the tool in the local tool repository and 138 | * validate the input against the tool's schema. 139 | * 140 | * Throws an error if the tool is not found, or if validation fails. 141 | * 142 | * @param toolName The name of the tool to run 143 | * @param input The input to the tool 144 | * @returns The result of the tool execution 145 | */ 146 | runTool: (toolName: string, input: any, token?: CancellationToken, updates?: WritableStream) => Promise 147 | 148 | /** 149 | * Get the list of tools in the local tool repository. 150 | * @param options Options for the format of the output. Can be either 'bedrock' or 'mcp' (the default) 151 | * @returns The tool repository in the requested output format 152 | */ 153 | getTools: (options?: T) => T extends { format: 'bedrock' } ? BedrockTools : Tools 154 | 155 | /** 156 | * Remove a tool from the local tool repository. 157 | * @param toolName The name of the tool to remove 158 | */ 159 | removeTool: (toolName: string) => void 160 | 161 | /** 162 | * Get the list of built-in tool names in the local tool repository. 163 | * @returns The list of built-in tool names 164 | */ 165 | getBuiltInToolNames: () => string[] 166 | 167 | /** 168 | * Get the list of built-in write tool names in the local tool repository. 169 | * @returns The list of built-in write tool names 170 | */ 171 | getBuiltInWriteToolNames: () => string[] 172 | } 173 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Commit Message Guidelines 24 | 25 | Commit messages merged to main branch must follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification format. This is required to ensure readable, standard format of commits history. We also rely on it to setup automation for generating change logs and making releases. 26 | 27 | ### Commit message format 28 | 29 | The commit message should be structured as follows: 30 | 31 | ``` 32 | ([optional scope]): 33 | [optional body] 34 | [optional footer(s)] 35 | ``` 36 | 37 | The header is mandatory and the scope of the header is optional. Examples: 38 | 39 | ``` 40 | docs: correct spelling of CHANGELOG 41 | ``` 42 | 43 | ``` 44 | feat(runtimes): allow provided config object to extend other configs 45 | BREAKING CHANGE: `extends` key in config file is now used for extending other config files 46 | ``` 47 | 48 | See more examples at https://www.conventionalcommits.org/en/v1.0.0/#examples. 49 | 50 | ### Types 51 | 52 | Type can have one of the following values: 53 | 54 | * **build**: changes to the build system 55 | * **chore**: any housekeeping changes, which don't fall in other category 56 | * **ci**: changes to CI script and workflows 57 | * **docs**: changes to documentation 58 | * **feat**: a new feature 59 | * **fix**: a bug fix 60 | * **style**: visual-only changes, not impacting functionality 61 | * **refactor**: refactorings not impacting functionality, which are not features or bug fixes 62 | * **perf**: changes that improve performance of code 63 | * **test**: adding or fixing tests 64 | 65 | ### Scope 66 | 67 | The scope should indicate a package, affected by the change. List of support scopes, and corresponding packages: 68 | 69 | * **chat-client-ui-types**: `./chat-client-ui-types` 70 | * **runtimes**: `./runtimes` 71 | * **types**: `./types` 72 | 73 | Empty scopes are allowed, and can be used for cases when change is not related to any particular package, e.g. for `ci:` or `docs:` 74 | 75 | ### Footer 76 | 77 | One or more footers may be provided one blank line after the body. 78 | 79 | **Breaking Change** must start with `BREAKING CHANGE:` words, following with description of the breaking change. 80 | 81 | ### Usage of Conventional Commit Types 82 | 83 | The commit contains the following structural elements, to communicate intent to the consumers of your library: 84 | 85 | * **fix**: a commit of the type fix patches a bug in your codebase (this correlates with PATCH in Semantic Versioning). 86 | * **feat**: a commit of the type feat introduces a new feature to the codebase (this correlates with MINOR in Semantic Versioning). 87 | * **BREAKING CHANGE**: a commit that has a footer `BREAKING CHANGE:`, or appends a `!` after the type/scope, introduces a breaking API change (correlating with MAJOR in Semantic Versioning). A BREAKING CHANGE can be part of commits of any *type*. 88 | 89 | These rules are used by our automation workflows to collect change logs, and to compute next Semantic Version of packages, impacted by commit. 90 | 91 | Since this repository is a shared shared monorepo with many packages, be careful when introducing changes impacting several packages. Extra care should be given when using version-impacting types (especially BREAKING CHANGE). 92 | 93 | 94 | ## Contributing via Pull Requests 95 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 96 | 97 | 1. You are working against the latest source on the *main* branch. 98 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 99 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 100 | 101 | To send us a pull request, please: 102 | 103 | 1. Fork the repository. 104 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 105 | 3. Ensure local tests pass. 106 | 4. Commit to your fork using clear commit messages. 107 | 5. Send us a pull request, answering any default questions in the pull request interface. 108 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 109 | 110 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 111 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 112 | 113 | 114 | ## Finding contributions to work on 115 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 116 | 117 | 118 | ## Code of Conduct 119 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 120 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 121 | opensource-codeofconduct@amazon.com with any additional questions or comments. 122 | 123 | 124 | ## Licensing 125 | 126 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 127 | -------------------------------------------------------------------------------- /runtimes/runtimes/chat/encryptedChat.ts: -------------------------------------------------------------------------------- 1 | import { jwtDecrypt } from 'jose' 2 | import { Connection } from 'vscode-languageserver' 3 | import { 4 | ChatParams, 5 | ChatResult, 6 | QuickActionParams, 7 | QuickActionResult, 8 | RequestHandler, 9 | chatRequestType, 10 | CancellationToken, 11 | EncryptedChatParams, 12 | EncryptedQuickActionParams, 13 | quickActionRequestType, 14 | ResponseError, 15 | LSPErrorCodes, 16 | InlineChatParams, 17 | inlineChatRequestType, 18 | InlineChatResult, 19 | } from '../../protocol' 20 | import { CredentialsEncoding, encryptObjectWithKey, isMessageJWEEncrypted } from '../auth/standalone/encryption' 21 | import { BaseChat } from './baseChat' 22 | import { OperationalTelemetryProvider, TELEMETRY_SCOPES } from '../operational-telemetry/operational-telemetry' 23 | 24 | // Default JWE configuration 25 | const KEY_MANAGEMENT_ALGORITHM = 'dir' 26 | const CONTENT_ENCRYPTION_ALGORITHM = 'A256GCM' 27 | 28 | type EncryptedRequestParams = EncryptedQuickActionParams | EncryptedChatParams 29 | 30 | export class EncryptedChat extends BaseChat { 31 | // Store key as both string and buffer since both are used 32 | private keyBuffer: Buffer 33 | 34 | constructor( 35 | connection: Connection, 36 | private key: string, 37 | private encoding?: CredentialsEncoding 38 | ) { 39 | super(connection) 40 | this.keyBuffer = Buffer.from(key, 'base64') 41 | } 42 | 43 | public override onChatPrompt(handler: RequestHandler) { 44 | this.registerEncryptedRequestHandler< 45 | EncryptedChatParams, 46 | ChatParams, 47 | ChatResult | null | undefined, 48 | ChatResult 49 | >(chatRequestType, handler) 50 | } 51 | 52 | public override onInlineChatPrompt( 53 | handler: RequestHandler 54 | ) { 55 | this.registerEncryptedRequestHandler< 56 | EncryptedChatParams, 57 | InlineChatParams, 58 | InlineChatResult | null | undefined, 59 | InlineChatResult 60 | >(inlineChatRequestType, handler) 61 | } 62 | 63 | public override onQuickAction(handler: RequestHandler) { 64 | this.registerEncryptedRequestHandler( 65 | quickActionRequestType, 66 | handler 67 | ) 68 | } 69 | 70 | private registerEncryptedRequestHandler< 71 | EncryptedRequestType extends EncryptedRequestParams, 72 | DecryptedRequestType extends ChatParams | QuickActionParams | InlineChatParams, 73 | ResponseType, 74 | ErrorType, 75 | >(requestType: any, handler: RequestHandler) { 76 | this.connection.onRequest( 77 | requestType, 78 | async (request: EncryptedRequestType | DecryptedRequestType, cancellationToken: CancellationToken) => { 79 | // Verify the request is encrypted as expected 80 | if (this.instanceOfEncryptedParams(request)) { 81 | // Decrypt request 82 | let decryptedRequest 83 | try { 84 | decryptedRequest = await this.decodeRequest(request) 85 | } catch (err: any) { 86 | let errorMessage = 'Request could not be decrypted' 87 | OperationalTelemetryProvider.getTelemetryForScope(TELEMETRY_SCOPES.RUNTIMES).emitEvent({ 88 | errorOrigin: 'caughtError', 89 | errorType: 'encryptedChatDecodeRequest', 90 | errorName: err?.name ?? 'unknown', 91 | errorCode: err?.code ?? '', 92 | errorMessage: errorMessage, 93 | }) 94 | if (err instanceof Error) errorMessage = err.message 95 | return new ResponseError(LSPErrorCodes.ServerCancelled, errorMessage) 96 | } 97 | 98 | // Preserve the partial result token 99 | decryptedRequest.partialResultToken = request.partialResultToken 100 | 101 | // Call the handler with decrypted params 102 | const response = await handler(decryptedRequest, cancellationToken) 103 | 104 | // If response is null, undefined or a response error, return it as is 105 | if (!response || response instanceof ResponseError) { 106 | return response 107 | } 108 | 109 | // Encrypt the response and return it 110 | const encryptedResponse = await encryptObjectWithKey( 111 | response, 112 | this.key, 113 | KEY_MANAGEMENT_ALGORITHM, 114 | CONTENT_ENCRYPTION_ALGORITHM 115 | ) 116 | 117 | return encryptedResponse 118 | } 119 | 120 | return new ResponseError( 121 | LSPErrorCodes.ServerCancelled, 122 | 'The request was not encrypted correctly' 123 | ) 124 | } 125 | ) 126 | } 127 | 128 | private instanceOfEncryptedParams(object: any): object is T { 129 | if ('message' in object && typeof object['message'] === `string`) { 130 | return isMessageJWEEncrypted(object.message, KEY_MANAGEMENT_ALGORITHM, CONTENT_ENCRYPTION_ALGORITHM) 131 | } 132 | 133 | return false 134 | } 135 | 136 | private async decodeRequest(request: EncryptedRequestParams): Promise { 137 | if (!this.key) { 138 | throw new Error('No encryption key') 139 | } 140 | 141 | if (this.encoding === 'JWT') { 142 | const result = await jwtDecrypt(request.message, this.keyBuffer, { 143 | clockTolerance: 60, 144 | contentEncryptionAlgorithms: [CONTENT_ENCRYPTION_ALGORITHM], 145 | keyManagementAlgorithms: [KEY_MANAGEMENT_ALGORITHM], 146 | }) 147 | 148 | if (!result.payload) { 149 | throw new Error('JWT payload not found') 150 | } 151 | return result.payload as T 152 | } 153 | throw new Error('Encoding mode not implemented') 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /runtimes/runtimes/util/pathUtil.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import { joinUnixPaths, basenamePath } from './pathUtil' 3 | 4 | describe('joinUnixPaths', function () { 5 | it('handles basic joining', function () { 6 | assert.strictEqual(joinUnixPaths('foo', 'bar'), 'foo/bar') 7 | assert.strictEqual(joinUnixPaths('foo'), 'foo') 8 | assert.strictEqual(joinUnixPaths(), '') 9 | assert.strictEqual(joinUnixPaths(''), '') 10 | }) 11 | 12 | it('ignores leading and trailing slashes', function () { 13 | assert.strictEqual(joinUnixPaths('/foo', 'bar'), 'foo/bar') 14 | assert.strictEqual(joinUnixPaths('foo/', 'bar'), 'foo/bar') 15 | assert.strictEqual(joinUnixPaths('foo', '/bar'), 'foo/bar') 16 | assert.strictEqual(joinUnixPaths('foo', 'bar/'), 'foo/bar') 17 | assert.strictEqual(joinUnixPaths('/foo/', '/bar/'), 'foo/bar') 18 | }) 19 | 20 | it('handles multiple consecutive slashes', function () { 21 | assert.strictEqual(joinUnixPaths('foo//', '//bar'), 'foo/bar') 22 | assert.strictEqual(joinUnixPaths('///foo///', '///bar///'), 'foo/bar') 23 | assert.strictEqual(joinUnixPaths('foo///bar'), 'foo/bar') 24 | }) 25 | 26 | it('handles dot segments correctly', function () { 27 | assert.strictEqual(joinUnixPaths('foo', '.', 'bar'), 'foo/bar') 28 | assert.strictEqual(joinUnixPaths('foo/./bar'), 'foo/bar') 29 | assert.strictEqual(joinUnixPaths('./foo/bar'), 'foo/bar') 30 | assert.strictEqual(joinUnixPaths('foo/bar/.'), 'foo/bar') 31 | }) 32 | 33 | it('handles double-dot segments correctly', function () { 34 | assert.strictEqual(joinUnixPaths('foo', '..', 'bar'), 'bar') 35 | assert.strictEqual(joinUnixPaths('foo/bar/..'), 'foo') 36 | assert.strictEqual(joinUnixPaths('foo/../bar'), 'bar') 37 | assert.strictEqual(joinUnixPaths('foo/bar/../baz'), 'foo/baz') 38 | assert.strictEqual(joinUnixPaths('foo/bar/../../baz'), 'baz') 39 | }) 40 | 41 | it('handles complex path combinations', function () { 42 | assert.strictEqual(joinUnixPaths('/foo/bar', '../baz/./qux/'), 'foo/baz/qux') 43 | assert.strictEqual(joinUnixPaths('foo/./bar', '../baz//qux/..'), 'foo/baz') 44 | assert.strictEqual(joinUnixPaths('/foo/bar/', './baz/../qux'), 'foo/bar/qux') 45 | assert.strictEqual(joinUnixPaths('foo', 'bar', 'baz', '..', 'qux'), 'foo/bar/qux') 46 | }) 47 | 48 | it('handles empty segments', function () { 49 | assert.strictEqual(joinUnixPaths('foo', '', 'bar'), 'foo/bar') 50 | assert.strictEqual(joinUnixPaths('', 'foo', '', 'bar', ''), 'foo/bar') 51 | }) 52 | 53 | it('handles paths with multiple consecutive dots', function () { 54 | assert.strictEqual(joinUnixPaths('foo', '...', 'bar'), 'foo/.../bar') 55 | assert.strictEqual(joinUnixPaths('foo/...bar'), 'foo/...bar') 56 | }) 57 | }) 58 | 59 | describe('basenameUnixPath', function () { 60 | it('handles basic filename extraction', function () { 61 | assert.strictEqual(basenamePath('/path/to/file.txt'), 'file.txt') 62 | assert.strictEqual(basenamePath('path/to/file.txt'), 'file.txt') 63 | assert.strictEqual(basenamePath('file.txt'), 'file.txt') 64 | assert.strictEqual(basenamePath('filename'), 'filename') 65 | }) 66 | 67 | it('handles directory paths', function () { 68 | assert.strictEqual(basenamePath('/path/to/dir/'), 'dir') 69 | assert.strictEqual(basenamePath('/path/to/dir'), 'dir') 70 | assert.strictEqual(basenamePath('path/to/dir/'), 'dir') 71 | assert.strictEqual(basenamePath('dir/'), 'dir') 72 | }) 73 | 74 | it('handles root and empty paths', function () { 75 | assert.strictEqual(basenamePath('/'), '') 76 | assert.strictEqual(basenamePath(''), '') 77 | assert.strictEqual(basenamePath('//'), '') 78 | assert.strictEqual(basenamePath('///'), '') 79 | }) 80 | 81 | it('handles extension removal', function () { 82 | assert.strictEqual(basenamePath('/path/to/file.txt', '.txt'), 'file') 83 | assert.strictEqual(basenamePath('/path/to/file.txt', 'txt'), 'file.') 84 | assert.strictEqual(basenamePath('file.js', '.js'), 'file') 85 | assert.strictEqual(basenamePath('file.js', 'js'), 'file.') 86 | }) 87 | 88 | it('handles extension removal edge cases', function () { 89 | assert.strictEqual(basenamePath('file.txt', '.js'), 'file.txt') 90 | assert.strictEqual(basenamePath('file', '.txt'), 'file') 91 | assert.strictEqual(basenamePath('file.txt.bak', '.txt'), 'file.txt.bak') 92 | assert.strictEqual(basenamePath('file.txt.bak', '.bak'), 'file.txt') 93 | }) 94 | 95 | it('handles Windows-style paths', function () { 96 | assert.strictEqual(basenamePath('C:\\path\\to\\file.txt'), 'file.txt') 97 | assert.strictEqual(basenamePath('path\\to\\file.txt'), 'file.txt') 98 | assert.strictEqual(basenamePath('C:\\path\\to\\dir\\'), 'dir') 99 | }) 100 | 101 | it('handles mixed path separators', function () { 102 | assert.strictEqual(basenamePath('/path\\to/file.txt'), 'file.txt') 103 | assert.strictEqual(basenamePath('path/to\\dir/'), 'dir') 104 | }) 105 | 106 | it('handles multiple consecutive slashes', function () { 107 | assert.strictEqual(basenamePath('/path//to///file.txt'), 'file.txt') 108 | assert.strictEqual(basenamePath('path///to//dir///'), 'dir') 109 | assert.strictEqual(basenamePath('///path///file.txt'), 'file.txt') 110 | }) 111 | 112 | it('handles special filenames', function () { 113 | assert.strictEqual(basenamePath('/path/to/.hidden'), '.hidden') 114 | assert.strictEqual(basenamePath('/path/to/..'), '..') 115 | assert.strictEqual(basenamePath('/path/to/.'), '.') 116 | assert.strictEqual(basenamePath('/path/to/...'), '...') 117 | }) 118 | 119 | it('handles invalid inputs', function () { 120 | assert.strictEqual(basenamePath(null as any), '') 121 | assert.strictEqual(basenamePath(undefined as any), '') 122 | assert.strictEqual(basenamePath(123 as any), '') 123 | }) 124 | 125 | it('handles complex extension scenarios', function () { 126 | assert.strictEqual(basenamePath('file.tar.gz', '.gz'), 'file.tar') 127 | assert.strictEqual(basenamePath('file.tar.gz', '.tar.gz'), 'file') 128 | assert.strictEqual(basenamePath('archive.tar.gz', 'tar.gz'), 'archive.') 129 | }) 130 | 131 | it('handles files without extensions', function () { 132 | assert.strictEqual(basenamePath('/path/to/README'), 'README') 133 | assert.strictEqual(basenamePath('/path/to/Makefile'), 'Makefile') 134 | assert.strictEqual(basenamePath('LICENSE', '.txt'), 'LICENSE') 135 | }) 136 | }) 137 | -------------------------------------------------------------------------------- /runtimes/runtimes/util/shared.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Names of directories relevant to the crash reporting functionality. 3 | * 4 | * Moved here to resolve circular dependency issues. 5 | */ 6 | export const crashMonitoringDirName = 'crashMonitoring' 7 | 8 | /** Matches Windows drive letter ("C:"). */ 9 | export const driveLetterRegex = /^[a-zA-Z]\:/ 10 | 11 | /** 12 | * Returns the identifier the given error. 13 | * Depending on the implementation, the identifier may exist on a 14 | * different property. 15 | */ 16 | export function getErrorId(error: Error): string { 17 | // prioritize code over the name 18 | return hasCode(error) ? error.code : error.name 19 | } 20 | 21 | /** 22 | * Derives an error message from the given error object. 23 | * Depending on the Error, the property used to derive the message can vary. 24 | * 25 | * @param withCause Append the message(s) from the cause chain, recursively. 26 | * The message(s) are delimited by ' | '. Eg: msg1 | causeMsg1 | causeMsg2 27 | */ 28 | export function getErrorMsg(err: Error | undefined, withCause: boolean = false): string | undefined { 29 | if (err === undefined) { 30 | return undefined 31 | } 32 | 33 | // Non-standard SDK fields added by the OIDC service, to conform to the OAuth spec 34 | // (https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1) : 35 | // - error: code per the OAuth spec 36 | // - error_description: improved error message provided by OIDC service. Prefer this to 37 | // `message` if present. 38 | // https://github.com/aws/aws-toolkit-jetbrains/commit/cc9ed87fa9391dd39ac05cbf99b4437112fa3d10 39 | // - error_uri: not provided by OIDC currently? 40 | // 41 | // Example: 42 | // 43 | // [error] API response (oidc.us-east-1.amazonaws.com /token): { 44 | // name: 'InvalidGrantException', 45 | // '$fault': 'client', 46 | // '$metadata': { 47 | // httpStatusCode: 400, 48 | // requestId: '7f5af448-5af7-45f2-8e47-5808deaea4ab', 49 | // extendedRequestId: undefined, 50 | // cfId: undefined 51 | // }, 52 | // error: 'invalid_grant', 53 | // error_description: 'Invalid refresh token provided', 54 | // message: 'UnknownError' 55 | // } 56 | const anyDesc = (err as any).error_description 57 | const errDesc = typeof anyDesc === 'string' ? anyDesc.trim() : '' 58 | let msg = errDesc !== '' ? errDesc : err.message?.trim() 59 | 60 | if (typeof msg !== 'string') { 61 | return undefined 62 | } 63 | 64 | // append the cause's message 65 | if (withCause) { 66 | const errorId = getErrorId(err) 67 | // - prepend id to message 68 | // - If a generic error does not have the `name` field explicitly set, it returns a generic 'Error' name. So skip since it is useless. 69 | if (errorId && errorId !== 'Error') { 70 | msg = `${errorId}: ${msg}` 71 | } 72 | 73 | const cause = (err as any).cause 74 | return `${msg}${cause ? ' | ' + getErrorMsg(cause, withCause) : ''}` 75 | } 76 | 77 | return msg 78 | } 79 | 80 | /** 81 | * Removes potential PII from a string, for logging/telemetry. 82 | * 83 | * Examples: 84 | * - "Failed to save c:/fooß/bar/baz.txt" => "Failed to save c:/xß/x/x.txt" 85 | * - "EPERM for dir c:/Users/user1/.aws/sso/cache/abc123.json" => "EPERM for dir c:/Users/x/.aws/sso/cache/x.json" 86 | */ 87 | function scrubNames(s: string, username?: string) { 88 | let r = '' 89 | const fileExtRe = /\.[^.\/]+$/ 90 | const slashdot = /^[~.]*[\/\\]*/ 91 | 92 | /** Allowlisted filepath segments. */ 93 | const keep = new Set([ 94 | '~', 95 | '.', 96 | '..', 97 | '.aws', 98 | 'aws', 99 | 'sso', 100 | 'cache', 101 | 'credentials', 102 | 'config', 103 | 'Users', 104 | 'users', 105 | 'home', 106 | 'tmp', 107 | 'aws-toolkit-vscode', 108 | 'globalStorage', // from vscode globalStorageUri 109 | crashMonitoringDirName, 110 | ]) 111 | 112 | if (username && username.length > 2) { 113 | s = s.replaceAll(username, 'x') 114 | } 115 | 116 | // Replace contiguous whitespace with 1 space. 117 | s = s.replace(/\s+/g, ' ') 118 | 119 | // 1. split on whitespace. 120 | // 2. scrub words that match username or look like filepaths. 121 | const words = s.split(/\s+/) 122 | for (const word of words) { 123 | const pathSegments = word.split(/[\/\\]/) 124 | if (pathSegments.length < 2) { 125 | // Not a filepath. 126 | r += ' ' + word 127 | continue 128 | } 129 | 130 | // Replace all (non-allowlisted) ASCII filepath segments with "x". 131 | // "/foo/bar/aws/sso/" => "/x/x/aws/sso/" 132 | let scrubbed = '' 133 | // Get the frontmatter ("/", "../", "~/", or "./"). 134 | const start = word.trimStart().match(slashdot)?.[0] ?? '' 135 | pathSegments[0] = pathSegments[0].trimStart().replace(slashdot, '') 136 | for (const seg of pathSegments) { 137 | if (driveLetterRegex.test(seg)) { 138 | scrubbed += seg 139 | } else if (keep.has(seg)) { 140 | scrubbed += '/' + seg 141 | } else { 142 | // Save the first non-ASCII (unicode) char, if any. 143 | const nonAscii = seg.match(/[^\p{ASCII}]/u)?.[0] ?? '' 144 | // Replace all chars (except [^…]) with "x" . 145 | const ascii = seg.replace(/[^$[\](){}:;'" ]+/g, 'x') 146 | scrubbed += `/${ascii}${nonAscii}` 147 | } 148 | } 149 | 150 | // includes leading '.', eg: '.json' 151 | const fileExt = pathSegments[pathSegments.length - 1].match(fileExtRe) ?? '' 152 | r += ` ${start.replace(/\\/g, '/')}${scrubbed.replace(/^[\/\\]+/, '')}${fileExt}` 153 | } 154 | 155 | return r.trim() 156 | } 157 | 158 | // Port of implementation in AWS Toolkit for VSCode 159 | // https://github.com/aws/aws-toolkit-vscode/blob/c22efa03e73b241564c8051c35761eb8620edb83/packages/core/src/shared/errors.ts#L455 160 | /** 161 | * Gets the (partial) error message detail for the `reasonDesc` field. 162 | * 163 | * @param err Error object, or message text 164 | */ 165 | export function getTelemetryReasonDesc(err: unknown | undefined): string | undefined { 166 | const m = typeof err === 'string' ? err : (getErrorMsg(err as Error, true) ?? '') 167 | const msg = scrubNames(m) 168 | 169 | // Truncate message as these strings can be very long. 170 | return msg && msg.length > 0 ? msg.substring(0, 350) : undefined 171 | } 172 | 173 | function hasCode(error: T): error is T & { code: string } { 174 | return typeof (error as { code?: unknown }).code === 'string' 175 | } 176 | -------------------------------------------------------------------------------- /chat-client-ui-types/src/uiContracts.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InsertToCursorPositionParams, 3 | ChatOptions, 4 | CodeSelectionType, 5 | ReferenceTrackerInformation, 6 | OPEN_TAB_REQUEST_METHOD, 7 | OpenTabResult, 8 | GET_SERIALIZED_CHAT_REQUEST_METHOD, 9 | GetSerializedChatResult, 10 | ChatPrompt, 11 | OpenFileDialogParams, 12 | DropFilesParams, 13 | } from '@aws/language-server-runtimes-types' 14 | export { InsertToCursorPositionParams } from '@aws/language-server-runtimes-types' 15 | 16 | export type AuthFollowUpType = 'full-auth' | 're-auth' | 'missing_scopes' | 'use-supported-auth' 17 | export function isValidAuthFollowUpType(value: string): value is AuthFollowUpType { 18 | return ['full-auth', 're-auth', 'missing_scopes', 'use-supported-auth'].includes(value) 19 | } 20 | 21 | export type GenericCommandVerb = 'Explain' | 'Refactor' | 'Fix' | 'Optimize' 22 | export type TriggerType = 'hotkeys' | 'click' | 'contextMenu' 23 | 24 | export const SEND_TO_PROMPT = 'sendToPrompt' 25 | export const ERROR_MESSAGE = 'errorMessage' 26 | export const INSERT_TO_CURSOR_POSITION = 'insertToCursorPosition' 27 | export const COPY_TO_CLIPBOARD = 'copyToClipboard' 28 | export const AUTH_FOLLOW_UP_CLICKED = 'authFollowUpClicked' 29 | export const GENERIC_COMMAND = 'genericCommand' 30 | export const CHAT_OPTIONS = 'chatOptions' 31 | export const DISCLAIMER_ACKNOWLEDGED = 'disclaimerAcknowledged' 32 | export const CHAT_PROMPT_OPTION_ACKNOWLEDGED = 'chatPromptOptionAcknowledged' 33 | export const STOP_CHAT_RESPONSE = 'stopChatResponse' 34 | export const OPEN_SETTINGS = 'openSettings' 35 | export const OPEN_FILE_DIALOG = 'openFileDialog' 36 | export const FILES_DROPPED = 'filesDropped' 37 | /** 38 | * A message sent from Chat Client to Extension in response to various actions triggered from Chat UI. 39 | */ 40 | export interface UiMessage { 41 | command: UiMessageCommand 42 | params?: UiMessageParams 43 | } 44 | 45 | export type UiMessageCommand = 46 | | typeof SEND_TO_PROMPT 47 | | typeof ERROR_MESSAGE 48 | | typeof INSERT_TO_CURSOR_POSITION 49 | | typeof AUTH_FOLLOW_UP_CLICKED 50 | | typeof GENERIC_COMMAND 51 | | typeof CHAT_OPTIONS 52 | | typeof COPY_TO_CLIPBOARD 53 | | typeof DISCLAIMER_ACKNOWLEDGED 54 | | typeof CHAT_PROMPT_OPTION_ACKNOWLEDGED 55 | | typeof STOP_CHAT_RESPONSE 56 | | typeof OPEN_SETTINGS 57 | | typeof OPEN_FILE_DIALOG 58 | | typeof FILES_DROPPED 59 | 60 | export type UiMessageParams = 61 | | InsertToCursorPositionParams 62 | | AuthFollowUpClickedParams 63 | | GenericCommandParams 64 | | ErrorParams 65 | | SendToPromptParams 66 | | ChatOptions 67 | | CopyCodeToClipboardParams 68 | | ChatPromptOptionAcknowledgedParams 69 | | StopChatResponseParams 70 | | OpenSettingsParams 71 | | OpenFileDialogParams 72 | | DropFilesParams 73 | 74 | export interface SendToPromptParams { 75 | selection: string 76 | triggerType: TriggerType 77 | prompt?: ChatPrompt 78 | autoSubmit?: boolean 79 | } 80 | 81 | export interface SendToPromptMessage { 82 | command: typeof SEND_TO_PROMPT 83 | params: SendToPromptParams 84 | } 85 | 86 | export interface InsertToCursorPositionMessage { 87 | command: typeof INSERT_TO_CURSOR_POSITION 88 | params: InsertToCursorPositionParams 89 | } 90 | 91 | export interface AuthFollowUpClickedParams { 92 | tabId: string 93 | messageId: string 94 | authFollowupType: AuthFollowUpType 95 | } 96 | 97 | export interface AuthFollowUpClickedMessage { 98 | command: typeof AUTH_FOLLOW_UP_CLICKED 99 | params: AuthFollowUpClickedParams 100 | } 101 | 102 | export interface GenericCommandParams { 103 | tabId: string 104 | selection: string 105 | triggerType: TriggerType 106 | genericCommand: GenericCommandVerb 107 | } 108 | 109 | export interface GenericCommandMessage { 110 | command: typeof GENERIC_COMMAND 111 | params: GenericCommandParams 112 | } 113 | 114 | export interface ChatPromptOptionAcknowledgedParams { 115 | messageId: string 116 | } 117 | 118 | export interface ChatPromptOptionAcknowledgedMessage { 119 | command: typeof CHAT_PROMPT_OPTION_ACKNOWLEDGED 120 | params: ChatPromptOptionAcknowledgedParams 121 | } 122 | 123 | export interface StopChatResponseParams { 124 | tabId: string 125 | } 126 | 127 | export interface StopChatResponseMessage { 128 | command: typeof STOP_CHAT_RESPONSE 129 | params: StopChatResponseParams 130 | } 131 | 132 | export interface ErrorParams { 133 | tabId: string 134 | triggerType?: TriggerType 135 | message: string 136 | title: string 137 | } 138 | 139 | export interface ErrorMessage { 140 | command: typeof ERROR_MESSAGE 141 | params: ErrorParams 142 | } 143 | 144 | export interface ChatOptionsMessage { 145 | command: typeof CHAT_OPTIONS 146 | params: ChatOptions 147 | } 148 | 149 | export interface CopyCodeToClipboardParams { 150 | tabId: string 151 | messageId: string 152 | code?: string 153 | type?: CodeSelectionType 154 | referenceTrackerInformation?: ReferenceTrackerInformation[] 155 | eventId?: string 156 | codeBlockIndex?: number 157 | totalCodeBlocks?: number 158 | } 159 | 160 | export interface CopyCodeToClipboardMessage { 161 | command: typeof COPY_TO_CLIPBOARD 162 | params: CopyCodeToClipboardParams 163 | } 164 | 165 | export interface OpenSettingsParams { 166 | settingKey: string 167 | } 168 | 169 | export interface FilesDroppedParams { 170 | tabId: string 171 | insertPosition: number 172 | files: string[] 173 | } 174 | 175 | /** 176 | * A message sent from Chat Client to Extension in response to request triggered from Extension. 177 | * As Chat Client uses PostMessage API for transport with integrating Extensions, this is a loose implementation of request-response model. 178 | * Responses order is not guaranteed. 179 | */ 180 | export interface UiResultMessage { 181 | command: UiMessageResultCommand 182 | params: UiMessageResultParams 183 | } 184 | 185 | export type UiMessageResultCommand = typeof OPEN_TAB_REQUEST_METHOD | typeof GET_SERIALIZED_CHAT_REQUEST_METHOD 186 | 187 | export type UiMessageResult = OpenTabResult | GetSerializedChatResult 188 | 189 | export type UiMessageResultParams = 190 | | { 191 | success: true 192 | result: UiMessageResult 193 | } 194 | | { 195 | success: false 196 | error: ErrorResult 197 | } 198 | export interface ErrorResult { 199 | message: string 200 | type: 'InvalidRequest' | 'InternalError' | 'UnknownError' | string 201 | } 202 | 203 | /* 204 | * A message injected into the client to dynamically add new features, namely the UI. 205 | */ 206 | export interface FeatureValue { 207 | boolValue?: boolean 208 | doubleValue?: number 209 | longValue?: number 210 | stringValue?: string 211 | } 212 | 213 | export interface FeatureContext { 214 | name?: string 215 | variation: string 216 | value: FeatureValue 217 | } 218 | -------------------------------------------------------------------------------- /runtimes/runtimes/auth/standalone/encryption.test.ts: -------------------------------------------------------------------------------- 1 | import { Readable } from 'stream' 2 | import assert from 'assert' 3 | import { 4 | EncryptionInitialization, 5 | isMessageJWEEncrypted, 6 | readEncryptionDetails, 7 | shouldWaitForEncryptionKey, 8 | validateEncryptionDetails, 9 | } from './encryption' 10 | import sinon from 'sinon' 11 | 12 | function createReadableStream(): Readable { 13 | const stream = new Readable() 14 | // throws error if not implemented 15 | stream._read = function () {} 16 | return stream 17 | } 18 | 19 | describe('readEncryptionDetails', () => { 20 | it('resolves with the parsed encryption details', async () => { 21 | const request: EncryptionInitialization = { 22 | version: '1.0', 23 | mode: 'JWT', 24 | key: 'encryption_key', 25 | } 26 | 27 | const stream = createReadableStream() 28 | stream.push(JSON.stringify(request)) 29 | stream.push('\n') 30 | 31 | const result = await readEncryptionDetails(stream) 32 | assert.deepEqual(result, request) 33 | }) 34 | 35 | it('rejects if no newline is encountered within the timeout', async () => { 36 | const clock = sinon.useFakeTimers() 37 | const stream = createReadableStream() 38 | const timeoutMs = 5000 39 | 40 | await assert.rejects(async () => { 41 | const promise = readEncryptionDetails(stream) 42 | clock.tick(timeoutMs) 43 | await promise 44 | }, /Encryption details followed by newline must be sent during first/) 45 | 46 | clock.restore() 47 | }) 48 | 49 | it('rejects if bad JSON is sent', async () => { 50 | const stream = createReadableStream() 51 | 52 | stream.push('badJSON') 53 | stream.push('\n') 54 | 55 | await assert.rejects(readEncryptionDetails(stream), /SyntaxError/) 56 | }) 57 | }) 58 | 59 | describe('validateEncryptionDetails', () => { 60 | it('does not throw for valid encryption details', () => { 61 | const validEncryptionDetails: EncryptionInitialization = { 62 | version: '1.0', 63 | key: 'secret_key', 64 | mode: 'JWT', 65 | } 66 | 67 | assert.doesNotThrow(() => validateEncryptionDetails(validEncryptionDetails)) 68 | }) 69 | 70 | it('throws for unsupported initialization version', () => { 71 | const invalidVersionEncryptionDetails: EncryptionInitialization = { 72 | version: '2.0', // Unsupported version 73 | key: 'secret_key', 74 | mode: 'JWT', 75 | } 76 | 77 | assert.throws( 78 | () => validateEncryptionDetails(invalidVersionEncryptionDetails), 79 | /Unsupported initialization version: 2.0/ 80 | ) 81 | }) 82 | 83 | it('throws for missing encryption key', () => { 84 | const missingKeyEncryptionDetails = { 85 | version: '1.0', 86 | // Missing key 87 | mode: 'JWT', 88 | } 89 | 90 | assert.throws( 91 | () => validateEncryptionDetails(missingKeyEncryptionDetails as EncryptionInitialization), 92 | /Encryption key is missing/ 93 | ) 94 | }) 95 | 96 | it('throws for unsupported encoding mode', () => { 97 | const invalidModeEncryptionDetails = { 98 | version: '1.0', 99 | key: 'secret_key', 100 | mode: 'AES', // Unsupported mode 101 | } 102 | 103 | assert.throws( 104 | () => validateEncryptionDetails(invalidModeEncryptionDetails as EncryptionInitialization), 105 | /Unsupported encoding mode: AES/ 106 | ) 107 | }) 108 | }) 109 | 110 | describe('shouldWaitForEncryptionKey', () => { 111 | it('should return true when --set-credentials-encryption-key is in process.argv', () => { 112 | const originalArgv = process.argv 113 | process.argv = ['--set-credentials-encryption-key'] 114 | 115 | assert.strictEqual(shouldWaitForEncryptionKey(), true) 116 | 117 | process.argv = originalArgv 118 | }) 119 | 120 | it('should return false when --set-credentials-encryption-key is not in process.argv', () => { 121 | const originalArgv = process.argv 122 | process.argv = ['--some-other-arg'] 123 | 124 | assert.strictEqual(shouldWaitForEncryptionKey(), false) 125 | 126 | process.argv = originalArgv 127 | }) 128 | }) 129 | 130 | describe('isMessageJWEEncrypted', () => { 131 | it('should return false if the message does not have 5 parts separated by periods', () => { 132 | const message = 'part1.part2.part3.part4' 133 | const result = isMessageJWEEncrypted(message, 'alg', 'enc') 134 | assert.strictEqual(result, false) 135 | }) 136 | 137 | it('should return false if the protected header is not valid base64url', () => { 138 | const message = 'invalid..part2.part3.part4.part5' 139 | const result = isMessageJWEEncrypted(message, 'alg', 'enc') 140 | assert.strictEqual(result, false) 141 | }) 142 | 143 | it('should return false if the protected header is not a valid JSON', () => { 144 | const message = 'aW52YWxpZA==.part2.part3.part4.part5' // "invalid" in base64url 145 | const result = isMessageJWEEncrypted(message, 'alg', 'enc') 146 | assert.strictEqual(result, false) 147 | }) 148 | 149 | it('should return false if the protected header does not contain the expected fields', () => { 150 | const header = Buffer.from(JSON.stringify({ wrongField: 'value' })).toString('base64url') 151 | const message = `${header}.part2.part3.part4.part5` 152 | const result = isMessageJWEEncrypted(message, 'alg', 'enc') 153 | assert.strictEqual(result, false) 154 | }) 155 | 156 | it('should return false if the protected header contains wrong algorithm', () => { 157 | const header = Buffer.from(JSON.stringify({ alg: 'wrongAlg', enc: 'enc' })).toString('base64url') 158 | const message = `${header}.part2.part3.part4.part5` 159 | const result = isMessageJWEEncrypted(message, 'alg', 'enc') 160 | assert.strictEqual(result, false) 161 | }) 162 | 163 | it('should return false if the protected header contains wrong encoding', () => { 164 | const header = Buffer.from(JSON.stringify({ alg: 'alg', enc: 'wrongEnc' })).toString('base64url') 165 | const message = `${header}.part2.part3.part4.part5` 166 | const result = isMessageJWEEncrypted(message, 'alg', 'enc') 167 | assert.strictEqual(result, false) 168 | }) 169 | 170 | it('should return true if the protected header contains the expected algorithm and encoding', () => { 171 | const header = Buffer.from(JSON.stringify({ alg: 'alg', enc: 'enc' })).toString('base64url') 172 | const message = `${header}.part2.part3.part4.part5` 173 | const result = isMessageJWEEncrypted(message, 'alg', 'enc') 174 | assert.strictEqual(result, true) 175 | }) 176 | }) 177 | --------------------------------------------------------------------------------