Extend the stats module to track per-user counts in addition to the existing per-command totals. CommandHook now receives the originating *models.Update so the hook can attribute invocations to a user; only the stats module consumes this hook today. Schema (sort keys under pk="stats"): count:<cmd> existing, per-command total user:<id> new, per-user total with cached username pair:<cmd>:<id> new, per (command, user) pair Subcommands (all public): /stats top commands (unchanged) /stats users top users overall /stats user <name> top commands for that user /stats cmd <name> top users of that command When the sender has no Telegram username the per-user/pair writes are skipped — the global per-command counter still increments. View helpers fan out KV reads in parallel to stay inside the webhook deadline. The existing read-modify-write race is unchanged; closing it would need atomic UpdateItem ADD on the KV interface.
AWS account setup
One-time setup steps for a fresh AWS account. After this is done, every push to main deploys via GitHub Actions OIDC; no human-in-loop AWS commands needed.
For the full onboarding walkthrough (prerequisites, Telegram wiring, cost guardrails), see
../docs/deploy-aws-free-tier-guide.md. This file is the condensed cheatsheet.
Region:
ap-southeast-1(Singapore). Change insamconfig.tomlif needed. Stack name:miti99bot. Change insamconfig.toml.
1. AWS account hygiene
- Enable MFA on the root user.
- Create an IAM admin user
admin(CLI access keys). Use only for the firstsam deploy --guided. - Set CLI default region:
aws configure set region ap-southeast-1 --profile admin aws configure set aws_access_key_id AKIA… --profile admin aws configure set aws_secret_access_key … --profile admin
2. SSM Parameter Store secrets
Create the four required secrets. Names must match template.yaml (/miti99bot/${StackEnv}/…).
aws ssm put-parameter --name /miti99bot/prod/telegram-bot-token \
--value "<bot-father-token>" --type SecureString --profile admin
aws ssm put-parameter --name /miti99bot/prod/telegram-webhook-secret \
--value "$(openssl rand -hex 32)" --type SecureString --profile admin
aws ssm put-parameter --name /miti99bot/prod/gemini-api-key \
--value "<google-ai-studio-key>" --type SecureString --profile admin
aws ssm put-parameter --name /miti99bot/prod/cron-shared-secret \
--value "$(openssl rand -hex 32)" --type SecureString --profile admin
Save the webhook + cron secrets locally — you'll set them on the Telegram side and on the EventBridge schedule headers.
3. GitHub OIDC identity provider
One-time per AWS account:
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com \
--thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1 \
--profile admin
(GitHub publishes the canonical thumbprint; verify on docs.github.com if rotated.)
4. Deploy IAM role for GitHub Actions
Edit aws/iam-github-oidc-trust.json if you are changing the AWS account or GitHub repo. This repo is already prefilled for account 225603493174 and tiennm99/miti99bot, and the trust allowlist is narrowed to refs/heads/main only (see "Trust policy invariants" below). If you change accounts, update .github/workflows/deploy.yml to match the same role ARN, then:
aws iam create-role \
--role-name github-deploy-miti99bot \
--assume-role-policy-document file://aws/iam-github-oidc-trust.json \
--profile admin
# Permissions: stack-scoped inline policy committed at aws/iam-github-deploy-policy.json.
aws iam put-role-policy \
--role-name github-deploy-miti99bot \
--policy-name miti99bot-deploy \
--policy-document file://aws/iam-github-deploy-policy.json \
--profile admin
Scoped to stacks/resources named
miti99bot*. See security audit F1 for rationale and plan for the cutover record.
Updating the deploy policy
When template.yaml adds a new resource type, the deploy role may need new IAM actions. Workflow:
- Edit
aws/iam-github-deploy-policy.json— add the action(s) + ARN pattern. - Apply out-of-band from a maintainer's
adminprofile (NOT via the workflow):aws iam put-role-policy --role-name github-deploy-miti99bot \ --policy-name miti99bot-deploy \ --policy-document file://aws/iam-github-deploy-policy.json --profile admin - Commit the JSON. Next deploy uses the new permissions.
Drift check — structural compare, not byte-diff. aws iam get-role-policy returns JSON whose key ordering / whitespace differs from the local file but may be semantically identical. Compare normalized:
diff <(aws iam get-role-policy --role-name github-deploy-miti99bot \
--policy-name miti99bot-deploy --profile admin \
--query PolicyDocument | jq -S .) \
<(jq -S . aws/iam-github-deploy-policy.json)
Non-empty output = INVESTIGATE before reapplying. AWS-side may have been intentionally patched during an outage; blindly re-applying overwrites that fix.
Trust policy invariants
aws/iam-github-oidc-trust.json constrains which GitHub Actions contexts can assume github-deploy-miti99bot. The current allowlist is intentionally narrow: only pushes to main can deploy.
To add a new branch / context (e.g., a future dev preview deploy):
- Edit
aws/iam-github-oidc-trust.json— add the newsubclaim to theStringLikearray. Examples:repo:tiennm99/miti99bot:ref:refs/heads/dev— pushes todevbranchrepo:tiennm99/miti99bot:environment:preview— workflows scoped to a GitHub Environment namedpreview(requirespermissions: id-token: write)
- Apply out-of-band:
aws iam update-assume-role-policy --role-name github-deploy-miti99bot \ --policy-document file://aws/iam-github-oidc-trust.json --profile admin - Commit. Test by triggering the new workflow path.
Reasons pull_request is NOT in the allowlist (do not re-add without reviewing): PR-context OIDC tokens are derivable from any contributor's PR. Granting the deploy role to PRs is equivalent to granting deploy access to every contributor. Combined with the inline policy's IAM/Lambda/DynamoDB actions, an attacker-controlled PR could exfiltrate or alter prod state.
5. Add GitHub repo secrets
In GitHub repo settings → Secrets and variables → Actions:
| Secret | Value |
|---|---|
ALERT_EMAIL (optional) |
Email for the $1 budget alert |
The deploy workflow now uses the repo's fixed AWS account ID directly for the OIDC role ARN, so AWS_ACCOUNT_ID no longer needs to be stored in GitHub.
6. First deploy (manual)
make build-lambda
AWS_PROFILE=admin sam deploy --template-file template.yaml --guided
Confirm:
- Stack name:
miti99bot - Region:
ap-southeast-1 - Capabilities:
CAPABILITY_IAM - Save to
samconfig.toml: yes (already committed; this just confirms)
After CREATE_COMPLETE:
aws cloudformation describe-stacks --stack-name miti99bot \
--query "Stacks[0].Outputs" --output table --profile admin
Note the FunctionUrl — point the Telegram webhook at it (see ../docs/deploy-aws-free-tier-guide.md Step 5).
7. Tighten — optional but recommended
Once the first deploy succeeds:
- Rotate / delete
adminCLI keys (use only via console for emergencies). - Trigger a workflow_dispatch deploy via GH Actions to confirm OIDC path works without the bootstrap user.
Replace the broad managed policies onDone 2026-05-18 — see step 4 (github-deploy-miti99botwith stack-scoped custom policies.aws/iam-github-deploy-policy.json) and plan.
Lambda Web Adapter layer ARN
Pinned in template.yaml parameter LambdaAdapterLayerArn. Bump by checking:
- https://github.com/awslabs/aws-lambda-web-adapter/releases (look at the
Releasespage for the latest layer version) - Format:
arn:aws:lambda:ap-southeast-1:753240598075:layer:LambdaAdapterLayerArm64:<version>
Cost expectations
After the stack is up but idle, monthly cost should be $0. If you ever see >$0.01 in Cost Explorer, investigate — most likely culprits: CloudWatch Logs ingestion volume, DynamoDB writes from a runaway loop, or accidental egress past the 100 GB free tier.