SAGECONNECT is a financial integration system that handles sensitive ERP data, multi-tenant Focaltec API credentials, and license validation against an external HMAC-signed service. This document covers the supported versions, the environment-variable model, the security-relevant runtime constraints, and how to report a vulnerability.
| Version | Supported |
|---|---|
| 2.3.x | ✅ |
| < 2.3 | ❌ |
The 2.3 milestone (Scheduler Lock Recovery) shipped on 2026-04-29 with full defense-in-depth timeout coverage and 17/17 STRIDE threats closed. Older versions did not have the auto-release timer, the singleton PortalClient, the child-process kill cascade, or the unified [TIMEOUT] log routing — security patches are not backported.
SAGECONNECT processes:
- Sage 300 ERP database credentials and direct SQL access.
- Multi-tenant API keys and secrets for
portaldeproveedores.mx. - CFDI (electronic invoice) documents and payment records.
- Supplier and vendor data, including RFCs and provider IDs.
- A license-server identifier and HMAC shared secret.
All configuration lives in a single .env file at the repo root (since v1.1). The legacy split (.env.credentials.database, .env.credentials.focaltec, .env.credentials.mailing, .env.path) was retired in v1.1 — only the unified .env is supported now. See .env.example for the canonical template with inline section comments.
Critical Practices
- Never commit
.envto version control (it is in.gitignore). - To preserve a server-local
.envacrossgit pull/git reset --hard, rungit update-index --skip-worktree .env. - Restrict file-system permissions on the production
.env(Windows ACLs: read/write only for the service account runningSageConnect). - Rotate API keys, database passwords, and the license
HMAC_SECRETon a regular cadence and after any suspected exposure.
src/config.js enforces a fail-fast contract at process startup:
- Required variables (
DB_*, portal credentials, paths, license keys, address defaults) are validated. Missing or empty values causeprocess.exit(1)with a[CONFIG ERROR]listing. - Numeric range guards reject misconfigured timeouts (
LOCK_TIMEOUT_MS,PORTAL_HTTP_TIMEOUT_MS,CHILD_PROCESS_TIMEOUT_MS,STEP_TIMEOUT_MS) — a value below the documented minimum exits 1 rather than allowing the defense-in-depth invariant to be silently broken. SAGECONNECT_API_KEYis optional but warns when absent ([CONFIG WARN]). Leaving it unset disables therequireApiKeymiddleware on/api/paymentsand/api/pos; only acceptable for local development.
- Singleton mssql pool with
trustServerCertificate: true(the prod Sage 300 servers ship with self-signed certs — TLS is on, validation is off). runQuery(query, database)always prependsUSE [database]to defeat pool-context leakage observed in production (PR #16). Thedatabaseparameter defaults toconfig.database.database; callers that target a different DB must pass it explicitly (PR #19 closed 7 latent regressions).- SQL queries use template-literal interpolation in many places (
.planning/codebase/CONCERNS.md§ Tech Debt). This is a documented pre-existing risk; new code should use parameterized requests where feasible. - The diagnostic scripts in
src/scripts/are read-only by convention; the few that write (payment-uuid-repair,mark-payment-invoices-paid) are run manually by the operator after explicit confirmation.
- All Focaltec calls go through the singleton
PortalClient(src/utils/PortalClient.js) with a hard 30 s timeout (PORTAL_HTTP_TIMEOUT_MS). - Per-tenant credentials are injected as headers (
PDPTenantKey,PDPTenantSecret) — never logged, never sent in URLs. - Express adds
helmet(security headers, CSP disabled for inline dashboard scripts), CORS (allow-list of methods + thex-api-keyheader only), andexpress-rate-limit(global 2000 req/15 min, write-endpoint 10 req/min). - Dashboard authentication:
SAGECONNECT_API_KEYis dual-purpose — it gates the dashboard XHR viarequireApiKeymiddleware and identifies this installation to the license server. The browser receives it through a server-side<meta name="x-app-key">injection (src/server.jsserveHtmlWithKey()); operators never paste it, and it never appears in URLs or logs.
The service requires a valid license at boot:
LicenseValidator.validate({startup: true})runs insrc/index.jsbefore the server or scheduler start. Failure exits the process with[LICENSE] Startup blocked.- Validation flow: POST to
LICENSE_API_URLwith theSAGECONNECT_API_KEYas client identifier → verify HMAC-SHA256 signature withHMAC_SECRET→ enforce 5-minute timestamp freshness (anti-replay) → cache the result for 24 h in a three-state model (VALID / INVALID / ERROR). - Defense-in-depth:
dns.resolve4()short-circuits when the configured license host resolves to a loopback or private range, defeating naive hosts-file redirection. - Operational events (validation failure, mid-cycle revocation) dispatch an email to
LICENSE_ADMIN_EMAILso the administrator notices even if the dashboard is closed.
- Tenant index
iis threaded through every controller / service / utility call; downstream code readsconfig.portal.tenants[i].{id,key,secret,database,externalId}. - Cross-tenant data access is prevented structurally: there is no shared state between tenants in
forResponse()iterations beyond config and singletons. SQL queries always carry an explicit DB target. - Each tenant's API key/secret is isolated — losing one tenant's credentials does not expose others.
Production runs as the SageConnect Windows service via Servy. Full deployment and rollback procedure: docs/DEPLOYMENT.md. Operator runbook: docs/OPERATIONS.md.
Hardening notes:
- Run the service under a dedicated Windows account with the minimum permissions needed (read/write on the install dir, read/write on the log directories, network access to the Sage SQL server + the Focaltec portal + the license server).
- The obfuscated production code ships in a separate repo (
FReptar0/sageconnect-dist) via theobfuscate-deploy.ymlGitHub Action; the source repo and the obfuscated repo do not share git history. Deploys on the server usegit fetch && git reset --hard origin/masteragainst the dist repo. - Servy auto-restart policy: 5 attempts max with 30 s graceful stop, then the service stops and waits for human intervention (prevents pathological restart loops from masking a config error).
- Servy stdout/stderr live in the install directory; the application's winston logs live in
logs/sageconnect/YYYY-MM-DD/. Both are rotated byscripts/Rotate-SageConnectLogs.ps1(PR #20) to keep file handles bounded under always-on.
The service does not exit between cron ticks. This is the dominant security-relevant runtime property: anything that retains state across cycles is a potential vector if not carefully bounded.
- File descriptors: the winston logger caches transports per
(date, fileName)and recycles them at midnight — seesrc/utils/LogGenerator.js. - Database connections: singleton pool with explicit
USE [DB]per query (src/utils/SQLServerConnection.js). - HTTP clients: singleton
axios.create({timeout: 30000})(src/utils/PortalClient.js). - Operation locks: auto-release on
setTimeout(LOCK_TIMEOUT_MS)with alock:timeoutEventEmitter event and an audit-log trail. - Child processes: SIGTERM → 30 s grace →
taskkill /F /Tto defeat hung GUI dialogs (Servy has no desktop session).
A worked example of what happens when these guards are missing — five latent always-on bugs surfaced in 1 h 56 m post-deploy, including an EMFILE crash loop — is in .planning/forensics/report-20260427-220000.md. New contributors should read it before touching long-running primitives.
- All application events go through
logGenerator(LOG_FILE, level, message)fromsrc/utils/LogGenerator.js, written tologs/sageconnect/YYYY-MM-DD/<ProcessName>.log. [TIMEOUT]log entries are routed cross-cutting (ChildProcess.log + CronScheduler.log + ForResponse.log + caller-specific) with mandatory keysstep,tenant,url,durationMs,err.- Email dispatch (
LICENSE_ADMIN_EMAIL) fires only for child-process timeouts and license revocation — by design (Phase 19 D-15: avoids inbox flood from transient axios timeouts). - Failed authentication attempts on the dashboard middleware return
401with no info leakage; rate limiting (10 req/min for writes) further bounds abuse surface. - No external error-tracking service is wired in (no Sentry / Datadog). The audit trail is the on-disk log directory.
If you discover a security issue:
- Do not open a public GitHub issue.
- Email hi@fernandomemije.dev with details (steps to reproduce, affected version, impact assessment).
- Allow reasonable time for investigation before any disclosure.
- Initial response: within 48 hours.
- Investigation: within 7 days.
- Fix development: within 30 days, depending on severity.
- Public disclosure: after the fix is deployed and users have been notified.
Because the system processes Mexican fiscal data (CFDIs, RFCs, payment supplements):
- Retention: log files are kept on disk indefinitely by default; operators should size disk and rotate per their internal policy.
- Auditability: the dashboard exposes
/api/schedulehistory and/api/operationsstatus so every cron run is traceable post-hoc. - Encryption at rest is the responsibility of the operator (Windows BitLocker / disk-level encryption on the prod box) — the application does not encrypt its own files.
- Maintainer: Fernando Rodríguez Memije.
- Email (security and business contact): hi@fernandomemije.dev.
For more on the licensing model, see LICENSE.md, EULA-en.md (controlling English version), and EULA-es.md (Spanish courtesy translation). This policy evolves as the threat landscape changes — refer to the latest version in the repository.