/share-links is the external-delivery surface for productions and matter packages. Replaces "zip up the binder + email a Dropbox link" with a token-protected URL the recipient can hit directly — no account, no Kodori login, no ZIP-archive juggling.
**The flow:**
1. Open a document, collection, or production page. 2. Click **Share via link** in the action row. A form opens — optionally type a label ("Smith production — opposing counsel") and a recipient hint (the email that will receive it; this is for YOUR records, NOT auth). Default expiration is 14 days; max 90. 3. Click **Create share link**. The plaintext URL appears ONCE in a green confirmation box. Click **Copy URL**. After dismissing the box, the URL can never be retrieved — only the SHA-256 hash is stored. 4. Email the URL to the recipient. They click it; the page loads at /share/<token>; they see a download view with no sidebar / nav / Kodori branding beyond a footer.
**Three target kinds:**
- *Document* — the recipient sees a download button for the single doc. - *Collection* — the recipient sees a list of every doc in the collection with per-row download links. - *Production* — same as collection, but per-doc downloads use the EXACT versionHash captured when the production was recorded. So a recipient downloading from "Production Set 1 — Jan 15" gets the bytes that were ORIGINALLY produced even if subsequent re-stamps changed the docs' current versions.
**Chain-of-custody.** Three new audit event types capture the full lifecycle:
- `share-link.created` — operator creates the link. - `share-link.accessed` — recipient hits the URL. actorId is `public-share:<prefix>` (the synthetic anonymous identifier) so the audit log distinguishes external accesses from authenticated ones. Each access also bumps the share-link's `accessCount` + `lastAccessedAt` for at-a-glance "did opposing counsel actually receive this?" review. - `share-link.revoked` — operator pulls the link.
**Manage all links at /share-links.** Lists every link in the workspace with status (active / expired / revoked / exhausted), recipient hint, access count, last-access timestamp, expiration date, and a Revoke button. Revocation is instant — the next access attempt returns 404.
**Search + filter on /share-links.** A text input at the top of the page filters across label + recipient hint + token prefix + target name (substring, case-insensitive). A status select filters to active / expired / revoked / exhausted. Both URL-state captured (`?q=...&status=...`) so filter combinations are bookmarkable and shareable. Counter renders "Showing N of M" when filters are applied so you can spot when results don't match expectations. Useful for "find every share-link I created for @jonesandsmith.com" or "show me everything that expired last quarter."
**Sortable columns.** Click any of the sortable column headers — Accesses, Last access, Expires — to sort the table. Clicking the same header flips direction; clicking a different one resets to desc. URL-state captured (`?sort=<key>&dir=asc|desc`) so sorted views are bookmarkable. Sort happens AFTER filter, so "highest-accessed link for matter Smith" is one click of the q filter then one click of the Accesses header. The active column shows an arrow indicator (↑ / ↓).
**Bulk revoke** — each active row has a checkbox; the "Select all active" header button selects every still-live link. Click "Revoke selected" to revoke up to 200 in one action — useful for cleaning up after matter closure. Per-row failures (already-revoked, not-found) are counted but don't halt the batch. Each successful revoke emits the same share-link.revoked event a single-row revoke would, with `bulk: true` in the payload so audit consumers can distinguish bulk operations.
**Direct email delivery (default off).** When the recipient hint is a valid email, tick "Email this URL directly to the recipient hint" and Kodori sends the URL via Resend with you as the reply-to address. Skips the copy-paste-into-Slack step. Standard subject format ("X shared a doc with you — Smith production") so opposing counsel's inbox filters catch the email. Best-effort — if Resend fails or the email is malformed, the share-link is still created and you get the URL back to copy manually.
**Access cap (default unlimited).** Tick the "Cap total accesses to N opens" checkbox on the create form to hard-stop the link after a chosen number of accesses (1-1000). Both the /share/[token] view and the underlying download routes 404 once the cap is hit. The standard "expire after first download" pattern is one click (cap=1); longer caps work identically. Stacks with the expiry-date TTL + manual revoke — whichever fires first wins. Capped links surface on /share-links with the access counter rendered as "<used> / <cap>" + an "exhausted" badge once the cap is reached.
**Recipient email verification (default off).** Tick "Require recipient to verify their email before unlocking" on the create form for high-stakes deliveries (HIPAA, restricted productions, attorney-client privileged content). When enabled, the recipient hits the URL and sees an email entry form instead of the doc — they enter their email, Kodori sends a 6-digit code (15-min TTL, max 5 attempts), they enter the code, Kodori sets a 24-hour signed cookie scoped to the share-link. Subsequent visits within the cookie window bypass the gate. Stacks with TTL + access cap + watermark — defense-in-depth without forcing one mechanism on every share. The audit chain logs only the email DOMAIN (not the full address) on verification events to avoid per-recipient ACL leakage to all admins; the full address is in the share_link_verifications table for in-product traceability.
**Verification roster (owner / admin only).** /share-links surfaces a "Verified" column for every link with verification required. The cell shows `<verified>` when no attempt failed and `<verified> / <attempted>` when some recipients tried but didn't complete. Click the count to land on /share-links/[id]/verifications — the per-link roster of every (email, requestedAt, verifiedAt, attempts) row with verified / pending / expired status badges. This is the audit-defensibility surface for "X verified at 14:32 before reading" without manual /audit drilling. When verification isn't required for a link, the column shows "—" and there's no roster page.
**Domain allowlist (optional, on top of verification).** When recipient verification is required, you can additionally pin verification to specific email domains. Tick "Require recipient verification" on the share-link form, then paste comma- or newline-separated domains in the "Restrict to email domains" textarea — `jonesandsmith.com`, `ourhealthnet.com`, etc. Subdomains match (`firm.com` accepts both `counsel@firm.com` and `counsel@mail.firm.com` because opposing counsel mailers commonly route through subdomain-pinned outbound services). Up to 25 domains per link. When a recipient enters an email whose domain isn't on the allowlist, verification refuses BEFORE emailing a code — they see a generic "not authorized to verify on this link" message (intentionally vague to prevent probing for valid domains). The audit chain still captures the gate-trip (`share-link.verification-failed` with `reason: domain-not-allowed`) so admins see the refusal in the verification roster. The roster page renders an emerald "Domain-restricted" callout listing allowed domains so operators can prove pinned-to-@firm.com posture without digging into config.
**Workspace default allowlist.** Owners and admins set a tenant-wide default on /settings/tenant ("Default share-link recipient domain allowlist"). When set, every new share-link with recipient verification on inherits that allowlist when the operator leaves the per-link textarea blank. Per-link entries always win — operators override by typing into the per-link field. Works server-side (in createShareLinkAction) so even API-driven share-link creation inherits the default. Saved fields land on the tenant.settings-updated audit event with from/to in the delta, so the workspace default itself is auditable like every other tenant setting.
**Workspace default expiry.** Same /settings/tenant page also lets owners / admins set a workspace-default expiry (1-90 days). Different firm postures want different defaults — 7 days for HIPAA delivery, 30 days for ediscovery rolling productions, 14 days for general matter packages. Empty = use the global default (14). When the operator doesn't supply an explicit expiry on the share-link form, the new createShareLinkAction reads the tenant default first, then falls back to the global default. Same audit posture as the allowlist — saves land on tenant.settings-updated.
**Workspace default "Email me when accessed".** Tri-state radio group on /settings/tenant — "Use global (on)" / "On" / "Off". HIPAA shops want default on (every access notifies); ediscovery platforms running high-volume delivery typically want default off so admins' inboxes don't fill up. Operators still pick per-link; this only changes the form's pre-fill and server-side fallback. Same audit posture — saves land on tenant.settings-updated.
**Workspace default access cap.** Same /settings/tenant page also lets owners / admins set a workspace-default access cap (1-1000). HIPAA shops typically set 1 ("every share-link expires after first download"); ediscovery rolling productions typically want no cap. Empty = no cap by default. Operators passing 0 / omitting the cap on the per-link form fall back to the tenant default; operators specifying a cap explicitly override. Server-side fallback in createShareLinkAction; same audit posture — saves land on tenant.settings-updated.
**Email notification on access (default on).** When you create a share link, a "Email me when this link is accessed" checkbox is on by default. The moment an external recipient opens the link, Kodori emails you with the access timestamp + recipient hint + a link to /share-links — useful for the litigator's chain-of-custody read ("did opposing counsel actually receive this?"). Throttled to one notification per (link, 4-hour window) so a recipient hitting the URL repeatedly doesn't spam your inbox. The audit log still records EVERY access via the `share-link.accessed` event regardless — the throttle is purely on email volume. Flip the toggle off on the creation form if you don't want notifications.
**Confidentiality watermark on PDFs.** PDFs served through document or collection share-links are stamped on-the-fly with a workspace-name header bar, a diagonal CONFIDENTIAL stamp at the page center, and a footer reading `Confidential · Shared via Kodori on YYYY-MM-DD · Token <prefix>`. The stamp is generated per-request via pdf-lib (it doesn't change the canonical content-addressable bytes in storage). A forwarded leak is traceable back to the originating share link via the token prefix in the footer cross-referenced against the audit log's `share-link.accessed` events. Non-PDF mimes (DOCX, images, ZIP) pass through bytes-unchanged because there's no analogous in-band watermark surface for those formats.
**Customize the watermark text per workspace.** Owners and admins set custom diagonal-stamp + header-bar text on /settings/tenant ("Share-link watermark text" section). Diagonal text auto-uppercases on render — drop in `ATTORNEYS' EYES ONLY`, `PRIVILEGED & CONFIDENTIAL`, `FOIA EXEMPT`, `TRADE SECRET`, etc. Header bar replaces the default workspace name (operators with a long matter prefix like `Smith v. Jones — Production Set 3` get cleaner output than the workspace-name fallback). Empty fields = use defaults. Each field capped at 80 characters. The footer (Kodori attribution + token prefix + date) is fixed and not customizable — it's the load-bearing chain-of-custody anchor. Saves are idempotent; only-changed fields land on the audit chain via tenant.settings-updated.
**Production-kind links DO NOT get watermarked.** Production share-links serve the verbatim Bates-stamped bytes captured at `recordProduction` time — those must match the privilege log byte-for-byte for ediscovery integrity. Adding a Kodori watermark to a produced PDF would invalidate the chain-of-custody claim. Document and collection links are watermarked because they carry no analogous integrity contract.
**Token security:**
- 32 bytes of crypto randomness, base64url-encoded → 256 bits of entropy. Brute-force is computationally infeasible. - Only the SHA-256 hash is stored. Database compromise doesn't leak active tokens. - Tokens are URL-as-secret — anyone with the URL has access until expiry or revocation. Operators delivering productions should treat the URL itself as confidential and email it via secure channels (encrypted email, secure file-transfer service, etc.).
**Coming next:** per-link recipient email verification (require the recipient to enter their email + receive a one-time code before the link unlocks); per-link download caps ("expire after first download" or "max 5 downloads"); HMAC-signed query parameters so the access timestamp is verifiable.