Docs · Conformance and findings

Security architecture

Who this is for: Operators, security reviewers, and integrators who need boundaries before they trust the hosted API with account data.

When you finish: You know what we store, what we never store, and where to report issues.

Threat modeling for the hosted product lives here. Engine correctness and PDOC conformance are separate topics (docs/conformance.md, CONFORMANCE.md).

Assets and scope

What we store, why and for how long is listed in one place, the privacy policy, section 4; this page only summarises it.

  • Customer email (PII): Stored in accounts.email in D1, in lower case, when you submit the signup form, before you verify it. An address never verified is deleted by the daily part of the cleanup once it is 30 days old.
  • Payment method (billing): Deferred (ADR-006). Checkout returns billing_unavailable. No Stripe secrets in production until paid billing turns on.
  • API keys (np_live_ / np_test_): Credential. SHA-256 digest only in D1 (ADR-004).
  • Webhook signing secrets: Credential. Plaintext in D1; shown once at register.
  • Payroll calculation inputs: Ephemeral. Not persisted (see below).
  • Rule JSON vendored in the engine: Public Apache-2.0; same bits in WASM and npm.

Out of scope for this document: CRA PDOC availability, customer payroll systems downstream of our JSON, and physical access to operator laptops. Quebec (QPIP (Québec Parental Insurance Plan) and QPP) support is unsupported by design (docs/jurisdictions.md).

Structural: we never store salary

/v1/deductions accepts gross_pay, bonuses, and year-to-date fields in the request body. The Worker runs takehome-core, returns JSON, and increments usage metering. The migrations define the account, its key hashes, its monthly usage count (and the day's live call count), webhook endpoints and delivery records, verification links (with the normalised address the resend limits count), verification send records (the time, the client network and a SHA-256 hash of the normalised address, kept at most 48 hours), the history of terms acceptances, per-account limit counters (budget_counters, one row per account and kind, written only while the account exists, plus rows of the system account _system for the global alert email caps and the daily cleanup marker; counters of past days are deleted by the daily cleanup; the older one-row-per-event rate_events is no longer written and the daily cleanup empties it), a marker table used while an account is deleted, and an unused Stripe events table. No column stores gross pay, net pay, or province for a calculation.

Browser calculators load WASM and compute locally after first fetch; gross pay does not leave the browser on that path (diagram 06).

Threat model (hosted API)

  • Stolen API key: Abuse of metered calculations. Mitigation: high-entropy keys; hash-at-rest; revoke with POST /v1/keys/revoke (the row is marked revoked and stops working at once; revoked keys stay as revoked records).
  • Forged Stripe webhook: Endpoint returns 404 while billing is deferred. When billing returns: STRIPE_WEBHOOK_SECRET signature verify.
  • SSRF via customer webhook URL: Worker probes internal networks. Mitigation: URL and DNS checks at registration and again before each delivery (the request then resolves the name on its own, so a DNS answer that changes in between is not caught by the check; the Worker has no private network of ours behind it); no redirects (diagram 07).
  • D1 outage during live call: Unmetered calculations. Mitigation: fail closed: any database failure, the key lookup included, answers HTTP 503 store and raises the store_unavailable alert; no employee payload (ADR-005).
  • Signup token leak: Key issuance for victim email. Mitigation: hashed token, 24h expiry; production must not set ECHO_VERIFY_URL (SECRETS.md).
  • Error response leak: Path or SQL exposure. Mitigation: OWASP-oriented tests; typed errors, no stack traces (services/api/tests/50-owasp-config.test.js).

Controls and boundaries

  • Authentication: Bearer API keys on /v1/deductions*, keys, webhooks, signup (where applicable).
  • CORS: Public read-only rule and conformance endpoints open; keyed routes closed (50-owasp-config.test.js).
  • Secrets: Provider credentials only in Cloudflare Worker secrets and operator machines (SECRETS.md).
  • Engine isolation: Rust core forbids IO, network, clock, and float arithmetic in library code (.cursor/rules/takehome.mdc).
  • Dependency and publish: OIDC publish to npm/PyPI; no long-lived publish tokens in git.

There is no separate OWASP mapping markdown file in this repository. Control intent is encoded in API tests (files 50-owasp-config.test.js, 54-insecure-design.test.js, and related suites under services/api/tests/).

Reporting

See root `SECURITY.md` for coordinated disclosure and response commitments.

Last reviewed: 2026-10-29
Engine: 0.1.0

Source: docs/security.md in the Takehome repository, rendered as written.