diff --git a/.github/workflows/build-examples.yml b/.github/workflows/build-examples.yml
index 54e76953..a4923c4f 100644
--- a/.github/workflows/build-examples.yml
+++ b/.github/workflows/build-examples.yml
@@ -28,6 +28,7 @@ jobs:
- github-issue
- cloudflare
- supabase
+ - salesforce
indent-runtime: [aws-lambda]
steps:
- name: Checkout
diff --git a/examples/aws-lambda-salesforce-webhook/.github/workflows/terraform.yml b/examples/aws-lambda-salesforce-webhook/.github/workflows/terraform.yml
new file mode 100644
index 00000000..2860f3f0
--- /dev/null
+++ b/examples/aws-lambda-salesforce-webhook/.github/workflows/terraform.yml
@@ -0,0 +1,81 @@
+name: deploy.indent-salesforce-webhook
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+
+jobs:
+ terraform:
+ name: 'Terraform'
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Setup Terraform
+ uses: hashicorp/setup-terraform@v1
+
+ - name: Configure AWS Credentials
+ uses: aws-actions/configure-aws-credentials@v1
+ with:
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+ aws-session-token: ${{ secrets.AWS_SESSION_TOKEN }} # if you have/need it
+ aws-region: ${{ secrets.AWS_REGION }}
+
+ - name: Terraform Format
+ id: fmt
+ run: terraform fmt -check -diff
+
+ - name: Build Webhook (terraform-aws-salesforce-webhook)
+ run: cd terraform-aws-salesforce-webhook && npm run deploy:prepare && npm install && npm run build
+
+ - name: Terraform Init
+ id: init
+ run: terraform init
+
+ - name: Terraform Plan
+ id: plan
+ if: github.event_name == 'pull_request'
+ run: terraform plan -input=false -no-color
+ continue-on-error: true
+ env:
+ TF_VAR_indent_webhook_secret: ${{ secrets.SALESFORCE_WEBHOOK_SECRET }}
+ TF_VAR_indent_pull_webhook_secret: ${{ secrets.SALESFORCE_PULL_WEBHOOK_SECRET }}
+ TF_VAR_okta_domain: ${{ secrets.SALESFORCE_ACCOUNT}}
+ TF_VAR_okta_token: ${{ secrets.SALESFORCE_ACCESS_TOKEN }}
+
+ - uses: actions/github-script@0.9.0
+ if: github.event_name == 'pull_request'
+ env:
+ PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const output = `#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\`
+ #### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\`
+ #### Terraform Plan 📖\`${{ steps.plan.outcome }}\`
+ Show Plan
+ \`\`\`${process.env.PLAN}\`\`\`
+
+ *Actor: @${{ github.actor }}, Event: \`${{ github.event_name }}\`*`;
+ github.issues.createComment({
+ issue_number: context.issue.number,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ body: output
+ })
+ - name: Terraform Plan Status
+ if: steps.plan.outcome == 'failure'
+ run: exit 1
+
+ - name: Terraform Apply
+ if: github.ref == 'refs/heads/main' && github.event_name == 'push'
+ run: terraform apply -input=false -auto-approve
+ env:
+ TF_VAR_indent_webhook_secret: ${{ secrets.SALESFORCE_WEBHOOK_SECRET }}
+ TF_VAR_indent_pull_webhook_secret: ${{ secrets.SALESFORCE_PULL_WEBHOOK_SECRET }}
+ TF_VAR_salesforce_instance_url: ${{ secrets.SALESFORCE_INSTANCE_URL }}
+ TF_VAR_salesforce_access_token: ${{ secrets.SALESFORCE_ACCESS_TOKEN }}
diff --git a/examples/aws-lambda-salesforce-webhook/.gitignore b/examples/aws-lambda-salesforce-webhook/.gitignore
new file mode 100644
index 00000000..a768d9f3
--- /dev/null
+++ b/examples/aws-lambda-salesforce-webhook/.gitignore
@@ -0,0 +1,12 @@
+data
+dist
+lib
+.env
+node_modules
+*.tfstate
+.terraform*
+*.tfstate.*
+terraform/config/*.tfvars
+!terraform/config/example.tfvars
+yarn.lock
+package-lock.json
\ No newline at end of file
diff --git a/examples/aws-lambda-salesforce-webhook/README.md b/examples/aws-lambda-salesforce-webhook/README.md
new file mode 100644
index 00000000..427caf1c
--- /dev/null
+++ b/examples/aws-lambda-salesforce-webhook/README.md
@@ -0,0 +1 @@
+# Indent + Salesforce
diff --git a/examples/aws-lambda-salesforce-webhook/main.tf b/examples/aws-lambda-salesforce-webhook/main.tf
new file mode 100644
index 00000000..3b129e2a
--- /dev/null
+++ b/examples/aws-lambda-salesforce-webhook/main.tf
@@ -0,0 +1,24 @@
+# terraform {
+# backend "s3" {
+# encrypt = true
+# bucket = ""
+# region = "us-west-2"
+# key = "indent/terraform.tfstate"
+# }
+# }
+
+module "salesforce-pull-webhook" {
+ source = "./terraform-aws-salesforce-webhook/terraform"
+
+ indent_webhook_secret = var.salesforce_pull_webhook_secret
+ salesforce_instance_url = var.salesforce_instance_url
+ salesforce_access_token = var.salesforce_access_token
+}
+
+module "salesforce-change-webhook" {
+ source = "./terraform-aws-salesforce-webhook/terraform"
+
+ indent_webhook_secret = var.salesforce_webhook_secret
+ salesforce_instance_url = var.salesforce_instance_url
+ salesforce_access_token = var.salesforce_access_token
+}
diff --git a/examples/aws-lambda-salesforce-webhook/outputs.tf b/examples/aws-lambda-salesforce-webhook/outputs.tf
new file mode 100644
index 00000000..849350ae
--- /dev/null
+++ b/examples/aws-lambda-salesforce-webhook/outputs.tf
@@ -0,0 +1,9 @@
+output "pull_api_base_url" {
+ value = module.salesforce-pull-webhook.api_base_url
+ description = "The URL of the deployed Lambda"
+}
+
+output "api_base_url" {
+ value = module.salesforce-change-webhook.api_base_url
+ description = "The URL of the deployed Lambda"
+}
diff --git a/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/.gitignore b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/.gitignore
new file mode 100644
index 00000000..5f162567
--- /dev/null
+++ b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/.gitignore
@@ -0,0 +1,10 @@
+data
+dist
+lib
+.env
+node_modules
+*.tfstate
+.terraform
+*.tfstate.*
+terraform/config/*.tfvars
+!terraform/config/example.tfvars
\ No newline at end of file
diff --git a/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/package.json b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/package.json
new file mode 100644
index 00000000..57623927
--- /dev/null
+++ b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "@indent/terraform-aws-salesforce-webhook",
+ "version": "0.0.0",
+ "description": "A Node.js starter for Terraform on AWS with Indent and Okta.",
+ "main": "index.js",
+ "private": true,
+ "scripts": {
+ "build": "tsc",
+ "clean:dist": "rm -rf dist",
+ "clean:modules": "rm -rf node_modules",
+ "clean:tf": "rm -rf terraform/.terraform && rm -rf terraform/terraform.tfstate*",
+ "clean:all": "npm run clean:dist; npm run clean:tf; npm run clean:modules",
+ "create:all": "npm run deploy:init; npm run deploy:prepare; npm run deploy:all",
+ "deploy:init": "cd terraform; terraform init",
+ "deploy:prepare": "npm install --production && ./scripts/build-layers.sh",
+ "deploy:all": "npm run build && npm run tf:apply -auto-approve",
+ "destroy:all": "npm run tf:destroy -auto-approve",
+ "tf:plan": "cd terraform && terraform plan -var-file ./config/terraform.tfvars",
+ "tf:apply": "cd terraform && terraform apply -compact-warnings -var-file ./config/terraform.tfvars",
+ "tf:destroy": "cd terraform && terraform destroy -auto-approve -var-file ./config/terraform.tfvars"
+ },
+ "author": "Indent Inc ",
+ "license": "Apache-2.0",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/indentapis/integrations.git"
+ },
+ "devDependencies": {
+ "@types/aws-lambda": "^8.10.39",
+ "@types/node": "^13.9.8",
+ "@types/node-fetch": "^2.5.5",
+ "typescript": "^3.8.3"
+ },
+ "dependencies": {
+ "@indent/runtime-aws-lambda": "canary",
+ "@indent/webhook": "latest",
+ "@indent/types": "latest",
+ "ts-node": "^8.5.4"
+ }
+}
\ No newline at end of file
diff --git a/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/readme.md b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/readme.md
new file mode 100644
index 00000000..275303e0
--- /dev/null
+++ b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/readme.md
@@ -0,0 +1 @@
+# Terraform AWS + Salesforce Webhook
diff --git a/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/scripts/build-layers.sh b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/scripts/build-layers.sh
new file mode 100755
index 00000000..50f39fc1
--- /dev/null
+++ b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/scripts/build-layers.sh
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+set -x
+set -e
+
+ROOT_DIR="$(pwd)"
+
+OUTPUT_DIR="$(pwd)/dist"
+
+LAYER_DIR=$OUTPUT_DIR/layers/nodejs
+
+mkdir -p $LAYER_DIR
+
+cp -LR node_modules $LAYER_DIR
+
+cd $OUTPUT_DIR/layers
+
+zip -q -r layers.zip nodejs
\ No newline at end of file
diff --git a/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/src/index.ts b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/src/index.ts
new file mode 100644
index 00000000..43f58495
--- /dev/null
+++ b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/src/index.ts
@@ -0,0 +1,6 @@
+import { getLambdaHandler } from '@indent/runtime-aws'
+import { SalesforceIntegration } from './integration'
+
+export const handle = getLambdaHandler({
+ integrations: [new SalesforceIntegration()],
+})
diff --git a/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/src/integration/index.ts b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/src/integration/index.ts
new file mode 100644
index 00000000..7bcda8aa
--- /dev/null
+++ b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/src/integration/index.ts
@@ -0,0 +1,208 @@
+import {
+ ApplyUpdateRequest,
+ BaseHttpIntegration,
+ BaseHttpIntegrationOpts,
+ FullIntegration,
+ HealthCheckResponse,
+ IntegrationInfoResponse,
+ PullUpdateRequest,
+ StatusCode,
+ WriteRequest,
+} from '@indent/base-integration'
+import {
+ ApplyUpdateResponse,
+ PullUpdateResponse,
+ Resource,
+} from '@indent/types'
+import jsforce from 'jsforce'
+import {
+ SalesforceUserInfoResponse,
+ SalesforceUserRolesResponse,
+} from './salesforce-types'
+
+const pkg = require('../package.json')
+const SALESFORCE_INSTANCE_URL = process.env.SALESFORCE_INSTANCE_URL
+const SALESFORCE_ACCESS_TOKEN = process.env.SALESFORCE_ACCESS_TOKEN
+
+export class SalesforceIntegration
+ extends BaseHttpIntegration
+ implements FullIntegration
+{
+ _conn
+ constructor(opts?: BaseHttpIntegrationOpts) {
+ super(opts)
+ if (opts) {
+ this._name = opts.name
+ }
+ }
+
+ HealthCheck(): HealthCheckResponse {
+ return { status: { code: 0 } }
+ }
+
+ GetInfo(): IntegrationInfoResponse {
+ return {
+ name: ['indent-salesforce-webhook', this._name].filter(Boolean).join('#'),
+ capabilities: ['ApplyUpdate', 'PullUpdate'],
+ version: pkg.version,
+ }
+ }
+
+ MatchApply(req: WriteRequest): boolean {
+ return req.events.some((e) =>
+ e.resources?.some((r) =>
+ r.kind?.toLowerCase().includes('salesforce.v1.user')
+ )
+ )
+ }
+
+ async ConnectSalesforce(): Promise {
+ try {
+ this._conn = new jsforce.Connection({
+ instanceUrl: SALESFORCE_INSTANCE_URL,
+ accessToken: SALESFORCE_ACCESS_TOKEN,
+ })
+ } catch (err) {
+ console.error('Error connecting to Salesforce:', err)
+ throw err
+ }
+ }
+
+ MatchPull(req) {
+ const lowercaseKinds = req.kinds.map((k) => k.toLowerCase())
+ return (
+ lowercaseKinds.includes('salesforce.v1.userrole') ||
+ lowercaseKinds.includes('salesforce.v1.user')
+ )
+ }
+
+ async PullUpdate(_req: PullUpdateRequest): Promise {
+ if (!this._conn) {
+ this.ConnectSalesforce()
+ }
+
+ let resources: Resource[] = []
+ const timestamp = new Date().toISOString()
+ let res = { status: { code: StatusCode.UNKNOWN, message: '' } }
+ if (this.MatchPull(_req)) {
+ if (_req.kinds.includes('salesforce.v1.userRole')) {
+ try {
+ const userRole: SalesforceUserRolesResponse = await this._conn.query(
+ 'SELECT Id, Name FROM UserRole'
+ )
+ console.log(`debug userRole: ${JSON.stringify(userRole, null, 1)}`)
+
+ resources = userRole.records.map((r) => ({
+ id: r.Id,
+ displayName: r.Name,
+ kind: 'salesforce.v1.userRole',
+ labels: {
+ description: r.Name,
+ timestamp,
+ },
+ })) as Resource[]
+ console.log(
+ `debug resources for UserRole: ${JSON.stringify(
+ resources,
+ null,
+ 1
+ )}`
+ )
+ res.status.code = StatusCode.OK
+ } catch (err) {
+ res.status.code = StatusCode.UNAVAILABLE
+ res.status.message = err.message
+ console.error(res.status.message)
+ }
+ }
+
+ if (_req.kinds.includes('salesforce.v1.user')) {
+ try {
+ const userInfo: SalesforceUserInfoResponse = await this._conn.query(
+ 'SELECT Id, Name, IsActive, UserRole.Name, Profile.UserLicense.Name FROM User'
+ )
+ console.log(`debug userInfo: ${JSON.stringify(userInfo, null, 1)}`)
+
+ const userInfoResources: Resource[] = userInfo.records.map((r) => ({
+ id: r.Id,
+ displayName: r.Name,
+ kind: 'salesforce.v1.user',
+ labels: {
+ description: r.Name,
+ timestamp,
+ 'salesforce/isActive': r.IsActive.toString(),
+ 'salesforce/role': r.UserRole?.Name ? r.UserRole?.Name : null,
+ 'salesforce/userLicense': r.Profile?.UserLicense?.Name
+ ? r.Profile?.UserLicense?.Name
+ : null,
+ },
+ })) as Resource[]
+ console.log(
+ `debug resources for UserInfo: ${JSON.stringify(
+ userInfoResources,
+ null,
+ 1
+ )}`
+ )
+
+ resources = resources.concat(userInfoResources)
+ res.status.code = StatusCode.OK
+ } catch (err) {
+ res.status.code = StatusCode.UNAVAILABLE
+ res.status.message = err.message
+ console.error(res.status.message)
+ }
+ }
+ }
+
+ return {
+ status: res.status,
+ resources,
+ }
+ }
+
+ async ApplyUpdate(_req: ApplyUpdateRequest): Promise {
+ if (!this._conn) {
+ this.ConnectSalesforce()
+ }
+ const auditEvent = _req.events.find((e) => /grant|revoke/.test(e.event))
+ const { event, resources } = auditEvent
+ const granted = getResourceByKind(resources, 'salesforce.v1.user')
+ let res = { status: { code: StatusCode.UNKNOWN, message: '' } }
+ if (this.MatchApply(_req)) {
+ try {
+ if (event === 'access/grant') {
+ if (granted.kind === 'salesforce.v1.user') {
+ const isActive = event === 'access/grant'
+ await this._conn.sobject('User').update({
+ Id: granted.id,
+ IsActive: isActive,
+ })
+ }
+ res.status.code = StatusCode.OK
+ } else if (event === 'access/revoke') {
+ if (granted.kind === 'salesforce.v1.user') {
+ // For deactivating users when revoking access
+ await this._conn.sobject('User').update({
+ Id: granted.id,
+ IsActive: false,
+ })
+ }
+
+ res.status.code = StatusCode.OK
+ }
+ } catch (err) {
+ res.status.code = StatusCode.UNAVAILABLE
+ res.status.message = err.message
+ console.error(res.status.message)
+ }
+ }
+ return res
+ }
+}
+
+function getResourceByKind(resources, kind) {
+ return resources.find(
+ (r) => r.kind && r.kind.toLowerCase().includes(kind.toLowerCase())
+ )
+}
diff --git a/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/src/integration/salesforce-types.ts b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/src/integration/salesforce-types.ts
new file mode 100644
index 00000000..3ca6e2a9
--- /dev/null
+++ b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/src/integration/salesforce-types.ts
@@ -0,0 +1,67 @@
+export type SalesforceMembersResponse = {
+ totalSize: boolean
+ done: boolean
+ records: SalesforceMember[]
+}
+
+export type SalesforceUserRolesResponse = {
+ totalSize: number
+ done: boolean
+ records: SalesforceRole[]
+}
+
+export type SalesforceMember = {
+ attributes: {
+ type: string
+ url: string
+ }
+ Id: string
+ Name: string
+ UserRole: SalesforceRole | null
+}
+
+export type SalesforceRole = {
+ attributes: {
+ type: string
+ url: string
+ }
+ Name: string
+ Id: string
+}
+
+export type SalesforceUserInfoResponse = {
+ totalSize: number
+ done: boolean
+ records: SalesforceUserInfo[]
+}
+
+export type SalesforceUserInfo = {
+ attributes: {
+ type: string
+ url: string
+ }
+ Id: string
+ Name: string
+ IsActive: boolean
+ Profile: SalesforceProfile | null
+ UserRole: SalesforceRole | null
+}
+
+export type SalesforceProfile = {
+ attributes: {
+ type: string
+ url: string
+ }
+ Id: string
+ Name: string
+ UserLicense: SalesforceUserLicense
+}
+
+export type SalesforceUserLicense = {
+ attributes: {
+ type: string
+ url: string
+ }
+ Id: string
+ Name: string
+}
diff --git a/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/terraform/apiGateway.tf b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/terraform/apiGateway.tf
new file mode 100644
index 00000000..14901b21
--- /dev/null
+++ b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/terraform/apiGateway.tf
@@ -0,0 +1,64 @@
+resource "aws_api_gateway_rest_api" "api_gateway_rest_api" {
+ name = "api_gateway"
+ description = "Api Gateway for Lambda"
+}
+
+resource "aws_api_gateway_resource" "api_gateway" {
+ rest_api_id = aws_api_gateway_rest_api.api_gateway_rest_api.id
+ parent_id = aws_api_gateway_rest_api.api_gateway_rest_api.root_resource_id
+ path_part = "{proxy+}"
+}
+
+resource "aws_api_gateway_method" "api_gateway_method" {
+ rest_api_id = aws_api_gateway_rest_api.api_gateway_rest_api.id
+ resource_id = aws_api_gateway_resource.api_gateway.id
+ http_method = "ANY"
+ authorization = "NONE"
+
+ request_parameters = {
+ "method.request.header.x-indent-signature" = true
+ "method.request.header.x-indent-timestamp" = true
+ }
+}
+
+resource "aws_api_gateway_integration" "api_gateway_integration" {
+ rest_api_id = aws_api_gateway_rest_api.api_gateway_rest_api.id
+ resource_id = aws_api_gateway_method.api_gateway_method.resource_id
+ http_method = aws_api_gateway_method.api_gateway_method.http_method
+
+ integration_http_method = "POST"
+ type = "AWS_PROXY"
+ uri = aws_lambda_function.lambda.invoke_arn
+}
+
+resource "aws_api_gateway_method" "api_gateway_root_method" {
+ rest_api_id = aws_api_gateway_rest_api.api_gateway_rest_api.id
+ resource_id = aws_api_gateway_rest_api.api_gateway_rest_api.root_resource_id
+ http_method = "ANY"
+ authorization = "NONE"
+
+ request_parameters = {
+ "method.request.header.x-indent-signature" = true
+ "method.request.header.x-indent-timestamp" = true
+ }
+}
+
+resource "aws_api_gateway_integration" "api_gateway_root_integration" {
+ rest_api_id = aws_api_gateway_rest_api.api_gateway_rest_api.id
+ resource_id = aws_api_gateway_method.api_gateway_root_method.resource_id
+ http_method = aws_api_gateway_method.api_gateway_root_method.http_method
+
+ integration_http_method = "POST"
+ type = "AWS_PROXY"
+ uri = aws_lambda_function.lambda.invoke_arn
+}
+
+resource "aws_api_gateway_deployment" "api_gateway_deployment" {
+ depends_on = [
+ aws_api_gateway_integration.api_gateway_integration,
+ aws_api_gateway_integration.api_gateway_root_integration,
+ ]
+
+ rest_api_id = aws_api_gateway_rest_api.api_gateway_rest_api.id
+ stage_name = "dev"
+}
diff --git a/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/terraform/config/.gitkeep b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/terraform/config/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/terraform/config/example.tfvars b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/terraform/config/example.tfvars
new file mode 100644
index 00000000..86a7296c
--- /dev/null
+++ b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/terraform/config/example.tfvars
@@ -0,0 +1,8 @@
+# Indent Webhook Secret is used to verify messages from Indent
+indent_webhook_secret = ""
+
+# Salesforce Instance Url - This is your Salesforce Instance URL
+salesforce_instance_url = ""
+
+# Salesforce Token - Your Salesforce API Access token
+salesforce_access_token = ""
diff --git a/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/terraform/iam.tf b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/terraform/iam.tf
new file mode 100644
index 00000000..1bdf083b
--- /dev/null
+++ b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/terraform/iam.tf
@@ -0,0 +1,52 @@
+data "aws_iam_policy_document" "lambda_assume_role_document" {
+ version = "2012-10-17"
+
+ statement {
+ actions = ["sts:AssumeRole"]
+
+ principals {
+ type = "Service"
+ identifiers = ["lambda.amazonaws.com"]
+ }
+
+ effect = "Allow"
+ }
+}
+
+data "aws_caller_identity" "current" {}
+
+data "aws_iam_policy_document" "lambda_document" {
+ version = "2012-10-17"
+
+ statement {
+ effect = "Allow"
+
+ actions = [
+ "logs:CreateLogGroup",
+ "logs:CreateLogStream",
+ "logs:PutLogEvents",
+ "cloudwatch:PutMetricData",
+ ]
+
+ resources = ["arn:aws:logs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:log-group:/aws/lambda/${local.name}:*"]
+ }
+}
+
+resource "aws_iam_policy" "lambda_policy" {
+ policy = data.aws_iam_policy_document.lambda_document.json
+}
+
+resource "aws_iam_role" "lambda_role" {
+ name = "${local.name}-role"
+ assume_role_policy = data.aws_iam_policy_document.lambda_assume_role_document.json
+
+ tags = local.tags
+}
+
+resource "aws_iam_policy_attachment" "lambda_attachment" {
+ name = "${local.name}-attachment"
+
+ roles = [aws_iam_role.lambda_role.name]
+
+ policy_arn = aws_iam_policy.lambda_policy.arn
+}
diff --git a/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/terraform/lambda.tf b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/terraform/lambda.tf
new file mode 100644
index 00000000..b331bd7f
--- /dev/null
+++ b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/terraform/lambda.tf
@@ -0,0 +1,44 @@
+data "archive_file" "function_archive" {
+ type = "zip"
+ source_dir = "${path.module}/../lib"
+ output_path = "${path.module}/../dist/function.zip"
+}
+
+resource "aws_lambda_layer_version" "deps" {
+ compatible_runtimes = ["nodejs14.x"]
+ layer_name = "${local.name}-dependency_layer"
+ filename = "${path.module}/../dist/layers/layers.zip"
+ source_code_hash = filesha256("${path.module}/../dist/layers/layers.zip")
+}
+
+resource "aws_lambda_function" "lambda" {
+ function_name = local.name
+ role = aws_iam_role.lambda_role.arn
+ filename = data.archive_file.function_archive.output_path
+ source_code_hash = data.archive_file.function_archive.output_base64sha256
+ memory_size = local.lambda_memory
+ handler = "index.handle"
+ runtime = "nodejs14.x"
+ timeout = "30"
+
+ layers = [aws_lambda_layer_version.deps.arn]
+
+ environment {
+ variables = {
+ "INDENT_WEBHOOK_SECRET" = var.indent_webhook_secret
+ "SALESFORCE_INSTANCE_URL" = var.salesforce_instance_url
+ "SALESFORCE_ACCESS_TOKEN" = var.salesforce_access_token
+ }
+ }
+}
+
+resource "aws_lambda_permission" "lambda" {
+ statement_id = "AllowAPIGatewayInvoke"
+ action = "lambda:InvokeFunction"
+ function_name = aws_lambda_function.lambda.function_name
+ principal = "apigateway.amazonaws.com"
+
+ # The "/*/*" portion grants access from any method on any resource
+ # within the API Gateway REST API.
+ source_arn = "${aws_api_gateway_rest_api.api_gateway_rest_api.execution_arn}/*/*"
+}
diff --git a/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/terraform/locals.tf b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/terraform/locals.tf
new file mode 100644
index 00000000..ecf430b3
--- /dev/null
+++ b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/terraform/locals.tf
@@ -0,0 +1,16 @@
+locals {
+ name = "indent-salesforce-webhook-${random_string.suffix.result}"
+ lambda_memory = 128
+
+ tags = {
+ Name = "Indent + Salesforce on AWS via Terraform"
+ GitRepo = "https://github.com/indentapis/integrations"
+ ProvidedBy = "Indent"
+ }
+}
+
+resource "random_string" "suffix" {
+ length = 4
+ upper = false
+ special = false
+}
diff --git a/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/terraform/outputs.tf b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/terraform/outputs.tf
new file mode 100644
index 00000000..74823a54
--- /dev/null
+++ b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/terraform/outputs.tf
@@ -0,0 +1,4 @@
+output "api_base_url" {
+ value = aws_api_gateway_deployment.api_gateway_deployment.invoke_url
+ description = "The URL of the deployed Lambda"
+}
diff --git a/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/terraform/provider.tf b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/terraform/provider.tf
new file mode 100644
index 00000000..9b799b02
--- /dev/null
+++ b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/terraform/provider.tf
@@ -0,0 +1,17 @@
+provider "aws" {
+ profile = var.aws_profile
+ region = var.aws_region
+ max_retries = 1
+}
+
+terraform {
+ required_providers {
+ random = {
+ source = "hashicorp/random"
+ }
+
+ aws = {
+ version = "~> 3.0"
+ }
+ }
+}
diff --git a/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/terraform/variables.tf b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/terraform/variables.tf
new file mode 100644
index 00000000..ceabd41c
--- /dev/null
+++ b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/terraform/variables.tf
@@ -0,0 +1,26 @@
+variable "aws_region" {
+ type = string
+ default = "us-west-2"
+}
+
+variable "aws_profile" {
+ type = string
+ default = "default"
+}
+
+variable "indent_webhook_secret" {
+ type = string
+ sensitive = true
+}
+
+variable "salesforce_instance_url" {
+ type = string
+ default = ""
+ sensitive = true
+}
+
+variable "salesforce_access_token" {
+ type = string
+ default = ""
+ sensitive = true
+}
diff --git a/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/tsconfig.json b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/tsconfig.json
new file mode 100644
index 00000000..1bfde1b4
--- /dev/null
+++ b/examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "exclude": ["node_modules"],
+ "compilerOptions": {
+ "module": "commonjs",
+ "esModuleInterop": true,
+ "lib": ["esnext", "dom"],
+ "moduleResolution": "node",
+ "allowSyntheticDefaultImports": true,
+ "noUnusedParameters": true,
+ "noUnusedLocals": true,
+ "sourceMap": true,
+ "target": "esnext",
+ "outDir": "lib"
+ },
+ "include": ["./src/**/*"]
+}
diff --git a/examples/aws-lambda-salesforce-webhook/variables.tf b/examples/aws-lambda-salesforce-webhook/variables.tf
new file mode 100644
index 00000000..9dd7f70c
--- /dev/null
+++ b/examples/aws-lambda-salesforce-webhook/variables.tf
@@ -0,0 +1,33 @@
+variable "aws_region" {
+ type = string
+ default = "us-west-2"
+}
+
+variable "aws_profile" {
+ type = string
+ default = "default"
+}
+
+variable "salesforce_webhook_secret" {
+ type = string
+ default = ""
+ sensitive = true
+}
+
+variable "salesforce_pull_webhook_secret" {
+ type = string
+ default = ""
+ sensitive = true
+}
+
+variable "salesforce_instance_url" {
+ type = string
+ default = ""
+ sensitive = true
+}
+
+variable "salesforce_access_token" {
+ type = string
+ default = ""
+ sensitive = true
+}
\ No newline at end of file
diff --git a/packages/beta/indent-integration-salesforce/CHANGELOG.md b/packages/beta/indent-integration-salesforce/CHANGELOG.md
new file mode 100644
index 00000000..420e6f23
--- /dev/null
+++ b/packages/beta/indent-integration-salesforce/CHANGELOG.md
@@ -0,0 +1 @@
+# Change Log
diff --git a/packages/beta/indent-integration-salesforce/package.json b/packages/beta/indent-integration-salesforce/package.json
new file mode 100644
index 00000000..de7b596c
--- /dev/null
+++ b/packages/beta/indent-integration-salesforce/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "@indent/integration-salesforce",
+ "main": "lib/index",
+ "types": "lib/index",
+ "version": "0.0.1-canary.01",
+ "scripts": {
+ "dev": "tsc -p tsconfig.build.json -w",
+ "test": "SALESFORCE_INSTANCE_URL=https://random.com SALESFORCE_ACCESS_TOKEN=example_token jest --config ../../../jest.config.js salesforce.*",
+ "build": "yarn run clean && yarn run compile",
+ "clean": "rimraf -rf ./lib",
+ "compile": "tsc -p tsconfig.build.json",
+ "prepublishOnly": "yarn run build"
+ },
+ "dependencies": {
+ "@indent/base-integration": "^0.0.1-canary.31",
+ "jsforce": "^1.11.1"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "license": "Apache-2.0",
+ "gitHead": "HEAD~1"
+}
\ No newline at end of file
diff --git a/packages/beta/indent-integration-salesforce/src/index.ts b/packages/beta/indent-integration-salesforce/src/index.ts
new file mode 100644
index 00000000..7bcda8aa
--- /dev/null
+++ b/packages/beta/indent-integration-salesforce/src/index.ts
@@ -0,0 +1,208 @@
+import {
+ ApplyUpdateRequest,
+ BaseHttpIntegration,
+ BaseHttpIntegrationOpts,
+ FullIntegration,
+ HealthCheckResponse,
+ IntegrationInfoResponse,
+ PullUpdateRequest,
+ StatusCode,
+ WriteRequest,
+} from '@indent/base-integration'
+import {
+ ApplyUpdateResponse,
+ PullUpdateResponse,
+ Resource,
+} from '@indent/types'
+import jsforce from 'jsforce'
+import {
+ SalesforceUserInfoResponse,
+ SalesforceUserRolesResponse,
+} from './salesforce-types'
+
+const pkg = require('../package.json')
+const SALESFORCE_INSTANCE_URL = process.env.SALESFORCE_INSTANCE_URL
+const SALESFORCE_ACCESS_TOKEN = process.env.SALESFORCE_ACCESS_TOKEN
+
+export class SalesforceIntegration
+ extends BaseHttpIntegration
+ implements FullIntegration
+{
+ _conn
+ constructor(opts?: BaseHttpIntegrationOpts) {
+ super(opts)
+ if (opts) {
+ this._name = opts.name
+ }
+ }
+
+ HealthCheck(): HealthCheckResponse {
+ return { status: { code: 0 } }
+ }
+
+ GetInfo(): IntegrationInfoResponse {
+ return {
+ name: ['indent-salesforce-webhook', this._name].filter(Boolean).join('#'),
+ capabilities: ['ApplyUpdate', 'PullUpdate'],
+ version: pkg.version,
+ }
+ }
+
+ MatchApply(req: WriteRequest): boolean {
+ return req.events.some((e) =>
+ e.resources?.some((r) =>
+ r.kind?.toLowerCase().includes('salesforce.v1.user')
+ )
+ )
+ }
+
+ async ConnectSalesforce(): Promise {
+ try {
+ this._conn = new jsforce.Connection({
+ instanceUrl: SALESFORCE_INSTANCE_URL,
+ accessToken: SALESFORCE_ACCESS_TOKEN,
+ })
+ } catch (err) {
+ console.error('Error connecting to Salesforce:', err)
+ throw err
+ }
+ }
+
+ MatchPull(req) {
+ const lowercaseKinds = req.kinds.map((k) => k.toLowerCase())
+ return (
+ lowercaseKinds.includes('salesforce.v1.userrole') ||
+ lowercaseKinds.includes('salesforce.v1.user')
+ )
+ }
+
+ async PullUpdate(_req: PullUpdateRequest): Promise {
+ if (!this._conn) {
+ this.ConnectSalesforce()
+ }
+
+ let resources: Resource[] = []
+ const timestamp = new Date().toISOString()
+ let res = { status: { code: StatusCode.UNKNOWN, message: '' } }
+ if (this.MatchPull(_req)) {
+ if (_req.kinds.includes('salesforce.v1.userRole')) {
+ try {
+ const userRole: SalesforceUserRolesResponse = await this._conn.query(
+ 'SELECT Id, Name FROM UserRole'
+ )
+ console.log(`debug userRole: ${JSON.stringify(userRole, null, 1)}`)
+
+ resources = userRole.records.map((r) => ({
+ id: r.Id,
+ displayName: r.Name,
+ kind: 'salesforce.v1.userRole',
+ labels: {
+ description: r.Name,
+ timestamp,
+ },
+ })) as Resource[]
+ console.log(
+ `debug resources for UserRole: ${JSON.stringify(
+ resources,
+ null,
+ 1
+ )}`
+ )
+ res.status.code = StatusCode.OK
+ } catch (err) {
+ res.status.code = StatusCode.UNAVAILABLE
+ res.status.message = err.message
+ console.error(res.status.message)
+ }
+ }
+
+ if (_req.kinds.includes('salesforce.v1.user')) {
+ try {
+ const userInfo: SalesforceUserInfoResponse = await this._conn.query(
+ 'SELECT Id, Name, IsActive, UserRole.Name, Profile.UserLicense.Name FROM User'
+ )
+ console.log(`debug userInfo: ${JSON.stringify(userInfo, null, 1)}`)
+
+ const userInfoResources: Resource[] = userInfo.records.map((r) => ({
+ id: r.Id,
+ displayName: r.Name,
+ kind: 'salesforce.v1.user',
+ labels: {
+ description: r.Name,
+ timestamp,
+ 'salesforce/isActive': r.IsActive.toString(),
+ 'salesforce/role': r.UserRole?.Name ? r.UserRole?.Name : null,
+ 'salesforce/userLicense': r.Profile?.UserLicense?.Name
+ ? r.Profile?.UserLicense?.Name
+ : null,
+ },
+ })) as Resource[]
+ console.log(
+ `debug resources for UserInfo: ${JSON.stringify(
+ userInfoResources,
+ null,
+ 1
+ )}`
+ )
+
+ resources = resources.concat(userInfoResources)
+ res.status.code = StatusCode.OK
+ } catch (err) {
+ res.status.code = StatusCode.UNAVAILABLE
+ res.status.message = err.message
+ console.error(res.status.message)
+ }
+ }
+ }
+
+ return {
+ status: res.status,
+ resources,
+ }
+ }
+
+ async ApplyUpdate(_req: ApplyUpdateRequest): Promise {
+ if (!this._conn) {
+ this.ConnectSalesforce()
+ }
+ const auditEvent = _req.events.find((e) => /grant|revoke/.test(e.event))
+ const { event, resources } = auditEvent
+ const granted = getResourceByKind(resources, 'salesforce.v1.user')
+ let res = { status: { code: StatusCode.UNKNOWN, message: '' } }
+ if (this.MatchApply(_req)) {
+ try {
+ if (event === 'access/grant') {
+ if (granted.kind === 'salesforce.v1.user') {
+ const isActive = event === 'access/grant'
+ await this._conn.sobject('User').update({
+ Id: granted.id,
+ IsActive: isActive,
+ })
+ }
+ res.status.code = StatusCode.OK
+ } else if (event === 'access/revoke') {
+ if (granted.kind === 'salesforce.v1.user') {
+ // For deactivating users when revoking access
+ await this._conn.sobject('User').update({
+ Id: granted.id,
+ IsActive: false,
+ })
+ }
+
+ res.status.code = StatusCode.OK
+ }
+ } catch (err) {
+ res.status.code = StatusCode.UNAVAILABLE
+ res.status.message = err.message
+ console.error(res.status.message)
+ }
+ }
+ return res
+ }
+}
+
+function getResourceByKind(resources, kind) {
+ return resources.find(
+ (r) => r.kind && r.kind.toLowerCase().includes(kind.toLowerCase())
+ )
+}
diff --git a/packages/beta/indent-integration-salesforce/src/salesforce-types.ts b/packages/beta/indent-integration-salesforce/src/salesforce-types.ts
new file mode 100644
index 00000000..3ca6e2a9
--- /dev/null
+++ b/packages/beta/indent-integration-salesforce/src/salesforce-types.ts
@@ -0,0 +1,67 @@
+export type SalesforceMembersResponse = {
+ totalSize: boolean
+ done: boolean
+ records: SalesforceMember[]
+}
+
+export type SalesforceUserRolesResponse = {
+ totalSize: number
+ done: boolean
+ records: SalesforceRole[]
+}
+
+export type SalesforceMember = {
+ attributes: {
+ type: string
+ url: string
+ }
+ Id: string
+ Name: string
+ UserRole: SalesforceRole | null
+}
+
+export type SalesforceRole = {
+ attributes: {
+ type: string
+ url: string
+ }
+ Name: string
+ Id: string
+}
+
+export type SalesforceUserInfoResponse = {
+ totalSize: number
+ done: boolean
+ records: SalesforceUserInfo[]
+}
+
+export type SalesforceUserInfo = {
+ attributes: {
+ type: string
+ url: string
+ }
+ Id: string
+ Name: string
+ IsActive: boolean
+ Profile: SalesforceProfile | null
+ UserRole: SalesforceRole | null
+}
+
+export type SalesforceProfile = {
+ attributes: {
+ type: string
+ url: string
+ }
+ Id: string
+ Name: string
+ UserLicense: SalesforceUserLicense
+}
+
+export type SalesforceUserLicense = {
+ attributes: {
+ type: string
+ url: string
+ }
+ Id: string
+ Name: string
+}
diff --git a/packages/beta/indent-integration-salesforce/test/salesforce.test.ts b/packages/beta/indent-integration-salesforce/test/salesforce.test.ts
new file mode 100644
index 00000000..2f5fb7bc
--- /dev/null
+++ b/packages/beta/indent-integration-salesforce/test/salesforce.test.ts
@@ -0,0 +1,188 @@
+import { HealthCheckResponse } from '@indent/base-integration'
+import {
+ ApplyUpdateResponse,
+ PullUpdateResponse,
+ Resource,
+} from '@indent/types'
+import jsforce from 'jsforce'
+import { SalesforceIntegration } from '../src'
+import {
+ SalesforceUserInfoResponse,
+ SalesforceUserRolesResponse,
+} from '../src/salesforce-types'
+
+jest.mock('jsforce', () => {
+ const originalJsForce = jest.requireActual('jsforce')
+ return {
+ ...originalJsForce,
+ Connection: jest.fn().mockImplementation(() => ({
+ query: jest.fn(),
+ sobject: (_objName) => ({
+ update: jest.fn(),
+ }),
+ })),
+ }
+})
+
+describe('SalesforceIntegration', () => {
+ let salesforceIntegration: SalesforceIntegration
+ let mockConnection: any
+
+ beforeEach(() => {
+ mockConnection = new jsforce.Connection()
+ salesforceIntegration = new SalesforceIntegration()
+ salesforceIntegration._conn = mockConnection
+ })
+
+ afterEach(() => {
+ jest.clearAllMocks()
+ })
+
+ describe('HealthCheck', () => {
+ it('should return a health check response', () => {
+ const response: HealthCheckResponse = salesforceIntegration.HealthCheck()
+ expect(response.status.code).toBe(0)
+ })
+ })
+
+ describe('GetInfo', () => {
+ it('should return integration information', () => {
+ const response = salesforceIntegration.GetInfo()
+ expect(response.name).toEqual(expect.any(String))
+ expect(response.capabilities).toEqual(['ApplyUpdate', 'PullUpdate'])
+ expect(response.version).toEqual(expect.any(String))
+ })
+ })
+
+ describe('PullUpdate', () => {
+ it('should return resources for salesforce.v1.userrole and salesforce.v1.user', async () => {
+ const pullUpdateRequest = {
+ kinds: ['salesforce.v1.userRole', 'salesforce.v1.user'],
+ }
+
+ const expectedResources: Resource[] = [
+ // Mocked resources for 'salesforce.v1.userrole'
+ {
+ id: 'roleId1',
+ displayName: 'Role 1',
+ kind: 'salesforce.v1.userRole',
+ labels: {
+ description: 'Role 1',
+ timestamp: expect.any(String),
+ },
+ },
+ // Mocked resources for 'salesforce.v1.user'
+ {
+ id: 'userId1',
+ displayName: 'User 1',
+ kind: 'salesforce.v1.user',
+ labels: {
+ description: 'User 1',
+ timestamp: expect.any(String),
+ 'salesforce/isActive': 'true',
+ 'salesforce/role': null,
+ 'salesforce/userLicense': null,
+ },
+ },
+ ]
+
+ const userRolesResponse: SalesforceUserRolesResponse = {
+ totalSize: 1,
+ done: true,
+ records: [
+ {
+ attributes: {
+ type: 'string',
+ url: 'string',
+ },
+ Id: 'roleId1',
+ Name: 'Role 1',
+ },
+ ],
+ }
+
+ const userInfoResponse: SalesforceUserInfoResponse = {
+ totalSize: 1,
+ done: true,
+ records: [
+ {
+ attributes: {
+ type: 'string',
+ url: 'string',
+ },
+ Id: 'userId1',
+ Name: 'User 1',
+ IsActive: true,
+ UserRole: null,
+ Profile: null,
+ },
+ ],
+ }
+
+ salesforceIntegration._conn.query
+ .mockResolvedValueOnce(userRolesResponse)
+ .mockResolvedValueOnce(userInfoResponse)
+
+ const response: PullUpdateResponse =
+ await salesforceIntegration.PullUpdate(pullUpdateRequest)
+
+ expect(response.resources).toStrictEqual(expectedResources)
+ })
+ })
+
+ describe('ApplyUpdate', () => {
+ it('should grant access for salesforce.v1.userrole and activate/deactivate users for salesforce.v1.user', async () => {
+ const applyUpdateRequest = {
+ events: [
+ {
+ event: 'access/grant',
+ resources: [
+ {
+ id: 'userId1',
+ kind: 'salesforce.v1.user',
+ },
+ ],
+ },
+ {
+ event: 'access/revoke',
+ resources: [
+ {
+ id: 'userId2',
+ kind: 'salesforce.v1.user',
+ },
+ ],
+ },
+ ],
+ }
+
+ const expectedApplyUpdateResponse: ApplyUpdateResponse = {
+ status: {
+ code: 0,
+ message: '',
+ },
+ }
+
+ const userRolesResponse: SalesforceUserRolesResponse = {
+ totalSize: 1,
+ done: true,
+ records: [
+ {
+ attributes: {
+ type: 'string',
+ url: 'string',
+ },
+ Id: 'roleId1',
+ Name: 'Role 1',
+ },
+ ],
+ }
+
+ mockConnection.query.mockResolvedValueOnce(userRolesResponse)
+
+ const response: ApplyUpdateResponse =
+ await salesforceIntegration.ApplyUpdate(applyUpdateRequest)
+
+ expect(response).toStrictEqual(expectedApplyUpdateResponse)
+ })
+ })
+})
diff --git a/packages/beta/indent-integration-salesforce/tsconfig.build.json b/packages/beta/indent-integration-salesforce/tsconfig.build.json
new file mode 100644
index 00000000..544bf9fe
--- /dev/null
+++ b/packages/beta/indent-integration-salesforce/tsconfig.build.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../../tsconfig.build.json",
+ "include": ["src"],
+ "exclude": ["test"],
+ "compilerOptions": {
+ "outDir": "./lib"
+ }
+}
diff --git a/templates/scripts/steps/catalog.ts b/templates/scripts/steps/catalog.ts
index f4f32cba..ee5e1678 100644
--- a/templates/scripts/steps/catalog.ts
+++ b/templates/scripts/steps/catalog.ts
@@ -218,6 +218,23 @@ export const catalog: CatalogItem[] = [
'this link',
},
},
+ {
+ name: 'salesforce',
+ displayName: 'Salesforce',
+ integrations: ['SalesforceIntegration'],
+ runtimes: ['AWS Lambda'],
+ environmentVariables: [
+ 'SALESFORCE_INSTANCE_URL',
+ 'SALESFORCE_ACCESS_TOKEN',
+ ],
+ capabilities: ['PullUpdate', 'ApplyUpdate'],
+ links: { repoSource: 'packages/beta/indent-integration-salesforce' },
+ readme: {
+ connection: [],
+ docsLink:
+ 'this link',
+ },
+ },
{
name: 'github-issue',
displayName: 'GitHub Issues',