Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ddead0f
Add characterization test suite
johnrb2 Jun 29, 2026
a3a9986
Replace aws-sdk v2 with @aws-sdk/client-s3 v3
johnrb2 Jun 29, 2026
f21c595
Replace request with @activeprospect/request-capture/request
johnrb2 Jun 29, 2026
fb8969d
Replace faker with @faker-js/faker
johnrb2 Jun 29, 2026
a56133c
Remove moment in favor of native Date
johnrb2 Jun 29, 2026
cfd7fed
Add serverless.yml and CI workflows for osls deployment
johnrb2 Jun 29, 2026
26f5401
Update release workflow to Node 24 and add engines field
johnrb2 Jun 29, 2026
637ced1
Keep lambda handler synchronous to avoid freezing in-flight posts
johnrb2 Jun 29, 2026
7c25f54
Use faker.person.jobTitle for the title field
johnrb2 Jun 29, 2026
107326f
Make lead and feedback submission fully async
johnrb2 Jun 29, 2026
cce7903
Address Bugbot review findings
johnrb2 Jun 29, 2026
4805db0
Document osls deployment and one-time cutover
johnrb2 Jun 29, 2026
0ec2e0c
Replace submitLead callback with a resolved body
johnrb2 Jun 29, 2026
74a9b78
Address Copilot review feedback
johnrb2 Jun 29, 2026
b30c0e0
Add ESLint (semistandard) and reformat repo
johnrb2 Jun 30, 2026
db37707
Resolve demo keys from env/file/SSM instead of bundled keys.json
johnrb2 Jun 30, 2026
6e4a08e
Deploy via GitHub OIDC and drop the keys.json CI placeholder
johnrb2 Jun 30, 2026
81ef4a2
Remove deploy.sh and document the SSM/OIDC model
johnrb2 Jun 30, 2026
2b6afd3
Redact API key in submitfeedback verbose logging
johnrb2 Jun 30, 2026
a7d110b
Address Copilot review feedback
johnrb2 Jun 30, 2026
6d6519b
Keep API key out of CodeQL-tracked data flow in verbose log
johnrb2 Jun 30, 2026
75c5a21
Handle submitLead rejection in the testlead CLI
johnrb2 Jun 30, 2026
634f376
Resolve demo keys from Secrets Manager at runtime instead of SSM
johnrb2 Jul 10, 2026
7872af2
Document the Secrets Manager runtime keys model
johnrb2 Jul 10, 2026
0323a45
Exclude demoConfig from the deploy artifact
johnrb2 Jul 10, 2026
f2a4679
Stop logging env-derived values in demo key resolver
johnrb2 Jul 10, 2026
fd8a60d
Harden feedback response parsing and secret payload check
johnrb2 Jul 10, 2026
93b56c8
Fix misleading CLI usage strings
johnrb2 Jul 10, 2026
5575a14
Point demo keys at the Doppler-synced Secrets Manager secret
johnrb2 Jul 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
.serverless/
16 changes: 16 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module.exports = {
env: {
es2022: true,
node: true,
mocha: true
},
extends: [
'eslint:recommended',
'semistandard'
],
rules: {
complexity: ['error', 10],
'no-useless-return': 'error',
'prefer-const': 'error'
}
};
45 changes: 45 additions & 0 deletions .github/workflows/deploy-staging.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Deploy test-sales-and-dev-leads to Staging AWS Account

on: workflow_dispatch

permissions:
id-token: write
contents: read

jobs:
deploy:
name: deploy
runs-on: ubuntu-latest
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: setup node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
always-auth: true
node-version: "24"
registry-url: https://registry.npmjs.org
scope: "@activeprospect"
cache: 'npm'

- name: Install dependencies
run: npm ci --ignore-scripts

# Assume the dedicated least-privilege deploy role via GitHub OIDC. No
# long-lived AWS keys are stored as secrets. The role only creates/updates
# the stack; no secret is resolved at deploy time (the Lambda reads the
# demo keys from Secrets Manager at runtime via its own function role).
- name: Configure AWS Credentials Action For GitHub Actions
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0
with:
aws-region: us-east-1
role-to-assume: arn:aws:iam::371005981288:role/testlead-deployer
role-session-name: testlead-deploy-github

- name: install osls deploy framework
run: npm i -g osls@3.70.0 --ignore-scripts

- name: osls deploy
run: osls deploy --stage staging
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 18
node-version: 24
- name: Install packages
run: npm install
env:
Expand Down
49 changes: 49 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: test

on:
push:
branches: [master]
pull_request:
branches: [master]

jobs:
test:
name: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
always-auth: true
node-version: '24'
registry-url: https://registry.npmjs.org
scope: "@activeprospect"
cache: 'npm'
- run: npm ci
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- run: npm run lint
- run: npm test

validate-serverless-template:
name: validate serverless template
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
always-auth: true
node-version: '24'
registry-url: https://registry.npmjs.org
scope: "@activeprospect"
cache: 'npm'
- run: npm ci
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- run: npm i -g osls@3.70.0
# serverless.yml no longer resolves any deploy-time variable (the secret
# is fetched at runtime via Secrets Manager), so packaging needs no AWS
# credentials.
- run: serverless package --stage staging
env:
AWS_REGION: 'us-east-1'
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ node_modules
.DS_Store
keys.json
demoConfig
.serverless
33 changes: 30 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,39 @@ In addition to being an interactive tool to send test leads and feedback, this t

Configuration for these submissions is controlled by two JSON files, read from the S3 bucket [`sales-and-dev-leads-config`](https://s3.console.aws.amazon.com/s3/buckets/sales-and-dev-leads-config?region=us-east-1&tab=objects) (also in the **LeadConduit staging** account): `leadSubmissions.json` and `feedbackSubmissions.json`. Examples of the format expected can be found in the manual/test invocation script `lib/manualdemo.js`.

Note that use of automated **feedback** by that Lambda function also requires the presence of `demoConfig/keys.json` in the deployed package, with your LeadConduit API key defined, like this:
Automated **feedback** also needs the demo API keys: a JSON map of account name to LeadConduit API key, matching the `accountname` values in `feedbackSubmissions.json`:

```
{
"apikey": "your_lc_api_key_here"
"ActiveProspect, Inc.": "your_lc_api_key_here",
"ActiveProspect, Inc. Demo": "another_lc_api_key"
}
```

Updates can be deployed based on the script `deploy.sh`.
These keys are not bundled into the deploy artifact and are never placed in the Lambda's environment. They live in an AWS Secrets Manager secret (`leadconduit-lambdas-staging-testlead-doppler`) in the **LeadConduit staging** account. Doppler is the source of truth and syncs into Secrets Manager via a single-secret sync of the `leadconduit-lambdas` project's `staging_testlead` config, so the secret's value is a JSON object of that config's keys — the map above is stored (as a JSON string) under the `DEMO_KEYS` key, alongside Doppler's `DOPPLER_*` metadata keys. At runtime the Lambda calls `GetSecretValue` (via `lib/demokeys.js`) using its function role, unwraps the `DEMO_KEYS` entry, and parses it; only the non-sensitive secret id is exposed as the `DEMO_KEYS_SECRET_ID` environment variable.

#### Local key resolution

When you run the keys-dependent code locally (e.g. `lib/manualdemo.js`), `getDemoKeys()` resolves the map with this precedence:

1. `DEMO_KEYS` — inline JSON in the environment (e.g. via `doppler run`). Highest priority.
2. A local JSON file — `DEMO_KEYS_FILE` if set, otherwise `demoConfig/keys.json` (the previous local-dev workflow).
3. AWS Secrets Manager — reads the `DEMO_KEYS_SECRET_ID` secret (default `leadconduit-lambdas-staging-testlead-doppler`) using your default AWS credentials, unwrapping the `DEMO_KEYS` key from the Doppler-synced payload. This is the source the deployed Lambda uses. If you are not logged in, it prints guidance: run `aws sso login --profile <profile>` (or `aws_auth`) and set `AWS_PROFILE` to select your role, then retry.

Configuration knobs (all optional): `DEMO_KEYS`, `DEMO_KEYS_FILE`, `DEMO_KEYS_SECRET_ID` (default `leadconduit-lambdas-staging-testlead-doppler`), `DEMO_KEYS_SOURCE` (force `env` | `file` | `secretsmanager`), and `DEMO_KEYS_DISABLE_AWS` (skip the Secrets Manager fallback, e.g. offline/CI). Standard AWS env (`AWS_PROFILE`, `AWS_REGION`) governs which role/region the Secrets Manager read uses.

### Deployment

Updates are deployed by the **Deploy test-sales-and-dev-leads to Staging AWS Account** GitHub Action (`.github/workflows/deploy-staging.yml`), triggered manually via `workflow_dispatch`. It packages and deploys the Lambda with [`osls`](https://github.com/oss-serverless/serverless) from `serverless.yml`, which codifies the runtime (`nodejs24.x`), the S3 read permission, the `secretsmanager:GetSecretValue` permission on the demo-keys secret, the every-minute schedule, and the `DEMO_KEYS_SECRET_ID` environment variable (the secret id only — never the keys). No secret is resolved at deploy time; the Lambda reads the demo keys from Secrets Manager at runtime. The workflow authenticates to AWS via GitHub OIDC, assuming the dedicated least-privilege `testlead-deployer` role — there are no long-lived AWS keys stored as secrets. The only repository secret it needs is `NPM_TOKEN` (to install the private dependency).

If you ever need to deploy outside CI (break-glass), authenticate to the **LeadConduit staging** account locally (`aws sso login` / `aws_auth`) and run `osls deploy --stage staging` from a checkout. The retired `deploy.sh` script (which only ran `update-function-code`) has been removed in favor of this path.

#### One-time cutover to `osls`

The original function and its every-minute trigger were created outside CloudFormation, so the first `osls deploy` cannot adopt them. Before the first deploy, perform this one-time cutover in the **LeadConduit staging** account (`us-east-1`):

1. Delete the manually-created EventBridge rule `test-sales-and-staging-leads` (its only target is this Lambda, so nothing else depends on it).
2. Delete the existing `test-sales-and-dev-leads` Lambda function.
3. Run the deploy workflow. CloudFormation then creates the function and its own schedule rule fresh, fully stack-managed.

This causes a brief gap (a couple of skipped synthetic submissions) while the function is recreated, which is harmless for this staging test-data generator. After the cutover, the schedule is owned by the stack — do not recreate the manual rule.
19 changes: 0 additions & 19 deletions deploy.sh

This file was deleted.

88 changes: 56 additions & 32 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,59 +1,83 @@
const { submitLead } = require('./lib/submitlead');
const { submitFeedback } = require('./lib/submitfeedback');
const AWS = require('aws-sdk');
const { getDemoKeys } = require('./lib/demokeys');
const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');

AWS.config.update({region: 'us-west-1'});
s3 = new AWS.S3({apiVersion: '2006-03-01'});
const s3 = new S3Client({});
Comment thread
johnrb2 marked this conversation as resolved.

function demoLeads(config) {
config.forEach(lead => {
if(!lead.fields) lead.fields = [ "first_name", "last_name", "email", "phone_1", "address_1", "city", "state", "postal_code", "company.name" ];
function demoLeads (config) {
return Promise.all(config.map(lead => {
if (!lead.fields) lead.fields = ['first_name', 'last_name', 'email', 'phone_1', 'address_1', 'city', 'state', 'postal_code', 'company.name'];
console.log(`Processing lead for ${lead.description} (${lead.probability}%)...`);
submitLead(lead);
});
return submitLead(lead);
}));
}

function demoFeedbacks(config) {
const keys = require('./demoConfig/keys.json');
config.forEach(feedback => {
async function demoFeedbacks (config) {
const keys = await getDemoKeys();
return Promise.all(config.map(feedback => {
feedback.apiKey = keys[feedback.accountname];
console.log(`Processing feedback for ${feedback.description} (${feedback.probability}%)...`)
submitFeedback(feedback);
});
if (!feedback.apiKey) {
// Fail fast rather than firing an unauthenticated request that would just
// return a confusing 401/403.
console.log(`Skipping feedback for ${feedback.description}: no API key for account '${feedback.accountname}'`);
return Promise.resolve();
}
console.log(`Processing feedback for ${feedback.description} (${feedback.probability}%)...`);
return submitFeedback(feedback);
}));
Comment thread
johnrb2 marked this conversation as resolved.
}

function lambda() {
function getConfig (bucket, key) {
return s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }))
.then((response) => response.Body.transformToString('utf-8'))
.then((body) => JSON.parse(body));
}

// Async handler: awaits both the S3 reads and every downstream post so the
// promise only settles once all work is done. This is important on Lambda,
// where an async handler freezes the execution environment as soon as its
// promise resolves (the event loop is not drained), so any unawaited HTTP
// would otherwise be cut off mid-flight.
async function lambda () {
const bucket = 'sales-and-dev-leads-config';
const leadConfig = 'leadSubmissions.json';
const feedbackConfig = 'feedbackSubmissions.json';

// The config-load and submission phases are kept in separate try/catch blocks
// so a failed S3 read is reported distinctly from a failure while processing
// the (successfully fetched) config.
let leadCfg;
try {
s3.getObject({Bucket: bucket, Key: leadConfig}, function(err, data) {
if (err) {
console.log(`Error from s3.getObject (${bucket}/${leadConfig})`, err);
} else {
demoLeads(JSON.parse(data.Body.toString('utf-8')));
}
});
leadCfg = await getConfig(bucket, leadConfig);
} catch (e) {
console.log(`Unable to load lead submission configuration from S3 (${bucket}/${leadConfig})`, e);
console.log(`Error loading lead submission configuration from S3 (${bucket}/${leadConfig})`, e);
}
if (leadCfg) {
try {
await demoLeads(leadCfg);
} catch (e) {
console.log('Error processing lead submissions', e);
}
}

let feedbackCfg;
try {
s3.getObject({Bucket: bucket, Key: feedbackConfig}, function(err, data) {
if (err) {
console.log(`Error from s3.getObject (${bucket}/${feedbackConfig})`, err);
} else {
demoFeedbacks(JSON.parse(data.Body.toString('utf-8')));
}
});
feedbackCfg = await getConfig(bucket, feedbackConfig);
} catch (e) {
console.log(`Unable to load lead submission configuration from S3 (${bucket}/${feedbackConfig})`, e);
console.log(`Error loading feedback submission configuration from S3 (${bucket}/${feedbackConfig})`, e);
}
if (feedbackCfg) {
try {
await demoFeedbacks(feedbackCfg);
} catch (e) {
console.log('Error processing feedback submissions', e);
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
}
}
}

module.exports = {
lambda,
demoLeads,
demoFeedbacks
}
};
Loading
Loading