CI's --parameter-overrides replaces (not merges with) samconfig.toml's
parameter_overrides, so BOT_OWNER_ID was empty in the deployed Lambda.
deploynotify silently skipped sending the post-deploy DM, and all
Protected/Private commands were denied.
Extract the values from samconfig.toml at deploy time so the file
remains the single source of truth for non-secret deploy params.
Also add 'stats' to samconfig.toml's ModulesCSV so local `sam deploy`
doesn't regress the stats module.
- Remove ReservedConcurrentExecutions: 1 (account lacks unreserved
concurrency headroom; deploy rolled back). Accept best-effort
counter semantics as documented.
- Add /stats to aws/telegram-commands.json so it appears in the
Telegram client's / autocomplete after the next deploy.
Implement a new stats module for the Telegram bot that tracks per-command usage with persistent KV storage. The module provides a /stats command displaying usage sorted by popularity with a 4096-byte Telegram message cap. Includes CommandHook integration for post-dispatch tracking via background goroutine (2s bounded context), proper test coverage, and registry initialization. Updated server config with stats factory and reserved concurrent execution control to prevent TOCTOU issues.
New internal/deploynotify package fires a single Telegram DM to
BOT_OWNER_ID on the first cold start that observes a new gitSHA.
Dedup via a DynamoDB KV record so repeat cold starts of the same
version stay silent. Send-then-write order means a failed Telegram
call doesn't permanently silence retries.
gitSHA is baked into the binary via `-ldflags "-X main.gitSHA=..."`
from Makefile; empty SHA (non-make builds) silently disables the
feature. No new env vars or IAM permissions.
The default lolesports schedule page is anchored near "now", so a
calendar-aligned /lolschedule_week issued midweek was silently dropping
events from Mon–Wed: we only walked pages.newer. fetchSchedulePage now
also returns pages.older, and fetchEventsInRange walks older until the
earliest collected event is ≤ from, then walks newer until ≥ to. Page
budget bumped 3 → 8 to accommodate dense regular-season weeks.
Previously a rolling 7-day window from today; now snaps to the Monday
of the current ICT week so the reply matches what users mean by "this
week". Adds ictWeekStartOf helper plus week-boundary tests.
Delete 7 plan dirs (6 implemented, 1 superseded) and 19 reports tied to
removed or non-existent plans. Active plans (aws-port, pre-deploy-wrapup,
cf-data-migration, trongtruonghop, iam-least-privilege, go-port-cloud-run)
and their referenced reports are preserved.
YAML 1.1 implicit conversion: bare 'OFF' parses as boolean false.
AWS::Scheduler::Schedule.FlexibleTimeWindow.Mode requires the string
'OFF' or 'FLEXIBLE'. CFN's EarlyValidation hook rejects the boolean,
causing 'Failed to create changeset' on every deploy attempt.
Verified by manual create-change-set against the SAM-packaged
template: Mode: false -> FAILED; Mode: "OFF" -> CREATE_COMPLETE
with 4 changes (BotFunction modify, LolscheduleDailyPushSchedule add,
SchedulerExecutionRole policy modify, MonthlyBudget remove).
Replaces the 10x *FullAccess managed policies with a single
stack-scoped inline policy (miti99bot-deploy) on the role.
Policy boundaries:
- All resource ARNs scoped to miti99bot* (covers future miti99bot-dev)
- iam:PassRole conditioned on iam:PassedToService = lambda + scheduler
- iam:UpdateAssumeRolePolicy excluded (no trust-rewrite escalation)
- iam:AttachRolePolicy excluded (SAM uses inline PutRolePolicy)
- Wildcards limited to actions with no resource-level support
(sts:GetCallerIdentity, s3:ListAllMyBuckets,
cloudformation:ListStacks, cloudformation:ValidateTemplate)
Rollback: aws/iam-rollback-fullaccess.sh re-attaches all 10
FullAccess policies with retry-on-throttle + final verification.
Apply via Phase 4 two-stage cutover (dual-attach trial then detach).
Policy is committed but NOT yet attached -- Phase 4 applies it.
Plan: plans/260518-1019-iam-least-privilege/phase-03-draft-custom-policy.md
Audit: plans/reports/code-reviewer-260518-1019-security-aws-infra.md
Drop refs/heads/dev and pull_request from the sub allowlist on
github-deploy-miti99bot trust policy. Only pushes to main can now
assume the deploy role.
dev was dormant (ci.yml has only contents:read; deploy.yml triggers
on push:main only). pull_request was dormant too (no workflow has
id-token:write under PR trigger). This closes the latent F2 path.
Plan: plans/260518-1019-iam-least-privilege/phase-01-narrow-oidc-trust-f2.md
Audit: plans/reports/code-reviewer-260518-1019-security-aws-infra.md
Captures the 2026-05-18 security review session output:
- plans/reports/code-reviewer-260518-1019-security-aws-infra.md
- plans/reports/code-reviewer-260518-1019-security-go-app.md
- plans/reports/researcher-260518-1019-security-dependencies.md
- docs/deploy-aws-free-tier-guide.md (adds free-tier hard rule +
accepted security trade-offs as project standards)
Plan for the two HIGH-severity findings (F1, F2) targeting
github-deploy-miti99bot OIDC role: plans/260518-1019-iam-least-privilege/.
Plan was red-team-reviewed (15 findings applied) and validate-interviewed
(4 decisions recorded). Zero unresolved contradictions. Implementation
not yet started; phase 1 is standalone and lowest risk.
Other audit findings (F3 CORS, F4 root handler, F5-F16) deferred to
future commits; rationale in audit report.
Supersedes c726c0a (never deployed). EventBridge ApiDestination has no
free tier — $0.20/M, rounds to $0 at our 30 invokes/month but still a
billable line item, which violates the project's hard free-tier rule.
Replace 4 resources with 2:
- drop: CronConnection, LolscheduleDailyPushApiDestination,
EventBridgeInvokeRole, LolscheduleDailyPushRule
- add: SchedulerExecutionRole, LolscheduleDailyPushSchedule
Schedule fires cron(0 1 * * ? *) at Lambda directly (free tier:
14M invocations/mo). Target.Input carries a synthetic Function URL v2
event so Lambda Web Adapter proxies it to /cron/lolschedule_daily_push
unchanged. The X-Cron-Token header is interpolated from the NoEcho
CronSharedSecret parameter.
Accepted trade-off: token value is visible to anyone with
scheduler:GetSchedule on the schedule. IAM gates that; the project's
security policy treats secret-in-Scheduler-Input as acceptable.
CloudFormation's AWS::Scheduler::Schedule Target schema has no property
for HTTPS universal invocation (URL, method, headers) — confirmed
against AWS docs. Switch to the legacy EventBridge Rule path which
supports HTTP targets natively via ApiDestination:
- AWS::Events::Connection: API_KEY auth, presents X-Cron-Token header.
ApiKeyValue stored in EventBridge service-linked secret on stack
update (no per-invoke SSM fetch, AWS-managed secret fees).
- AWS::Events::ApiDestination: POST to ${FunctionUrl}cron/lolschedule_daily_push.
- AWS::Events::Rule: cron(0 1 * * ? *) — daily 01:00 UTC / 08:00 ICT.
Targets ApiDestination with retry x2, 600s max age, DLQ to CronDLQ.
- EventBridgeInvokeRole replaces SchedulerExecutionRole (events.amazonaws.com
principal, events:InvokeApiDestination scoped to this destination only).
NoEcho CronSharedSecret CFN parameter restored; GHA fetches the SSM
SecureString and passes via --parameter-overrides so the value never
appears in template source or stack events.
Free-tier preserved: 1 invocation/day, well under EventBridge Rules +
ApiDestinations free quotas.
Reverts 585d996 + c70b9d0. Both CFN deploys failed at changeset
validation; prod stack was never mutated. Approach A
(AWS::Scheduler::Schedule with arn:aws:scheduler:::http-invoke target)
is unimplementable in pure CloudFormation — the Target schema has no
property for HTTP endpoint/method/headers, regardless of name.
Replacement landing in a follow-up commit.
Restored: plans/reports/brainstorm-260517-1411-eventbridge-schedule-fix.md
(useful design context even though Approach A invalidated).
CloudFormation rejects {{resolve:ssm-secure}} inside
AWS::Scheduler::Schedule HttpInvokeArgs.HeaderParameters (documented
property-allowlist limitation). Add a NoEcho CronSharedSecret parameter
and fetch the SSM SecureString in CI, then pass via --parameter-overrides.
Lambda runtime SSM-fetch flow unchanged.
Adds AWS::Scheduler::Schedule resource invoking the Function URL via
HTTPS universal target (arn:aws:scheduler:::http-invoke) at 01:00 UTC
daily. Token sourced from SSM via {{resolve:ssm-secure}}; failures
retried twice and DLQ'd to existing CronDLQ. Closes the deferred
phase-05 of 260510-0234-pre-deploy-wrapup.
Vietnamese disclaimer one-liner. Interpolates a target name (default "VNG")
and the sender — @username when set, else tg://user?id link with the display
name. User-supplied text HTML-escaped; reply routed through ReplyHTML to keep
forum-topic threading.
Captures the two parallel code-reviewer reports that drove the
audit-driven hygiene pass in the previous commit. Kept for future
reviewers and as the trail of which findings were applied vs.
deferred as cosmetic.
Concurrency
- lolschedule: serialize subscriber Get→mutate→Put via state.subscribersMu;
the single-slot list was previously losing writes under concurrent
/lolschedule_subscribe.
- trading: PriceClient memoises its default *http.Client so /trade_stats
reuses TLS connections across held tickers.
Observability
- server/log_middleware: defer the req log line and recover panics so a
panicking cron handler still emits the structured req entry CloudWatch
filters on for 5xx alerting.
- server/router (cron): inner recover with cron-name context captures the
panicking job before the middleware's safety net does.
- telegram/webhook: rune-safe truncation in dispatch logs — Vietnamese,
Korean, and emoji previews no longer ship as garbled bytes.
- lolschedule/api_client: same rune-safe fix for error-body log truncation.
- telegram/webhook: gate the post-recover WriteHeader(200) so a panicking
handler that already touched w doesn't trigger superfluous-WriteHeader.
Correctness
- twentyq: clearGame error during solved-relaunch is logged instead of
silently swallowed (was a permanent deadlock vector on KV failure).
- misc /mstats: KV read failure replies "Could not load stats. Try again
later." to the user instead of returning into the dispatcher; matches the
pattern other modules use.
- migrate_cf_data trading-audit-dump: surface f.Close error so a truncated
JSONL never passes silently as a complete audit dump.
Operator ergonomics
- migrate_cf_data (all 4 subcommands): signal.NotifyContext for SIGINT /
SIGTERM. Ctrl-C mid-Scan now propagates cleanly instead of leaving a
half-converted DynamoDB table.
- ai/ratelimit: doc the Lambda-recycle memory bound to match keylock.Map
so a future reviewer doesn't re-flag the unbounded map.
I/O-changing (user-approved)
- lolschedule daily push auto-prunes subscribers whose Telegram error
matches a terminal marker (blocked / deactivated / chat gone). Transient
errors keep the chat on the list. Subscribe message updated to mention
the auto-cleanup.
- twentyq seed pool grown 50 → 178; repeat-collision threshold moves from
~9 plays to ~17 (birthday paradox).
- util /info flipped Public → Protected — chat/thread/sender IDs are no
longer enumerable by every group member.
- cmd/server WriteTimeout 6min → 75s (cron 60s + 15s slack). No-op on
Lambda; matters only for local non-Lambda runs.
- webhook + cron rejection paths drop response bodies (no fingerprintable
text for internet scanners hitting the public Function URL). Status
codes preserved for CloudWatch metrics; structured log lines carry the
rejection reason for operator triage.
Tests added: TestTruncateRunes, TestRunDailyPush_PrunesDeadSubscribers,
TestIsTerminalSendError, TestInfo_DeniedToNonOwner,
TestInfo_DeniedToChannelMessageNoFrom, plus owner-allowed counterparts.
Telegram routes outgoing messages with no message_thread_id to a forum
supergroup's General topic. Commands sent in a topic were being answered
in General. chathelper.Reply / ReplyHTML now take *models.Message and
forward both ChatID and MessageThreadID; help.go SendMessage and
loldle trySendSticker do the same. Adds regression tests asserting the
field is forwarded for forum topics and omitted for private/regular groups.
v9 is the first Node 24-native release and still accepts golangci-lint
>= v2.1.0, so the pinned v2.12.2 binary keeps working. Removes the
final Node 20 deprecation annotation from the CI run.
GitHub forces Node 20 actions to run on Node 24 starting 2026-06-02
and removes Node 20 entirely on 2026-09-16. Bump every action that
emitted the deprecation warning to the latest major that natively
declares runs.using: node24:
actions/checkout v4 -> v6
actions/setup-go v5 -> v6
aws-actions/setup-sam v2 -> v3
aws-actions/configure-aws-credentials v4 -> v6
Verified action.yml runtime declarations (node24) for checkout v6
and setup-go v6. configure-aws-credentials v5 changed only invalid-
boolean-input handling, which we don't pass. setup-sam v3 is solely
the Node bump. golangci-lint-action stays at v7 (already Node 24,
never appeared in the deprecation warning).
The go-telegram/bot v1.20.0 MatchTypeCommand does byte-exact equality
on the full bot_command entity, which in groups is "cmd@botname"
(length 15 for /help@miti99bot) — never equal to the registered name
"cmd". All 27 commands silently missed when used in group chats.
Swap RegisterHandler(..., MatchTypeCommand, ...) for a local matchFunc
that strips the @suffix before comparing. Telegram only routes
/cmd@otherbot to otherbot, so the suffix is safe to drop
unconditionally.
Add a structured `dispatch` log line in the webhook (update_id,
chat_id, chat_type, text preview) so the next silent-drop symptom
shows up in CloudWatch without code archaeology — the existing
[TGBOT] [UPDATE] line only prints struct pointers.
12 new TestMatchCommand sub-tests cover DM, group, mid-text, wrong
command, non-command entity, and bounds edge cases.
Pre-existing 9 errcheck violations introduced by d67517e blocked
golangci-lint on every CI run after the migration toolchain landed.
- Wrap defer Close() in anonymous func to match house style
(cf. internal/modules/lolschedule/api_client.go:162).
- Mark Fprint*/Fprintln to io.Writer with _, _ = (best-effort writes
to os.Stdout / bytes.Buffer; errors not actionable for callers).
No behavior change. go vet, golangci-lint, go test ./... all clean
locally.
Append two steps to .github/workflows/deploy.yml that POST setWebhook
and setMyCommands against the freshly-deployed Function URL, reading
credentials from SSM. Mirrors `make telegram-setup` but inlined to
avoid the Makefile's --profile admin assumption.
Token and webhook-secret are masked via ::add-mask:: before any echo.
Jobs fail loudly on Telegram API errors via `jq -e .ok`.
Mark the manual setWebhook snippets in docs/deploy-aws.md and
docs/deploy-aws-free-tier-guide.md as break-glass.
- phase-01: mark complete; record live D1+KV inventory outcome (9 durable
KV keys, trading source is KV not D1, retired modules skipped)
- phase-02: mark complete; toolchain verified end-to-end against prod CF
- phase-03: rewrite to flat KV copy (D1-transform branch dropped)
- plan.md: amend staging-only decision to permit pre-cutover bulk import
against the live table while it has no AWS-served writes
- runbook: lock matrix and trading source, document Phase 02 commands
Operator-run tooling that moves durable Cloudflare KV data into the live
AWS DynamoDB table, plus a runtime swap of the `value` attribute from
Binary to String so payloads are human-readable in the AWS console.
- cmd/migrate_cf_data: subcommands inventory, kv-import (idempotent via
attribute_not_exists), trading-audit-dump, convert-value-to-string
- internal/migration: policy allowlist, CF KV+D1 REST clients, DynamoDB
writer, report formatter with per-prefix counts + tests
- internal/storage/dynamodb_kv.go: Put writes MemberS, Get reads MemberS;
dropped empty-bytes sentinel (DynamoDB allows empty strings)
Lock the Cloudflare-to-AWS migration matrix and runbook against the live DynamoDB runtime shape, and gate AWS cutover on verified migration parity instead of assuming a symmetric rollback.
The deploy workflow no longer relies on a missing AWS_ACCOUNT_ID secret,
preventing invalid assume-role ARNs and matching the repo's documented AWS setup.
Rename:
- Go module github.com/tiennm99/miti99bot-go → github.com/tiennm99/miti99bot
- CloudFormation stack miti99bot-aws-port → miti99bot
- Drop "port", "Cloud Run", "GCP", "cutover", "Phase NN" framing from
active code and docs — project reads as canonical AWS-Lambda from now on.
AWS deploy guide + flow fix:
- New docs/deploy-aws-free-tier-guide.md — Ubuntu 24.04 ARM64 onboarding
with project-local venv (pip awscli + sam-cli), SSM secrets via read -s,
idempotent OIDC provider + role creation, $1 budget alarm.
- Drop sam build from the pipeline — provided.al2023 + makefile builder
expects a Makefile in CodeUri (build/lambda/, the output dir), so the
step always fails. sam deploy --template-file template.yaml now reads
the raw template and zips build/lambda/ directly.
- Rollback section rewritten — use continue-update-rollback /
cancel-update-stack / git-SHA redeploy. Drop the broken
--use-previous-template recipe.
- DynamoDB free-tier row corrected (on-demand is 2.5M read / 1M write
request units, not 25 RCU/WCU).
Updated:
- README.md fully rewritten (drops port/legacy framing, lists modules,
points new users at the free-tier guide).
- aws/README.md retitled "AWS account setup", phase numbers stripped.
- Makefile / .github/workflows/deploy.yml — sam deploy flow.
- samconfig.toml — stack_name = "miti99bot".
- Go comments — Cloud Run → Lambda, Cloud Scheduler → EventBridge
Scheduler, Cloud Logging → CloudWatch Logs.
- Struct field GCPProject → FirestoreProject (env GOOGLE_CLOUD_PROJECT
unchanged).
Plus advisory reports under plans/reports/ from the code-reviewer +
researcher passes that informed the fixes.
Verified: go vet ./..., go build ./..., go test ./... all green.
Removes six modules (loldle-ability/emoji/quote/splash, semantle, doantu)
and prunes the framework deps that were only there to serve them:
- ai.Embedder + Client.Embed + embeddingModel const (semantle only)
- Deps.Embedder + BuildOptions.Embedder
- Deps.Env + Build(env) param + ModuleEnv config field + PHOW2SIM allowlist (doantu only)
- internal/champname package (loldle now owns its lookup helpers directly)
- template.yaml: Phow2simAPIURL parameter + PHOW2SIM_API_URL Lambda env
Active catalog: util, misc, wordle, loldle, lolschedule, twentyq, trading.
go build / vet / test all pass.
- Portfolio, holdings, prices, symbols packages with Telegram command handlers
- Handlers: trade_topup (add credits), trade_buy, trade_sell, trade_convert (coming-soon), trade_stats
- Per-user keylock for concurrency safety
- KBS (VN) as price source; ticker validation regex ^[A-Z0-9]{1,16}$
- Scope-trimmed: no SQL history, no retention cron, no daily refresh, no leaderboard, no forex
(matches upstream miti99bot's current VN-stocks-only state)
- Full unit test coverage for format, portfolio, prices, symbols, handlers
- Extend module.Deps struct with optional Bot field
- Add Bot to registry.BuildOptions and thread through builders
- Pass bot instance from main.go into module factory options
- Enables cron handlers to send messages and access bot state
- Add new modules: doantu, semantle, twentyq, ai (Gemini integration)
- Update module registry with new command registration
- Update tests and documentation for module system
- Update README with new module references
- Implement DynamoDB client wrapper and KV provider
- Full CRUD test coverage for both local and AWS modes
- AWS SDK v2 dependency added to go.mod/go.sum
- Research reports comparing AWS/GCP free tiers and greenfield trade-offs
- New AWS port plan with 7 phases (infrastructure, deployment, integration)
- Mark GCP plan deploy phases as superseded by new AWS plan