# Sharing a streamline by link — `listing.json`

An organization shares **its own streamlines** at an unguessable link, `https://streamliner.work/s/<id>`. Owners and
admins do it from the streamline's Library editor (**Share Link**). The link belongs to the organization: its page says
**Shared by <organization>**, and any of the organization's owners and admins can stop it. Members don't share. Personal
streamlines aren't shared by link (move one to an organization first); a `.streamline.json` file can still be sent by
hand. Anyone with the link sees a listing page and taps **Get** to add it to *On this device*. Links aren't searchable
or reviewed; there is no public catalog yet.

**Two switches decide it.** The streamline opts in, in its private half, and stays unshareable until an owner or admin
turns it on and publishes:

```json
// private/agents/<kind>.json
{ "sharing": { "link": true } }
```

The organization can turn sharing off for every streamline (`private/tenant.json` `discover: { sharing: "off" }`; the
default is `"admins"`). Keep streamlines like payroll off.

**What a shared copy is.** A recipient's copy always runs on the **recipient's own model key**, never the
organization's (the relay and the organization's key serve invited members only). So the app builds the shared file in
the personal format ([personal-streamline.md](personal-streamline.md)) from both published halves: the instructions
(briefs written in first), the model, the servers and their tool rules (a tool that waits for a tap still does; the
allowlist becomes `tools.allow`; `deny` becomes `hidden`), the cards, welcome and quick phrases. It leaves out what never
leaves the account: `credentialRef` and any key reference, roles (and tools only some roles may use), members and the
directory, network rules, budgets and limits, the fallback model, sealed forms and views, the overview, and everything in
`private/tenant.json`. configcheck warns `sharing.leftOut` when a shareable streamline relies on one of those. Before the
first share, and before any version that widens what runs without a tap, the app shows what becomes public and asks for
Face ID. The server can't read the opt-in (the shared file has no private half), so the app enforces it; the server
decides who may publish (owners and admins) and scans every file for keys.

This page is the format, so you can prepare and check a listing folder.

## Contents
- The folder
- `listing.json`
- What the app computes, never you
- How a version is signed
- Checklist

## The folder

```
find-a-flight/
  listing.json
  kiwi-flights.streamline.json      its "id" is the one listing.json names
```

Check it with `node scripts/check.mjs find-a-flight/` (configcheck): the schema, and that every id in `streamlines`
names a streamline file beside it (`listing.streamline-missing`).

## `listing.json`

```json
{
  "$schema": "https://streamliner.work/schema/v1/listing.schema.json",
  "schema": "streamline.listing/1",
  "name": "Find a Flight",
  "subtitle": "Kiwi.com · Made by Streamliner, not by Kiwi.com",
  "summary": "Say where and when. Get flights with prices and a button to book.",
  "streamlines": ["kiwi_flights"],
  "version": "1.0",
  "releaseNotes": "First version.",
  "service": { "name": "Kiwi.com", "domain": "kiwi.com" },
  "visibility": "link",
  "ageRating": "4+"
}
```

| Field | Req | Notes |
|---|---|---|
| `schema` | yes | `streamline.listing/1` |
| `name` | yes | ≤ 60 characters; usually the streamline's name |
| `subtitle` | no | ≤ 80; say *Made by …, not by <Company>* when you built it for someone else's product |
| `summary` | yes | ≤ 300, plain text |
| `streamlines` | yes | exactly **one** id for a link share (a pack waits for the catalog; `listing.pack` warns) |
| `version` | yes | what people see: `1`, `1.2`, `1.2.3` |
| `releaseNotes` | no | ≤ 2000 |
| `support.url` | no | https; never a sign-up or pricing page |
| `service` | no | `{ name, domain }`; never set `claimed` (only Streamliner does, after a domain check) |
| `visibility` | no | `link`; `public` is refused until review exists |
| `ageRating` | no | `4+`, `9+`, `12+`, `17+` |
| `category` | no | `business`, `money`, `work`, `developers`, `everyday` (for the catalog, later) |

## What the app computes, never you

The listing page's label (what it connects to, whether you sign in, what runs without asking, what asks first, which
links buttons may open) comes from the streamline file itself, not from `listing.json`. Write the tool policy
honestly in the streamline ([tool-policy.md](tool-policy.md)); the label shows it. `scripts/listing-label.mjs` in the
repo prints the same label.

## How a version is signed

The app signs each version with the device's own sharing key: a manifest (`streamline.listing-version/1`) listing
`listing.json` and `streamlines/<id>.streamline.json` with their sha256, signed Ed25519 over its JCS bytes. The
listing's **channel** is the key's fingerprint, so a reader checks the signature and the channel without trusting the
server. Sharing again publishes what the organization has published since, as the next version at the same link, from the device that first shared it. **Stop
Sharing** (any owner or admin) ends the link; copies people already got keep working.

## Checklist

- [ ] The streamline's private half says `"sharing": { "link": true }`, published.
- [ ] One streamline, its `id` set, and the listing names it.
- [ ] No key, token or sign-in anywhere (the server refuses a key-shaped string: `has_secret`).
- [ ] Every MCP server is `https://`.
- [ ] configcheck is clean on the folder.
