# Credentials

A credential is how a request proves it may act on your project. There are two: a **key** you
hold, and a **token** your backend mints from a key for one of your own users. The prefix on the
string says which one it is and where it belongs.

## The command line

The `bitskiff` commands on this page come from `@bitskiff/cli`, and `bitskiff init` is the way in:
you approve it once in the [console](https://console.dev.bitskiff.com), and it stores an organization
key that every key command then runs under. The tarball is on the
[downloads page](/docs/downloads/), and `npx` runs it with nothing installed:

```sh
npx --yes https://docs.dev.bitskiff.com/downloads/0.3.0/bitskiff-cli.tgz init
```

The organization key it stores acts as you. It holds `projects:read`, `projects:create` and
`keys:manage` for the setup, and every project word: the session words, `tokens:mint`,
`project:read`, `project:manage`, `usage:read`, `audit:read` and `webhooks:manage`. So the secret
key you make next with `keys create` is never wider than it. The people and billing words stay in
the console.

It also mints a host key into `bitskiff.json` in the directory you run it in, as `key`, beside the
project's slug and the api it talks to.

`--join-url <url>` sets the project's join URL, mints a client key for that page's origin, and
records both there as `joinUrl` and `clientKey`. It writes a hello world page beside that file only
when the directory is empty, or when you pass `--scaffold`; in a project you already have,
`bitskiff.json` is the one file it adds.

Already made an organization key in the console's Keys panel, where its type is Organization?
`login` stores that key instead:

```sh
npx --yes https://docs.dev.bitskiff.com/downloads/0.3.0/bitskiff-cli.tgz login
```

The CLI already knows the api it was published beside; the [CLI page](/docs/packages/cli/) has the
endpoint each environment answers on, for a CLI pointed at another one.

## Prefixes

| Prefix | What it is | Where it belongs |
|---|---|---|
| `bsk_pk_` | publishable key: a host key or a client key | page source, bounded by an origin allowlist |
| `bsk_sk_` | secret key | your server, or a device you control. Never a browser |
| `bsk_ak_` | organization key | your command line and your CI. Never a browser |
| `bsk_at_` | token | wherever you hand it: a signed-in page, an app, a client |

Two other prefixes belong to a session rather than to your project: `bsk_ht_` is the host token a
host holds on the session it is already running, and `bsk_rt_` is a client's resume token.

A third, `bsk_cs_`, is a console sign-in -- it belongs to the person signed in to the console, and
to no session. None of the three names a scope, a permission or a subject, and none of them opens
anything new.

A key names a scope -- one project, or the organization -- a list of permissions and a list of
limits. A token names one subject, permissions no wider than its key's, limits no higher, and an
expiry. Those two are the whole set: a project holds keys and tokens and nothing else.

## Publishable keys

A publishable key is public: it ships in page source. There are two, told apart by the one word
each holds:

| Name | Holds | Where it goes |
|---|---|---|
| **host key** | `sessions:open` | a host page: `new BitskiffHost({ key })` |
| **client key** | `sessions:join` | a client page or app: `new BitskiffClient({ key })` |

A host key is what a browser host opens with:

```js
import { BitskiffHost } from '@bitskiff/host';

const session = await new BitskiffHost({ key: 'bsk_pk_...' }).open();   // your host key
```

```ts
import { BitskiffHost, type Session } from '@bitskiff/host';

const session: Session = await new BitskiffHost({ key: 'bsk_pk_...' }).open();   // your host key
```

Create one with `init`, in the directory of the code that uses it. It leaves the key in
`bitskiff.json` for that code to read:

```sh
npx --yes https://docs.dev.bitskiff.com/downloads/0.3.0/bitskiff-cli.tgz init
```

`init` uses the project the directory's `bitskiff.json` names, else makes a new project named for the directory. `keys create --type
publishable` makes a further key and prints its string once; the [CLI page](/docs/packages/cli/)
has it.

When a command is refused:

- **Refused `invalid_key`?** The stored key no longer works. Run
  `npx --yes https://docs.dev.bitskiff.com/downloads/0.3.0/bitskiff-cli.tgz logout`, then `init` again.
- **Refused `forbidden` with `details.reason` of `missing_permission`?** The stored key lacks the
  permission `details.permission` names. Run `logout`, then `init` to sign in again.
- **Refused `forbidden` without that reason?** The key may not use that project. Pass `--project` of
  a project it may use.
- **Key from `BITSKIFF_ORG_KEY`?** `logout` does not remove it. Unset the variable instead.

A publishable key holds its one word and no other, and the list cannot be edited. A call outside
its class is refused `forbidden` (403) with `details.reason` of `not_for_class`. A host key
presented on a join is refused `forbidden` with `missing_permission`: a client page takes a client
key.

A publishable key is stored in clear and readable in full in the console, because the page it ships
in prints it anyway; the CLI names it by its `keyId` and prints its string only when it creates it.

The origin allowlist is what bounds it instead. A key created with no origin allows
`http://localhost` and `http://127.0.0.1` on any port, which is how `init` makes a host key, so a
page served locally works with nothing set up. A client key made for a page allows that page's
origin only.

`keys update` takes `--origin <your page's origin>` (repeatable) for a deployed page. Read the list
before you change it, because `--origin` replaces all of it.

```sh
npx --yes https://docs.dev.bitskiff.com/downloads/0.3.0/bitskiff-cli.tgz keys list
npx --yes https://docs.dev.bitskiff.com/downloads/0.3.0/bitskiff-cli.tgz keys update <keyId> --origin https://app.example.com --origin 'http://localhost:*'
```

An entry is `scheme://host[:port]`, `http` or `https`, with no path. The entry `https://*.example.com`
matches any host below the apex and never the apex itself, so `app.example.com` and
`a.b.example.com` both match and `example.com` does not.
An entry with no port matches the scheme's default port and no other, so a page served on `:8443`
needs that port written out, or `:*`.

An open or a join from an origin the list does not carry is refused `origin_not_allowed` (403) with
`details.reason` of `mismatch`. An open with no browser origin at all -- a page opened from disk,
an Electron main process, Node -- is refused the same code with `details.reason` of `absent`; that
host holds a secret key or a token instead.

The allowlist makes a key copied into another site's page fail loudly. It does not stop a scraped key driven from a script: the key's own limits bound
that one.

### Client keys

A client key is what your own client page joins with, and it is made for you. Set the project's
join URL to your page and a client key for that page's origin comes with it:

- **From the command line:** `init --join-url <url>` mints the key and saves it in `bitskiff.json`
  as `clientKey`.
- **In the console:** set the join URL in the project's settings. A project that holds no client
  key gets one.

To make one by hand, create it in the console's Keys panel by picking *A client page (joins
sessions)*, or from the command line. The console's form asks for no origin, so add your page's
afterwards with `keys update <keyId> --origin`, where `<keyId>` is the new client key's id. The
command line takes the origin at creation, and `<hostKeyId>` is the host key's id, which
`keys list` prints:

```sh
npx --yes https://docs.dev.bitskiff.com/downloads/0.3.0/bitskiff-cli.tgz keys create --type publishable --preset client-page --origin <your page's origin> --host-key <hostKeyId>
```

A client key joins its own project's sessions and no other project's. Its second condition,
`hostKeys`, narrows that to the sessions opened by the host keys it names. `--host-key` sets it and
repeats; leave it off and the key joins sessions opened by any host key of the project.

A project with one app never needs it. A project with two, such as a game and a quiz, gives each
client page a key that serves only its own host key.

A join the key is not for -- another project's session, or one opened by a host key its
`hostKeys` does not name -- is refused `wrong_client` before the client takes a place.

A native app that sends no `Origin` header declares one with `origin` beside its client key, and
that is checked against the same allowlist. An app shell that does send a real `Origin` header, such as
Capacitor on iOS with `capacitor://localhost`, is matched on that header.

A client key opens no sessions. Revoking one stops new joins with it. Under `manual` the clients
already in keep their places; under `compromise` they lose them within 5 seconds.

The [sample client page](/join/) is the one client that holds no key. See
[Client pages](/docs/guides/client-page/).

## Secret keys

A secret key is for a server, a CI job, or a device you control. It carries any permission your
project's routes name, and you pick the list when you create it:

```sh
npx --yes https://docs.dev.bitskiff.com/downloads/0.3.0/bitskiff-cli.tgz keys create --type secret --label backend \
  --permission sessions:open --permission tokens:mint
```

`--preset host-only` is `sessions:open` alone, `--preset monitor` is `sessions:read` and
`usage:read`, and `--preset full-access` is every project word. A preset only fills the list in
once. It is not a tier, and a key created under one does not pick up a word added later.

The string is shown once, when the key is created, and every listing after that shows a masked
form. Put it where your other secrets live.

Permissions are editable after the key exists, and `--permission` replaces the whole list the way
`--origin` does, so read the key first:

```sh
npx --yes https://docs.dev.bitskiff.com/downloads/0.3.0/bitskiff-cli.tgz keys update <keyId> --permission sessions:open --permission sessions:read
```

The console's **Keys** panel lists every key with its permissions, creates one and revokes one.
Editing a key and rotating one are command-line commands; the console does neither.

A request whose key lacks the word a route names is refused `forbidden` (403), with
`details.reason` of `missing_permission` and `details.permission` naming the word it wanted. A
secret key presented from a browser is refused `secret_key_in_browser` (403) whatever its
permissions: the browser sends an `Origin`, and a secret key is not a browser's credential.

Rotation replaces a key with no outage in the middle:

```sh
npx --yes https://docs.dev.bitskiff.com/downloads/0.3.0/bitskiff-cli.tgz keys rotate <keyId> --overlap 24h
```

It makes a successor, prints the successor's string once, and leaves the old key verifying until
the overlap runs out, so both work while you roll the new string out.

Revoking ends a key at once and takes a reason, which is asked for and never assumed, because it decides what happens to the
sessions the key opened: `manual` leaves them running until they end on their own, `compromise`
ends them now.

```sh
npx --yes https://docs.dev.bitskiff.com/downloads/0.3.0/bitskiff-cli.tgz keys revoke <keyId> --reason manual
```

## Permissions

A credential carries the words its work needs, and a route names the one word it wants. This is
the whole vocabulary the service has, and the last column says who may hold each word:

| Permission | What a credential holding it may do | Who may hold it |
|---|---|---|
| `sessions:open` | Open a session. The one word a host key carries | A key of either scope, or a token |
| `sessions:read` | List sessions, read one, and read one session's meters | A key of either scope, or a token |
| `sessions:manage` | Change a running session, and boot a client or change its grants | A key of either scope, or a token |
| `sessions:join` | Join a session. A client key's one word, and a join token's | A token, or a client key |
| `sessions:claim` | Take a fixed code's claim at open from a host key. A secret key or a token needs only sessions:open | A host key only, when the developer chose it |
| `tokens:mint` | Mint a token, list the tokens still outstanding, and end one early | A project key or an organization key |
| `project:read` | Read the project this request names: what it is, its settings and its limits | A project key or an organization key |
| `project:manage` | Change the project's settings and its limits, and ask for a raise | A project key or an organization key |
| `usage:read` | Read what the project has used, and the events it raised | A project key or an organization key |
| `audit:read` | Read the audit log: every key, limit and webhook change, with who made it | A project key or an organization key |
| `webhooks:manage` | Read and set where the project's events are delivered | A project key or an organization key |
| `projects:read` | List an organization's projects | An organization key only |
| `projects:create` | Create a project in an organization | An organization key only |
| `keys:manage` | Create, change, rotate and revoke the organization's and the project's keys | An organization key only |
| `members:invite` | Invite someone into an organization, read what is open, and revoke one | An organization key only |
| `members:manage` | Read an organization's members and set what each one may do | An organization key only |
| `billing:read` | Read the billing accounts an organization can see | An organization key only |

The prefixes `project:` and `projects:` are one letter apart and not the same permission. The words
`project:read` and `project:manage` are about the one project a request names. The words
`projects:read` and `projects:create` are about the list of projects an organization holds, and a
key that manages a project carries neither.

A token may name only the four session words, because a word the subject check cannot narrow would
hand a page the whole project.

## Organization keys

An organization key (`bsk_ak_`) is scoped to the organization rather than to one project, and it is
what tooling holds: `bitskiff login` stores one. It reaches one project's routes by naming that
project in an `X-Bitskiff-Project` header.

Six words in the table above are an organization key's alone, and a project key asking for one is
refused: `projects:read` and `projects:create` for the list of projects, `keys:manage` for the
keys, `members:invite` and `members:manage` for the people, and `billing:read` for the billing
accounts. Every other word an organization key may hold too.

Give a job the words it needs and no more: a nightly hosting check holds `sessions:open`, a
support tool holds `sessions:read` and `audit:read`.

A member creates one in the console, and only there: a leaked key that could create organization
keys would leave one behind that outlives its own revocation, so `bitskiff keys create --scope org`
refuses and says so. Organization keys expire, 90 days by default and one year at most.

## Tokens

Mint a token when the holder is not you and will not stay you: a page behind your own sign-in, a
device you want to end one at a time, a partner service.

Keep a key when the holder is you: a server you run, a fleet you own, a job you schedule. The
common name for minting per user is token
vending, and it is one call from a backend holding a secret key with `tokens:mint`:

```js
// Your backend, behind your own sign-in. This code never runs in a browser.
const endpoint = process.env.BITSKIFF_ENDPOINT;   // your endpoint, from `bitskiff whoami`
const key = process.env.BITSKIFF_SECRET_KEY;      // bsk_sk_..., holding tokens:mint

export async function mintToken(subject) {        // `subject` is your own id for the holder
  const response = await fetch(`${endpoint}/v1/tokens`, {
    method: 'POST',
    headers: { authorization: `Bearer ${key}`, 'content-type': 'application/json' },
    body: JSON.stringify({
      subject,                                    // your own id for them, 1 to 128 characters
      permissions: ['sessions:open'],             // the default
      limits: { concurrentSessions: 1 },          // also the default
      ttl: '10m',                                 // also the default
    }),
  });
  if (!response.ok) throw new Error(`mint refused: ${String(response.status)}`);
  // The answer is { tokenId, token, expiresAt }: `expiresAt` is Unix ms, and a 64-bit number
  // arrives as a JSON string.
  const minted = await response.json();
  return minted.token;                            // bsk_at_..., yours to hand out once
}
```

```ts
// Your backend, behind your own sign-in. This code never runs in a browser.
const endpoint = process.env.BITSKIFF_ENDPOINT;   // your endpoint, from `bitskiff whoami`
const key = process.env.BITSKIFF_SECRET_KEY;      // bsk_sk_..., holding tokens:mint

interface Minted {
  tokenId: string;
  token: string;
  expiresAt: string;                              // Unix ms; a 64-bit number arrives as a string
}

export async function mintToken(subject: string): Promise<string> {
  const response = await fetch(`${endpoint}/v1/tokens`, {
    method: 'POST',
    headers: { authorization: `Bearer ${key}`, 'content-type': 'application/json' },
    body: JSON.stringify({
      subject,                                    // your own id for them, 1 to 128 characters
      permissions: ['sessions:open'],             // the default
      limits: { concurrentSessions: 1 },          // also the default
      ttl: '10m',                                 // also the default
    }),
  });
  if (!response.ok) throw new Error(`mint refused: ${String(response.status)}`);
  const minted = (await response.json()) as Minted;
  return minted.token;                            // bsk_at_..., yours to hand out once
}
```

Hand the string to the SDK where a key goes: `new BitskiffHost({ key: token })` on a host. The SDK
never reads it.

The `ttl` field is a [duration string](/docs/guides/limits/#durations) and defaults to `10m`. A
value above your project's `tokensMaxTtl`, `1h` by default, is refused `validation` (400) with
`details.field` of `ttl` rather than lowered to fit.

`tokensMaxTtl` is raisable, to `720h` (thirty days), which is how a fleet holds a token long enough
to ride out a backend outage: see [Raises](/docs/guides/limits/#raises).

The `permissions` field defaults to `["sessions:open"]` and may name only the session words --
`sessions:open`, `sessions:read`, `sessions:manage`, `sessions:join` -- because a word the subject
check cannot narrow would hand a page the whole project.

A limit above the minting key's own is refused `quota_above_ceiling` (400). Naming a permission the key does not hold is not refused, and
it grants nothing: a token is only ever its key narrowed, so the extra word has no effect. Narrow
the key later and every token it has already minted narrows with it.

The subject is yours. It is a string we store, count against and attribute to, and never parse,
resolve or show to anyone but you: your own id for a person, a device id, or a compound such as
`team:acme/user:8123`. The service carries it on the session, on the client's limits and on the
usage rows, which is what makes usage and limits read by subject.

### Join tokens

A client page can present a token too. Mint it with `["sessions:join"]` and, when you know the
session, its `sessionId`:

- **`sessionId`** pins the token to one session, so a token captured on its way cannot be redeemed
  anywhere else. `sessionId` on any other permission list is refused `validation`.
- **Expiry** is `1h` at most, whatever the project allows. The ceiling is the lower of
  `tokensMaxTtl` and `1h`, so raising `tokensMaxTtl` lengthens a host's token and never this one.
  A `ttl` above the ceiling is refused `validation`.
- **Subject** names who is joining. The service meters, limits and attributes the client under
  it. On the host, `client.state.subject` is the subject of the token that client presented, so
  the host can greet your user by your own id. A client that joined with no token has no
  `subject`.

The client still joins with the join code, and the token rides along on `join`. A token never
stands in for the client key:

```js
import { BitskiffClient } from '@bitskiff/client';

const code = location.hash.slice(1);                          // your page read its own link

const { token } = await fetch('/api/join-token').then((r) => r.json()); // bsk_at_...

const client = new BitskiffClient({ key: 'bsk_pk_...' });     // your client key
await client.join(code, { token });
```

```ts
import { BitskiffClient } from '@bitskiff/client';

const code = location.hash.slice(1);                          // your page read its own link

const { token } = (await fetch('/api/join-token').then((r) => r.json())) as { token: string };

const client = new BitskiffClient({ key: 'bsk_pk_...' });     // your client key
await client.join(code, { token });
```

The SDK does not hold the join code across a redirect through your own sign-in: `inspect` and
`join` take the code as your page passes it, so your page is what carries it across. A join code is good for 30
to 60 seconds from the moment it is shown -- see
[Join codes](/docs/guides/lifecycle/#join-codes) for the window -- so mint the token before the page
joins, as above.

One token, minted from the command line, is the fastest way to see this work before you write the
backend:

```sh
npx --yes https://docs.dev.bitskiff.com/downloads/0.3.0/bitskiff-cli.tgz tokens mint --subject user_8123 --permission sessions:join --session <sessionId> --ttl 10m
npx --yes https://docs.dev.bitskiff.com/downloads/0.3.0/bitskiff-cli.tgz tokens list --subject user_8123
npx --yes https://docs.dev.bitskiff.com/downloads/0.3.0/bitskiff-cli.tgz tokens revoke <tokenId>
```

These commands read `BITSKIFF_SECRET_KEY` from the environment rather than the key `bitskiff login`
stored. Ending a token stops it opening anything new and ends no session already running. A token
the service refuses is `invalid_token` (401) and never `invalid_key`, with `details.reason` of
`expired`, `revoked` or `unknown`: the first calls for another token, the last two call for a
signed-in person.

## Limits

A permission says whether you may; a limit says how much, how fast and how long. Both a key and a
token carry limits, and the more restrictive value along the chain always wins, so a key can only
narrow what the project allows and a token can only narrow what its key allows.

Set a limit with `--limit <name>=<value>` on `bitskiff keys create`, on `bitskiff keys update`, and
on `bitskiff tokens mint`. These are the names each one takes:

| Credential | Limits it takes |
|---|---|
| a key | `concurrentSessions`, `concurrentClients`, `sessionOpensPerMinute`, `clientMinutesPerDay`, `ticketMintsPerMinute`, `tokensMaxTtl`, `requireToken` |
| a token | `concurrentSessions`, `concurrentClients`, `sessionOpensPerMinute`, `clientMinutesPerDay`, `joinsPerMinute` |

A token's limits are counted per subject rather than per project, so one subject's runaway page
does not spend the project's share. `concurrentSessions` on a token defaults to 1.
[Limits and capacity](/docs/guides/limits/) has the numbers and what each refusal means.

## Next steps

- [Limits and capacity](/docs/guides/limits/)
- [Session lifecycle](/docs/guides/lifecycle/)
- [Node hosts](/docs/guides/node-hosts/)
- [Client pages](/docs/guides/client-page/)
