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 optionalversion
(from initialization) are applied beforepath
. - Auth: If
requiresAuth
is true, the SDK adds the bearer token viagetAccessToken()
. On expiry,onTokenExpired()
(if provided) is invoked. - Caching: Controlled by SDK
cache
options (enabled, ttl, storage, storagePath). Per-callcache.ttl
overrides default;forceRefresh
skips the cache. - Debug: Per-call
debug
or SDK-widedebug
/DebugOptions
emits HTTP/SDK logs. - Typing: Use
call<YourType>()
withexpects
='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 prefixesbaseUrl
and optionalversion
. - For authenticated calls, set
requiresAuth: true
; the SDK will retrieve and attach the token. - Turn on
debug: true
(or a specificDebugOptions
field) to inspect the HTTP request/response. - Fine-tune caching globally via SDK options (
cache.enabled
,ttl
,max
,storage
,storagePath
) or per call viacache
.
🔑 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 loadsexpiresAt
if present.TokenManager.setConfig({ refreshThreshold, debug })
→ sets debug and refresh window.TokenManager.set(token, refresh, expiresIn)
→ persists tokens and computesexpiresAt
.TokenManager.validateOrRefresh(refreshFn)
→ if close to expiry and a refresh token exists, callsrefreshFn
.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’sLogger
. - Expiration source: on
use()
, the manager reads a persistedexpiresAt
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 yourrefreshFn()
. - Time helpers:
getTimeUntilExpiration()
,getRefreshThreshold()
, andgetExpiresAt()
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 fromexpiresIn
. - 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
) ornull
. 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 useexpects: '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 thedebug
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.session — SessionClient
Methods
login(payload: LoginPayload): Promise<LoginResponse>
isAuthenticated(): Promise<boolean>
refreshToken(): Promise<AuthResponse>
logout(): Promise<void>
login
POSTs credentials, returnsLoginResponse
, and persists tokens viaTokenManager.set()
.isAuthenticated
checks token presence and time-to-expiration.refreshToken
POSTs the stored refresh token and updates access token/expiry.logout
clears tokens viaTokenManager.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.account — AccountClient
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
, passrefreshTokenFn
, gate auth, and useexpects
appropriately. - Mocks: endpoints can be simulated via
mockHandlers
(e.g.,authMockHandlers
) using the SDK's mock adapter for testing.