Turisteo logo Turisteo SDK • Docs

Turisteo SDK for TypeScript

Typed, modular, and developer-friendly SDK to interact with the Turisteo platform from web, mobile, or backend apps. Built for performance and DX.

✨ Features

  • Full TypeScript support with rich types, interfaces, and enums.
  • Clean, modular design and adapter-based token storage.
  • DTO-driven request/response contracts for safety and clarity.
  • Guest sessions and centralized ApiError handling.
  • Escape hatch via sdk.call() for not‑yet‑wrapped endpoints.

🚀 Installation

Create an .npmrc with your GitHub PAT (scope: read:packages):

@turisteo:registry=https://npm.pkg.github.com/
//npm.pkg.github.com/:_authToken=YOUR_TOKEN

Do not commit your .npmrc.

⚡ Quickstart

import { initializeNodeSDK, type TuristeoSDKOptions } from "@turisteo/turisteo-sdk-ts";
import { MemoryStorageAdapter } from "@turisteo/turisteo-sdk-ts/core/token-manager/adapters/memory-storage.adapter";

const config: TuristeoSDKOptions = {
  apiKey: "test-api-key",
  clientId: "test-project-key",
  bundleId: "com.turisteo.test",
  storage: "sqlite",
  mock: false,
  appVersion: "1.0.0",
  environment: "local",
  debug: true,
  cache: { enabled: true },
};

const sdk = await initializeNodeSDK(config, new MemoryStorageAdapter());

🌐 Web (React)

Share one browser SDK instance across the app with a typed context:

import { createContext, ReactNode, useContext } from 'react';
import { initializeBrowserSDK, TuristeoSDK, TuristeoSDKOptions } from '@turisteo/turisteo-sdk-ts/dist/browser';

const TuristeoSDKContext = createContext(null);
export function useTuristeo() {
  const ctx = useContext(TuristeoSDKContext);
  if (!ctx) throw new Error('useTuristeo must be used within a TuristeoProvider');
  return ctx;
}

let sdkInstance: TuristeoSDK | null = null;
export async function initializeSharedSDK() {
  if (sdkInstance) return sdkInstance;
  const config: TuristeoSDKOptions = {
    apiKey: import.meta.env.VITE_API_KEY || 'dev-api-key',
    clientId: import.meta.env.VITE_CLIENT_ID || 'dev-client-id',
    platform: import.meta.env.VITE_PLATFORM || 'web',
    storage: import.meta.env.VITE_STORAGE || 'in-memory',
    storagePath: import.meta.env.VITE_STORAGE_PATH || './data.db',
    appVersion: '1.0.0',
    environment: (import.meta.env.VITE_APP_ENV as 'local' | 'local') || 'development',
    bundleId: import.meta.env.VITE_BUNDLE_ID || 'com.turisteo.dev-cp-vendor-admin',
    cache: { enabled: true, storage: 'in-memory' },
  };
  sdkInstance = await initializeBrowserSDK(config);
  return sdkInstance;
}

export function TuristeoProvider({ sdk, children }: { sdk: TuristeoSDK; children: ReactNode; }) {
  return {children};
}
import { useEffect, useState } from 'react';
import { BrowserRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from 'react-query';
import { TuristeoSDK } from '@turisteo/turisteo-sdk-ts/dist/browser';
import { ScreenLoader } from './components/common/screen-loader';
import { initializeSharedSDK, TuristeoProvider } from './lib/turisteo/sdk';

export function App() {
  const queryClient = new QueryClient();
  const [sdk, setSdk] = useState(null);
  useEffect(() => { initializeSharedSDK().then(setSdk); }, []);
  if (!sdk) return ;
  return (
    
      
        {/* ... */}
      
    
  );
}

📱 React Native

Initialize once, coerce booleans from env, and expose via context:

import { createContext, useContext, ReactNode, FC } from 'react';
import { initializeReactNativeSDK, TuristeoSDK, TuristeoSDKOptions } from '@turisteo/turisteo-sdk-ts/react-native';
import Config from 'react-native-config';

type SDKContextType = TuristeoSDK | null;
const TuristeoSDKContext = createContext(null);
export function useTuristeo(): TuristeoSDK { const c = useContext(TuristeoSDKContext); if (!c) throw new Error('useTuristeo must be used within a TuristeoProvider'); return c; }

let sdkInstance: TuristeoSDK | null = null;
const toBoolean = (val?: string, def = true) => (val === undefined ? def : val.toLowerCase() === 'true');

export async function initializeSharedSDK(): Promise {
  if (sdkInstance) return sdkInstance;
  const config: TuristeoSDKOptions = {
    apiKey: Config.SDK_APP_API_KEY || 'dev-api-key',
    appVersion: Config.SDK_APP_VERSION || '1.0.0',
    platform: Config.SDK_APP_APP_PLATFORM,
    environment: (Config.SDK_APP_APP_ENV as 'local' | 'development') || 'development',
    bundleId: Config.SDK_APP_BUNDLE_ID || 'com.turisteo.app.dev',
    clientId: Config.SDK_APP_CLIENT_ID || 'dev-turisteo-react-native',
    cache: { enabled: toBoolean(Config.SDK_APP_ENABLE_CACHE, true), storage: 'in-memory' },
  };
  sdkInstance = await initializeReactNativeSDK(config);
  return sdkInstance;
}

export const TuristeoProvider: FC<{ sdk: TuristeoSDK; children: ReactNode; }> = ({ sdk, children }) => (
  {children}
);
import { useEffect, useState } from 'react';
import { TuristeoSDK } from '@turisteo/turisteo-sdk-ts/react-native';
import { initializeSharedSDK, TuristeoProvider } from '@lib/turisteo/sdk';

const RNApp = () => {
  const [sdk, setSdk] = useState(null);
  useEffect(() => { initializeSharedSDK().then(setSdk); }, []);
  if (!sdk) return null;
  return (
    
      {/* Providers and App */}
    
  );
};

🖥️ Node

Server-side bootstrap with optional .env:

import dotenv from 'dotenv';
dotenv.config();

import { initializeNodeSDK, type TuristeoSDKOptions } from '@turisteo/turisteo-sdk-ts';

async function main() {
  const config: TuristeoSDKOptions = {
    apiKey: process.env.API_KEY || 'test-api-key',
    clientId: process.env.CLIENT_ID || 'test-project-key',
    bundleId: 'com.turisteo.test',
    platform: 'node',
    appVersion: '1.0.0',
    environment: 'local',
    baseUrl: 'http://localhost',
    storage: 'sqlite',
    mock: false,
    cache: { enabled: true },
  };

  const turisteo = await initializeNodeSDK(config);
  try {
    const isLoggedIn = await turisteo.auth.session.isAuthenticated();
    if (!isLoggedIn) {
      await turisteo.auth.session.login({
        identifier: process.env.IDENTIFIER || 'user@example.com',
        password: process.env.PASSWORD || 'password123',
      });
    }

    const spots = await turisteo.call({ method: 'GET', path: '/spots', requiresAuth: false, debug: true });
    console.log(spots);
  } catch (err) {
    console.error('An error occurred:', err);
  }
}

main();

🔧 Raw API Calls (Escape Hatch)

Use sdk.call<T>() for endpoints not yet wrapped by semantic methods. It respects your SDK configuration (auth, headers, base URL/version, cache, debug) and preserves strong typing via the generic T and the expects hint.

Signature

public async call<T = any>(config: RequestConfig & { expects: 'array' }): Promise<T[] | null>
public async call<T = any>(config: RequestConfig & { expects: 'single' }): Promise<T | null>
public async call<T = any>(config: RequestConfig): Promise<T | T[] | null>

If expects is omitted, return type can be T | T[] | null.

RequestConfig

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';

interface RequestConfig<T = any> {
  method: HttpMethod;              // HTTP verb
  path: string;                    // relative path e.g. "spots?take=10"
  body?: any;                      // payload for POST/PUT
  requiresAuth?: boolean;          // attach access token if available
  headers?: Record<string, string>;// per-call headers
  debug?: boolean | DebugOptions;  // enable logs for this call
  cache?: {
    ttl?: number;                  // override default TTL (seconds)
    forceRefresh?: boolean;        // bypass cache for this call
  };
}

Caching, auth, and debug inherit from the SDK instance but can be overridden per call.

Behavior
  • Base URL & Version: The SDK’s baseUrl and optional version (from initialization) are applied before path.
  • Auth: If requiresAuth is true, the SDK adds the bearer token via getAccessToken(). On expiry, onTokenExpired() (if provided) is invoked.
  • Caching: Controlled by SDK cache options (enabled, ttl, storage, storagePath). Per-call cache.ttl overrides default; forceRefresh skips the cache.
  • Debug: Per-call debug or SDK-wide debug/DebugOptions emits HTTP/SDK logs.
  • Typing: Use call<YourType>() with expects = 'single' or 'array' to lock the return shape.

Examples

GET list (expects array)

import type { Spot } from '@turisteo/turisteo-sdk-ts/types';

const spots = await sdk.call<Spot>({
  method: 'GET',
  path: 'spots?take=10',
  requiresAuth: false,
  expects: 'array',
});
// spots: Spot[] | null

GET single (paginated wrapper)

import type { PaginatedResult, Comment } from '@turisteo/turisteo-sdk-ts/types';

const comments = await sdk.call<PaginatedResult<Comment>>({
  method: 'GET',
  path: 'spots?take=10',
  requiresAuth: false,
  debug: true,
  expects: 'single',
});
// comments: PaginatedResult | null

POST with body

type CreatePostDto = { text: string };

type Created = { id: string; text: string };

const created = await sdk.call<Created>({
  method: 'POST',
  path: 'social/posts',
  requiresAuth: true,
  headers: { 'Content-Type': 'application/json' },
  body: { text: 'Hello Turisteo!' } satisfies CreatePostDto,
  expects: 'single',
});
// created: Created | null

Per-call cache control

// Override TTL and force network fetch
const fresh = await sdk.call<unknown>({
  method: 'GET',
  path: 'spots/nearby?lat=18.4&lng=-69.9',
  cache: { ttl: 120, forceRefresh: true },
  expects: 'single',
});

Custom headers

const data = await sdk.call<unknown>({
  method: 'GET',
  path: 'spots',
  headers: { 'x-client-id': 'dashboard', 'accept-language': 'en-US' },
  expects: 'single',
});

Error handling

try {
  const result = await sdk.call<unknown>({ method: 'GET', path: 'spots', expects: 'single' });
} catch (e) {
  // Network or SDK-level error; SDK raises ApiError consistently
  console.error(e);
}
Tips
  • Use expects: 'array' when the endpoint returns a list to keep strict typing (e.g., Spot[]).
  • Prefer relative path (e.g., "spots", "social/posts"); the SDK prefixes baseUrl and optional version.
  • For authenticated calls, set requiresAuth: true; the SDK will retrieve and attach the token.
  • Turn on debug: true (or a specific DebugOptions field) to inspect the HTTP request/response.
  • Fine-tune caching globally via SDK options (cache.enabled, ttl, max, storage, storagePath) or per call via cache.

🔑 Token Storage

Token handling is centralized by TokenManager with a pluggable TokenStorageAdapter. The manager persists tokens + expiration, exposes helpers to check lifetime, and can proactively refresh when nearing expiry via a caller-provided refreshFn.

Interfaces

export interface TokenStorageAdapter {
  getToken(): Promise;
  getRefreshToken(): Promise;
  getExpiresAt(): Promise;
  set(token: string, refresh: string, expiresAt: number): Promise;
  clear(): Promise;
}

Bring your own storage: memory, SecureStore/Keychain, AsyncStorage, SQLite, etc.

Lifecycle

  • TokenManager.use(adapter) → sets the adapter and loads expiresAt if present.
  • TokenManager.setConfig({ refreshThreshold, debug }) → sets debug and refresh window.
  • TokenManager.set(token, refresh, expiresIn) → persists tokens and computes expiresAt.
  • TokenManager.validateOrRefresh(refreshFn) → if close to expiry and a refresh token exists, calls refreshFn.
  • TokenManager.clear() → clears tokens and resets expiration state.
Behavior & Config
  • Refresh threshold: configured in seconds via setConfig({ refreshThreshold }); internally stored in ms.
  • Debug logs: enable with setConfig({ debug: true }); logs are printed via the SDK’s Logger.
  • Expiration source: on use(), the manager reads a persisted expiresAt from the adapter (if any).
  • Validation rule: validateOrRefresh() does nothing if there is no access token. If time remaining < threshold and a refresh token exists, it awaits your refreshFn().
  • Time helpers: getTimeUntilExpiration(), getRefreshThreshold(), and getExpiresAt() provide visibility into token state.

Methods

class TokenManager {
  static async use(adapter: TokenStorageAdapter): Promise;
  static setConfig(cfg: { refreshThreshold?: number; debug?: boolean }): void;
  static async set(token: string, refresh: string, expiresIn: number): Promise;
  static async getToken(): Promise;
  static async getRefreshToken(): Promise;
  static async validateOrRefresh(refreshFn: () => Promise): Promise;
  static getTimeUntilExpiration(): number;        // ms remaining (0 if unknown)
  static getRefreshThreshold(): number;           // ms threshold
  static async getExpiresAt(): Promise;
  static async clear(): Promise;
}

Adapters (examples)

In‑Memory (Web/Node)

export const MemoryAdapter = (): TokenStorageAdapter => {
  let t: string | null = null, r: string | null = null, e: number | null = null;
  return {
    async getToken() { return t; },
    async getRefreshToken() { return r; },
    async getExpiresAt() { return e; },
    async set(token, refresh, expiresAt) { t = token; r = refresh; e = expiresAt; },
    async clear() { t = r = null; e = 0; },
  };
};

React Native (AsyncStorage)

import AsyncStorage from '@react-native-async-storage/async-storage';

const KEY = {
  token: 'turisteo.token',
  refresh: 'turisteo.refresh',
  exp: 'turisteo.expiresAt',
};

export const RNStorageAdapter = (): TokenStorageAdapter => ({
  async getToken() { return (await AsyncStorage.getItem(KEY.token)) || null; },
  async getRefreshToken() { return (await AsyncStorage.getItem(KEY.refresh)) || null; },
  async getExpiresAt() { return Number(await AsyncStorage.getItem(KEY.exp)) || null; },
  async set(token, refresh, expiresAt) {
    await AsyncStorage.multiSet([[KEY.token, token],[KEY.refresh, refresh],[KEY.exp, String(expiresAt)]]);
  },
  async clear() { await AsyncStorage.multiRemove([KEY.token, KEY.refresh, KEY.exp]); },
});

Usage

Web / React

import { TokenManager } from '@turisteo/turisteo-sdk-ts/core/token-manager';
import { MemoryAdapter } from './adapters/memory';

await TokenManager.use(MemoryAdapter());
TokenManager.setConfig({ refreshThreshold: 20, debug: false }); // seconds

// later, after login:
await TokenManager.set(accessToken, refreshToken, /* expiresIn */ 3600);

// on app focus / interval:
await TokenManager.validateOrRefresh(async () => {
  // call your refresh endpoint and update tokens
});

React Native

import { TokenManager } from '@turisteo/turisteo-sdk-ts/core/token-manager';
import { RNStorageAdapter } from './adapters/rn-async-storage';

await TokenManager.use(RNStorageAdapter());
TokenManager.setConfig({ refreshThreshold: 20, debug: false });

// after login
await TokenManager.set(accessToken, refreshToken, 3600);

// e.g., on AppState change to 'active'
await TokenManager.validateOrRefresh(async () => { /* refresh flow */ });

Node

import { TokenManager } from '@turisteo/turisteo-sdk-ts/core/token-manager';
import Keyv from 'keyv';

const store = new Keyv();
const NodeAdapter = (): TokenStorageAdapter => ({
  async getToken() { return (await store.get('t')) || null; },
  async getRefreshToken() { return (await store.get('r')) || null; },
  async getExpiresAt() { return (await store.get('e')) || null; },
  async set(t, r, e) { await store.set('t', t); await store.set('r', r); await store.set('e', e); },
  async clear() { await store.delete('t'); await store.delete('r'); await store.delete('e'); },
});

await TokenManager.use(NodeAdapter());
TokenManager.setConfig({ refreshThreshold: 20, debug: false });
Tips
  • Store expiresAt as an absolute epoch (ms) — TokenManager.set() calculates this from expiresIn.
  • Use validateOrRefresh() on app start, resume, or before critical calls to keep sessions fresh.
  • Call clear() on logout to remove tokens from storage.
  • Toggle verbose logs with setConfig({ debug: true }) while developing.

🧯 API Error Handling

The SDK provides a unified ApiError class to wrap HTTP and application-level errors. This ensures consistent error handling and allows developers to branch logic based on HTTP status codes and error messages.

Class Definition

export class ApiError extends Error {
  constructor(
    public statusCode: number,
    message: string,
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

When It's Thrown

  • When an HTTP response status code is outside the 2xx range.
  • When the API signals an application-specific error condition.
  • When a network or parsing error is caught and wrapped by the SDK.

Example Usage

try {
  const spots = await sdk.call({
    method: 'GET',
    path: 'spots',
    requiresAuth: true,
    expects: 'array',
  });
} catch (err) {
  if (err instanceof ApiError) {
    console.error(`API Error (${err.statusCode}):`, err.message);
    if (err.statusCode === 401) {
      // Handle authentication errors
    }
  } else {
    console.error('Unexpected error:', err);
  }
}

Best Practices

  • Always check err instanceof ApiError before handling.
  • Use statusCode for branching logic (e.g., 400 vs 401 vs 500).
  • Enable SDK debug mode in development to log detailed error information.

📚 API Surface

The SDK exposes typed clients organized by domain. Below are the primary namespaces and their responsibilities. Each client uses the shared HttpClient, integrates with TokenManager when needed, and accepts a refreshTokenFn (for proactive refresh) where applicable.

auth

  • auth.session — login, logout, refresh, and session state (SessionClient).
  • auth.account — account registration and profile (AccountClient).

social

  • social.post — posts & comments via a base client (create, like, unlike, edit, delete, addComment, getComments).
Shared Behaviors
  • Auth gate: Methods that require authentication call TokenManager.validateOrRefresh(refreshTokenFn) before performing the request.
  • Return shapes: Most semantic methods return a single entity (e.g., Post, User) or null. Collections are returned via explicit pagination DTOs (e.g., PaginatedCommentResponse).
  • Request typing: Methods set expects: 'single' where a single object is expected; unpaginated lists should use expects: 'array' when implemented.
  • Caching: Some reads support per-call cache options (e.g., getComments(..., { forceRefresh })).
  • Debugging: Each client uses a scoped Logger that can be enabled via the debug flag.

🧠 Semantic Methods

Typed, purpose-specific methods built on top of the low-level HTTP client. Expand a section to view signatures and usage.

auth.sessionSessionClient

Methods

login(payload: LoginPayload): Promise<LoginResponse>
isAuthenticated(): Promise<boolean>
refreshToken(): Promise<AuthResponse>
logout(): Promise<void>
  • login POSTs credentials, returns LoginResponse, and persists tokens via TokenManager.set().
  • isAuthenticated checks token presence and time-to-expiration.
  • refreshToken POSTs the stored refresh token and updates access token/expiry.
  • logout clears tokens via TokenManager.clear().

Example

const ok = await sdk.auth.session.isAuthenticated();
if (!ok) {
  const res = await sdk.auth.session.login({
    identifier: 'user@example.com',
    password: 'password123',
  });
  // res.token.accessToken, res.token.refreshToken, res.token.expiresIn
}
auth.accountAccountClient

Methods

register(payload: RegisterPayload): Promise<User | null>
getProfile(): Promise<User | null>
  • register — public registration; does not require auth.
  • getProfile — requires auth; validates/refreshes tokens before request.

Example

const me = await sdk.auth.account.getProfile();
if (!me) {
  // not logged in or profile not found
}
Notes
  • All method responses follow the DTOs defined under types/dto in the SDK.
  • When implementing new domains, follow the same pattern: inject HttpClient, pass refreshTokenFn, gate auth, and use expects appropriately.
  • Mocks: endpoints can be simulated via mockHandlers (e.g., authMockHandlers) using the SDK's mock adapter for testing.

👥 Social API Surface

The SocialClient namespaces social features into feed, post, and follow. Each sub‑client uses the shared HttpClient, validates tokens via TokenManager.validateOrRefresh(), and supports per‑call cache controls where applicable.

Structure

class SocialClient {
  public feed: FeedClient;
  public post: PostClient;               // extends BasePostAndCommentsClient
  public follow: FollowClient;
}
  • feed — read feeds (me, following, global) with pagination and optional forceRefresh.
  • post — create/edit/delete posts and manage comments (create, list).
  • follow — follow/unfollow users and list followers/following.

Shared Behaviors

  • Auth Gate: Methods marked as requiring auth call TokenManager.validateOrRefresh(refreshTokenFn) before requests.
  • Typing: Methods set expects: 'single' when a single entity is returned; paginated responses use DTOs.
  • Caching: Feeds support cache: { forceRefresh } to bypass cache on demand.
  • Debug: Each client has a scoped Logger honoring the debug flag.

🧠 Semantic Methods — Social

social.feedFeedClient

Methods

getMyFeed(
  params?: { page?: number; take?: number },
  forceRefresh = false,
): Promise<PaginatedFeedResponse>

getFollowingFeed(
  params?: { page?: number; take?: number },
  forceRefresh = false,
): Promise<PaginatedFeedResponse>

getGlobalFeed(
  params?: { page?: number; take?: number },
  forceRefresh = false,
): Promise<PaginatedFeedResponse>
  • All feed reads require auth and validate/refresh tokens.
  • Pagination via page and take; default order is DESC.
  • Returns an empty, well‑formed page when the backend returns an invalid shape.

Example

// page 1, 10 items, bypass cache
const my = await sdk.social.feed.getMyFeed({ page: 1, take: 10 }, true);

// following feed (cached)
const following = await sdk.social.feed.getFollowingFeed({ page: 2, take: 20 });

// global feed
const global = await sdk.social.feed.getGlobalFeed();
social.followFollowClient

Methods

followUser(userUuid: string): Promise<void>
unfollowUser(userUuid: string): Promise<void>
getFollowers<T>(): Promise<T | null>
getFollowing<T>(): Promise<T | null>
  • followUser/unfollowUser require auth and use POST/DELETE.
  • getFollowers / getFollowing are typed as generic T to match your DTO shape.

Example

await sdk.social.follow.followUser('uuid-user-123');
await sdk.social.follow.unfollowUser('uuid-user-123');

// Explicit DTO typing
interface FollowersDto { users: Array<{ id: string; username: string }>; }
const followers = await sdk.social.follow.getFollowers();
social.postPostClient

Methods

create(payload: CreatePostPayload): Promise<Post | null>
like(postUuid: string): Promise<Post | null>
unlike(postUuid: string): Promise<Post | null>
edit<T>(params: { postUuid: string; payload: T }): Promise<Post | null>
delete(postUuid: string): Promise<boolean>
addComment(params: { postUuid: string; content: string }): Promise<Comment | null>
getComments(
  postUuid: string,
  options?: { page?: number; take?: number; forceRefresh?: boolean },
): Promise<PaginatedCommentResponse>
  • Auth‑gated methods validate/refresh tokens prior to calling the API.
  • getComments uses URLSearchParams and can bypass cache per call.
  • Warns and returns an empty, well‑formed page on invalid backend shapes.

Example

const post = await sdk.social.post.create({ text: 'Hello Turisteo!' });
await sdk.social.post.like(post!.id);
await sdk.social.post.addComment({ postUuid: post!.id, content: '🔥' });
const comments = await sdk.social.post.getComments(post!.id, { page: 1, take: 10 });
const ok = await sdk.social.post.delete(post!.id);
Mocks

For testing, endpoints can be simulated via the mock adapter using MockHandlerMap. Example handlers show paginating feeds and filtering comments by postId.

export const userFeedMockHandlers: MockHandlerMap = {
  [`GET ${'${API_PATHS.social.feed.me}'}`]: async (db, config) => {/* paginate */},
  [`POST ${'${API_PATHS.social.post.create}'}`]: (db, config) => db.create('posts', config.body),
  [`GET ${'${API_PATHS.social.post.comments(":postId")}'}`]: async (db, config) => {/* filter by postId */},
};