Same-day settlement visibility plus maximal data capture from the
reporting feed. The handler gains an allowlisted endpoint parameter
(EventBridge passes {"endpoint":"current-day"} on a new 16/19/22 UTC
weekday rule; the 9am previous-day sweep is unchanged and remains
authoritative). Every response's exact bytes archive to a new
Retain-protected bucket before classification, so the feed is
replayable and auditable even across parser changes. Summary rows
become per-date boa_balance# snapshots (latest-wins on run_at, no
TTL) instead of being discarded. The staleness sweep is gated to
previous-day runs so intraday runs don't re-alert the backlog three
times a day. History events carry a via:<endpoint> audit tag outside
the replay-idempotence identity. Fixtures are sanitized real API
captures; classification histograms assert against live-verified
counts.
Refs: #66, #69
Two agentic-review findings on the payments-dashboard security surface:
- CRITICAL (CWE-862): the /slack/events endpoint (slackAppHome) performed no
Slack signing-secret verification, so an unauthenticated caller could forge
app_home_opened/block_actions events and exfiltrate payment data via
DynamoDB scans + views.publish. Add HMAC-SHA256 signature verification with
a 5-minute replay window over the raw request body, mirroring the existing
expenseReceiver pattern. Requests failing verification get a 401 before any
body parsing, DynamoDB access, or Slack API call. Adds the
SLACK_SIGNING_SECRET_NAME env var and an additive secretsmanager grant for
payments-dashboard/slack-signing-secret.
- HIGH (CWE-532): full BoA CashPro response bodies + headers were logged to
CloudWatch and persisted to DynamoDB (response_body/response_headers), which
could expose account numbers/PII. Drop those fields from both boa_txn#
records and stop logging raw bodies/headers on both the main submit path and
the backfill retry path; log status + txnId only (txnId still correlates to
BoA support for the full body).
Verification: sam validate, node --check both handlers. /sh-security-review
(detector fan-out + verifier) and GPT-4.1 cross-family review run; the
cross-family review confirmed the IAM grant is purely additive.
The two async-invoke OnFailure DLQs (payments-processPaymentCsv-async-dlq
and payments-processPayrollEmail-async-dlq) had no CloudWatch alarm, so a
failed async invocation could sit in the DLQ unnoticed. Add a
messages-present alarm for each, mirroring PayrollBatchDLQAlarm exactly:
AWS/SQS ApproximateNumberOfMessagesVisible, Maximum, threshold > 0,
Period 300, EvaluationPeriods 1, TreatMissingData notBreaching, ALARM-only
to the site-alerts SNS topic.
* Add CloudWatch alarm coverage for payments-dashboard (Wave 1)
Add 22 CloudWatch alarms to round out observability:
- Lambda Errors alarms for the 4 functions that lacked them
(slackAppHome, fetchBoaTransactions, expenseReceiver, expenseProcessor)
- Lambda Throttles alarms for all 6 functions
- Lambda Duration alarms for all 6 functions (~80% of timeout, Maximum)
- DynamoDB throttle/system-error alarms for the PaymentsDashboard table
(ReadThrottleEvents/WriteThrottleEvents/SystemErrors, TableName dim)
- API Gateway v2 alarms on the implicit ServerlessHttpApi
(5xx, 4xx, Latency p99; ApiId dim)
All alarms publish to the site-alerts SNS topic, ALARM-only, missing data
notBreaching, matching the existing payments-<fn>-errors convention from PR #48.
Documents the full alarm inventory under a new README Monitoring section.
* Drop DynamoDB SystemErrors alarm (never fires at TableName dimension)
AWS/DynamoDB SystemErrors does not emit at the TableName-only dimension,
so payments-dashboard-table-system-errors could never fire. Remove the
alarm resource and its README entry; keep Read/WriteThrottleEvents.
Add PublicAccessBlockConfiguration (BlockPublicAcls, IgnorePublicAcls,
BlockPublicPolicy, RestrictPublicBuckets all true) to PaymentsCsvBucket
and PayrollEmailBucket. Codifies the already-private runtime state
(account-level and bucket-level S3 BPA already enabled). Zero functional
change; clears checkov CKV_AWS_53/54/55/56.
The PaymentsDashboard table was migrated to SSE-KMS (alias/seahaven-dynamodb,
INFRA-95/M-3) out-of-band, but no function role had kms perms. fetchBoaTransactions
failed 6/6 daily runs with kms:Decrypt AccessDeniedException, taking the BoA
transaction feed 100% down; the other 3 table consumers were latently broken too.
- Add kms:Decrypt/GenerateDataKey/DescribeKey to processPayrollEmail,
processPaymentCsv, fetchBoaTransactions (read-write) and kms:Decrypt/DescribeKey
to slackAppHome (read-only). Actions match the lambda permissions boundary.
- Declare SSESpecification on the table to reconcile out-of-band drift (idempotent).
- Resolve the CMK arn from SSM /seahaven/dynamodb/cmk-arn.
GPT-4.1 cross-review: no blockers.
Adds seahaven-lambda-execution-boundary to Globals.Function so every
SAM-auto-generated Lambda execution role carries the boundary. Required
for the INFRA-97 github-cfn-execution-role scope-down to safely permit
iam:CreateRole on this stack.
No explicit AWS::IAM::Role resources exist in this template; the
Globals entry covers all six functions.
Refs: INFRA-103
Bring the interim CLI-created dead-letter queues, error alarms, and
SendMessage role policies for payments-processPaymentCsv and
payments-processPayrollEmail under CloudFormation control (H-8 drift).
- Add per-function async-invoke OnFailure SQS DLQs (CFN-named
payments-<fn>-async-dlq, 14d retention to match payments-payroll-batch-dlq)
- Wire EventInvokeConfig OnFailure on both functions (SAM auto-generates the
scoped sqs:SendMessage policy on each execution role); explicit retry/age
defaults locked in
- Add ALARM-only Lambda Errors alarms (Sum, threshold>0) -> site-alerts
Interim CLI resources (queues, alarms, role policies, event-invoke-configs)
removed post-deploy after the CFN-managed versions were confirmed live.
Audit M-22: S3 and DynamoDB traffic from the VPC was billed through
the NAT gateway; gateway endpoints are free and keep it on the AWS
backbone.
Audit L-15: payroll-batch messages were silently lost after max
receives. Adds a 14-day DLQ with maxReceiveCount 3 and an ALARM-only
notification to site-alerts so a caught failure is actually seen.
API tokens and credentials must live in Secrets Manager per
secrets-and-config.md, but the four original payment Lambdas still
read 10 SecureString SSM params. Move them to three grouped secrets
(slack-bot-token plaintext, boa-check-mgmt and boa-reporting as JSON),
matching the pattern the expense Lambdas already use. IAM is scoped to
secretsmanager:GetSecretValue per secret; the VPC Lambdas reach the
public endpoint over the existing NAT path. Test/reissue scripts and
the client-ssm dependency are updated/removed accordingly.
Refs: #3
* Merge expense-approval-bot into payments-dashboard
Port the Slack reaction-driven expense routing workflow (receiver +
processor) from expense-approval-bot into this stack as JavaScript ESM.
Secrets copied to payments-dashboard/ prefix in Secrets Manager.
* Fix review findings from PR #28
- Add length check before timingSafeEqual to prevent RangeError on
malformed signatures (returns 401 instead of 500)
- Check event.type === reaction_added to prevent reaction_removed
from advancing expenses
- Move getPermalink call behind isOrigin check to skip unnecessary
API call on non-origin stage transitions
- Switch runtime to nodejs22.x and architecture to arm64
- Add explicit CloudWatch log groups with 60-day retention for all Lambdas
- Rename SQS queue from payments-contractor-batch to payments-payroll-batch
(now handles both employee and contractor batching)
- Remove stale comment
SES receives Gusto payroll emails at payroll@int.seahaven.com, stores
to S3, Lambda parses and posts a combined Slack notification (employee
payroll + contractor payments in one message) after a 10-minute SQS
batching window.
Removes Aurora Serverless, notifyPayroll Lambda, and @aws-sdk/client-rds-data.
Adds mailparser, SQS delay queue, and processPayrollEmail Lambda.
Queries Aurora for new payroll runs and contractor payments, sends
formatted Slack message with gross pay breakdown and total bank
withdrawal amount. Runs weekdays at 2pm ET, tracks notified payrolls
in DynamoDB to avoid duplicates.
Adds public subnet in second AZ, Aurora security group, DB subnet group,
and Serverless v2 cluster with Data API enabled and Secrets Manager credentials.
Capture response headers, body, and transactionId on every check-issue
API call; persist as boa_txn#<ts>#<action> records with 90-day TTL.
Add "Recent BoA Submissions" section to App Home showing the last 10
with click-to-copy transactionId.
Motivation: BoA support asked for a transactionId from a past
successful call and we had no way to recover it from CloudWatch
summary logs alone.
Also adds simulate-csv.cjs (dry-run preview) and stampli-uploader.sh
(launchd-invoked S3 uploader), and gitignores debug artifacts.
- Fix transaction code mapping (475=Cleared, 255=Returned) in fetchBoaTransactions
- Match on customerReference instead of bankReference for check number matching
- Add bank-confirmed protection: CSV cannot override status once bank confirms Cleared
- Add status progression guard: CSV cannot regress status backward in lifecycle
- Cleared status is permanent — cannot be voided, cancelled, or changed
- Enable daily fetchBoaTransactions schedule (9am ET weekdays)
- Add production test script and dry run simulation script
- Add OAuth client-credentials token exchange to both Lambda handlers
- Fix sandbox/prod base URL (api-sb.bofa.com / api.bofa.com)
- Fix issueAction casing to add_Issue / cancel_Issue per API docs
- Fix transaction inquiry response parsing (accountTransactions array)
- Move all BoA config (app IDs, bank ID) from env vars to SSM params
- Update template.yaml with correct SSM param names and policies
- Add project README with architecture, API details, and SSM param reference
- CSV processor detects new checks and submits add_issue to CashPro
- Checks updated to voided/cancelled trigger cancel_issue to CashPro
- Added SSM params for boa-api-token, boa-account-number, boa-company-id
to both processPaymentCsv and fetchBoaTransactions Lambdas
- Increased CSV processor timeout to 120s for API calls
- New fetchBoaTransactions Lambda: daily 9am ET schedule (disabled until API key set)
Calls CashPro Previous Day Transaction Inquiry, filters for codes 255/475,
matches bankReference to check_number, updates clear_status in DynamoDB
- CSV processor switched to UpdateCommand to preserve clearing data on re-upload
- Slack dashboard now shows Scheduled, Outstanding, Cleared, and Returned sections
- ACH payments auto-assumed cleared once past due date
- VPC with NAT Gateway for static outbound IP (52.86.95.107) for BoA API
- Payments stored as individual DynamoDB items keyed by check number
- Slack bot token fetched from SSM at runtime instead of CF parameter
- Slack Lambda scans payment items instead of reading single blob
Two Lambda functions:
- processPaymentCsv: S3 trigger, parses CSV, stores dashboard in DynamoDB
- slackAppHome: API Gateway endpoint for Slack events, publishes Home tab view