7175daee874d67058c41dd79c578dc70efbae207
3
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3912c74a7b |
feat(api): Phase 6 PR-2 — docs polish (Stripe-inspired) (#497)
* feat(api): Phase 6 PR-2 — docs polish (Stripe-inspired) Ships the developer-facing documentation surface for the v1 REST API. Mirrors Stripe's structure (landing → cookbooks → concepts → reference → errors → changelog) at /docs/api with a sticky-sidebar layout in the gnubok editorial-monochrome aesthetic. Every page is also served as plain Markdown via a sibling .md URL so agents and LLM crawlers can ingest the same content without HTML parsing — the existing /llms.txt already promised /docs/api references that this PR makes real. Single source of truth for endpoint metadata is the existing Zod registry (lib/api/v1/registry.ts). The reference pages auto-generate from it: adding a new endpoint surfaces in the docs on the next build with no manual sync. The error reference pulls directly from lib/errors/structured-errors.ts STRUCTURED_ERRORS. CONTENT LAYER (lib/docs/): - content/landing.ts — introduction, auth, base URL, response envelope, the four core principles (dry-run, idempotency, strict-mode, inline audit), pointers to every other section. - content/versioning.ts — versioning + deprecation policy (Stripe dated format), idempotency, dry-run, strict-mode write semantics, inline audit blocks. - content/webhooks.ts — webhook concept guide. Full Node.js (express + crypto) and Python (Flask + hmac) signature-verification samples that match lib/webhooks/signing.ts exactly. Lifecycle, event-type catalogue, payload shape, request headers, common pitfalls, auto-disable behaviour, audit + retention. - content/errors.ts — generated from STRUCTURED_ERRORS. Groups by domain (generic, bookkeeping, periods, invoices, supplier-invoices, transactions, reports, imports, documents, salary, company, provider). Every code is anchorable so the docs_url field on every error envelope finally points somewhere real. - content/reference.ts — generated from listEndpoints(). Groups by resource (companies, customers, invoices, suppliers, supplier-invoices, transactions, journal-entries, fiscal-periods, accounts, documents, employees, salary-runs, reports, imports, compliance, webhooks, operations, voucher-gap-explanations, reconciliation). Each endpoint section: summary, description, useWhen, doNotUseFor, pitfalls, scope, idempotent/reversible/dry-run flags, request + response examples. - content/changelog.ts — initial entry for API version 2026-05-12 covering every endpoint shipped in Phases 1-6. Lists what's coming in Phase 6 PR-3 (hardening + remaining cookbooks). - content/cookbook/quickstart.ts — five-minute send-your-first-invoice guide. Demonstrates auth, dry-run, idempotency, audit-block patterns in one continuous narrative. - content/cookbook/webhooks.ts — end-to-end webhook setup, sig verification, retry handling, idempotency on receiver side, replay patterns, auto-disable behaviour. Companion to the concept page. - content/cookbook/index.ts — recipe registry. 4 placeholder recipes (ingest-bank-transactions, file-vat-declaration, run-payroll-and-agi, year-end-closing) link to their reference pages with a "coming after Phase 6 PR-3 hardening" note. Narrative cookbook quality benefits from a focused pass after the substrate stabilises. - nav.ts — single source of truth for the sidebar nav, used by the layout AND the landing-page resource grid. - markdown.tsx — shared <DocsMarkdown> component using react-markdown (already a dep) with Hedvig serif headlines, Geist mono code blocks, hairline section borders, paper-white surfaces — same editorial aesthetic as the dashboard. LAYOUT (components/docs/DocsLayout.tsx): Two-column sticky-sidebar layout. Top header carries the gnubok mark + section links (API reference, Cookbooks, Errors, Changelog, openapi.json). Sidebar groups: Getting started, Cookbooks, Concepts, API reference, Reference. Active page highlighted with the same warm-beige bg the dashboard sidebar uses. ROUTES (app/docs/api/, app/llms-full.txt/): - /docs/api → landing - /docs/api/errors → error reference - /docs/api/webhooks → webhook concept - /docs/api/versioning → versioning + idempotency + dry-run - /docs/api/changelog → release notes - /docs/api/reference → resource overview - /docs/api/reference/[slug] → per-resource pages (19 resources, all generated from the registry; generateStaticParams listed) - /docs/api/cookbook/[slug] → recipe pages (8 entries, 2 fully written + 6 aliases/placeholders) - /llms-full.txt → everything concatenated for one-shot LLM ingestion Every page has a sibling .md route (e.g. /docs/api/errors.md) serving the raw Markdown for agents — same content, no HTML wrapper, same 5-min cache. Honours the existing /llms.txt promise that "every .md URL under /docs/api is served as plain Markdown". CI GUARD (lib/api/v1/__tests__/spec-snapshot.test.ts): Vitest snapshot test that locks down (a) the endpoint count, (b) the sorted set of method+path keys, (c) the set of distinct scopes referenced. CI fails if any drift unexpectedly so a Zod-schema change can't ship a silent API break — when you intentionally add/remove an endpoint, run with -u to refresh the snapshot, review the diff, and commit alongside the route change. The snapshot diff itself is a self-describing changelog entry. Initial snapshot: 100 endpoints, 17 distinct scopes, full key set sorted. Fourth assertion in the test guarantees every endpoint declares the agent-facing metadata (summary, description, useWhen, doNotUseFor, pitfalls, example) the reference pages depend on — so a registerEndpoint call that omits any of these fields is caught at CI time rather than rendering an empty section in the docs. INFRA TOUCH (lib/api/v1/load-routes.ts): Added the 5 Phase 6 webhook route imports so the registry includes them on the docs builders' path. Required for the /docs/api/reference/ webhooks page to render. The webhook routes' registerEndpoint calls already exist; this just side-effect-imports them where the spec generator can see them. Coming in Phase 6 PR-3 (hardening — separate PR): - 90-day TTL cleanup cron for non-accounting webhook deliveries - claim_due_webhook_deliveries SQL function (FOR UPDATE SKIP LOCKED) - Per-route rate limits on :test, :retry, webhook :create - V16 audit-log on webhook lifecycle events - DNS-rebinding pinned-IP HTTPS agent - Integration tests for webhook routes + *.pg.test.ts for triggers - Populated previous_attributes for update-style webhook events - The remaining 4 cookbook recipes (ingest-bank-transactions, file-vat-declaration, run-payroll-and-agi, year-end-closing) once the engine surface is fully stable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(api): address PR-497 review round 1 — CI fix + 5 small docs items CI BLOCKER (the reason core-only failed): 1. **Type error on `[slug].md/route.ts` dynamic routes** — Next.js 16's route-type inference can't extract the dynamic segment from a directory whose name contains a literal suffix like `[slug].md/`. It types `params` as `Promise<{}>` and rejects our handler that declares `params: Promise<{ slug: string }>`. The framework still ROUTES requests correctly (URL `/docs/api/cookbook/quickstart.md` reaches the handler) — only the typed `params` is unusable. Fix: drop the typed `params` parameter on the two affected handlers (cookbook + reference) and parse the slug from `request.url.pathname` directly. Inline comment documents the workaround so the next person to touch these doesn't try to "fix" it back to the typed pattern. GREPTILE INLINE (2 items): 2. **Python sample was missing `import json` and `import os`** — the webhook signature-verify sample uses both but only imported `hmac`, `hashlib`, `time`, and `flask`. Added the two missing imports. 3. **`buildResourcePages()` perf — called twice per request** (P2). Each call iterates every registered endpoint, groups by resource, sorts, and serialises Markdown for all 19 resource pages. Memoised at module level — the registry is populated once at module load and immutable for the process lifetime, so a single derivation is safe to cache. Halves the cost on the HTML routes' `generateMetadata` + page render pair, and the .md route handlers (which Next.js doesn't statically pre-render) are now constant-time after the first GET. SWEDISH-COMPLIANCE PRECISION (3 items): 4. **`webhooks.ts`: behandlingshistorik vs räkenskapsinformation distinction.** The previous "Audit + retention" section conflated the two — webhook delivery rows are *behandlingshistorik* (system- event log) per BFNAR 2013:2 kap 8 §, NOT räkenskapsinformation themselves. The 7-year retention from BFL 7 kap 1 § attaches to the underlying verifikation/faktura/AGI XML in its own table, not to the delivery envelope. Updated the section to draw the distinction and clarify gnubok's 7-year retention on accounting-event delivery rows is an operational audit-trail policy, not a statutory obligation passed through to the integrator. 5. **`changelog.ts`: same distinction in the Phase 6 PR-1 entry** — replaced the "räkenskapsinformation" framing with the correct behandlingshistorik framing + the operational-policy note. 6. **Quickstart cookbook: ML 17 kap 24 § p.8 note about `beskattningsunderlag per skattesats`.** The "What just happened" section now explicitly notes that the rendered PDF contains every ML 17 kap 24 § field (including taxable amount per VAT rate) and that the JSON response's summary fields are convenience aggregates for the integration — the binding faktura content is the PDF. Forecloses the misreading that `subtotal + vat_total` is sufficient compliance. 7. **Changelog: BFL 7 kap caveat on SIE export.** The `/reports/ sie-export` line now warns that a SIE4 export alone does NOT satisfy BFL 7 kap archiving obligations — SIE captures account positions and verifikationer but lacks system documentation and behandlingshistorik. Treat as a portability format (Fortnox/Visma/Bokio migration), not as a complete archive. Closes the misreading the swedish-sie-import-export skill flagged. DEFENSIBLE DEFERS (round 1 final): - **CM-8 SPDX-License-Identifier headers per file** (Compliance Swarm). The repo declares AGPL-3.0-or-later in the root LICENSE file, which satisfies licensing for the project as a whole. Per-file SPDX headers are a REUSE-conformance feature; we can address as a sweep across the entire codebase if/when REUSE conformance becomes a requirement. Out of scope for a docs PR. - **`/llms-full.txt` exposes payroll endpoint metadata publicly** (A.8.12). By design — the entire point of the file is one-shot LLM ingestion of the public docs corpus. Endpoint METADATA (path, scope, description) is non-sensitive; actual payroll DATA is gated behind `payroll:read` scope and requires a real API key. Adding an auth gate would defeat the agent-discovery purpose. - **Secret rotation endpoint** (Art.25(2)). Real product gap (delete + recreate is the current rotation path), but it's feature work, not docs. Tracked for Phase 6 PR-3 alongside the other webhook hardening. - **DNS rebinding pinned-IP HTTPS agent** (Art.5(1)(f)). Already documented in this changelog as a Phase 6 PR-3 item; bot is just re-flagging that it's not yet shipped. - **A.5.34 changelog cites GDPR Art.5(1)(c) for personnummer masking without linking privacy policy.** The citation is informational context for developers, not a privacy notice to data subjects. Data- subject notices live at /privacy. Adding a pointer is reasonable; adding it would consume real estate that's better spent on the technical detail. Defer. - **Compliance Swarm A.5.21 third-party attribution in llms-full.txt**. False positive — all markdown content in this PR is original first-party text. No third-party snippets to attribute. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(api): address PR-497 review round 2 — 6 small precision fixes All CI green after round 1 (core-only fixed). Compliance Swarm: 7 → 10 findings is the documented oscillation pattern — net-new actionable items are 6 small fixes; the rest are recurring defers (plaintext-secret variants, SPDX, planned PR-3 features the changelog already lists as "coming soon"). FIXED: 1. **Slug allow-list validation in `[slug].md` routes** (V1.2.5 ×2, medium). The cookbook + reference .md routes parse the slug from the URL pathname (round-1 workaround for Next.js 16's failed inference on `[slug].md/` directories) and pass it to a dictionary-based lookup. The lookup itself is safe — findRecipe / buildResourcePages can't reach SQL or filesystem from a bad slug — but the explicit allow-list gate keeps the contract safe if the lookup mechanism ever changes (file-load, RPC, etc.). Added `Set<string>(COOKBOOK_SLUGS)` + `Set<string>(RESOURCE_SLUGS)` guard before any lookup runs. 2. **Changelog: BFL 5 kap 5 § cited on both `/reverse` AND `/correct`** (swedish-compliance precision). The previous wording cited BFL 5:5 only on `/reverse` (storno) and described `/correct` as plain "rättelse" — but BFL 5:5 governs rättelse generally, and storno is the canonical method of rättelse, so both endpoints satisfy 5:5. Updated to: "/{id}/reverse (storno) and /{id}/correct (rättelse) — both satisfy BFL 5 kap 5 § (storno is the canonical method of rättelse)". 3. **Changelog AGI: explicit that XML is for manual submission** (swedish-compliance / swedish-payroll). Previously said "/generate-agi produces AGI XML" — could be misread as auto-submission to Skatteverket. Now states explicitly that the response carries `data.xml` for the integrator to upload via Skatteverket Mina Sidor (or via the optional `skatteverket` extension), and that the AGI deadline (12th / 17th of the following month) is the integrator's responsibility. Aligns with the route file's existing doNotUseFor + pitfalls metadata. 4. **Quickstart: F-skatt note qualified** (swedish-invoice-compliance). The "What just happened" section previously said the PDF "contains the F-skatt note" — only valid if the seller actually holds F-skatt. Updated to: "The 'Godkänd för F-skatt' note is included automatically when company_settings.has_f_skatt is set — confirm this on the company settings page before sending invoices in production." Also tightened the beskattningsunderlag wording to mention "one line per distinct rate on multi-rate invoices" — closes the swedish-compliance note about the multi-rate claim in the landing needing explicit support in the cookbook. 5. **Cookbook placeholder VAT description: "compute and review" not "submit"** (swedish-vat). The placeholder previously said "Compute momsdeklaration rutor and submit to Skatteverket" — but no Skatteverket-submission endpoint exists in the v1 surface; the API only computes the rutor 05–62 values for manual filing. Updated description in BOTH cookbook/index.ts AND nav.ts (where the same string was duplicated): "Compute momsdeklaration rutor 05–62 and reconcile against the GL before manual submission to Skatteverket." Title also flipped from "File a VAT declaration" to "Compute and review a VAT declaration". 6. **Cookbook nav for AGI: aligned to "generate" semantics**. nav.ts AGI summary used to say "file AGI" — same misreading risk as #3. Now: "Calculate, approve, mark paid, book, generate AGI XML for manual Skatteverket upload." DEFERS (round 2 final — every remaining swarm finding is in one of these buckets): - **🟠 Art.32 plaintext webhook secret + 4 sibling framings** (V14, V11.1, A.8.24, CC6.1). Established defer per Stripe / GitHub / Slack precedent; documented inline in lib/webhooks/signing.ts. Bot is re-flagging via the docs surface this round; underlying position unchanged. - **🟡 Art.5(1)(e) 90-day TTL non-accounting cron + V16 audit log + V2.4 rate limits**. All explicitly listed in the changelog as "Coming soon (Phase 6 PR-3 hardening)". Bot is reading the same text we wrote; not blocking. - **🟠 A.8.11 personnummer masking lacks an automated test**. Real product-hardening request, but it's feature work in the employees test surface, not docs. Tracked. - **🟡 CM-8 SPDX-License-Identifier headers per file**. Established defer from round 1 — root LICENSE covers AGPL-3.0-or-later for the project as a whole; per-file SPDX is a REUSE-conformance sweep that's its own effort. - **🟡 SR-3 SBOM dependencies**. False positive — next/server, react, next/link, next/navigation are existing project deps, not new in this PR. - **swedish-compliance "SIE disclaimer could note immutability requirement"** — the current disclaimer accurately calls out system documentation + behandlingshistorik as missing; adding immutability would over-stuff a one-line caveat. Defer with the understanding that the SIE skill itself documents the immutability requirement for any consumer that follows the reference. If round 3 plateaus (Compliance Swarm count stable, no net-new inline items), that's the merge-ready signal per Phase 4 lessons. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(api): address PR-497 review round 3 — 4 small precision fixes Compliance Swarm: 10 → 3 (down 70%) — net-new actionable items are the 4 below; remaining 3 swarm findings are either trivial defense- in-depth (URL decode, fixed here) or out-of-repo decisions (personnummer disclosure DPO confirmation). FIXED: 1. **URL-decode slug before allow-list check** in both .md route handlers (V1.2.5 ×2 low). The closed allow-list is pure ASCII so a percent-encoded value can't decode to a legitimate slug, but the explicit decode-then-check pattern keeps the contract correct under any future encoding-quirk runtime. try/catch around decodeURIComponent so a malformed % sequence (which throws) returns a clean 404 rather than a 500. 2. **Quickstart: explicit `delivery_date` requirement** (swedish- invoice-compliance / ML 17:24 field 7). The previous wording listed "supply date" as a covered field but didn't note that the API does NOT default delivery_date to invoice_date — integrators shipping invoices for goods delivered on a different date than the invoice date must pass delivery_date explicitly or the rendered PDF is non-compliant. Added explicit pass-it-yourself note. 3. **Quickstart: F-skatt strengthened from "verify" to "legal requirement"** (swedish-invoice-compliance / Peppol BIS 3.0 SE-R-005). The previous "confirm on settings page" wording risked integrators treating the F-skatt note as optional UX. It's a legal requirement on every faktura issued by a company that holds F-skatt registration — and a FATAL Peppol BIS 3.0 validation failure (SE-R-005) for B2G invoices when missing. Reframed as a compliance assertion: the PDF includes it automatically when the setting is true; verifying the setting is correct before production is the integrator's responsibility. 4. **Changelog: SIE post-import VAT code reconfiguration warning** (swedish-sie-import-export). The /imports/sie line previously noted the file format support but didn't warn that SIE files do NOT carry VAT codes or tax-rate-to-account mappings. After migrating from Fortnox / Visma / BL / SpeedLedger / Bokio, integrators must manually reconfigure VAT codes before the first momsdeklaration — skipping this is the most common source of incorrect VAT submissions in migrated bookkeeping. DEFERS (round 3 final — these are the architectural floor): - **🟠 A.5.34 personnummer field name + masking logic disclosed in public docs** (Compliance Swarm). Defensible: documenting that personnummer is masked is a transparency benefit (GDPR Art.13/14 intent), not a privacy disclosure risk. The DPO confirmation prompt is a reasonable governance ask but is an out-of-repo decision — the docs change is appropriate as written. - **AGI penalty amounts (625 / 1,250 SEK)**. Operational guidance for integrators building deadline-tracking; not strictly API-doc material. The deadline (12th / 17th) is documented; integrators who automate compliance can read SFL for penalties. - **VAT period thresholds in the cookbook placeholder**. Belongs in the actual cookbook content when written, not in the placeholder description. - **`invoice.credited` event-naming verification**. False positive — the emitter uses `credit_note.created` (which IS in the docs); there is no `invoice.credited` event in the codebase. Naming is consistent. - **Webhook retention sentence reordering** (swedish-compliance stylistic). Current wording leads with what delivery rows ARE (behandlingshistorik), then clarifies what they are NOT (räkenskapsinformation) — clean teaching arc, the qualifier is prominent. Reordering doesn't change clarity. Round 3 stop signal hit (per Phase 4 lessons): swarm count plateauing at the architectural floor with all remaining findings either defers, false positives, or out-of-repo decisions. Greptile has posted no inline comments since round 1's two items (both fixed). This should be merge-ready. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(api): address PR-497 review round 4 — 5 small precision fixes Compliance Swarm: 3 → 5 (slight uptick from oscillation, but 0 critical, 1 actionable; remaining 4 are recurring or philosophical). swedish-compliance: 7 advisories — 3 actionable precision items incorporated below; the others are forward-looking notes for cookbook content that ships in Phase 6 follow-ups. FIXED: 1. **Spec-snapshot test enforces ep.scope is explicitly defined** (CC6.3, real future-bug prevention). Previously the test asserted every endpoint declared the agent-facing metadata fields the docs depend on, but `scope` could be `undefined` — a registerEndpoint call that silently dropped the field would make the wrapper treat the route as unauthenticated. Added an assertion that `ep.scope !== undefined` (the literal sentinel `null` is allowed for genuinely public endpoints like /api/v1/health). The 4 spec tests still pass — confirming no current endpoint has undefined scope and the gate works prospectively. 2. **F-skatt: integrator responsibility for `has_f_skatt` accuracy** (swedish-invoice-compliance). The previous "verify on settings page" framing didn't connect the flag to the live Skatteverket registration. Now: "The integrator is responsible for keeping has_f_skatt in sync with the company's live Skatteverket registration status. Update via PATCH /api/v1/companies/{id}/ settings or the settings page — a flag that's false while the company is actually F-skatt-registered produces non-compliant invoices, not merely a missing optional note." 3. **AGI deadline qualified by turnover** (swedish-payroll). The previous wording listed "12th / 17th of the following month" with no condition. Now: "12th of the following month for large employers, 17th for companies with annual turnover ≤ 40 MSEK." Aligns with the swedish-payroll skill's AGI filing deadline section. 4. **SIE import warning includes behandlingshistorik gap** (swedish-sie-import-export + swedish-accounting-compliance). The previous warning covered the VAT-code reconfiguration requirement but didn't note that SIE files also do NOT transfer behandlingshistorik (the source system's processing log per BFNAR 2013:2 kap 8 §) or systemdokumentation. Added: "The behandlingshistorik gap must either be preserved separately (export from the source system + archive alongside the SIE file) or accepted with documented justification — gnubok starts a fresh behandlingshistorik from the import date forward." 5. **Webhook 7-year retention: voluntary policy vs statutory obligation** (swedish-accounting-compliance). The previous wording said gnubok keeps delivery rows "for 7 years as an operational audit-trail policy" — the 7-year figure could be misread as statutory. Tightened in BOTH webhooks.ts and changelog.ts: the 7-year statutory retention under BFL 7 kap 1 § applies ONLY to the underlying verifikation/faktura/AGI XML; gnubok's 7-year policy on delivery rows is voluntary and chose the duration to align conveniently with the statutory horizon on the underlying records. DEFERS (round 4 final, all in the architectural-floor bucket): - **🟠 A.5.34 personnummer field name + masking logic disclosed in public docs** (recurring from round 3). Defensible — documenting PII handling is a transparency benefit (GDPR Art.13/14 intent), not a privacy disclosure risk. The DPO confirmation prompt is a reasonable governance ask but is an out-of-repo decision. - **🟡 A.8.23 DNS-rebinding "coming soon" item**. The bot is reading the changelog's own deferral list. Already tracked for Phase 6 PR-3 hardening. - **🟠 CC6.6 SSRF protection details exposed in /llms-full.txt**. Stripe / GitHub / Slack publish their full webhook security posture publicly (signature format, rejected IP ranges, retry policy) — documenting protections IS the trust pattern. Obscurity is not security; the SSRF protection is enforced in code, not in the docs. - **🟡 CC6.7 public CDN caching**. withPublicSecurityHeaders() already applies the appropriate headers (CSP, X-Content-Type- Options, X-Frame-Options). The 5-min cache is appropriate for static developer documentation; the alternative (no caching) is cost without security benefit since the content is intended to be public. - swedish-compliance "VAT 2026-04-01 livsmedel rate change" — forward-looking; ships when the actual VAT cookbook is written. - swedish-compliance "year-end IB/UB continuity" — forward-looking; ships when the year-end cookbook is written. - 2 verify-only notes (rättelse implementation, future salary- journal/avgifter-basis masking sweep) — not actionable in this PR. Round 4 stop signal: every remaining swarm finding is in the deferred or recurring bucket; the actionable item (CC6.3) is shipped. swedish-compliance is now in advisory mode (no errors, just stylistic suggestions and future-cookbook notes). Per Phase 4 lessons, this is the merge-ready signal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(api): address PR-497 review round 5 — 4 small precision fixes (last actionable items) Compliance Swarm: 5 → 2 (down to architectural floor — 1 high + 1 medium). swedish-compliance: 7 advisories, 4 actionable precision items addressed below; the others are forward-looking notes for content that ships in Phase 6 follow-ups. Trajectory: 7 → 10 → 3 → 5 → 2. Plateaued. FIXED: 1. **Webhook secret storage guidance: secrets manager, not env file** (A.8.5 high). Added explicit instruction to the cookbook that the returned secret is signing material and must live in a secrets manager (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, Doppler, 1Password Connect, ...) — not a plaintext .env file or config commit. Treated with the same care as a database password. 2. **AGI deadline correction: 17th = January and August only** (swedish-payroll). Round 4's wording said "12th (large employers) / 17th (≤40 MSEK)" — but per the swedish-payroll skill the 17th applies in January and August specifically, not generally to all months for sub-40 MSEK companies. Other months are the 12th regardless of employer size. Fixed to: "the 12th of the following month for every reporting period EXCEPT January and August, where companies with annual turnover ≤ 40 MSEK get the 17th." This would've caused integrators automating sub-40 MSEK deadline tracking to misfile by 5 days from February through July and September through December. 3. **F-skatt SE-R-005 broader scope** (swedish-invoice-compliance / swedish-e-invoicing). The previous wording framed SE-R-005 as primarily a Peppol B2G validation rule. Reframed: the F-skatt note is a legal requirement on every faktura issued by a Swedish momsregistrerad seller that holds F-skatt registration — applies to PDF/paper AND Peppol/e-invoice formats. The buyer uses it to determine A-skatt withholding obligation (omitting it can shift tax liability onto the buyer); B2G is just where the validation is automated as a FATAL Peppol BIS 3.0 check. 4. **SIE behandlingshistorik gap: full räkenskapsår scope** (swedish-accounting-compliance). Round 4's wording said the integrator "must either preserve [behandlingshistorik] separately or accept the gap with documented justification" and that gnubok "starts a fresh behandlingshistorik from the import date forward." The "documented justification" framing implied the gap was acceptable as a default. Per BFNAR 2013:2 kap 8 §, the obligation attaches to the entire räkenskapsår, not from the import date. Reframed as: "must be preserved separately... best practice for a mid-year migration: export the source system's behandlingshistorik for the full fiscal year and archive it alongside the SIE file." DEFERS (round 5 final — these are the architectural-floor items that will recur indefinitely): - **🟡 A.8.20 DNS-rebinding gap** (Compliance Swarm). Already documented in the changelog as a Phase 6 PR-3 deferral item; the bot is reading the same text we wrote. - **swedish-compliance: VAT 2026-04-01 livsmedel rate change**. Forward-looking — for the actual VAT cookbook recipe content, which ships post-Phase-6. - **swedish-compliance: year-end IB/UB continuity**. Forward-looking — same. - **swedish-compliance: SIE warning placement note**. Forward-looking — for the imports reference page when authored. - **swedish-compliance: BFNAR 2013:2 citation correct, webhook retention correct**. No-op confirmations. - **swedish-compliance: delivery_date pre-payment scenario**. Real but extremely narrow edge case (faktura utfärdad före leverans). Defer with the understanding that anyone using the API for pre-payment invoicing will read the full invoice reference, not rely solely on the quickstart. This is the merge-ready signal per Phase 4 lessons-learned: every remaining swarm finding is in the deferred or recurring bucket; swedish-compliance is in pure-advisory mode (forward-looking notes for cookbook content that ships later); CI is fully green; Greptile posted nothing past round 1's two items (both fixed). Ship it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
32ad6da28c |
feat(api): v1 invoice + customer reads (Phase 2 PR-A) (#451)
* feat(api): v1 invoice + customer read endpoints (Phase 2 PR-A) First slice of the Phase 2 invoices vertical. Read-only endpoints landing in this PR; writes + webhooks land in PR-B and PR-C. After all three a developer can ship an end-to-end invoicing integration. New endpoints (all wrapped, scoped, cursor-paginated): - GET /api/v1/companies/:companyId/invoices — list, filters: status, customer_id, document_type, currency. Cursor on (invoice_date DESC, id DESC). Customer name embedded inline; ?expand=customer for full record, ?expand=items for line items. - GET /api/v1/companies/:companyId/invoices/:id — detail with embedded customer. ?expand=items,payments. - GET /api/v1/companies/:companyId/customers — list, filters: customer_type, search (name/org_number prefix), include_archived. Cursor on (created_at ASC, id ASC). - GET /api/v1/companies/:companyId/customers/:id — detail. ?expand=invoices embeds open invoices in a single round-trip. Shared infra: - lib/api/v1/expand.ts — parseExpand() validates ?expand=a,b,c against a per-endpoint allowlist; unknown keys yield VALIDATION_ERROR with the full invalid list and the allowlist (agent-friendly). - All four routes register with the Zod schema registry so they show up in /api/v1/openapi.json with x-action-risk and use-when / do-not-use-for metadata. - Compound keyset filter on both list endpoints (per Greptile review on PR #450) — no skipped or duplicated rows on page boundaries. Tests: - lib/api/v1/__tests__/expand.test.ts (8 tests) - app/api/v1/companies/[companyId]/invoices/__tests__/route.test.ts (10 tests) - app/api/v1/companies/[companyId]/customers/__tests__/route.test.ts (8 tests) Full repo suite green (3127/3127), build clean, lint clean on v1 paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(api): address PR #451 review (Greptile + compliance swarm) - Greptile P1 (customers search) + OWASP V1.2.5: customer search term now escapes both PostgREST .or() delimiters (,()) AND SQL LIKE wildcards (% _ \). '100%' searches for the literal string instead of any customer containing '100'. - OWASP V8.2.1 + V16.1: detail endpoints now UUID-validate the :id path param before touching the database, and no longer echo the raw id in the NOT_FOUND response details. Adds a structured warn log on 404 with the queried (id, companyId) for audit purposes. - V4.5 + Art.25(1) + A.8.3 / A.8.11 + CC6.3 + PI1.3 (~12 findings): every select('*') replaced with explicit column lists per the documented Zod schemas. Includes joined sub-queries — customer:customers(...), items:invoice_items(...), payments:invoice_payments(...). Future schema migrations adding sensitive columns must now update these projections before the field becomes visible on the public API. - A.8.5: hardcoded 'Bearer gnubok_sk_x' in test fixtures replaced with 'Bearer test-fixture-not-a-real-key' to avoid false-positive secret scanner alerts. Fixture UUIDs upgraded to valid v4 format (Zod 4's .uuid() enforces version+variant digits). - Art.5(1)(f): customer-invoices expansion soft-degrade now logs only the error code + message rather than the full Supabase error object. Pushing back on: - Art.5(1)(b) org_number in customer list — Bolagsverket-public data (same triage as PR #450; required by integration use case) - Art.25(2) customer_name always-joined — denormalising via trigger is a real schema migration for a marginal data-flow gain - A.8.15 _partial flag on soft-degrade — ?expand is documented as a hint 50/50 v1 tests; 3131/3131 full suite; build clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(api): second-pass review on PR #451 — partial_expansions + fake fixtures Address the residual compliance-swarm findings after the first fix round: - CC6.1 (medium): the customer-detail handler now sets meta.partial_expansions=['invoices'] when the ?expand=invoices subquery fails, signalling the degraded response to the caller without escalating to error-level logs (alert fatigue). The primary resource still returns with an empty invoices array. New ResponseOptions.partialExpansions threaded through buildMeta(). 1 new test for the failure path, plus a happy-path assertion that the flag is absent. - A.8.33 (low): SAMPLE_CUSTOMER fixture's org_number and vat_number replaced with 'TEST-0000-0001' / 'SETEST00000001' — cannot be confused with real Bolagsverket entries or pass external VIES validation. Pushing back on: - CC6.3 (medium) — separate scope for ?expand=items on invoices: every accounting API I know (Stripe, QuickBooks, Fortnox) treats line items as part of the invoice resource. Splitting would violate principle of least surprise for integrators; the plan deliberately treats invoices:read as covering the full invoice including items. 3132/3132 vitest pass; build clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(api): third-pass review on PR #451 — PII minimisation refinements Third compliance-swarm sweep (6 → 0 highs, 4 medium, 2 low). Addressed: - Art.5(1)(c) personnummer leakage: customer LIST response now masks org_number AND vat_number for customer_type IN ('individual', 'eu_individual') — for sole traders (enskild firma) org_number IS the personnummer. Business customers' Bolagsverket-public org_numbers stay visible. Detail endpoint (deliberate single-record fetch) unchanged. - Art.5(1)(c) over-broad invoice-list expansion: ?expand=customer on the invoice LIST endpoint now uses a new CUSTOMER_LIST_CONTEXT_COLUMNS projection (id, name, customer_type, email, country, archived_at) — full address/phone/notes/vat_number stay on the customer DETAIL endpoint. Drops PII transmitted in bulk-list contexts by ~60%. - A.8.15 permission-error differentiation: customer-detail soft-degrade for ?expand=invoices now bumps Postgres error class 42 (insufficient privilege, RLS denial) to error-level log so Sentry alerts on misconfigurations. Transient errors stay at warn. - PI1.1 ISO-4217 currency: invoice list ?currency now requires /^[A-Z]{3}$/ instead of accepting any 3-8 char string. Two new tests. Pushing back on: - Art.5(1)(f) UUID logging on 404 — UUIDs have 122 bits of entropy; you cannot enumerate the space, so the "log scraping = enumeration" framing doesn't hold. Operational audit value > theoretical risk. - Art.25(1) notes-by-default in customer DETAIL — kept inline. Detail is a deliberate single-record fetch; the dashboard shows notes inline; agents calling /customers/{id} reasonably expect them. Notes are already excluded from the LIST endpoint AND from the invoice-list ?expand=customer projection (above). 3135/3135 vitest pass; build clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
db592d922d |
feat(api): v1 REST API foundation — auth wrapper, scopes, registry, smoke endpoints (#450)
* feat(api): v1 REST API foundation — auth wrapper, scopes, registry, smoke endpoints Lay the substrate for the public REST API at /api/v1/*: Bearer-auth wrapper that reuses the existing api_keys + idempotency machinery, an extended scope catalogue (companies, events, webhooks, operations, documents, compliance), v1 response envelopes (data + meta with request_id, api_version, audit block, cursor pagination), an error envelope with recovery_hint / docs_url / valid_alternatives derived from the existing structured-error registry, and a Zod schema registry that generates the OpenAPI 3.1 spec with x-action-risk / x-idempotent / x-reversible / x-dry-run-supported extensions. Ships discovery routes (/llms.txt, /.well-known/skills/index.json) and three smoke endpoints (GET /api/v1/health, /api/v1/companies, /api/v1/openapi.json) so the wrapper is exercised end-to-end. Includes the api_keys.mode (test|live) migration and 41 unit tests covering auth, scope, company-membership, idempotency replay, dry-run, pagination, response shape, and scope resolution. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(api): harden v1 foundation — cursor validation, security headers, forensic logs Address compliance-swarm findings on PR #450: - OWASP V2.3: decodeDefaultCursor now validates the cursor's ts as ISO 8601 and id as UUID. A crafted cursor previously could inject untyped strings into a query's .gt(field, value); PostgREST would have rejected them, but validating here keeps the failure mode predictable (stale cursor → reset) rather than 400-ing. - OWASP V3.4: public discovery routes (llms.txt, .well-known/skills, openapi.json) now stamp X-Content-Type-Options: nosniff, Referrer-Policy, X-Frame-Options: DENY. New lib/api/v1/security-headers.ts helper. - OWASP V16: security event logs (missing token, validation failure, insufficient scope, company-membership deny) now include source IP (x-forwarded-for / x-real-ip) and User-Agent for forensic correlation. - OWASP V8.2.1 / ISO A.8.3: GET /api/v1/companies emits a warn log when the PostgREST archived_at filter unexpectedly returns a row with a null company join, surfacing silent data-integrity regressions instead of hiding them behind the existing pickCompany() === null filter. Pushing back on (not changed): - GDPR Art.32 cursor HMAC signing — cursors only paginate within a user's own user_id scope; cross-tenant probe surface doesn't exist yet. - GDPR Art.25 org_number in list — Bolagsverket public-record data, removing forces N+1 fetches to make the response useful. - SOC 2 CC6.3 service-role bypasses RLS — defense-in-depth IS the design; the wrapper's company_members membership check is the technical control. - ISO A.8.12 public OpenAPI spec — intentional, mirrors Stripe/Twilio. 5 new pagination tests cover the cursor validators. 46/46 v1 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(auth): detect Supabase duplicate-signup obfuscation on register Supabase obfuscates duplicate signups to prevent user enumeration: when an email already belongs to a confirmed account, signUp returns data.user with identities: [] and no error, and sends no email. Without detecting this case we showed the "check your email" screen to the user, who then waited for a mail that never arrived. Detect the empty-identities response and surface it via duplicateEmail state so the UI can branch on it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(api): second-pass hardening — CSP, IP truncation, cursor scope comment Address the second compliance-swarm sweep on PR #450: - OWASP V3.2: PUBLIC_SECURITY_HEADERS now includes Content-Security-Policy default-src 'none'; frame-ancestors 'none'. Free win for JSON/text-only public routes (no script, style, image, or form contexts). - GDPR Art.5(1)(f): truncate IPs before logging — IPv4 to /24, IPv6 to /48. Preserves diagnostic value (ASN, abuse-pattern correlation, city-level geolocation) while eliminating point-of-presence identification. Standard pattern used by Google Analytics anonymize_ip. Exported truncateIp() so other surfaces can adopt it. - OWASP V8.2.1: explicit comment in GET /api/v1/companies documenting that the cursor's joined_at is applied AFTER user_id filter, so a tampered cursor can only reorder rows the caller already owns. Cursors deliberately unsigned; trade-off documented. Pushing back on second-pass findings (not changed): - ISO A.8.12 / SOC 2 CC6.3 health/llms.txt/skills exposing service name + API version + MCP URL — these are intentional disclosures for a public 3rd-party developer API; hiding them is theatre. - GDPR Art.32 logging granted scopes on INSUFFICIENT_SCOPE — diagnostic value during incident response outweighs the theoretical privilege-profile leak; an attacker who already breached the log store has bigger problems. - OWASP V2.2 route-level Zod for cursor — decodeDefaultCursor already validates strictly; route-level Zod is stylistic. - GDPR Art.25(2) org_number/entity_type in list — Bolagsverket-public data; entity_type materially affects which API calls make sense. - ISO A.8.15 x-forwarded-for trusted-proxy CIDR — overkill behind Vercel's edge which rewrites the leftmost value. 50/50 v1 tests pass (4 new for truncateIp). Build green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(api): third-pass hardening — Host header injection, anon client, HSTS Address the third compliance-swarm sweep on PR #450: - SOC 2 CC6.1 (3× high): llms.txt, openapi.json, and .well-known/skills built URLs from the inbound Host header. A spoofed Host could poison agent discovery with attacker-controlled endpoints. New lib/api/v1/base-url.ts centralises canonical base-URL derivation via NEXT_PUBLIC_APP_URL (already a required env var per CLAUDE.md). - ISO A.8.2 / A.8.5 (2× high): the wrapper's public-scope code path now uses an anon-key Supabase client (RLS-respecting) instead of the service-role client. A future accidental DB call from a public handler is constrained to anon-accessible rows. Least-privilege at the infrastructure layer. - OWASP V3.2 (medium): PUBLIC_SECURITY_HEADERS now includes Strict-Transport-Security: max-age=31536000; includeSubDomains. - GDPR Art.5(1)(f) (medium): truncateIp now logs a warn when a non-empty x-forwarded-for / x-real-ip payload fails to parse, surfacing spoofed or unexpected proxy values to security monitoring instead of silently dropping them. The raw value is never logged. - CC2.3 (low): llms.txt now links the SECURITY.md disclosure policy with the security@arcim.io reporting address so agents have a clear responsible-disclosure path. Pushing back on third-pass findings (not changed): - Cursor HMAC signing — user_id filter is the authorisation boundary; cursor scope is bounded to within-user rows. Documented in code. - org_number in companies list — Bolagsverket public data; the swarm's "could be enskild firma personnummer" framing isn't accurate (enskild firma org_number IS the personnummer, but it's already in the public Bolagsverket business register). - Health endpoint information disclosure — intentional for a public developer API; matches Stripe/Twilio convention. - llms.txt / skills index MCP URL disclosure — that's the file's purpose. - Cache-Control public on discovery routes — content is by definition public; getCanonicalBaseUrl() removes the previous spoof concern. - Duplicate-email screen — user's own input; out of scope for this PR. 50/50 v1 tests pass; @supabase/supabase-js#createClient mocked so the public-path tests don't need real env vars. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): widen validateApiKey result assertions to include mode field The core-only CI job failed on two pre-existing api-keys.test.ts assertions that used strict toEqual matching against the old (userId, companyId, scopes) shape. The wrapper migration in this PR widened that shape with mode, apiKeyId, and apiKeyName. Update both existing assertions to match the current shape and add a third test that exercises the mode='test' path. 3027/3027 vitest tests now pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(api): fourth-pass hardening — env guards, IP range check, headers on wrapped routes Address the fourth compliance-swarm sweep on PR #450: - ISO A.5.17 / SOC 2 CC6.1 (high): createAnonClient now fails closed with an explicit Error if NEXT_PUBLIC_SUPABASE_URL or _ANON_KEY are missing, surfacing misconfiguration on the first request instead of throwing deeper in the handler with no context. - GDPR Art.5(1)(f): truncateIp now rejects IPv4 with out-of-range octets (>255). '999.999.999.999' now returns undefined instead of a pseudo-IP that would pollute abuse-pattern analysis. Edge octets (0, 255) still accepted. 2 new tests. - OWASP V3.2 / V3.3: the wrapper's stampHeaders step now applies the full security header set to every wrapped v1 response (CSP, HSTS, X-Frame, Referrer-Policy, X-Content-Type-Options) PLUS X-Robots-Tag: noai, noimageai so authenticated payloads are excluded from AI training sets. Public discovery routes (llms.txt, skills index, openapi.json) deliberately omit X-Robots-Tag — being AI-discoverable is the whole point of those surfaces. - New WRAPPED_RESPONSE_HEADERS export separates the two contexts. Pushing back on: - SOC 2 CC6.1 medium "API key prefix in public docs aids brute force" — inverted logic. Every public API publishes its key prefix specifically so secret scanners (GitHub Advanced Security, GitLeaks) can detect leaks. Stripe (sk_live_), GitHub (ghp_), OpenAI (sk-) all do this. - SOC 2 CC6.3 medium "formal risk register for unsigned cursors" — org -level documentation, outside this PR. Code-comment already documents the trade-off. - SOC 2 CC2.3 low "llms.txt hardcodes security@arcim.io" — same address as SECURITY.md; no drift risk. Flagged separately (not changed): the register-page duplicate-email detection in this branch defeats Supabase's user-enumeration obfuscation (GDPR Art.5(1)(c) × 2, ISO A.8.11). Substantive product decision: UX (no infinite-wait for non-existent accounts) vs security (no enumeration). GitHub and Stripe Atlas pick UX; some pick security. Owner's call. 3029/3029 vitest tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(api): address Greptile review on PR #450 - P1 (companies/route.ts): keyset pagination was missing its tiebreaker. The cursor encoded (joined_at, id) but the filter only applied .gt('joined_at', ts) — same-joined_at rows on a page boundary could be skipped or duplicated. Also the encoded id was companies.id while the sort was on company_members, mismatched. Fixed: select + sort + encode on company_members.id, apply compound joined_at.gt.{ts} OR (joined_at.eq.{ts} AND id.gt.{cursor_id}) via .or(). Side benefit — eliminates the broken-cursor-on-null-join case (#2) because company_members.id is always present, no null guard needed. - P2 (registry.ts): ZodUnion branch had a dead ternary (['x','y','z','w'].length > 0 ? undefined : 'object') that always yielded undefined. Removed; emit { oneOf: [...] } without top-level type (correct JSON Schema for a union). - P2 (with-api-v1.ts): public-endpoint path was short-circuiting before Bearer-token validation, contradicting the JSDoc and PR description. Now opportunistically validates a supplied token for rate-limit attribution + key tracking; missing/invalid token silently falls back to anon (the route is public by definition, so we don't 401). Two new tests cover both branches. 3031/3031 vitest tests pass; build green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |