# Identity & access — reference

The exhaustive surface for identity, app contexts, the scope model, access profiles, roles,
and credentials: every method, its parameters, validation rules, limits, the response
envelope, error codes, and an honest **Notes & limits** section. For concepts see
[explanation.md](explanation.md); for runnable guides see [how-to.md](how-to.md).

For the canonical, always-current request and response field shapes, use the **generated API
reference** (rendered from the OpenAPI specification). This page is the durable map and the
honest edges — not a regenerated copy of the raw endpoint schemas.

---

## Client construction

```ts
import { VectrosClient } from '@vectros-ai/sdk';
const client = new VectrosClient({ token, environment });
```

| Field | Meaning |
|---|---|
| `token` | The bearer credential: `sk_*`, `ssk_*`, or `st_*`. |
| `environment` | The API base URL, e.g. `https://api.vectros.ai` (production) or `https://api.staging.vectros.ai` (staging). |

Identity and access methods live under two sub-clients: `client.identity.*` (users, orgs,
clients) and `client.auth.*` (contexts, roles, access profiles, scoped keys, token minting,
the identity-binding check, and the cross-context principal lookup).

### Response envelope

List, lookup, and version-history methods return the standard page envelope:

```ts
{ data: T[], nextCursor: string | null }
```

Drain by feeding `nextCursor` back as the next call's `startFrom` until it is `null`. The
identity-binding check (`ping`) and token minting are **not** enveloped — they return a plain
object.

---

## Identity plane — `client.identity.*`

Three dimensions — **users**, **orgs**, **clients** — each with the same lifecycle. All are
tenant-wide and idempotent on the caller-supplied `externalId`.

| Method | Purpose |
|---|---|
| `createUser` / `createOrg` / `createClient` | Create (idempotent by `externalId`). |
| `getUser` / `getOrg` / `getClient` | Fetch one by Vectros `id`. |
| `updateUser` / `updateOrg` / `updateClient` | Full-replace update (PUT) of the body. |
| `deleteUser` / `deleteOrg` / `deleteClient` | Delete by `id`. |
| `listUsers` / `listOrgs` / `listClients` | List, filterable; enveloped. |
| `getUserVersions` / `getOrgVersions` / `getClientVersions` | Version history; enveloped. |
| `lookup…` | Exact lookup by external id / declared fields (see the generated reference for per-dimension shapes). |

### Fields by dimension

| Field | User | Org | Client | Notes |
|---|---|---|---|---|
| `externalId` | ✓ | ✓ | ✓ | **Your** id; create is idempotent on it. Capped at 256 chars; permissive about characters. |
| `id` | ✓ | ✓ | ✓ | Vectros-assigned UUID; returned on create/get/list. |
| `email` | ✓ | — | — | Users carry `email`, **not** `name`. |
| `name` | — | ✓ | ✓ | Orgs/clients carry `name`, **not** `email`. |
| `type` | ✓ | — | — | `HUMAN` (default) or `SERVICE`. |
| `orgId` | — | — | ✓ | A client's owning org. |
| `payload` | ✓ | ✓ | ✓ | Free-form `Record<string, unknown>` attribute bag, round-tripped as-is. |
| `status` | ✓ | ✓ | ✓ | `ACTIVE` on create; lifecycle states apply. |

**Filters.** `listUsers` / `listOrgs` accept `externalId`. `listClients` accepts `externalId`
and `orgId`. All list methods accept `limit` and `startFrom` (cursor).

**Idempotency semantics.** A second create with an existing `externalId` returns the
**existing** identity unchanged — the duplicate call's other fields are dropped, it does
**not** update. Use the update method to change an identity.

### CLI equivalents

```
vectros identity create --type <user|org|client> --external-id <id>
                        [--name <n>] [--email <e>] [--service] [--org <orgId>] [--metadata <json>]
vectros identity list   --type <user|org|client> [--external-id <id>] [--limit <n>]
vectros identity get    --type <user|org|client> --id <vectrosId>
vectros identity delete --type <user|org|client> --id <vectrosId>
```

`--name` and `--email` are mutually dimension-specific (`--email` only for users; `--name`
only for orgs/clients). `--service` only applies to users; `--org` only to clients.

---

## App contexts — `client.auth.*`

The isolation partition. `contextId` must match `^[a-z][a-z0-9-]{2,30}$` (starts with a
lowercase letter; lowercase letters, digits, hyphens; 3–31 chars total).

| Method | Purpose |
|---|---|
| `createAppContext` | Create (idempotent by `contextId`). `name` is required. **Root `sk_*` only.** |
| `getAppContext` | Fetch one by `contextId`. |
| `updateAppContext` | Update `name` / `description` (the path supplies `contextId`; the body's `contextId` is required by the schema but ignored — it is immutable). |
| `listAppContexts` | List; enveloped. |
| `deleteAppContext` | **Confirm-gated async cascade** — see below. **Root `sk_*` only.** |

**Root-only lifecycle.** Creating and deleting an app context require a **root `sk_*` key**.
A scoped key or token (`ssk_*` / `st_*`) cannot create or tear down a context — not even one
carrying the wildcard `*` scope. (Get / update / list are reachable with appropriate scope.)

**Delete contract.** `deleteAppContext({ contextId, confirm })` requires `confirm` to equal
`contextId`:

- Without a matching `confirm` → **400**, and nothing is touched (the rejected delete is a
  no-op; child roles/profiles still exist).
- With a matching `confirm` → accepted (**202**); the context flips `active → purging`
  immediately and drains all of its records, documents, folders, schemas, roles, and profiles
  in the background, reaching `deleted` when the drain completes.

**Reserved context.** Certain context ids are reserved by the platform and cannot be created
by a tenant — `vectros-admin` (backs the hosted admin surfaces) and `default` (the base
context auto-provisioned for every tenant).

**Errors.** Malformed `contextId` → 400 with a partner-friendly message. Get on a
well-formed but never-created `contextId` → 404 (not 500). Cross-tenant probes collapse to
404.

### CLI equivalents

```
vectros context create <contextId> [--name <n>]
vectros context list
vectros context get <contextId>
```

(The CLI exposes create/list/get; context teardown is performed through the API delete with
its confirm gate.)

---

## The scope model

### Scope grammar

An allowed action is `resource:ops[:qualifier]`:

- **`resource`** — one of the data-plane resources `records`, `schemas`, `search`,
  `documents`, `folders`, `inference` (control-plane resources exist for the control surface
  but are not mintable through the bootstrap gate — see below).
- **`ops`** — any combination of the letters **`c`** create, **`r`** read, **`u`** update,
  **`d`** delete. E.g. `records:r`, `records:cru`, `documents:crud`.
- **`qualifier`** — optional tail that **narrows** (e.g. `records:r:intake_form` = read only
  the `intake_form` record type). It never widens.

**What does NOT grant access (author the letter form instead):**

| Form | Effect at runtime |
|---|---|
| `records:r`, `search:r`, … | ✓ Grants the named operations. |
| `*` (the single literal wildcard) | ✓ Grants **everything** — the shape root keys carry. Reserve it. |
| A coarse verb (`read`, `write`, `delete`) | ✗ Grants **nothing**. |
| `resource:*` (operations-wildcard) | ✗ Grants **nothing at runtime**. Author explicit `c`/`r`/`u`/`d` letters. |

> The operations-wildcard `resource:*` is *accepted by the bootstrap scope gate* (it stays
> within the data plane), but it does **not** grant access at the runtime enforcement layer.
> Always author the explicit letter form. The only wildcard that grants anything is the bare
> literal `*`.

### `dataScope`

A map from an ownership field to the list of allowed values:

```json
{ "dataScope": { "clientId": ["client_abc", null] } }
```

| Field | Meaning |
|---|---|
| `userId` | Confine to rows owned by the listed users. |
| `orgId` | Confine to rows owned by the listed orgs. |
| `clientId` | Confine to rows owned by the listed clients. |

- Enforced as a **server-side filter below** any caller-supplied filter; cannot be widened by
  the caller.
- Multiple values in one field's list → union (OR). Multiple fields → intersection (AND).
- **Strict by default.** A scoped credential must include the matching filter on every list
  and search call, or the request is rejected (e.g. *"clientId is required by token scope"*).
- **`null` sentinel.** Include JSON `null` in a field's value list to additively grant access
  to **tenant-level** rows (no value for that field). Opt-in only — never implicit.

### The bootstrap scope gate (data-plane allowlist)

The CLI / blueprint **bootstrap** flow mints scoped keys only for the data plane. The
allowlist is exactly:

```
records, schemas, search, documents, folders, inference
```

Any other resource — the control plane `keys`, `profiles`, `app-contexts`, `users`,
`billing`, `admin`, `clients`, `orgs`, or any unrecognized resource — and the literal `*`
are **hard-rejected**: the bootstrap mints nothing and exits non-zero. There is no override
flag; control-plane scoped keys are created deliberately in the developer portal.

---

## Access profiles — `client.auth.*`

The per-principal, per-context permission binding.

| Method | Purpose |
|---|---|
| `createAccessProfile` | Create/bind (idempotent by `(context, principalId)`). |
| `getAccessProfile` | Fetch one by `(contextId, principalId)`. |
| `updateAccessProfile` | Update scopes/role/status/overrides. |
| `deleteAccessProfile` | Remove the binding. |
| `listAccessProfiles` | List a context's profiles; enveloped. |
| `listProfilesForPrincipal` | Cross-context: every context a principal is bound to; enveloped. |

### Profile fields

| Field | Meaning |
|---|---|
| `principalId` | The bound principal: `usr_<userId>` or `key_<keyId>`. Must start with `usr_` or `key_`; structural characters like `:` are rejected with 400. |
| `scopes` | Inline clauses, each `{ allowed_actions: string[] }` (snake_case on the wire). **XOR with `roleId`.** |
| `roleId` | Reference to a reusable role. **XOR with `scopes`.** |
| `status` | `active` or `suspended`. Suspending denies access without deletion. |
| `identityOverrides` | Ownership values stamped onto what the principal touches. Accepts **`orgId`** and **`clientId`** only (each `{ value: "<id>" }`). `userId` and the tenant id are sacred and **rejected with 400**. |

**XOR enforcement.** A profile carries exactly one of inline `scopes` or a `roleId`. Updating
to set one **clears** the other (empty-string / empty-array sentinels). On create, the unset
half is absent or an empty sentinel.

**Idempotency.** A repeat create for an existing `(context, principalId)` returns the
**existing** profile unchanged.

**Cross-context lookup.** `listProfilesForPrincipal({ principalId })` returns every profile
for that principal across all contexts. A principal with no profiles returns an **empty array
(200)**, not 404. A malformed `principalId` (e.g. containing `:`) → 400.

### CLI equivalents (`vectros access`)

```
vectros access grant  --principal <usr_|key_> --context <c> (--role <r> | --actions <csv>)
vectros access revoke --principal <usr_|key_> --context <c>
vectros access list   (--context <c> | --principal <usr_|key_>)
vectros access get    --principal <usr_|key_> --context <c>
```

`--role` and `--actions` are mutually exclusive (exactly one). `--actions` mints a
single-clause inline profile. `access list` requires exactly one of `--context` (a context's
members) or `--principal` (a principal's contexts).

---

## Roles — `client.auth.*`

Reusable, context-scoped, identity-agnostic permission shapes.

| Method | Purpose |
|---|---|
| `createRole` | Create (idempotent by `roleId`). |
| `getRole` | Fetch by `(contextId, roleId)`. |
| `updateRole` | Update name / scopes. |
| `deleteRole` | Delete — **blocked with 409** while a profile still references the role. |
| `listRoles` | List a context's roles; enveloped. |

### Role fields

| Field | Meaning |
|---|---|
| `roleId` | The stable role handle within the context. |
| `name` | Human-readable name. |
| `description` | Optional. |
| `scopes` | One or more clauses, each `{ allowed_actions: string[] }`. Roles may be **multi-clause** — *any clause that matches* grants access. |

Roles support an ownership placeholder `${{ self.* }}` (resolves to the acting principal at
runtime) and a `null` data-scope sentinel (additively grants tenant-level/owner-less records).
These richer forms are authored through blueprints.

**Referential integrity.** Deleting a role referenced by a profile is rejected with 409 —
remove or re-point the profile first. Deleting an unreferenced role succeeds; the platform
does not cascade.

### CLI equivalents (`vectros role`)

```
vectros role create --context <c> --role-id <id> --name <n> --actions <csv> [--description <d>]
vectros role list   --context <c>
vectros role get    --context <c> --role-id <id>
vectros role delete --context <c> --role-id <id>
```

The CLI `role create` authors **single-clause** roles from `--actions`.

---

## Credentials

### Types

| Prefix | Lifetime | Scope | Use |
|---|---|---|---|
| `sk_live_*` / `sk_test_*` | Permanent (revoke to retire) | Wildcard within its tenant | Server-to-server from your own backend. |
| `ssk_live_*` / `ssk_test_*` | Permanent (revoke to retire) | Bound to a profile; identity-bearing | Agents, bots, long-running workers; audit attribution. |
| `st_*` | 1h default / 24h max | Embedded in the token | Front-end-safe per-session credentials. |

The raw secret of an `sk_*`/`ssk_*` is shown **once** at creation and never re-readable; the
platform stores only a hash.

### Scoped key lifecycle — `client.auth.*`

| Method | Purpose |
|---|---|
| `createScopedKey` | Mint an `ssk_*` for a principal that already has a profile in the context; raw secret returned **once**. |
| `getScopedKey` | Metadata for one key (no secret). |
| `revokeScopedKey` | Soft-delete; stops working within ~5 minutes (authorizer cache). |
| `listScopedKeys` | The tenant's keys; enveloped (single page — no cursor input on this endpoint). |

`createScopedKey` fields: `keyName`, `tenantId`, `contextId`, `userId` (the bare principal
user id), optional `label`. A re-issue of an existing `(tenant, context, principal, keyName)`
tuple returns the key **without** the secret (the platform never re-discloses) — rotate to
get a fresh secret.

### CLI equivalents (`vectros key`)

```
vectros key issue  --principal <p> --context <c> [--name <n>] [--label <l>] [--format human|raw|env|json]
vectros key list   [--principal <p>] [--context <c>]
vectros key get    <keyId>
vectros key revoke <keyId>
vectros key rotate --principal <p> --context <c> [--name <n>] [--format …]
```

`key rotate` has no dedicated endpoint — it revokes the matching active key and mints a fresh
one. There is no in-place rotation.

### Token minting — `client.auth.mintToken`

```ts
const { token, expiresAt } = await client.auth.mintToken({
  scope: { allowedActions: string[], dataScope?: {...}, identity?: {...} },
  userId?: string,                 // mint on behalf of a real user in the tenant
  expiresInSeconds?: number,       // default 3600, max 86400
});
```

| Field | Meaning |
|---|---|
| `scope.allowedActions` | Required; array of `resource:ops[:qualifier]` strings. Malformed entries → 400 at mint. |
| `scope.dataScope` | Optional; the data the token may read/write. |
| `scope.identity` | Optional; ownership values stamped onto resources the token creates. |
| `userId` | Optional; must reference a real user in the caller's tenant (unknown id → 400 naming the field). |
| `expiresInSeconds` | Optional; default 3600 (1h), capped at 86400 (24h). |

Returns `{ token: "st_…", expiresAt: <unix-seconds> }`. Tokens cannot be revoked in flight —
expiry is the only lever; mint short.

### Identity-binding check — `client.auth.ping`

Returns the authenticated principal's identity: `status`, `tenantId`, `environment`,
`principalType` (`root_key` | `scoped_key` | `token`), and `principalKeyId`. For a
`scoped_key`, `allowedActions` is present (and a `dataScope` if bound to a user/org). For a
`token`, `tokenExpiresAt` is present. An invalid credential is denied at the edge with **403**.

---

## Error codes

| Code | When |
|---|---|
| **400** | Malformed `contextId` / `principalId` / scope token; `userId`/ownership id not a real row in the tenant; `identityOverrides.userId` (sacred field); context delete without a matching `confirm`. |
| **403** | Invalid or unauthorized credential; a scoped action the credential's scope does not permit. Messages are uniform on purpose — they do not reveal which check failed. |
| **404** | Get on a non-existent (or cross-tenant) context/identity; list/create under a non-existent parent context. Cross-tenant probes collapse to 404. |
| **409** | Delete a role still referenced by a profile. |

A scoped credential lacking permission for a list/search filter required by its `dataScope`
is rejected (strict scope) with a message naming the required field.

---

## Notes & limits

What this surface does **not** do, stated plainly:

- **No identity PATCH.** Users, orgs, and clients update via full-replace **PUT** only —
  there is no partial-update on the identity plane.
- **No schema PATCH on the identity plane** and no reparenting of identities — orgs/clients
  are referenced by id, not moved through a hierarchy.
- **Single scope clause on `mintToken`.** The token-mint endpoint serializes **one**
  `(allowedActions, dataScope)` clause per request. Multi-clause shapes are expressed through
  a **role** (referenced by a profile) or a blueprint, not minted directly as a single
  multi-clause token.
- **Single-clause access-profile create.** The profile-create API accepts one inline clause;
  reach for a multi-clause role for compound shapes.
- **`identityOverrides` is `orgId` / `clientId` only.** `userId` and the tenant id are sacred
  and rejected — a profile cannot forge another user's identity.
- **`resource:*` grants nothing at runtime.** Author explicit `c`/`r`/`u`/`d` letters. Only
  the bare literal `*` grants everything (and that is the root-key shape).
- **`s` is an advanced op letter, beyond `c`/`r`/`u`/`d`.** A fifth op, `s`, grants
  reveal of a type's **sensitive** fields (e.g. `customer:rs` = read + reveal-sensitive). It
  is a per-type capability, not part of the standard CRUD set — the blueprint/bootstrap scope
  gate accepts only `c`/`r`/`u`/`d`, so packs are authored with those; `s` is minted
  deliberately where sensitive-field reveal is intended.
- **No in-place key rotation.** Rotation is revoke-then-reissue; a key's raw secret is shown
  once and never re-readable.
- **Revocation is not instant.** A revoked `sk_*`/`ssk_*` keeps working until the edge
  authorizer cache expires — up to about five minutes. `st_*` tokens cannot be revoked at all;
  they expire on their lifetime. Plan offboarding with this window in mind.
- **`listScopedKeys` is a single page** — the endpoint takes no cursor input; filter
  client-side by context/principal.
- **Cross-tenant existence is unobservable.** Probing for another tenant's id returns the same
  uniform 404 as a non-existent id; scope-mismatch failures return the same generic shape.

---

## Where to go next

- [explanation.md](explanation.md) — the concepts: contexts as the isolation moat, the
  identity plane, the scope model, and the three credential types.
- [how-to.md](how-to.md) — runnable guides for every method above.
- The **generated API reference** (rendered from the OpenAPI specification) — canonical,
  always-current request/response field shapes.
- The **blueprint walkthroughs** — end-to-end builds that wire contexts, profiles, roles, and
  scoped keys together.
