# @jtl-software/cloud-apps-rest-sdk

TypeScript REST client for the JTL ERP API. Generated with [Microsoft Kiota](https://learn.microsoft.com/openapi/kiota/overview) from the official OpenAPI specification.

[![npm](https://img.shields.io/npm/v/@jtl-software/cloud-apps-rest-sdk.svg)](https://www.npmjs.com/package/@jtl-software/cloud-apps-rest-sdk)

## Installation

```bash
npm install @jtl-software/cloud-apps-rest-sdk
```

That's it. The SDK bundles the Kiota HTTP transport (`@microsoft/kiota-http-fetchlibrary`), JSON serializer (`@microsoft/kiota-serialization-json`) and abstractions as direct dependencies. You don't need to install or wire them yourself.

Requires Node.js 20 or later. ESM only (`"type": "module"`).

## Quick start

```ts
import {
  createJtlClient,
  createTenantContext,
} from "@jtl-software/cloud-apps-rest-sdk";

const { getCurrentTenant, withTenant } = createTenantContext();

const client = createJtlClient({
  auth: {
    clientId: process.env.OAUTH_CLIENT_ID!,
    clientSecret: process.env.OAUTH_CLIENT_SECRET!,
  },
  tenantId: getCurrentTenant,
});

// In your request handler, pass the tenant as the first argument of the withTenant scope for the SDK to pick up.
await withTenant(tenantIdFromRequest, async () => {
  const info = await client.v2.info.get();
  console.log(info);
});
```

That's the whole setup. The SDK handles the OAuth 2.0 client-credentials flow internally: it fetches a token on first use, caches it, refreshes it ~60 seconds before `expires_in`, and de-duplicates concurrent fetches. Every outgoing request gets `Authorization: Bearer <token>` and `X-Tenant-ID` (resolved per request, see [Tenant ID](#tenant-id)).

## Configuration

`createJtlClient(options)` accepts:

| Option | Type | Description |
| --- | --- | --- |
| `auth` | `ClientCredentialsOptions \| string \| (() => string \| Promise<string>)` | Required. See [Authentication](#authentication) below. |
| `tenantId` | `() => string \| undefined` | Resolver called on every request, injects the result as `X-Tenant-ID`. See [Tenant ID](#tenant-id). |
| `baseUrl` | `string` | Optional. Overrides the API base URL. Defaults to `https://api.jtl-cloud.com/erp`. |

## Authentication

`auth` accepts three shapes:

### 1. Client-credentials (recommended)

Pass `{ clientId, clientSecret }` and the SDK runs the OAuth 2.0 client-credentials flow for you:

```ts
createJtlClient({
  auth: {
    clientId: "your-client-id",
    clientSecret: "your-client-secret",
    // Optional:
    // tokenUrl: "https://auth.jtl-cloud.com/oauth2/token",  // default shown
    // scope: "...",
    // renewBeforeExpiry: 60_000,  // ms before expires_in to fetch a new token
  },
});
```

### 2. Static bearer token

If you already have a token (e.g. fetched by your auth gateway):

```ts
createJtlClient({ auth: process.env.ACCESS_TOKEN! });
```

The SDK won't refresh this. Useful for short-lived scripts and testing.

### 3. Custom async resolver

For any other auth flow, pass a function. Return a token, the SDK calls it on every request, you decide what to cache:

```ts
createJtlClient({
  auth: async () => {
    return await myAuthService.getToken();
  },
});
```

## Tenant ID

A backend using this SDK proxies requests from many users on different tenants to the JTL ERP API. The same cloud-app OAuth credentials authenticate against all of them, only the `X-Tenant-ID` header on each outbound request changes. Share one `JtlClient` (and its token cache) across all tenants, and resolve the tenant per request via `createTenantContext()`, which wraps Node's `AsyncLocalStorage`.

`createTenantContext()` returns `{ withTenant, getCurrentTenant }`:

- `withTenant(tenantId, fn)`: executes `fn` with `tenantId` as the active value for any async work it triggers.
- `getCurrentTenant()`: returns the active tenant ID, or `undefined` if called outside a `withTenant` scope.

Pass `getCurrentTenant` as the `tenantId` option on `createJtlClient`, then call `withTenant` from your request middleware. If `getCurrentTenant()` returns `undefined` at request time the SDK omits the header. The ERP API rejects most v2 endpoints without `X-Tenant-ID`, so make sure your middleware sets it before any handler that calls the SDK.

This mirrors the C# SDK's `AddHttpContextTenantContext`.

### Express example

How your backend obtains a tenant ID is entirely your app's concern, the SDK doesn't care. It could come from:

- a JTL session token your frontend forwards to you (the example below),
- your own app's JWT (cookie or `Authorization` header) that you signed yourself,
- a database lookup keyed by your own user ID,
- any other mechanism that yields a string at request time.

The example below shows the JTL-session-token shape because it's the common case for cloud apps. It uses [`jose`](https://www.npmjs.com/package/jose) for JWT verification, but if you already have your own auth/session middleware that knows the tenant, just pass that value into `withTenant` instead, the rest of the wiring is the same.

```ts
import express, { type NextFunction, type Request, type Response } from "express";
import { createRemoteJWKSet, jwtVerify } from "jose";
import {
  createJtlClient,
  createTenantContext,
} from "@jtl-software/cloud-apps-rest-sdk";

const { getCurrentTenant, withTenant } = createTenantContext();

const client = createJtlClient({
  auth: {
    clientId: process.env.OAUTH_CLIENT_ID!,
    clientSecret: process.env.OAUTH_CLIENT_SECRET!,
  },
  tenantId: getCurrentTenant,    // resolved per request
});

// JTL signs session tokens with EdDSA. Cache the JWKS once at startup;
// jose handles refresh internally.
const JWKS = createRemoteJWKSet(
  new URL("https://api.jtl-cloud.com/account/.well-known/jwks.json"),
);

async function tenantFromSessionToken(token: string): Promise<string> {
  const { payload } = await jwtVerify(token, JWKS);
  return (payload as { tenantId: string }).tenantId;
}

const app = express();

// Verify the inbound session token from your frontend, pull the tenant
// ID out of its payload, and scope it for the downstream handlers.
// The header name (`X-Session-Token` here) is your app's choice;
// don't confuse it with the ERP API's `X-Tenant-ID`, which the SDK
// sets for you.
app.use(async (req: Request, res: Response, next: NextFunction) => {
  const sessionToken = req.header("x-session-token");
  if (!sessionToken) {
    res.status(401).json({ error: "Missing X-Session-Token header" });
    return;
  }
  try {
    const tenantId = await tenantFromSessionToken(sessionToken);
    withTenant(tenantId, next);
  } catch {
    res.status(401).json({ error: "Invalid session token" });
  }
});

app.get("/companies", async (_req, res) => {
  // X-Tenant-ID injected automatically by the SDK from the current scope.
  res.json(await client.v2.companies.get());
});

app.listen(3000);
```

Only the middleware changes if you obtain the tenant ID differently, the `createJtlClient` setup and the handlers stay identical. The same pattern works with Fastify (`fastify.addHook("onRequest", ...)`), Hono (`app.use(async (c, next) => withTenant(id, next))`), or any other framework with per-request middleware.

## API shape

The client exposes the v2 surface of the JTL ERP API as a fluent builder. Examples:

```ts
await client.v2.companies.get();
await client.v2.salesOrders.get({ queryParameters: { top: 50 } });
await client.v2.salesOrders.byId("ORDER-1").get();
await client.v2.salesOrders.post(newOrder);
```

Refer to the generated `.d.ts` files in your editor for the full surface.

## Advanced

The high-level factory above suits the common case. If you need more control, the SDK also exports the building blocks:

- `createClientCredentialsTokenProvider(options)`: returns the cached `() => Promise<string>` used internally. Useful if you want to share one token cache across multiple clients or pass the function as a custom `auth` resolver.
- `createDefaultAdapter(options)`: returns the underlying `RequestAdapter` so you can construct the generated client yourself or chain additional middleware.
- `createApiClient(adapter)`: the bare-bones constructor from the generated code; takes any `RequestAdapter` and gives you the typed client.
- `createTenantContext()`: AsyncLocalStorage helper for per-request tenant scoping. See [Tenant ID](#tenant-id).

## Documentation

- Source and full integration guide: [github.com/jtl-software/jtl-platform-erp-sdk](https://github.com/jtl-software/jtl-platform-erp-sdk)
- Companion C# packages: [`JTL.Platform.Erp`](https://www.nuget.org/packages/JTL.Platform.Erp), [`JTL.Platform.Auth`](https://www.nuget.org/packages/JTL.Platform.Auth)
- Microsoft Kiota documentation: [learn.microsoft.com/openapi/kiota](https://learn.microsoft.com/openapi/kiota/overview)

## Support

Issues and feature requests: [github.com/jtl-software/jtl-platform-erp-sdk/issues](https://github.com/jtl-software/jtl-platform-erp-sdk/issues)

## License

Copyright (c) JTL-Software GmbH. See the repository for license terms.
