Files
accounted/lib/entitlements/keys.ts
T
Mattsson 5d7952a01e feat(mcp): model-free document upload via signed URL (#1378)
* feat(mcp): model-free document upload via signed URL (#748)

Adds gnubok_create_document_upload + gnubok_complete_document_upload so
document bytes reach storage through a short-lived signed PUT URL and
never pass through the model context. Fixes silent base64 corruption on
real-size PDFs and the context blowup on batch uploads.

- pending/ staage keys with TTL cleanup; completion validates magic
  bytes + SHA-256, moves bytes to the WORM key and adopts the reserved
  UUID as document id, making retries and concurrent completions
  idempotent
- legacy gnubok_upload_document kept for clients without file access,
  description now points to the signed-URL pair; shared mime resolution
  and inbox-item creation extracted
- both new tools mapped in TOOL_SCOPE_MAP (transactions:write) and
  MCP_TOOL_CAPABILITY_MAP (ai) so the paywall and scope gates hold
- payload guard ceiling 58.5K to 59K after trimming the create tool's
  outputSchema to upload_id/upload_url/expires_at

Fixes #748

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mcp): satisfy capability-map lock and phantom-column scanner

The exact-entries lock in capability-maps.test.ts now includes the
signed-URL pair as dispatch-only AI tools, and the inbox insert uses a
literal payload (explicit UUID instead of a conditional spread) so the
no-phantom-columns scanner can resolve every column.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 18:41:05 +02:00

121 lines
5.8 KiB
TypeScript

/**
* Capability keys: the single namespace behind the SaaS paywall AND the
* per-tenant modularity / marketplace vision. Each key names one gateable
* feature; a company "has" it when an unexpired capability_grant exists
* (entitlement) and it isn't explicitly disabled (enablement).
*
* These keys are a STABLE CONTRACT: grant rows, the future marketplace catalog,
* and per-tenant module toggles all reference them. Add keys; never rename one.
*/
export const CAPABILITY = {
/** AI assistant chat, onboarding composer, and document field extraction (Anthropic/Bedrock). */
ai: 'ai',
/** Bank sync / PSD2 (Enable Banking). Freeze-and-retain: tokens are NOT revoked on downgrade. */
bank_sync: 'bank_sync',
/** Skatteverket filing/sync (VAT, AGI, skattekonto) via BankID. */
skatteverket: 'skatteverket',
/** Outbound transactional email: invoices, reminders, payslips (Resend). Auth/account email is never gated. */
email_send: 'email_send',
/** Org-number lookup / enrichment (TIC). NOT gated: identity/lookup is always free. */
org_lookup: 'org_lookup',
/** EU VAT-number validation (VIES). NOT gated: identity/lookup is always free. */
vat_validation: 'vat_validation',
/** Riksbanken FX auto-fetch. NOT gated at launch (kept free); manual rate entry is always allowed. */
currency_rates: 'currency_rates',
/** Cloud backup to Google Drive. NOT gated at launch (kept free: never hold a customer's data hostage). */
cloud_backup: 'cloud_backup',
/** Migration import from other systems (Fortnox/Visma/Bokio/BL/Briox). Kept open so new payers can migrate IN. */
migration: 'migration',
/** Bolagsverket iXBRL årsredovisning filing. Reserved (extension not yet enabled). */
bolagsverket: 'bolagsverket',
/** Stripe Connect: auto payment links on invoices + payment/payout sync. */
stripe_payments: 'stripe_payments',
} as const
export type CapabilityKey = (typeof CAPABILITY)[keyof typeof CAPABILITY]
/**
* The set actually withheld from non-payers (manual tier) at the 2026-07-07
* cutover. Founder decision (2026-06-28): gate the high-value recurring external
* services only.
*
* KEPT FREE on purpose:
* - identity & lookup: TIC org_lookup, VIES vat_validation, BankID login:
* they aid onboarding/data quality; gating them is friction in the wrong place.
* - currency_rates (FX auto-fetch) and cloud_backup.
* Internal bookkeeping is always fully usable on the manual tier.
*
* NOTE: bank_sync and skatteverket stay PAID even though their flows use BankID
* as an auth step: what's charged for is the bank data sync and the VAT/AGI
* filing service, not the identity check.
*/
export const PAID_CAPABILITIES: readonly CapabilityKey[] = [
CAPABILITY.ai,
CAPABILITY.bank_sync,
CAPABILITY.skatteverket,
CAPABILITY.email_send,
CAPABILITY.stripe_payments,
] as const
/**
* Paid MCP tools → required capability. The MCP/agent path is a paid chokepoint
* just like the HTTP routes, so the dispatcher gates these the same way it gates
* API-key scope (see mcp-server `tools/call`). External-service WRITE tools
* appear here: send_invoice (email) and the two Skatteverket submissions. The
* read/local SKV tools (generate_agi, vat_declaration_validate/status, agi_status)
* stay free: the §4 carve-out forbids blocking a statutory filing obligation.
*
* The document upload tools invoke AI (Bedrock document OCR via
* extractInvoiceFields), so they are gated on CAPABILITY.ai: the same paywall
* the HTTP inbox upload/attach/retry paths enforce. Without these entries a
* free-tier API key could trigger paid AI extraction. bank_sync has no MCP
* tool (bank sync is cron/HTTP only).
*/
export const MCP_TOOL_CAPABILITY_MAP: Readonly<Partial<Record<string, CapabilityKey>>> = {
gnubok_send_invoice: CAPABILITY.email_send,
gnubok_vat_declaration_submit: CAPABILITY.skatteverket,
gnubok_agi_submit: CAPABILITY.skatteverket,
// AI document OCR (Bedrock): the inbox's paid extraction, reachable via MCP.
gnubok_create_document_upload: CAPABILITY.ai,
gnubok_complete_document_upload: CAPABILITY.ai,
gnubok_upload_document: CAPABILITY.ai,
} as const
/**
* Paid pending-operation types → required capability. Keyed by
* `pending_operations.operation_type`. This is the commit-time twin of
* MCP_TOOL_CAPABILITY_MAP: it gates the actual external-service call inside
* commitPendingOperation, so an operation staged during the trial cannot be
* committed once the grant has expired, regardless of caller (MCP approve tool
* or the UI approval path). Keep the values in sync with MCP_TOOL_CAPABILITY_MAP.
*/
export const PAID_OPERATION_CAPABILITY_MAP: Readonly<Partial<Record<string, CapabilityKey>>> = {
send_invoice: CAPABILITY.email_send,
submit_vat_declaration: CAPABILITY.skatteverket,
submit_agi: CAPABILITY.skatteverket,
} as const
/**
* Extension workspace → required capability, keyed by `sector/slug`. This is the
* page/nav twin of the API-route gates: an extension whose entire value is a
* paid service should not just 403 its writes but be hidden from the sidebar and
* blocked at the page so a non-payer never lands on a dead workspace.
*
* invoice-inbox is fully gated on `ai`: its reason to exist is the AI field
* extraction (extractInvoiceFields / gnubok_upload_document), already the paid
* chokepoint on every other surface (HTTP upload/attach/retry, the MCP tool).
* Both the sidebar item and the /e/[sector]/[slug] page read this map so the two
* surfaces can never drift apart.
*/
export const EXTENSION_REQUIRED_CAPABILITY: Readonly<Partial<Record<string, CapabilityKey>>> = {
'general/invoice-inbox': CAPABILITY.ai,
} as const
/** Which paid capability (if any) an extension workspace requires to be usable. */
export function requiredCapabilityForExtension(
sector: string,
slug: string,
): CapabilityKey | undefined {
return EXTENSION_REQUIRED_CAPABILITY[`${sector}/${slug}`]
}