Skip to content
Docs

Command palette

Jump to a page or search the docs.

Changelog

Passports

How Prepare sale (M-Vender) shares a car as a field-by-field snapshot behind a hashed link, how the buyer opens it on the web without signing up, and how Passports (M-Passaporte) shows visits, moves an expiry and revokes a link.

Markdown

What it does

A Passport (RF39, US38) is what a seller shows a buyer: a copy of the sections the owner ticks, never a view of the account (RN08). Prepare sale offers five sections with their counts: Photos, Services and mileage, Anonymized receipts, Specs and Costs and values. The car's make, model and year always go; plate, VIN, address and garage location never can (RN09), and materializePassport from @autofolio/domain throws if one is ever asked for. The link is valid for 7, 30 or 90 days.

"Create link" builds the snapshot (ADR-04 of the handoff) and stores it in share_package with:

  • the SHA-256 of a random 256-bit token (base64url); the link <origin>/p/<token> is returned once and never stored;
  • the token's first eight characters as a hint, so the owner tells links apart (autofolio.app/p/7Kq2wM9x…); the other 35 keep 210 bits secret;
  • copies of the car's photos (at most 30, cover first) under passports/<id>/, named by position so a public URL carries no internal id, made with a server-side S3 copy before the row is written; a failure removes the copies already made;
  • a ★ consent_record row (passport_share, version passport-2026-10) for the owner who shared it.

Receipts are shared as each service's date, shop and item names only. A receipt file can carry names and addresses that cannot be removed reliably, so no file and no amount leaves the account. Services carry date, title, reading and trust, never their cost or note.

The view the buyer gets

passportView(snapshot, expiresAt, sign) in passport.service.ts turns a stored snapshot into PassportView (passport.contract.ts): the car, validUntil, and each section or null, with photos signed for 15 minutes. "Preview as buyer" (passport.preview) builds the same view for the current choice without storing or copying anything, so the preview is what the buyer gets. Distances are km; each reader's page shows its own unit.

The buyer's page

/p/:token in apps/web (D-Passaporte, US40, RF40) needs no account. Its loader calls passport.open { token }, a public procedure that runs as system code (asSystem, SystemReason passport.open): it reads a live package by the token's hash, never a live table, and writes one ★ share_access row per visit with the date and the country Cloudflare's edge resolved (cf-ipcountry, on Workers only). No address is stored with it; the address only meters openings in rate_limit (60 a minute, swept by the auth cleanup), past which the call answers TOO_MANY_REQUESTS.

A revoked, expired or unknown token answers null, and the page shows the same neutral notice for all three (D-Passaporte-Revogado), so it never tells whether a link existed. A live one renders Summary (with this owner since, last mileage, services with a receipt, then specs and costs when shared, each estimate with source, date and confidence), Services (date, mileage, title and trust, then the anonymized receipts) and Photos, from the same PassportView as the owner's preview. Tabs the owner did not share are absent.

The page speaks the buyer's language (?lang=, the site cookie or Accept-Language among pt, en and de), else the owner's account language. Dates and money follow that language; distance is in its region's unit (mi for en-US). Responses under /p/ carry X-Robots-Tag: noindex, Cache-Control: private, no-store and Referrer-Policy: no-referrer, robots.txt disallows the path, and the log scrubber replaces any /p/<token> with [redacted].

Procedures

ProcedureFailures
passport.prepare { vehicleId }NOT_FOUND (car), CONFLICT (not in the garage)
passport.preview { vehicleId, sections, days }INPUT_VALIDATION, NOT_FOUND (car), CONFLICT (not in the garage)
passport.create { vehicleId, sections, days }INPUT_VALIDATION, NOT_FOUND (car), CONFLICT (not in the garage)
passport.list { vehicleId?, limit? }INPUT_VALIDATION
passport.get { id }NOT_FOUND (link)
passport.setExpiry { id, days }INPUT_VALIDATION, NOT_FOUND (link), CONFLICT (revoked, cleaned)
passport.revoke { id }NOT_FOUND (link); revoking a revoked link does nothing
passport.open { token }INPUT_VALIDATION, TOO_MANY_REQUESTS; null when not available

All but open are for Members of the Organization; open is public. list and get answer each link's status (active, expired, revoked), views and last visit; get adds its latest 100 visits.

Revoking

Revoke deletes the copied photos, then sets revoked_at and drops the snapshot in one statement; a retry after a failure finishes the job. The row and its visits stay. A CHECK keeps a revoked package snapshot-free, and the tenant update policy lets a live package change only its expiry or be revoked; a revoked one never changes again.

An expired link keeps its snapshot for PASSPORT_EXPIRED_KEEP_DAYS (30 days), so the owner can still extend it. Then the cron tick's passports job (passport.jobs.ts, 20 links a tick) deletes its photo copies and its snapshot, like a revoke; the link stays expired and its expiry no longer moves (passportExtendable, which also hides the row in the app).

Data and isolation

share_package and share_access are isolated by Active Organization; share_access and consent_record are append-only. No audit_log event is written: the consent record (who shared, when, which text) and the package's created_by and revoked_at are the trail, and withAudit's transaction would hold the storage calls.

App

Prepare sale (screen-M-Vender, route /prepare-sale/[vehicleId]) opens from the vehicle profile. Its toggles carry the prototype's ids (item-photos, item-services, item-docs, item-specs, item-costs); "Valid for" opens a 7 / 30 / 90-day sheet; "Create link" (criar-link) shows the Link created sheet with the link, Copy link, Share and Passports. "Preview as buyer" opens /passport-preview/[vehicleId].

Passports (screen-M-Passaporte, route /passports) opens from Profile and from M-Vender. Each card shows the car, the link's hint and status, views and last visit, and Revoke (after a confirmation); a card opens the link (/passports/[id]) with its expiry, what it shares and every visit.

The demo garage's M3 link carries copies of its three photos. In the English demo garage it opens at /p/demo-token, which flow F6 uses: npx playwright test (root playwright.config.ts, local only) seeds that garage through the dev seed sign-in, so the web app must run with DEV_SEED_LOGIN=true.

autofolio://dev/M-Vender opens the seeded M3; autofolio://dev/M-Passaporte shows its seeded live link with seven visits. Flow F5 ticks the receipts and creates a link.

Key files

Edit on GitHubDocumentation is in English.

We use essential cookies to make our app work. With your consent, we also use optional cookies to improve your experience. Read our Cookie Policy and Privacy Policy.