Commit Graph
113 Commits
Author SHA1 Message Date
tiennm99 af993d640d docs(plans): add VNAppMob key parsing bug-fix plan 2026-06-15 12:20:44 +07:00
tiennm99 67387ff350 fix(gold): parse VNAppMob JWT object wrapper 2026-06-15 12:20:40 +07:00
tiennm99 c889d77472 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 abde3a1c87 feat(gold): remove XAU/USD fallback and use VNAppMob SJC only 2026-06-15 11:04:06 +07:00
tiennm99 ae2b97ed26 chore(plans): add VNAppMob integration plan and completion report 2026-06-15 10:49:19 +07:00
tiennm99 57356ac312 docs(gold): document VNAppMob SJC pricing and env vars 2026-06-15 10:49:19 +07:00
tiennm99 b37a8dcd6f feat(gold): integrate VNAppMob SJC gold price with auto-refresh API key 2026-06-15 10:49:19 +07:00
tiennm99 2dc78733a1 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 5f0f6f3fcd fix(migrate): check fmt.Fprintf return to satisfy errcheck lint 2026-06-12 15:12:07 +07:00
tiennm99 e6ad2891dd 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 8b4690df6a 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 73be8415e7 fix(coin): accept symbol-first trades 2026-06-12 11:07:27 +07:00
tiennm99 31f6bc0a32 feat(coin): register telegram commands 2026-06-12 10:53:19 +07:00
tiennm99 3cb176d5b2 feat(coin): add crypto paper trading 2026-06-12 10:47:02 +07:00
tiennm99 89ea8f428d feat(gold): add gold commands to Telegram menu 2026-06-11 22:28:24 +07:00
tiennm99 2be3303563 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 1293377584 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 e099e8a766 fix(deploy): enable gold module in ModulesCSV 2026-06-11 21:57:05 +07:00
tiennm99 1c74cfc9c6 fix(gold): drop redundant embedded selectors in portfolio tests 2026-06-11 21:49:03 +07:00
tiennm99 3cad105a81 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 0a795b1436 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 a826525145 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 2304657cb6 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 7c74917d80 fix: simplify trade income event fetcher 2026-06-05 14:26:57 +07:00
tiennm99 340172206a feat: add trade income events check 2026-06-05 14:21:38 +07:00
tiennm99 8b6f8b9ce7 feat: register trade income commands 2026-06-05 13:58:22 +07:00
tiennm99 442876c812 feat(trading): show remaining VND after buy/sell success 2026-06-05 08:48:45 +07:00
tiennm99 daeaf0c605 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 2313be5024 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 a5c5bd9da9 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 ab53d9639f 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 ed0c5c1d7b 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 5e40ffe637 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 85579e56ef 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 db8ee9c49b 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 a422913972 fix(dispatcher): place nolint:gosec on go statement, not inside goroutine 2026-05-22 15:14:07 +07:00
tiennm99 a1ed521c0e fix(stats): resolve golangci-lint G118 and QF1001 violations 2026-05-22 15:10:26 +07:00
tiennm99 3f1f264e4a 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 0475a69b89 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 a34320bc77 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 33d88b97cc 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 3b05dc9fa8 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 bc4e9ad4a9 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 5721a1f1b0 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 70b7dfc6a0 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 dc8d77b787 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 148388e5a2 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 efe79be9ac 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 2d1864f1e0 refactor(deploy): swap cron to Scheduler + synthetic HTTP event (free tier only)
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.
2026-05-18 15:36:59 +07:00
tiennm99 c726c0aca5 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