Security & maintenance¶
Secrets¶
| Secret | Purpose |
|---|---|
TOKEN_ENC_KEY |
Encrypts the stored M365 refresh token and the stored Cove API token at rest |
CF_ACCESS_TEAM_DOMAIN / CF_ACCESS_AUD |
Verifying the Cloudflare Access token |
COVE_API_USER / COVE_API_PASSWORD |
Legacy. Not used on a current deployment — see below |
There is no Microsoft Graph client secret (delegated OAuth + PKCE). Secrets are set with wrangler secret put and are never committed. Tokens and the Cove visa are never logged.
Where the live Cove credentials actually are¶
The Cove credentials live in the database, not in Cloudflare. Admins rotate them from Admin → Cove API with no terminal and no deploy (see Cove API setup). The token is encrypted with AES-GCM under TOKEN_ENC_KEY before it is written, is never returned by any API route, and is never shown in the UI after saving.
COVE_API_USER / COVE_API_PASSWORD are a legacy fallback, kept only so deployments that predate this change don't break. If the database record is missing or can't be decrypted, the app reads them instead of failing. On a current deployment they are not set and not needed — don't add them, and don't rely on them being current if they are.
Access control¶
- Every request is checked against the Cloudflare Access token server-side.
/adminis restricted twice: an Access policy on the path, and anadminEmailscheck in the Worker.- Reports can only be sent from the pinned shared mailbox, enforced in code.
Email deliverability (verify before go-live)¶
Confirm the sending domain is set up so reports aren't marked as spam or spoofable:
- SPF — your domain's SPF record must include Microsoft 365 (
include:spf.protection.outlook.com). - DKIM — in the Microsoft 365 Defender portal → Email & collaboration → Policies & rules → Threat policies → Email authentication settings → DKIM, enable DKIM signing for the domain (publish the two CNAMEs it gives you).
- DMARC — publish a DMARC TXT record at
_dmarc.<domain>, e.g.v=DMARC1; p=quarantine; rua=mailto:dmarc@yourdomain. - Verify — send a report with Send test to me, open the email's "Show original"/headers, and confirm
SPF=pass,DKIM=pass,DMARC=pass.
Set a sensible Reply-To on the shared mailbox so replies reach a monitored inbox.
Monitoring & failures¶
- Failed scheduled runs alert admins by email and show a banner on the dashboard.
- Cove and Microsoft 365 are both health-checked once a day. A failure emails admins and raises a dashboard banner, so a broken connection surfaces within a day instead of on the 1st of the month.
- History records every run with its error message; Admin → Audit log records who did what (connect, run, add/remove admin, schedule/recipient changes, Cove credential rotations — including failed attempts, never the token itself).
- Server logs are structured JSON (
{ level, message, timestamp, ... }) — view them withnpx wrangler tailor Cloudflare Workers Logs. Secrets and tokens are never logged. - Watch for a Reconnect Microsoft 365 prompt — it means the M365 token needs re-consent.
Backups¶
-
Database (D1): export periodically. From the project folder:
npx wrangler d1 export cove_audit --remote --output backup-$(date +%Y%m%d).sqlStore the file somewhere safe. To restore into a fresh database:
npx wrangler d1 execute cove_audit --remote --file backup-YYYYMMDD.sql. - Report archive (R2): each run's PDF + xlsx live in thecove-audit-archivebucket and are downloadable from History.
Data retention¶
The app prunes its own data automatically. In Admin → Data retention, set Keep for (months) (default 24; the config default is report.retentionMonths). Each day the cron:
- deletes
report_runsand their archived R2 files older than the retention period, - prunes
admin_actions(audit log) older than the retention period.
Admins can also click Run cleanup now for an immediate prune. Retention is clamped to 1–120 months. For a defence-in-depth backstop you can additionally add an R2 lifecycle rule on the bucket, but it's optional now that the app cleans up.
Rotating TOKEN_ENC_KEY¶
This key encrypts two stored secrets: the M365 refresh token and the Cove API token. Rotating it makes both unreadable, so both need re-entering. There is no automatic re-encryption.
Plan for about ten minutes, and do it when a report isn't due.
- Generate a new key:
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))". - Set it:
npx wrangler secret put TOKEN_ENC_KEY(paste the new value). npm run deploy.- Microsoft 365 — go to Admin and click Reconnect Microsoft 365. The new token is stored under the new key.
- Cove — go to Admin → Cove API, click Update credentials, and paste a current Cove API token. Saving re-encrypts it under the new key. If you no longer have the token, issue a new one first (Rotating the API token).
- Click Test Cove API to confirm.
Have the Cove token in hand before you start
Between steps 3 and 5 the app cannot read its stored Cove token, so reports cannot pull device data — there is no longer a Cloudflare secret behind it to catch the gap. Cove only shows a token once at creation, so if you can't produce the current one you'll need to create a new API user in the Cove console mid-procedure. Sort that out first.
The signal that the re-encryption is still outstanding is a cove_credentials_decrypt_failed entry in the logs.
Do this on a schedule (e.g. yearly) or immediately if the key is ever exposed.
Running all the tests¶
Run these from the project folder (cd "C:\LocalDev\Nable Cove Reports"). Run them in this order; all should pass before you deploy:
npm run typecheck # TypeScript — no type errors
npm run lint # ESLint — no lint errors
npm test # unit tests (Vitest)
npm run test:int # D1 integration tests (real database)
npm run db:migrate:local # apply migrations locally (needed once before the E2E run)
npm run test:e2e # end-to-end browser flow (Playwright)
What each one is:
| Command | What it checks |
|---|---|
npm run typecheck |
Types compile across client, server, and shared code |
npm run lint |
Code style / correctness rules |
npm test |
Fast unit tests (parsing, report building, formatting, email/PDF rendering) |
npm run test:int |
Database models against a real local D1 |
npm run test:e2e |
A full click-through: sign in → add recipient → schedule → run |
Quick pre-deploy check
The fast subset is npm run typecheck && npm run lint && npm test. Run the integration and E2E tests too before any big change. CI (GitHub Actions) runs typecheck, lint, unit tests, and build automatically on every push/PR.
Routine maintenance¶
- Keep dependencies patched; run
npm auditand bump pinned versions deliberately.
Updating the app¶
- Make changes and run the tests above (at minimum
npm run typecheck,npm run lint,npm test). - Apply any new migrations:
npm run db:migrate:remote. - Deploy:
npm run deploy.