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

# TypeScript SDK

> @rotascale/sdk: the same nine gates and six outcomes, from Node.

```bash theme={"system"}
npm install @rotascale/sdk
```

## Configuration

```ts theme={"system"}
import { Rotascale } from "@rotascale/sdk";

const rs = new Rotascale();                      // reads the environment
const rs2 = new Rotascale({ baseUrl: "...", apiKey: "..." });
```

<Warning>
  **The environment variable differs from the Python SDK, and one of them is
  going to trip you up.** TypeScript reads `ROTASCALE_API_URL`. Python reads
  `ROTASCALE_URL`. Set both if you run both in the same deployment.
</Warning>

<Warning>
  TypeScript also falls back to `https://api.rotascale.com` when neither is set,
  and **there is no such deployment**. RotaGrant runs single-tenant inside your
  environment, so a default hosted URL cannot be right for anybody. Set `baseUrl`
  or `ROTASCALE_API_URL` explicitly; the Python SDK raises instead, which is the
  better behaviour of the two.
</Warning>

| Option                 | Default                                                    |
| ---------------------- | ---------------------------------------------------------- |
| `baseUrl`              | `ROTASCALE_API_URL`, then a hosted URL that does not exist |
| `apiKey`               | `ROTASCALE_API_KEY`                                        |
| `enforcementTimeoutMs` | short: enforcement is on the hot path                      |
| `captureTimeoutMs`     | longer: capture is off the critical path by construction   |
| `failOpenEnforcement`  | **off**, and it logs every time it is used                 |

## Authorising an action

Options object rather than positional arguments, which is the one deliberate
divergence from the Python shape.

```ts theme={"system"}
const decision = await rs.authorize({
  grantId: "grt_01KZXYD62VG0",
  scope: { tool: ["payments.settle"] },
  amountMinor: 25_000_00,
  currency: "EUR",
  trajectoryId: t.id,
});

if (decision.allowed) await settle();
```

`throwOnRefusal` is on by default, so the unsafe path is one you write
deliberately rather than get by forgetting to check a return value.

## Handling refusals

```ts theme={"system"}
import { Blocked, Exhausted, Gated, ReviewRequired,
         EnforcementUnavailable, RequestRefused } from "@rotascale/sdk";

try {
  await rs.authorize({ grantId, scope: { tool: ["payments.settle"] },
                       amountMinor: 25_000_00, currency: "EUR" });
  await settle();
} catch (err) {
  if (err instanceof Exhausted) escalate("ceiling reached");
  else if (err instanceof Gated) escalate("context not clean");
  else if (err instanceof ReviewRequired) park(err.reviewId);
  else if (err instanceof Blocked) stop();
  else if (err instanceof EnforcementUnavailable) stop();
  else throw err;
}
```

<Note>
  `Exhausted` and `Gated` extend `Blocked`, so check them **before** `Blocked`.
  An `instanceof Blocked` branch placed first swallows all three and you lose the
  only field that tells you what to do next.
</Note>

`RequestRefused` is TypeScript-only and covers a malformed request rather than a
governance decision. It has no Python counterpart because the Python client
raises the underlying HTTP error.

## Trajectories and middleware

```ts theme={"system"}
import { witness, watchOpenAI, watchAnthropic, watchMcp } from "@rotascale/sdk";

await witness(rs, { agent: "refund-assistant", ticket: "TICKET-98130" }, async (t) => {
  const openai = watchOpenAI(client, t);
  await openai.chat.completions.create({ /* ... */ });

  await t.authorize({ grantId, scope: { tool: ["billing.refund"] },
                      amountMinor: 1200, currency: "EUR" });
});
```

`currentTrajectory()` reaches the active trajectory without threading it
through every function.

As in Python, **middleware records and does not authorise**. Only your code
knows which action is consequential. See [Framework middleware](/sdk/middleware).

## What it deliberately does not do

No governance fact is derived client-side. `enforcing` and `suppressed` read
fields the server sends; there is no second opinion about what a refusal means,
because two SDKs disagreeing about that is worse than either being wrong alone.
