npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

swcombine.js

v0.1.0

Published

TypeScript SDK for the Star Wars Combine API

Downloads

213

Readme

swcombine.js

TypeScript SDK for the Star Wars Combine API

Installation

bun install

Features

✅ 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 mode

Using 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 endpoints

    • helloWorld() - Test endpoint (no auth required)
    • helloAuth() - Test authenticated endpoint
    • permissions() - List available OAuth permissions
    • rateLimits() - Get current rate limit status
    • time() - Get server time
  • client.character - Character data

    • get() - Get basic character info
    • creditlog.get() - Get credit transaction history
    • permissions.get() - Get OAuth permissions
    • skills.get() - Get character skills
    • privileges.get() - Get all privileges
    • privilege.get(id) - Get specific privilege
    • privilege.post(id, data) - Grant or revoke a privilege (requires character_privileges)
    • credits.get() - Get current credits
    • credits.post(data) - Transfer character credits (requires character_credits_write)
    • messages.get(id?) - Get messages or specific message
    • messages.put(data) - Send a message (requires messages_send)
    • messages.delete(id) - Delete a message (requires messages_delete)
    • handlecheck.get(handle) - Get character UID by handle (no authentication required)
  • client.faction - Faction data

    • get(id?) - Get faction info (your faction or specific ID)
    • budgets.get() - Get all budgets
    • budget.get(id) - Get specific budget
    • creditlog.get() - Get credit transaction history
    • members.get() - Get faction members
    • stockholders.get() - Get stockholders
    • credits.get() - Get faction credits
    • credits.post(data) - Transfer faction credits (requires faction_credits_write permission)
  • client.inventory - Inventory data

    • get(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 details
      • entity.<entityType>.properties.get(entityUid) - Get entity properties
      • entity.<entityType>.properties.post(entityUid, data) - Set entity properties
      • entity.<entityType>.tags.get(entityUid) - Get entity tags
      • entity.<entityType>.tags.post(entityUid, data) - Add entity tags
      • entity.<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 URL
  • handleCallback(code) - Exchange authorization code for tokens
  • getAccessToken() - Get valid access token (auto-refreshes)
  • isAuthenticated() - Check if user is authenticated
  • getStoredTokens() - Get stored tokens
  • setTokens(tokens) - Manually set tokens
  • revoke() - Revoke refresh token and clear storage
  • logout() - 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:oauth

Testing

bun test

Important Notes

SWC Combine OAuth Quirks

The SWC Combine API has some non-standard OAuth behavior:

  1. Authorization header: Uses Authorization: OAuth TOKEN instead of Bearer
  2. Refresh tokens: Only returned on first token exchange when access_type=offline
  3. 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

Links