swcombine.js
v0.1.0
Published
TypeScript SDK for the Star Wars Combine API
Downloads
213
Maintainers
Readme
swcombine.js
TypeScript SDK for the Star Wars Combine API
Installation
bun installFeatures
✅ Phase 1: OAuth 2.0 Authentication (Complete)
- Three flexible authentication modes:
- Full OAuth mode with automatic token refresh and storage
- Direct access token mode (for pre-obtained tokens)
- Utility OAuth mode (URL generation and manual token refresh without storage)
- Full OAuth 2.0 flow implementation
- Authorization URL generation
- Token exchange (authorization code → access token)
- Automatic token refresh (when using storage)
- Manual token refresh utility method
- Token revocation
- Custom token storage support
- Built-in memory storage
- Strongly-typed OAuth scopes (100+ scopes with descriptions)
✅ Phase 2: HTTP Client (Complete)
- Resource-based API client (
client.character.get()) - Automatic authentication with OAuth tokens
- Comprehensive error handling (typed error classes)
- Rate limit tracking and RateLimitError
- Support for 4 core resources:
api,character,faction,inventory - Inventory resource with 11 entity types (ships, vehicles, stations, cities, facilities, planets, items, npcs, droids, creatures, materials)
- Advanced filtering with InventoryFilters interface
- Entity-specific operations (properties and tags management)
- Custom request options (headers, timeout, abort signals)
🚧 Phase 3: API Resources (Coming Soon)
- Additional API resources (events, market, etc.)
- Type-safe response types from API spec
- Auto-generated resource methods
- Full TypeScript support for all endpoints
Quick Start
Authentication
The SDK supports three authentication modes:
1. Full OAuth Mode (Recommended for Server-Side)
Complete OAuth flow with automatic token management and refresh:
import { OAuthManager, SWCClient, MemoryTokenStorage } from "swcombine.js";
// Initialize OAuth manager with storage for token persistence
const storage = new MemoryTokenStorage(); // Or implement custom TokenStorage
const oauth = new OAuthManager(
{
clientId: "your-client-id",
clientSecret: "your-client-secret",
redirectUri: "https://example.com/callback",
defaultScopes: ['character_read', 'faction_members'],
},
storage
);
// Step 1: Generate authorization URL
const authUrl = oauth.getAuthorizationUrl({
state: "random-csrf-token",
accessType: "offline", // Request refresh token
});
// Redirect user to authUrl...
// Step 2: Handle callback (after user authorizes)
const tokens = await oauth.handleCallback(authorizationCode);
// Step 3: Create API client (tokens auto-refresh)
const client = new SWCClient({ auth: oauth });
const character = await client.character.get();
// Step 4: Revoke when done
await oauth.revoke();2. Direct Access Token Mode
Use when you already have an access token (no auto-refresh):
import { SWCClient } from "swcombine.js";
// Create client with direct access token
const client = new SWCClient({ auth: "your-access-token" });
// Make API calls (token won't auto-refresh)
const character = await client.character.get();3. Utility OAuth Mode
Use OAuthManager as a utility without storage (for URL generation or manual token refresh):
import { OAuthManager } from "swcombine.js";
// Initialize without storage
const oauth = new OAuthManager({
clientId: "your-client-id",
clientSecret: "your-client-secret",
redirectUri: "https://example.com/callback",
});
// Generate authorization URL
const authUrl = oauth.getAuthorizationUrl({
scopes: ['character_read'],
});
// Manually refresh a token (without storage)
const newTokens = await oauth.refreshAccessToken(refreshToken);
// Use newTokens.access_token with direct access token modeUsing the HTTP Client
Once authenticated, use the SWCClient to interact with the API:
import { SWCClient } from "swcombine.js";
// Create client (with OAuth manager or direct token)
const client = new SWCClient({ auth: oauth }); // or { auth: "token" }
// Character endpoints
const character = await client.character.get();
const skills = await client.character.skills.get();
const credits = await client.character.credits.get();
const creditlog = await client.character.creditlog.get();
const messages = await client.character.messages.get();
const message = await client.character.messages.get("123"); // Specific message
// Transfer character credits
await client.character.credits.post({
recipient: "Character Name",
amount: 50000,
reason: "Payment for services", // optional
});
// Send a message
await client.character.messages.put({
receivers: "Character1;Character2;Character3",
communication: "Hello everyone!",
});
// Delete a message
await client.character.messages.delete("message-uid");
// Grant or revoke a privilege
await client.character.privilege.post("privilegegroup/privilege", {
revoke: "1", // optional - set to revoke instead of grant
});
// Get character UID by handle
const characterInfo = await client.character.handlecheck.get("CharacterName");
// Faction endpoints
const myFaction = await client.faction.get();
const specificFaction = await client.faction.get("123");
const members = await client.faction.members.get();
const budgets = await client.faction.budgets.get();
const stockholders = await client.faction.stockholders.get();
// Transfer faction credits
await client.faction.credits.post({
recipient: "Character Name",
amount: 100000,
budget: "budget-uid", // optional
reason: "Payment for services", // optional
});
// Inventory endpoints
const inventory = await client.inventory.get("character-uid");
// List entity types (ships, vehicles, stations, etc.)
const ships = await client.inventory.ships.get("character-uid");
const ownedShips = await client.inventory.ships.get("character-uid", "owner");
// List with filters
const filteredShips = await client.inventory.ships.get("character-uid", "owner", {
filters: {
filter_type: ["tags"],
filter_value: { tags: ["combat", "transport"] },
filter_inclusion: { tags: "includes" },
},
});
// Get specific entity
const ship = await client.inventory.entity.ships.get("ship-uid");
// Manage entity properties
const shipProps = await client.inventory.entity.ships.properties.get("ship-uid");
await client.inventory.entity.ships.properties.post("ship-uid", {
property_name: "custom_name",
property_value: "My Ship",
});
// Manage entity tags
const shipTags = await client.inventory.entity.ships.tags.get("ship-uid");
await client.inventory.entity.ships.tags.post("ship-uid", { tags: ["combat"] });
await client.inventory.entity.ships.tags.delete("ship-uid", { tags: ["old-tag"] });
// API utility endpoints
const rateLimits = await client.api.rateLimits();
const permissions = await client.api.permissions();
const serverTime = await client.api.time();Error Handling
The client throws typed errors for different failure scenarios:
import {
SWCClient,
RateLimitError,
AuthenticationError,
ValidationError,
NotFoundError,
ServerError,
NetworkError,
} from "swcombine.js";
try {
const character = await client.character.get();
console.log(character);
} catch (error) {
if (error instanceof RateLimitError) {
// Rate limit exceeded
console.log(`Rate limited! Reset at ${error.rateLimit.reset}`);
console.log(`Remaining: ${error.rateLimit.remaining}/${error.rateLimit.limit}`);
} else if (error instanceof AuthenticationError) {
// 401 - Need to re-authenticate
console.log("Authentication failed");
} else if (error instanceof ValidationError) {
// 400 - Bad request
console.log("Invalid request:", error.response);
} else if (error instanceof NotFoundError) {
// 404 - Resource not found
console.log("Resource not found");
} else if (error instanceof ServerError) {
// 5xx - Server error
console.log(`Server error (${error.statusCode}):`, error.message);
} else if (error instanceof NetworkError) {
// Network/fetch failure
console.log("Network error:", error.message);
}
}Advanced Options
Customize individual requests with options:
// Custom headers
const character = await client.character.get({
headers: {
"X-Custom-Header": "value",
},
});
// Request timeout (in milliseconds)
const skills = await client.character.skills.get({
timeout: 5000, // 5 second timeout
});
// Abort signal for cancellation
const controller = new AbortController();
const promise = client.faction.members.get({
signal: controller.signal,
});
// Cancel the request
controller.abort();Timestamp Utility - Combine Galactic Time (CGT)
Convert between Unix timestamps, JavaScript Dates, and Star Wars Combine Galactic Time:
import { Timestamp } from "swcombine.js";
// Get current CGT
const now = Timestamp.now();
console.log(now.toString()); // "Year 27 Day 134, 8:45:23"
// Convert from Unix timestamp
const timestamp = Timestamp.fromUnixTimestamp(1735920000);
console.log(timestamp.toString("day")); // "Year 27 Day 12"
// Convert from Date
const date = new Date();
const cgt = Timestamp.fromDate(date);
// Create specific CGT moment
const moment = new Timestamp({
year: 25,
day: 60,
hour: 12,
minute: 30,
});
// Access components
console.log(moment.getYear()); // 25
console.log(moment.getDay()); // 60
console.log(moment.getHour()); // 12
console.log(moment.getMinute()); // 30
// Time arithmetic
const future = moment.add({ days: 5, hours: 3 });
const past = moment.subtract({ years: 1 });
// Get duration between timestamps
const duration = moment.getDurationTo(future);
console.log(duration); // { years: 0, days: 5, hours: 3, minutes: 0, seconds: 0 }
// Formatting options
now.toString("full"); // "Year 27 Day 134, 8:45:23"
now.toString("minute"); // "Year 27 Day 134, 8:45"
now.toString("day"); // "Year 27 Day 134"
now.toString("shortFull"); // "Y27 D134, 8:45:23"
now.toString("shortDay"); // "Y27 D134"
// Custom formatting with tags
now.toString("Day {d} of Year {y} at {hms}");
// "Day 134 of Year 27 at 08:45:23"
// Convert back to Unix/Date
const unixSeconds = moment.toUnixTimestamp("sec");
const unixMs = moment.toUnixTimestamp("ms");
const dateObj = moment.toDate();Custom Token Storage
By default, tokens are stored in memory. Implement custom storage for persistence:
import { TokenStorage, StoredTokens } from "swcombine.js";
class FileTokenStorage implements TokenStorage {
async get(): Promise<StoredTokens | null> {
// Read from file, database, etc.
}
async set(tokens: StoredTokens): Promise<void> {
// Save to file, database, etc.
}
async clear(): Promise<void> {
// Clear stored tokens
}
}
const oauth = new OAuthManager(config, new FileTokenStorage());Working with Scopes
All OAuth scopes are strongly typed using string literal union types. Get full IntelliSense and type safety with simple strings!
import { Scopes } from "swcombine.js";
import type { ScopeKey } from "swcombine.js";
// Use string literals with full type safety and IntelliSense
const myScopes: ScopeKey[] = [
'character_read',
'faction_members',
'messages_read',
];
// Get scope information programmatically
const scope = Scopes['character_read'];
console.log(scope.name); // "character_read"
console.log(scope.description); // "Read basic character information..."
console.log(scope.inherits); // [] (array of inherited scopes)
// Check what a scope inherits
const allScope = Scopes['character_all'];
console.log(allScope.inherits); // ['character_credits_write', ...]Available Scope Categories:
- Character (read, stats, skills, credits, location, events, etc.)
- Messages (read, send, delete)
- Personal Inventory (ships, vehicles, stations, cities, facilities, planets, items, NPCs, droids, materials, creatures)
- Faction (read, members, stocks, credits, budgets, datacards)
- Faction Inventory (ships, vehicles, stations, cities, facilities, planets, items, NPCs, droids, materials, creatures)
See src/auth/scopes.ts for the complete list of 100+ available scopes.
API Reference
SWCClient
Main client for interacting with the Star Wars Combine API.
Constructor
new SWCClient(oauth: OAuthManager, baseUrl?: string)
new SWCClient(config: { oauth: OAuthManager, baseUrl?: string })Resources
client.api- API utility endpointshelloWorld()- Test endpoint (no auth required)helloAuth()- Test authenticated endpointpermissions()- List available OAuth permissionsrateLimits()- Get current rate limit statustime()- Get server time
client.character- Character dataget()- Get basic character infocreditlog.get()- Get credit transaction historypermissions.get()- Get OAuth permissionsskills.get()- Get character skillsprivileges.get()- Get all privilegesprivilege.get(id)- Get specific privilegeprivilege.post(id, data)- Grant or revoke a privilege (requirescharacter_privileges)credits.get()- Get current creditscredits.post(data)- Transfer character credits (requirescharacter_credits_write)messages.get(id?)- Get messages or specific messagemessages.put(data)- Send a message (requiresmessages_send)messages.delete(id)- Delete a message (requiresmessages_delete)handlecheck.get(handle)- Get character UID by handle (no authentication required)
client.faction- Faction dataget(id?)- Get faction info (your faction or specific ID)budgets.get()- Get all budgetsbudget.get(id)- Get specific budgetcreditlog.get()- Get credit transaction historymembers.get()- Get faction membersstockholders.get()- Get stockholderscredits.get()- Get faction creditscredits.post(data)- Transfer faction credits (requiresfaction_credits_writepermission)
client.inventory- Inventory dataget(uid)- Get inventory overview for character or faction- Entity type resources (ships, vehicles, stations, cities, facilities, planets, items, npcs, droids, creatures, materials):
<entityType>.get(uid, assignType?, options?)- List entities with optional filters
- Entity-specific resources:
entity.<entityType>.get(entityUid)- Get specific entity detailsentity.<entityType>.properties.get(entityUid)- Get entity propertiesentity.<entityType>.properties.post(entityUid, data)- Set entity propertiesentity.<entityType>.tags.get(entityUid)- Get entity tagsentity.<entityType>.tags.post(entityUid, data)- Add entity tagsentity.<entityType>.tags.delete(entityUid, data)- Remove entity tags
OAuthManager
Main class for handling OAuth flows.
Constructor
new OAuthManager(config: OAuthConfig, storage?: TokenStorage)Methods
getAuthorizationUrl(options?)- Generate authorization URLhandleCallback(code)- Exchange authorization code for tokensgetAccessToken()- Get valid access token (auto-refreshes)isAuthenticated()- Check if user is authenticatedgetStoredTokens()- Get stored tokenssetTokens(tokens)- Manually set tokensrevoke()- Revoke refresh token and clear storagelogout()- Clear stored tokens without revoking
Low-Level Functions
For manual control:
import {
generateAuthorizationUrl,
exchangeCodeForToken,
refreshAccessToken,
revokeToken,
} from "swcombine.js";
const url = generateAuthorizationUrl({
clientId: "...",
redirectUri: "...",
scopes: ['character_read', 'faction_members'],
});
const tokens = await exchangeCodeForToken({
code: "...",
clientId: "...",
clientSecret: "...",
redirectUri: "...",
});
const newTokens = await refreshAccessToken({
refreshToken: "...",
clientId: "...",
clientSecret: "...",
});
await revokeToken({
token: "...",
clientId: "...",
});Examples
Run the OAuth example:
bun run example:oauthTesting
bun testImportant Notes
SWC Combine OAuth Quirks
The SWC Combine API has some non-standard OAuth behavior:
- Authorization header: Uses
Authorization: OAuth TOKENinstead ofBearer - Refresh tokens: Only returned on first token exchange when
access_type=offline - Consent persistence: Subsequent requests with same scopes won't re-prompt users
API Endpoint Implementation Status
This section tracks which API endpoints have been implemented in the SDK.
API Resource (5/6 implemented - 83%)
- [x]
GET /api/helloworld-client.api.helloWorld() - [x]
GET /api/helloauth-client.api.helloAuth() - [x]
GET /api/permissions-client.api.permissions() - [x]
GET /api/ratelimits-client.api.rateLimits() - [x]
GET /api/time-client.api.time() - [ ]
POST /api/time- Convert CGT/timestamps
Character Resource (14/14 implemented - 100%) ✅
- [x]
GET /character-client.character.get() - [x]
GET /character/{uid}/creditlog-client.character.creditlog.get() - [x]
GET /character/{uid}/permissions-client.character.permissions.get() - [x]
GET /character/{uid}/skills-client.character.skills.get() - [x]
GET /character/{uid}/privileges-client.character.privileges.get() - [x]
GET /character/{uid}/privileges/{group}/{priv}-client.character.privilege.get(id) - [x]
POST /character/{uid}/privileges/{group}/{priv}-client.character.privilege.post(id, data) - [x]
GET /character/{uid}/credits-client.character.credits.get() - [x]
POST /character/{uid}/credits-client.character.credits.post(data) - [x]
GET /character/{uid}/messages-client.character.messages.get() - [x]
PUT /character/{uid}/messages-client.character.messages.put(data) - [x]
GET /character/{uid}/messages/{msguid}-client.character.messages.get(id) - [x]
DELETE /character/{uid}/messages/{msguid}-client.character.messages.delete(id) - [x]
GET /character/handlecheck/{handle}-client.character.handlecheck.get(handle)
Faction Resource (9/11 implemented - 82%)
- [x]
GET /faction-client.faction.get() - [x]
GET /faction/{uid}-client.faction.get(id) - [x]
GET /faction/{uid}/budgets-client.faction.budgets.get() - [x]
GET /faction/{uid}/budget/{budgetuid}-client.faction.budget.get(id) - [x]
GET /faction/{uid}/creditlog-client.faction.creditlog.get() - [x]
GET /faction/{uid}/members-client.faction.members.get() - [ ]
POST /faction/{uid}/members- Update member info fields - [x]
GET /faction/{uid}/stockholders-client.faction.stockholders.get() - [x]
GET /faction/{uid}/credits-client.faction.credits.get() - [x]
POST /faction/{uid}/credits-client.faction.credits.post(data) - [ ]
GET /factions- List all factions
Inventory Resource (3/11 implemented - 27%)
⚠️ Note: Current implementation uses different structure than API spec
- [x]
GET /inventory/{uid}-client.inventory.get(uid) - [x]
GET /inventory/{uid}/{entity_type}/{assign_type}-client.inventory.{entityType}.get(uid, assignType, options) - [x]
GET /inventory/{entity_type}/{uid}-client.inventory.entity.{entityType}.get(entityUid) - [ ]
GET /inventory/{entity_type}/{uid}/properties- Get entity properties - [ ]
POST /inventory/{entity_type}/{uid}/{property}- Update specific property - [ ]
GET /inventory/{entity_type}/{uid}/tags- Get entity tags - [ ]
PUT /inventory/{entity_type}/{uid}/tag/{tag}- Add/modify tag - [ ]
DELETE /inventory/{entity_type}/{uid}/tag/{tag}- Remove tag
Datacard Resource (0/4 implemented - 0%)
- [ ]
GET /datacard/{uid}- Get datacard details - [ ]
POST /datacard/{uid}- Assign datacard - [ ]
DELETE /datacard/{uid}- Revoke datacard - [ ]
GET /datacards/{uid}- List faction datacards
Events Resource (0/2 implemented - 0%)
- [ ]
GET /events/{event_mode}/{event_type?}- Get events collection - [ ]
GET /event/{uid}- Get specific event
Galaxy Resource (0/10 implemented - 0%)
- [ ]
GET /galaxy/cities- List all cities - [ ]
GET /galaxy/cities/{uid}- Get city details - [ ]
GET /galaxy/planets- List all planets - [ ]
GET /galaxy/planets/{uid}- Get planet details - [ ]
GET /galaxy/sectors- List all sectors - [ ]
GET /galaxy/sectors/{uid}- Get sector details - [ ]
GET /galaxy/stations- List all stations - [ ]
GET /galaxy/stations/{uid}- Get station details - [ ]
GET /galaxy/systems- List all systems - [ ]
GET /galaxy/systems/{uid}- Get system details
Implementation Summary
- API Resource: 5/6 endpoints (83%)
- Character Resource: 14/14 endpoints (100%) ✅
- Faction Resource: 9/11 endpoints (82%)
- Inventory Resource: 3/11 endpoints (27% - needs refactoring)
- Datacard Resource: 0/4 endpoints (0%)
- Events Resource: 0/2 endpoints (0%)
- Galaxy Resource: 0/10 endpoints (0%)
Overall: 31/58 endpoints implemented (53%)
Development Roadmap
- [x] Phase 1: OAuth 2.0 Authentication
- [x] URL generation
- [x] Token exchange
- [x] Token refresh
- [x] Token revocation
- [x] Token management
- [x] Tests
- [x] Phase 2: HTTP Client
- [x] Resource-based client architecture
- [x] Automatic authentication
- [x] Comprehensive error handling
- [x] Rate limit tracking
- [x] Core resources (api, character, faction)
- [x] Inventory resource with 11 entity types
- [x] Advanced filtering with InventoryFilters
- [x] Entity-specific operations (properties, tags)
- [x] Tests
- [ ] Phase 3: API Resources & Types
- [ ] Additional resources (events, market, etc.)
- [ ] Parse response specs
- [ ] Type-safe response types
- [ ] Auto-generate from API spec
License
MIT
