Commit Graph
230 Commits
Author SHA1 Message Date
tiennm99 fd2e84f8fd docs(plans): add VNAppMob key parsing bug-fix plan 2026-06-15 12:20:44 +07:00
tiennm99 b2e8385706 fix(gold): parse VNAppMob JWT object wrapper 2026-06-15 12:20:40 +07:00
tiennm99 e0b0418173 fix(gold): use GET for VNAppMob key refresh endpoint
VNAppMob /api/request_api_key returns 405 for POST and accepts GET.
2026-06-15 11:17:50 +07:00
tiennm99 12dc515047 feat(gold): remove XAU/USD fallback and use VNAppMob SJC only 2026-06-15 11:04:06 +07:00
tiennm99 966e4afd51 chore(plans): add VNAppMob integration plan and completion report 2026-06-15 10:49:19 +07:00
tiennm99 ae92539043 docs(gold): document VNAppMob SJC pricing and env vars 2026-06-15 10:49:19 +07:00
tiennm99 a6bf022e27 feat(gold): integrate VNAppMob SJC gold price with auto-refresh API key 2026-06-15 10:49:19 +07:00
tiennm99 8c2c9a7c84 chore: remove one-time stock-key migration tool
Migration complete and verified in production; the trading->stock
DynamoDB partition copy is no longer needed and the old trading rows
have been deleted.
2026-06-12 15:46:03 +07:00
tiennm99 6d0a30c4c0 fix(migrate): check fmt.Fprintf return to satisfy errcheck lint 2026-06-12 15:12:07 +07:00
tiennm99 32417e4196 refactor(stock): rename trading module to stock
Rename the `trading` module to `stock` across code, config, and docs for
naming consistency with the sibling coin/gold modules.

- Module: internal/modules/trading -> internal/modules/stock (package,
  factory, registry key in cmd/server)
- Commands: trade_* -> stock_* (telegram-commands.json)
- Env/SAM params: TRADING_INCOME_EVENTS_* -> STOCK_INCOME_EVENTS_*
- ModulesCSV updated in template.yaml, samconfig.toml, deploy.yml
- Add cmd/migrate-stock-key: idempotent, non-destructive DynamoDB
  partition copy (pk=trading -> pk=stock) with dry-run default

Production DynamoDB data already migrated (8 keys: 2 portfolios + 6
symbol-cache rows); old trading partition retained as rollback snapshot.
2026-06-12 14:52:12 +07:00
tiennm99 62555dee23 fix(deploy): include coin module in ModulesCSV overrides
coin was registered in the catalog and template default but absent from
the ModulesCSV override in both deploy.yml and samconfig.toml, so the
deployed Lambda never loaded it and all coin_* commands went unregistered.

Add a registration checklist to docs/deploy-aws.md to prevent recurrence.
2026-06-12 11:22:16 +07:00
tiennm99 49a6e7091a fix(coin): accept symbol-first trades 2026-06-12 11:07:27 +07:00
tiennm99 b234b54b2e feat(coin): register telegram commands 2026-06-12 10:53:19 +07:00
tiennm99 9b379b3d6b feat(coin): add crypto paper trading 2026-06-12 10:47:02 +07:00
tiennm99 c9809e716a feat(gold): add gold commands to Telegram menu 2026-06-11 22:28:24 +07:00
tiennm99 8ba575ab1d fix(deploy): include gold module in CI ModulesCSV override
CI's --parameter-overrides replaces samconfig.toml values, so every
push reverted the stack to the pre-gold module list.
2026-06-11 22:21:19 +07:00
tiennm99 98947f7bee feat(gold): add multi-provider gold price fallback chain
Replace goldprice.org (403s datacenter IPs) with a provider chain:
gold-api.com -> Swissquote public quotes -> NBP daily fixing (PLN/gram
converted via the shared FX table). Per-provider failures are logged;
a full-chain outage surfaces as a retryable error instead of the
silent no-price reply.
2026-06-11 22:11:30 +07:00
tiennm99 fdbf240b0e fix(deploy): enable gold module in ModulesCSV 2026-06-11 21:57:05 +07:00
tiennm99 645fa4f1ab fix(gold): drop redundant embedded selectors in portfolio tests 2026-06-11 21:49:03 +07:00
tiennm99 63d257878f fix(storage): report missing CAS support as unsupported instead of conflict
- prefix.go: return errors.ErrUnsupported when inner store lacks CompareAndSwap, enabling fail-fast instead of infinite retries
- Adds comprehensive CAS semantics test coverage for all KV backends:
  * memory_kv_test.go: new tests for basic operations and CAS failures
  * prefix_test.go: tests for wrapped CAS errors and unsupported operations
  * dynamodb_kv_test.go, firestore_kv_test.go: CAS failure scenarios
- portfolio_test.go: test retry exhaustion, business-error short-circuit, concurrent updates, and fail-fast on unsupported CAS
2026-06-11 21:40:16 +07:00
tiennm99 8c33dc27e4 feat(gold): add compare-and-swap portfolio updates with multi-backend support
Implement atomic UpdatePortfolio with retry pattern for concurrent-safe updates.
Add CAS support to DynamoDB, Firestore, Memory, and prefix stores.
2026-06-11 21:12:58 +07:00
tiennm99 2501deb4f8 feat(gold): add /gold_price command showing spot price in USD and VND
Read-only price lookup. Displays XAU USD/oz, USD/VND rate, and VND/luong.
No arguments, no portfolio mutation, no keylock needed.
2026-06-11 18:13:39 +07:00
tiennm99 d1a01830ae feat(gold): add gold paper trading module
Opt-in module for gold paper trading with VND currency.
Commands: /gold_topup, /gold_buy, /gold_sell, /gold_stats.
Pricing: spot XAU USD converted to VND per luong via GoldPrice.org + ExchangeRate-API.
Features: FX caching, dust normalization (1e-9), HTTPS-only URL validation,
per-user key locking, namespace isolation from trading module.
2026-06-11 17:13:02 +07:00
tiennm99 5536c24219 fix: simplify trade income event fetcher 2026-06-05 14:26:57 +07:00
tiennm99 537bfc85e9 feat: add trade income events check 2026-06-05 14:21:38 +07:00
tiennm99 3a628eab97 feat: register trade income commands 2026-06-05 13:58:22 +07:00
tiennm99 55ed77aed1 feat(trading): show remaining VND after buy/sell success 2026-06-05 08:48:45 +07:00
tiennm99 ec77a24db7 chore: drop CF→AWS migration tooling and stale JS-port references
The CF→AWS data migration (closed 2026-05-16) is long done and the
tooling isn't wired into any production path. Remove the one-shot binary,
its support package, and the migration runbook.

In live code, replace 'JS-parity' / 'same shape as JS' / 'cross-runtime
KV migration' comments with the real, stable reason for each behavior
(wire-format invariant, null-vs-zero distinction, CloudWatch alarm field
name, etc.). 24 files touched across lolschedule, loldle, wordle, twentyq,
trading, misc, util, server, metrics, ai, keylock.

- delete cmd/migrate_cf_data/
- delete internal/migration/
- delete docs/cf-to-aws-migration-runbook.md
2026-05-25 09:39:17 +07:00
tiennm99 c901f3ad40 fix(lolschedule): route daily push to originating forum topic
Subscribers previously stored only chatID; the daily-push cron sent
without MessageThreadID so Telegram routed to General regardless of the
topic the user subscribed from.

- Storage shape: []Subscriber{ChatID, ThreadID} keyed by (ChatID, ThreadID)
  so the same chat can subscribe in multiple topics independently.
- Decoder accepts legacy []int64 rows (ThreadID=0); successful mutations
  rewrite the slot in the new shape, no manual migration needed.
- Cron forwards MessageThreadID on every send.
- Auto-prune classifies terminal errors: chat-wide kills (bot blocked,
  deactivated, kicked, deleted, group upgraded) wipe every entry for the
  ChatID; topic-only "have no rights to send" prunes just (ChatID, ThreadID).
2026-05-25 09:39:01 +07:00
tiennm99 1c50023261 feat(stats): add per-user analytics with /stats subcommands
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.
2026-05-22 18:01:12 +07:00
tiennm99 cf7b8366b3 fix(deploy): include stats in CI ModulesCSV override
SAM CLI's --parameter-overrides replaces samconfig.toml's overrides
rather than merging them, so every CI deploy reset the stack's
ModulesCSV to the prior value (which predated the stats module).
The live Lambda's MODULES env was missing stats, so /stats was
dispatched but never handled — the module factory was never called.
2026-05-22 17:27:26 +07:00
tiennm99 819d4a985e fix(stats): fan out per-key GetItem to avoid handler deadline timeout
Sequential List + N GetItem made /stats latency scale with the number
of tracked commands. On a cold Lambda container with ~25 commands the
cumulative DynamoDB round-trips can exceed the 10s webhook handler
deadline; the trailing chathelper.Reply -> b.SendMessage then fails on
a cancelled ctx and the dispatcher only logs the error, leaving the
user with no reply at all.

Fan the per-key GetJSONs out into goroutines joined by sync.WaitGroup.
Wall-clock latency collapses to ~1 round-trip while preserving the
per-key error isolation (a single GetJSON failure still drops only its
own row). Storage interface unchanged; no race regression vs. the prior
per-item write pattern.
2026-05-22 16:36:57 +07:00
tiennm99 ebdef77883 fix(deploy): hardcode non-secret CFN params instead of sed-parsing samconfig
The sed-based extraction from samconfig.toml worked locally (BusyBox sed)
but failed in CI with GNU sed:
  sed: -e expression #1, char 40: invalid reference \1 on 's' command's RHS

After YAML literal and bash single-quote unescaping, the sed pattern's
parens lacked the BRE \(...\) escaping, so there was no capture group
and \1 was invalid. Rather than fight escape layers for a non-secret
value, just inline the IDs in the workflow.
2026-05-22 15:43:07 +07:00
tiennm99 704e74ca69 fix(deploy): pass BotOwnerID and AdminUserIDs from samconfig.toml in CI
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.
2026-05-22 15:39:42 +07:00
tiennm99 7f71e3cf6f fix(stats): unblock deploy and register /stats in Telegram command menu
- 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.
2026-05-22 15:30:28 +07:00
tiennm99 4f7a6d17f0 fix(dispatcher): place nolint:gosec on go statement, not inside goroutine 2026-05-22 15:14:07 +07:00
tiennm99 214017c772 fix(stats): resolve golangci-lint G118 and QF1001 violations 2026-05-22 15:10:26 +07:00
tiennm99 2abc138bb1 feat(stats): add command usage statistics module with persistence
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.
2026-05-22 15:06:46 +07:00
tiennm99 ddb9d61bde chore(plans): add deploy-notify-owner plan (completed)
Plan + 2 phase files for the deploynotify feature. Both phases
marked completed.
2026-05-22 11:40:34 +07:00
tiennm99 22c817b0db feat(deploynotify): DM owner once per new deployed git SHA
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.
2026-05-22 11:40:25 +07:00
tiennm99 3182179e65 fix(lolschedule): paginate older pages to cover past days in week range
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.
2026-05-22 11:03:28 +07:00
tiennm99 c9e44485be fix(lolschedule): align /lolschedule_week to ICT calendar week (Mon–Sun)
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.
2026-05-22 10:36:53 +07:00
tiennm99 1e96843d39 chore(plans): remove completed/superseded plans and orphaned reports
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.
2026-05-18 17:56:59 +07:00
tiennm99 0a642b0946 fix(deploy): quote FlexibleTimeWindow.Mode as string (EarlyValidation)
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).
2026-05-18 17:26:19 +07:00
tiennm99 78eb493754 docs(security): bootstrap README reflects scoped deploy policy (F1, F2)
Step 4 now uses put-role-policy with the committed JSON instead of
attaching 10x FullAccess managed policies.

New subsections:
- Updating the deploy policy: edit JSON, apply out-of-band, commit,
  with jq -S structural-diff drift check (AWS normalizes JSON
  server-side, so byte-diff false-positives).
- Trust policy invariants: procedure to add a sub claim safely, and
  documented rationale for why pull_request is excluded.

Section 7 bullet 3 (replace broad policies) marked done.

Plan: plans/260518-1019-iam-least-privilege/phase-05-update-bootstrap-docs.md
2026-05-18 16:57:25 +07:00
tiennm99 1d3aca439d feat(security): scoped IAM inline policy + rollback script for github-deploy-miti99bot (F1)
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
2026-05-18 16:57:15 +07:00
tiennm99 719e63fe21 fix(security): narrow OIDC trust to main only (F2)
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
2026-05-18 16:57:04 +07:00
tiennm99 3b74aa3abb docs(security): audit reports + IAM least-privilege plan + project policy
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.
2026-05-18 16:50:38 +07:00
tiennm99 b132557e06 refactor(deploy): swap cron to Scheduler + synthetic HTTP event (free tier only)
Supersedes 9050d6b (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.
2026-05-18 15:36:59 +07:00
tiennm99 acf65929ba feat(deploy): wire lolschedule cron via EventBridge Rule + ApiDestination
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.
2026-05-18 15:14:06 +07:00