* feat(invoicing): opt-in invoice email from the company's own sending domain
Companies holding the custom_sender_domain capability grant can register
their own domain (Resend sending-only profile), publish DKIM/SPF, and once
verified every invoice email (send, reminders, recurring, payment
confirmation, MCP/v1 sends) leaves as "<name> <faktura@their-domain>"
instead of the platform sender. Reply-To is unchanged.
- New table company_sending_domains (RLS: members read, owner/admin write;
audit trigger), types, archive-export classification.
- New capability key custom_sender_domain: manually granted per company,
deliberately outside PAID_CAPABILITIES (never trial-seeded, never written
by the Stripe sync). Without the grant the settings section is hidden and
nothing changes.
- Email extension: sending-domain routes (GET/POST/PATCH/DELETE, verify),
Resend domain lifecycle without orphan adoption, domain.updated handling
on the delivery webhook, explicit From support in the Resend adapter.
- Core resolveInvoiceSender(): verified + enabled + entitled, else the
platform sender; never throws.
- Settings -> Invoicing: "Avsändare vid fakturautskick" section (sv/en).
- Unit tests for the resolver, domain helpers, routes, From header; pg-real
test for RLS and constraints.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(invoicing): harden sending-domain writes, sender fallback, review findings
Skeptic refutations:
- Tenant JWTs could insert/update company_sending_domains with status =
'verified' and an arbitrary domain through PostgREST (RLS only checked
membership), then send invoice mail as that domain. New migration
20260822130000 adds a BEFORE trigger: tenants may only open a pending
claim and edit sender_local_part/sender_name/enabled; domain and
verification state are service-role only. claim/verify helpers now take
a service-role writer for those columns; the route's RLS client still
does the insert.
- A company domain Resend later rejects made every invoice send fail: the
Resend adapter retries once as the platform sender when an explicit
company From is rejected (nothing was sent, so no double send).
Review findings:
- domain.updated webhook: discriminated outcome; DB errors answer 500 so
Svix retries, unknown domains are acknowledged.
- Display names are RFC 5322-quoted only when they carry specials.
- Sender local part is a strict dot-atom (no trailing/consecutive dots),
in code and in the CHECK constraint; resend_domain_id index is UNIQUE.
- IME composition guard on the claim input; event bus reset in tests;
settings section skips its request for non-admins.
Deferred (needs a product call): persisting the effective From address in
the invoice delivery log touches the hardened evidence triggers; recorded
in DECISIONS.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(invoicing): bind sending-domain verification to the claimed domain; fix pg test
Skeptic re-check found a TOCTOU: during the claim's Resend round-trip a
tenant could delete and re-insert its pending row under the same id with a
reserved domain, and the service-role writer updated by id alone. Now:
- the claim's verification-state write filters on (id, company_id, domain,
resend_domain_id IS NULL) and rolls back on zero rows;
- verify and the domain.updated webhook compare Resend's domain name with
the row before writing verified;
- resolveInvoiceSender refuses reserved platform domains and non-hostnames
at send time (reserved-domain logic moved to lib/email/domain-name.ts and
shared with the claim validator).
pg-real: the case-insensitive uniqueness assertion now expects the
domain_shape CHECK (lowercase enforced) for an uppercase variant and the
unique index for a same-case duplicate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Em dashes (—) and en dashes (–) had spread across comments, docs, tests,
and a few UI strings, reading as AI-generated boilerplate rather than
house style. Replaced each with punctuation matching its context: colon
for explanatory clauses, comma for asides, plain hyphen for numeric/legal
ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for
paired-dash asides. messages/en.json and messages/sv.json were fixed by
hand together to keep sv/en in sync.
Left untouched where the dash is the functional subject rather than
decorative punctuation: date-range-parser.ts's separator regex,
charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE
encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the
agent system-prompt files that already instruct against em dashes, and
a golden iXBRL test fixture compared byte-for-byte.
Also fixes two bugs surfaced along the way: an off-by-one in
ApiKeysPanel's scope-label split (a leftover from an earlier partial
pass), and a charset-repair test that had lost the literal en-dash it
exists to verify.
Regenerated the agent atom seed migration (skills:generate) since 27
SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes,
with an explicit carve-out for the functional-dash cases above.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Add LICENSE (AGPL-3.0-or-later), CONTRIBUTING.md, SECURITY.md, DCO, and NOTICE files.
Rewrite README for open-source audience with self-hosting instructions.
Redesign color palette to grayscale chrome theme across all components.
Add transaction uncategorize API route with tests.
Fix VAT account name mismatches in migration 052.
Improve import page with SIE file support and loading skeleton.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove all sector-specific extensions (construction, ecommerce, export,
hotel, restaurant, tech) — only general-purpose extensions remain
- Move NE-bilaga and SRU export from extensions to core reports (lib/reports/)
- Move moms-box-mapping from extensions/export/shared to lib/vat/
- Replace per-extension API routes with catch-all dispatcher
(app/api/extensions/ext/[...path]/route.ts)
- Add manifest.json for each extension with metadata, env vars, and deps
- Add api-routes.ts pattern for extension-defined API endpoints
- Add code generation scripts (generate-extension-registry, create-extension)
- Add extensions.config.json for opt-in extension loading
- Add extensions.schema.json for config validation
- Add email service interface with noop default (lib/email/service.ts)
- Add CI workflow (core-build.yml) to verify core builds with zero extensions
- Add migration 045: expand account_type CHECK for untaxed_reserves
- Update CLAUDE.md with comprehensive extension system documentation
- Update all report engines and bookkeeping services for new imports
- Clean up extensions.schema.json to only list existing extensions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>