# Publishing to the registry

Community plugins ship through the s2script registry on this site: one identity, one version, dual artifacts (`.s2sp` for operators + types tarball for authors). Publish is open; review is additive — new versions start **unreviewed** with a public disclaimer.

The CLI is **`s2s`**, shipped in **`@s2script/sdk`**. Use `s2s …` when the package is installed, or `npx @s2script/sdk …` otherwise.

## Prerequisites

1. Create an account at [/signup](https://s2script.com/signup)
2. Claim a scope under [Organizations](https://s2script.com/account/orgs) (e.g. `@acme`) — one scope per org
3. Authorize the CLI ([CLI login](https://s2script.com/cli/authorize)) or mint a [deploy token](https://s2script.com/account/tokens)

Unscoped names (`rtv`) are owned by the first user who deploys them — no org required.

## Authenticate

### Local (device login)

```bash
s2s login
# or: npx @s2script/sdk login
```

The CLI calls `POST /api/v1/auth/device/start`, prints a short code, and opens [/cli/authorize](https://s2script.com/cli/authorize). Approve in the browser; the CLI's next `POST /api/v1/auth/device/poll` returns a `s2s_…` deploy token, which it stores in `~/.s2script/credentials.json`.

Paste path (headless boxes):

```bash
s2s login --token s2s_…
```

### CI

```yaml
env:
  S2SCRIPT_TOKEN: ${{ secrets.S2SCRIPT_TOKEN }}
run: npx @s2script/sdk deploy --ci
```

`--ci` never prompts; missing `S2SCRIPT_TOKEN` exits non-zero.

## package.json

```json
{
	"name": "@edge/heal-zones",
	"version": "1.0.0",
	"types": "api.d.ts",
	"s2script": {
		"apiVersion": "1.x",
		"publishes": { "@edge/heal-zones": "1.0.0" },
		"pluginDependencies": { "@s2script/cs2": "^1.0.0" }
	}
}
```

If `s2script.publishes` is set, `"types"` must point at an existing `.d.ts`. The CLI packs that file into a types-only tarball; the registry rejects uploads that omit it (`400 publishes_requires_types`). Runtime-only plugins omit `publishes` and skip types.

## Deploy

```bash
s2s deploy
```

1. Runs the publishes ⇒ types gate
2. Builds the `.s2sp`
3. Packs types when needed
4. Reads `README.md` beside package.json, if there is one (SDK 0.28.0 and later)
5. Packs the package's source into a tarball, choosing files the way `npm pack` does (SDK 0.29.0 and later; see [Source tarball](#source-tarball))
6. `POST /api/v1/deploy` with `Authorization: Bearer s2s_…`: one `application/octet-stream` zip holding `manifest.json`, `plugin.s2sp`, and optionally `types.tgz`, `README.md` and `source.tgz`. The manifest also carries package.json's `repository` field.

Success returns `{ name, version, reviewState: "unreviewed" }`. Manage versions, yank, descriptions and the README from the [publisher dashboard](https://s2script.com/account/packages).

## Source tarball

Like `npm publish`, every `s2s deploy` (single plugin and workspace) also uploads the package's source as an npm-shaped tarball: gzip+ustar, every entry under `package/`, so the same files give the same bytes. The registry stores it as `source.tgz` with the version. Anyone who can see the package can download it from the package page or `/api/v1/download/source?name=<name>&version=<version>`.

The package page's **Code** tab shows it the way npm's does: a file tree for any version that has source, with each file highlighted and every line linkable (`?tab=code&v=1.2.0&path=src/plugin.ts#L12`). Markdown files render as a page. Binary files and files over 1 MB are not shown there, but they are in the download.

File selection follows npm-packlist (npm 10):

- With a `files` array in package.json, only what it lists is packed — globs, `!` negation, and directories all work. The package root's own ignore file is not read (`files` replaces it). Ignore files in subdirectories still apply.
- Without `files`, everything is packed except what `.npmignore` excludes. With no `.npmignore`, `.gitignore` is used instead. Ignore files apply at every depth. A workspace member also honours ignore files up to the npm workspace root.
- Always packed: `package.json`, README, COPYING, LICENSE/LICENCE, and the files `main` / `bin` name.
- Never packed: VCS directories, the package root's `node_modules` (a nested one is packed, as on npm), `.npmrc`, the ignore files themselves, root lockfiles, and editor litter.
- One deliberate difference from npm: `.env` and `.env.*` are never packed. `.env.example` and `.env.sample` are.

A tarball over the registry's 16 MiB cap is refused locally. `s2s pack --dry-run` lists what is packed; then narrow `files` or add a `.npmignore`. Versions deployed before SDK 0.29.0 have no source.

### `s2s pack`

```bash
s2s pack [dir] [--dry-run] [--json] [--pack-destination <dir>]
```

What it lists is exactly what a deploy would send. It prints npm's Tarball Contents / Details report to stderr and writes `<scope>-<name>-<version>.tgz` to the current directory (or `--pack-destination`); only the filename goes to stdout. `--dry-run` lists without writing. `--json` prints a JSON array instead of the report.

> [!WARNING]
> The source of a public package is public. Check `s2s pack --dry-run` before your first deploy.

## Package page

Each package has a public page at `/plugins/<name>`. Below the description, a plugin's page has two tabs: **README** and **Config**. Link straight to the Config tab with `/plugins/<name>?tab=config`.

**README.** Markdown that explains what the plugin does, its commands, permissions and setup. Keep it as `README.md` next to package.json: since SDK 0.28.0, `s2s deploy` sends it, and each deploy that carries one replaces the page's README. You can also write or edit it on the package's page in the [publisher dashboard](https://s2script.com/account/packages), which has a preview. A deploy without a README keeps the current one, so an edit made on the site stays until your next deploy that has one.

The README renders like these docs, with tables, highlighted code fences and blockquotes. Raw HTML shows as text, and links must be `https:`, `http:`, `mailto:` or on this site. Images must be `https:`. The limit is 100,000 characters.

**Config.** Built from the [`s2script.config`](https://s2script.com/docs/concepts/config) block in your package.json, as published in the newest non-yanked version. You don't write it separately. Each option gets a row with its key, type, default, range or allowed values, and description. Below the table are the block as declared in package.json and the config file the runtime writes on first load. Options with a `group` are listed under that heading. A `label` shows under the key.

**Source.** A link under the package name to where its code can be browsed. It comes from package.json's standard `repository` field, which `s2s deploy` sends since SDK 0.28.0. Any npm form works, including `{ "url": …, "directory": … }`, which links into a monorepo directory on GitHub, GitLab and Codeberg. You can also set it on the package's page in the [publisher dashboard](https://s2script.com/account/packages): an https URL, or `owner/repo` for GitHub. A deploy without `repository` keeps the current link.

> [!IMPORTANT]
> Mark secrets such as API keys and webhook URLs `"sensitive": true`. The page then hides their default. Any other default is public.

## Restricted packages

Like npm, a package's access is **public** or **restricted**. A restricted package is visible only to the members of its org, whatever their role. Everyone else gets the same 404 as for a package that doesn't exist, and it's left out of the [plugin catalog](https://s2script.com/plugins). Members find it on their [dashboard](https://s2script.com/account/packages).

Publish one restricted with npm's flag, or set it in package.json:

```bash
s2s deploy --access restricted
```

```json
{
	"name": "@edge/anticheat",
	"publishConfig": { "access": "restricted" }
}
```

- Only a scoped package can be restricted (`400 restricted_requires_scope`), because only an org has members.
- Access is set when the package is first published. A later deploy that asks for a different access is refused (`409 access_mismatch`). A deploy that doesn't name an access keeps the current one.
- A public bundle can't include a restricted plugin. A restricted bundle can include either.
- Restricted artifacts never go through `packages.s2script.com`. Downloads are checked against the token and never cached.

> [!NOTE]
> `"private": true` in package.json is a different thing: npm's "never publish". `s2s deploy` still refuses those packages.

**Installing.** `s2s install` and `s2s add` send your login, so members install restricted packages as usual. A server or CI job that only installs should use a **read-only** token. Create one at [Deploy tokens](https://s2script.com/account/tokens), optionally bound to the org, and set it as `S2SCRIPT_TOKEN`. Publish tokens can download too. For the npm-compatible `/npm/*` surface, give npm the same token with `//www.s2script.com/npm/:_authToken=${S2SCRIPT_TOKEN}` in `.npmrc`.

## Operator & author consumers

| Who              | Command                        | Artifact        |
| ---------------- | ------------------------------ | --------------- |
| Dependent author | `s2s add @edge/heal-zones@^1`  | types only      |
| Server operator  | `s2s install @edge/heal-zones` | `.s2sp` closure |

Types are also available via the npm-compatible surface at `/npm/*` (never serves `.s2sp`). `s2s add` for a plugin dependency downloads **types only** (registry metadata + types artifact), not the producer binary — operators still `s2s install` the `.s2sp` closure. Protocol 2 producers stamp host API **3.x**; see [inter-plugin interfaces](https://s2script.com/docs/concepts/interfaces#api-3).

`s2s install --disabled` puts that closure in `plugins/disabled/` and records it under `"disabled"` in `plugins.json` until `s2s enable`. See [Install a plugin without loading it](https://s2script.com/docs/getting-started#install-a-plugin-without-loading-it).

## Yank

From the dashboard, or:

```bash
curl -X POST https://s2script.com/api/v1/yank \
  -H "Authorization: Bearer $S2SCRIPT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"@edge/heal-zones","version":"1.0.0"}'
```

Yanked versions are hidden from default resolve; lockfile pins can still warn.

## Reserved

`@s2script/*` is first-party only. Community tokens cannot deploy into that scope.

## Next

- [Authoring plugins](https://s2script.com/docs/authoring) — scaffold, build, plugin shape
- [Inter-plugin interfaces](https://s2script.com/docs/concepts/interfaces) — `publishes` / producer-as-import
- [Plugin catalog](https://s2script.com/plugins) — public package pages + disclaimer
