> ## Documentation Index
> Fetch the complete documentation index at: https://docs.factrove.app/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Describe what the running code does. Do not invent hosted pricing, launch dates, or security guarantees. Share and viewer links are a configured base URL plus an artifact path. Show localhost and a placeholder origin such as https://library.example.com.

# Sharing links

> Create public artifact links, optionally protect them with a password, and understand unlock, lockout, and Retry-After.

Artifacts use private or organization-shared access. The owner of a private artifact can share it with the current organization from the viewer. That switch does not create, change, or remove a public link. The owner can separately enable a public link, optionally protected by a password. Organization members can read an `org_shared` artifact without that link. A share URL is the public base origin plus `/share/{id}/{token}`. Anyone with that URL can ask the API for the public detail. There is no assumption that a particular domain is hosted for you.

## Create and revoke a link

In the library or viewer, use Share. From the CLI:

```bash theme={null}
pnpm factrove share "$artifact_id"
pnpm factrove share "$artifact_id" --revoke
```

Owner HTTP routes:

* `POST /api/v1/artifacts/{id}/share` creates the link, or returns the existing one
* `GET /api/v1/artifacts/{id}/share` reads the current state
* `DELETE /api/v1/artifacts/{id}/share` revokes it

Only the creator can change the share. Another member who can read an `org_shared` artifact gets the same `404 ARTIFACT_NOT_FOUND` as a missing id. A request with no credentials is `401 UNAUTHENTICATED` before the artifact is loaded.

Local URL:

```text theme={null}
http://localhost:3000/share/{id}/{token}
```

Placeholder self-hosted URL:

```text theme={null}
https://library.example.com/share/{id}/{token}
```

The token is 32 bytes of base64url. The API stores AES-256-GCM ciphertext bound to the artifact id, plus a SHA-256 hash. The encryption key comes from `SHARE_TOKEN_KEY` when that is set, otherwise from `BETTER_AUTH_SECRET`. Rotating that input revokes existing public links. Sharing again mints a new token.

An invalid id, an invalid token, an unshared artifact, and a revoked token all return the same `404 ARTIFACT_NOT_FOUND` body. Those responses do not say whether the artifact exists.

## Password-protected links

A share can carry an optional password. The owner sets it on create or later with `POST /share` and JSON `{ "password": "..." }`. `DELETE /api/v1/artifacts/{id}/share/password` keeps the URL and clears the password. Revoking the share also clears the password.

Password rules from the shared contracts:

* 10 to 128 characters after NFC normalization
* at least four distinct characters
* not all whitespace
* not on the bundled common-password list

The API hashes the password with Argon2id and stores only the PHC string. The CLI never prints a password. Use `--password` to be prompted twice, or pipe it:

`--password` prompts twice and needs a terminal. `--password-stdin` reads a pipe and removes one trailing newline. Set `LINK_PASSWORD` before the pipe.

```bash theme={null}
pnpm factrove share "$artifact_id" --password
printf '%s' "$LINK_PASSWORD" | pnpm factrove share "$artifact_id" --password-stdin
pnpm factrove share "$artifact_id" --remove-password
```

## Opening a protected link

The public page loads `GET /api/v1/public/artifacts/{id}/{token}` without cookies. If the link has a password and the visitor has no grant, the API returns `401 PASSWORD_REQUIRED` and the page shows a password form. A dead link is still 404, so the form does not confirm that an old URL once existed.

`POST /api/v1/public/artifacts/{id}/{token}/unlock` with `{ "password": "..." }`:

| Result | Status | Body |
| - | - | - |
| Correct password | 200 | `{ grant: { token, expiresAt } }` |
| Link has no password | 200 | `{ grant: null }` |
| Wrong password | 401 | `PASSWORD_INCORRECT` |
| Inactive link | 404 | `ARTIFACT_NOT_FOUND` |
| Missing or non-JSON body | 400 | `INVALID_REQUEST` (no throttle charged) |

The grant lasts 15 minutes. The browser keeps it in `sessionStorage` on the web origin and sends `X-Factrove-Share-Grant` on the credentials-omitted detail request. It is not a cookie and it is not put in the page URL.

The sandboxed iframe cannot send headers, so a granted detail returns `contentUrl` with a `ticket` query. That ticket lasts 60 seconds and is honored at most 3 times per API process. Reverse proxies should not log `?ticket=`. Protected content without a valid ticket is `401 PASSWORD_REQUIRED`.

Changing or removing the password, revoking the link, and rotating the share-token key all invalidate existing grants. A plain re-share (`POST /share` without a password), while the current key can still read the token, keeps the same token and those grants still work. Revoking, or sharing again after that key no longer reads the stored token, mints a new URL.

## Rate limits and Retry-After

> [!NOTE]
>
> **Current behaviour.** This is the only place that calls out the pre-check view charge. A later change may charge a public view only after the link and password check. Until that change, the API does what this section says.

Public detail and public content charge the visitor's view allowance before the link lookup and before the password or ticket check. A `401` or `404` still uses that allowance. The limit is 60 view units per 60 seconds per client address, in memory on each API process. IPv6 addresses share one bucket by their /64. IPv4 addresses stay separate.

A detail that is allowed also grants two companion content loads for 30 seconds. A content request spends one companion when one remains, and otherwise charges another view unit. That choice is also made before lookup. Unlock does not use this limiter. The view `429` is `RATE_LIMITED` with message `Too many requests. Try again later.` It does not set `Retry-After`.

Password limits are separate. They are also in memory on each API process, and processes do not share those counters.

* 10 failures per 10 minutes per client address, using the same IPv6 /64 key (refunded after a match or an infrastructure failure)
* 30 wrong passwords per 10 minutes per current share token hash
* 20 verifies per minute and 100 per 10 minutes per IP, with one in-flight verify per address

Every password-throttle `429 RATE_LIMITED` (`Too many password attempts. Try again later.`) sends `Retry-After`. An exhausted share sends that 429 with no hash verification. Someone who has the link and keeps failing can lock that link for up to ten minutes at a time. A saturated Argon2 gate, or a share whose remaining check slots are all in flight, is `503 UNLOCK_UNAVAILABLE` with `Retry-After`.

The owner share payload may include `guessesLockedOut` when the share budget is exhausted.

## Lockout recovery

Revoke clears the stored password. A later plain share creates an unprotected link, including after this recovery and after share-token key rotation. Supply the password again:

```bash theme={null}
pnpm factrove share "$artifact_id" --revoke
pnpm factrove share "$artifact_id" --password
```

`--password` needs a terminal. The stdin form is `printf '%s' "$LINK_PASSWORD" | pnpm factrove share "$artifact_id" --password-stdin`. Revoke, then share, mints a new token hash. The previous URL returns 404.

## What visitors see

The public JSON is the filename, name, size, upload time, content URL, and `passwordProtected`. `name` is null when the artifact has no library name. It omits the creator, organization, access type, and share URL. HTML uses the same sandboxed iframe policy as a signed-in viewer. Inline scripts and styles run. Network requests, external scripts and styles, forms, and nested frames do not. See [Prepare an artifact](https://docs.factrove.app/concepts.md#prepare-an-artifact). Markdown is not offered on the public route.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.