npm package: @sentimex/ts-core-sdk
Description: Core TypeScript SDK for Sentimex (REST + WS, token-based auth).
Latest version: 0.1.4

📦 @sentimex/ts-core-sdk

Core TypeScript SDK for building Sentimex apps and integrations (including Based mini-apps).  
Provides a small, dependency-light client for JWT-based auth, REST endpoints, and a browser/Node-safe WebSocket.

---

## Features

- 🔐 **Token-first auth** via `TokenManager` (supports `fetchToken` + `refreshWith`)
- 🌐 **Tiny REST client** with automatic 401 refresh retries
- 📡 **Cross-env WebSocket client** (browser WebSocket or Node’s `ws`)
- 🧰 **Strong TypeScript types** included
- 🧾 **ESM + CJS builds**

---

## Installation

```sh
# npm
npm install @sentimex/ts-core-sdk

# yarn
yarn add @sentimex/ts-core-sdk

# pnpm
pnpm add @sentimex/ts-core-sdk
```

> **Node 18+ recommended.**

---

## Importing

<details>
<summary>ESM</summary>

```ts
import { SentimexClient, TokenManager, AuthError } from "@sentimex/ts-core-sdk";
```
</details>

<details>
<summary>CommonJS</summary>

```js
const { SentimexClient, TokenManager, AuthError } = require("@sentimex/ts-core-sdk");
```
</details>

---

## Quick Start

### 1. Create a client with token sources

You provide functions to issue and refresh access tokens.  
**In production, your backend should hold the API key and return short-lived JWTs to the client.**

```ts
import { SentimexClient } from "@sentimex/ts-core-sdk";

const userId = "some-user-id";              // your app's user identifier (preferred user address) MUST BE CONSTANT/UNIQUE for your app
const baseUrl = "https://jkbapi.app";       // SaaS base URL

const client = new SentimexClient(
  userId,
  {
    // Initial/placeholder tokens (SDK will call fetchToken() immediately if expired)
    initial: { accessToken: "", refreshToken: "", expiresIn: 0 },

    // Called when we need a *new* token (e.g., first run or refresh cannot be used)
    async fetchToken() {
      const r = await fetch(`${baseUrl}/private/saas/auth/terminal/issue`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          // In production, do NOT expose your API key here; call your own backend.
          "X-Api-Key": "<YOUR_API_KEY_FOR_LOCAL_DEV_ONLY>"
        },
        body: JSON.stringify({ user_id: userId }),
      });
      const j = await r.json();
      return {
        accessToken: j.access_token,
        refreshToken: j.refresh_token ?? "",
        expiresIn: j.expires_in ?? 0,
      };
    },

    // Called to refresh an access token using a refresh token
    refresh: {
      async refreshWith(refreshToken: string) {
        const r = await fetch(`${baseUrl}/private/saas/auth/terminal/refresh`, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "X-Api-Key": "<YOUR_API_KEY_FOR_LOCAL_DEV_ONLY>"
          },
          body: JSON.stringify({ refresh_token: refreshToken }),
        });
        const j = await r.json();
        return {
          accessToken: j.access_token,
          refreshToken: j.refresh_token ?? refreshToken,
          expiresIn: j.expires_in ?? 0,
        };
      },
    },

    skewSeconds: 30,  // refresh slightly before expiry
  },
  {
    baseUrl,                            // REST base
    wsUrl: "wss://jkbapi.app/v1/stream/tweets",   // WebSocket base
    // webSocketImpl: globalThis.WebSocket,       // optional override
  }
);
```

**The SDK will automatically:**
- Attach `Authorization: Bearer <token>` for REST
- Retry once on 401 by refreshing tokens
- Refresh your token a few seconds before expiry

---

## REST: Common Calls

All REST calls live under `/private/saas/v2/terminal`.

```ts
// Profile for the current user (Status shows import progress state)
const me = await client.getProfile();

// Paginated list of tracked profiles (All/Followed/Unfollowed)
const page1 = await client.getTrackedProfiles(1, 25, "follow_all");

// Search tracked profiles (server-side)
const search = await client.searchTracked("elon", 1, 25, "follow_only");

// Follow / Block (unfollow) a profile by Twitter ID
await client.followTrackedProfile("44196397");  // follow @elonmusk
await client.blockTrackedProfile("44196397");   // unfollow

// Bulk delete all blocks
await client.deleteAllBlocks();

// Count of currently-following profiles
const { following_count } = await client.getFollowingCount();

// Trigger an import by username (server builds your social graph)
await client.importByUsername("elonmusk");

// Convenience helper: import + poll profile Status until complete
const doneProfile = await client.importAndAwait("elonmusk", {
  intervalMs: 3000,
  maxAttempts: 15,
  finalizeWaitMs: 1000,
  reconnectWs: true,
  onProgress({ status, attempts, lastProfile }) {
    // status: 0=none, 1=mapping, 2=ready, 3=finalized
    console.log("progress:", { status, attempts });
  },
});

// Tweets by token (server-side token concept)
const tweets = await client.getLatestTweetsByToken("SOME_TOKEN");
```

**FollowFilter values:**
- `"follow_all"` (default)
- `"follow_only"`
- `"unfollow_only"`

Types for common responses are exported (see [Types](#types) below).

---

## WebSocket

The SDK exposes a browser/Node-safe WS client.  
In browsers it uses `globalThis.WebSocket`.  
In Node it will lazy-load the `ws` package if no global is available.

```ts
// 1) Connect and observe connection status
await client.connect(
  (msg) => {
    // Receive normalized message (or raw JSON if unrecognized)
    console.log("WS message:", msg);
  },
  (status, evt) => {
    // "open" | "close" | "error"
    console.log("WS status:", status);
  }
);

// 2) Subscribe to a feed (server understands your filter payload)
client.subscribe({ category: "trader" });

// 3) Disconnect when done
client.disconnect();
```

- The WS client auto-reconnects with backoff and keeps a heartbeat.
- It also sends the latest access token on open (and can force refresh if empty).

---

## Errors

- `AuthError` is thrown by the REST layer when a request returns 401.
- The client automatically attempts to refresh once; if the refresh also fails you’ll see the error thrown.
- Network or HTTP non-200 responses are thrown as generic `Error`.

```ts
try {
  const me = await client.getProfile();
} catch (e) {
  if (e instanceof AuthError) {
    // token refresh failed or unavailable
  } else {
    // network/server error
  }
}
```

---

## Types

This SDK re-exports helpful types:

```ts
import type {
  ProfileResponse,
  PaginatedResponse,
  TrackingProfile,
  TweetData,
  TweetType,
  User,
  Media,
  WebsocketData,
  FullTweetNode,
} from "@sentimex/ts-core-sdk";

import type { FollowFilter } from "@sentimex/ts-core-sdk";
```

**Helpers:**

```ts
import { parseTweet, toTweetNode } from "@sentimex/ts-core-sdk";
```

**Lower-level classes if you need them:**

```ts
import { HttpClient, TokenManager, WSClient, AuthError } from "@sentimex/ts-core-sdk";
```

---

## Tree-Shaking & Builds

- **ESM:** `dist/index.js`
- **CJS:** `dist/index.cjs`
- **Types:** `dist/index.d.ts`
- **Package publishes only `dist/`**

---

## Usage Notes

- **Server/Client split:** In production, keep your API key on your server. Expose endpoints that return short-lived JWTs (what `fetchToken`/`refreshWith` call). The examples use `X-Api-Key` only for local dev demonstration.
- **Node environments:** If you run WS in Node, make sure `ws` is installed (the SDK will import it automatically if needed).

---

## Minimal Example

```ts
import { SentimexClient } from "@sentimex/ts-core-sdk";

const client = new SentimexClient("demo-user", {
  initial: { accessToken: "", refreshToken: "", expiresIn: 0 },
  async fetchToken() { /* …return { accessToken, refreshToken, expiresIn } */ },
  refresh: { async refreshWith(rt) { /* … */ } },
});

const me = await client.getProfile();
console.log("Status:", me.Status);

await client.connect(msg => console.log("WS:", msg));
client.subscribe({ category: "custom" });
```

---

## License

MIT © Sentimex