UNPKG

@spawnco/server

Version:

Server SDK

901 lines (808 loc) 28.3 kB
import type { InventoryItem, LeaderboardEntry, SpawnServerSDK__V0, TokenPayload, User, } from "@spawnco/sdk-types"; import { TokenVerifier } from "./token-verifier"; interface Env { SPAWN_VARIANT_ID?: string; SPAWN_CONFIG_VERSION?: string; SPAWN_API_URL?: string; SPAWN_SDK_API_KEY?: string; SPAWN_ROOM_ID?: string; } export interface IStorage { get<T>(key: string): Promise<T | null>; set<T>(key: string, value: T): Promise<void>; delete(key: string): Promise<void>; } interface CreateSDKOptions { storage: IStorage; } export function createSDK<TConfig = any>( env: Env, options?: CreateSDKOptions ): SpawnServerSDK__V0<TConfig> { const apiUrl = env.SPAWN_API_URL || "https://www.spawn.co"; const jwksUrl = `${apiUrl}/api/.well-known/jwks.json`; const tokenVerifier = new TokenVerifier(jwksUrl); if (!env.SPAWN_VARIANT_ID) { throw new Error("SPAWN_VARIANT_ID not set in environment"); } const makeGlobalKey = (key: string) => `${env.SPAWN_VARIANT_ID}:global:${key}`; const makeRoomKey = (key: string, roomId: string) => `${env.SPAWN_VARIANT_ID}:room:${roomId}:${key}`; const makeUserKey = (key: string, userId: string) => `${env.SPAWN_VARIANT_ID}:user:${userId}:${key}`; const sdk: SpawnServerSDK__V0<TConfig> = { config: { get: async () => { if (!env.SPAWN_VARIANT_ID) { throw new Error("SPAWN_VARIANT_ID not set in environment"); } if (!env.SPAWN_SDK_API_KEY) { throw new Error("SPAWN_SDK_API_KEY not set in environment"); } const configVersion = await sdk.config.effectiveVersion(); const response = await fetch( `${apiUrl}/api/sdk/v1/${env.SPAWN_VARIANT_ID}/config/get/${configVersion}`, { headers: { Authorization: `Bearer ${env.SPAWN_SDK_API_KEY}`, }, } ); if (!response.ok) { throw new Error(`Failed to fetch config: ${response.statusText}`); } const { config } = (await response.json()) as { config: TConfig }; return config as TConfig; }, version: async () => { if (!env.SPAWN_VARIANT_ID) { throw new Error("SPAWN_VARIANT_ID not set in environment"); } if (!env.SPAWN_SDK_API_KEY) { throw new Error("SPAWN_SDK_API_KEY not set in environment"); } const configVersion = await sdk.config.effectiveVersion(); const response = await fetch( `${apiUrl}/api/sdk/v1/${env.SPAWN_VARIANT_ID}/config/get/${configVersion}`, { headers: { Authorization: `Bearer ${env.SPAWN_SDK_API_KEY}`, }, } ); if (!response.ok) { throw new Error( `Failed to fetch config version: ${response.statusText}` ); } const { version } = (await response.json()) as { version: string }; return version; }, effectiveVersion: async () => { const configVersion = env.SPAWN_CONFIG_VERSION; // If config version is explicitly set, return it if (configVersion && configVersion !== "live") { return String(configVersion); } // Otherwise, fetch the live config version if (!env.SPAWN_VARIANT_ID) { return "1"; // Default fallback } if (!env.SPAWN_SDK_API_KEY) { console.warn("SPAWN_SDK_API_KEY not set, using default version 1"); return "1"; // Default fallback } try { const response = await fetch( `${apiUrl}/api/sdk/v1/${env.SPAWN_VARIANT_ID}/config/live`, { headers: { Authorization: `Bearer ${env.SPAWN_SDK_API_KEY}`, }, } ); if (response.ok) { const liveConfig = (await response.json()) as { version: number; }; return String(liveConfig?.version || 1); } } catch (error) { console.error("Failed to fetch live config version:", error); } return "1"; // Default fallback }, }, sparks: { verifyPurchase: async ({ purchaseId, price, }: { purchaseId: string; price: number; }) => { const response = await fetch( `${apiUrl}/api/sdk/v1/${env.SPAWN_VARIANT_ID}/sparks/transactions/${purchaseId}/claim`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${env.SPAWN_SDK_API_KEY}`, }, body: JSON.stringify({}), } ); if (!response.ok) { throw new Error(`Failed to verify purchase: ${response.statusText}`); } const { success, transaction } = (await response.json()) as { success: boolean; transaction: { amountPaid: number }; }; return success && transaction.amountPaid === price; }, }, user: { token: { verify: async (token) => { try { // Use JWKS-based verification const verified = (await tokenVerifier.verify( token )) as TokenPayload; // Check if token is expired const now = Math.floor(Date.now() / 1000); if (verified.exp <= now) { return { valid: false }; } // Construct user object from token payload const user: User = { id: verified.sub, username: verified.username || verified.email || "Anonymous", avatarUrl: undefined, // Not included in token isGuest: verified.isGuest, }; return { valid: true, user, expires: new Date(verified.exp * 1000), payload: verified, }; } catch (error) { console.error("Token verification failed:", error); return { valid: false }; } }, }, }, chat: { checkIsMuted: async (userId: string) => { if (!env.SPAWN_VARIANT_ID) { throw new Error("SPAWN_VARIANT_ID not set in environment"); } if (!env.SPAWN_SDK_API_KEY) { throw new Error("SPAWN_SDK_API_KEY not set in environment"); } const response = await fetch( `${apiUrl}/api/sdk/v1/${env.SPAWN_VARIANT_ID}/chat/mute-status?userId=${encodeURIComponent( userId )}`, { headers: { Authorization: `Bearer ${env.SPAWN_SDK_API_KEY}`, }, } ); if (!response.ok) { // Fail open (treat as not muted) to avoid blocking due to network errors return false; } const result = (await response.json()) as { isMuted?: boolean }; return Boolean(result.isMuted); }, }, storage: { room: { get: async (key: string) => { if (!env.SPAWN_VARIANT_ID) { throw new Error("SPAWN_VARIANT_ID not set in environment"); } if (!env.SPAWN_SDK_API_KEY) { throw new Error("SPAWN_SDK_API_KEY not set in environment"); } if (!env.SPAWN_ROOM_ID) { throw new Error("SPAWN_ROOM_ID not set in environment"); } if (options?.storage) { const value = await options.storage.get( makeRoomKey(key, env.SPAWN_ROOM_ID) ); if (value !== null) { return value; } } const response = await fetch( `${apiUrl}/api/sdk/v1/${env.SPAWN_VARIANT_ID}/storage/room/${encodeURIComponent(key)}`, { headers: { Authorization: `Bearer ${env.SPAWN_SDK_API_KEY}`, "X-Room-Id": env.SPAWN_ROOM_ID, }, } ); if (response.status === 404) { return null; } if (!response.ok) { throw new Error( `Failed to get storage value: ${response.statusText}` ); } const { value } = (await response.json()) as { value: any }; return value; }, set: async (key: string, value: any) => { if (!env.SPAWN_VARIANT_ID) { throw new Error("SPAWN_VARIANT_ID not set in environment"); } if (!env.SPAWN_SDK_API_KEY) { throw new Error("SPAWN_SDK_API_KEY not set in environment"); } if (!env.SPAWN_ROOM_ID) { throw new Error("SPAWN_ROOM_ID not set in environment"); } if (options?.storage) { await options.storage.set( makeRoomKey(key, env.SPAWN_ROOM_ID), value ); return; } const response = await fetch( `${apiUrl}/api/sdk/v1/${env.SPAWN_VARIANT_ID}/storage/room/${encodeURIComponent(key)}`, { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${env.SPAWN_SDK_API_KEY}`, "X-Room-Id": env.SPAWN_ROOM_ID, }, body: JSON.stringify({ value }), } ); if (!response.ok) { throw new Error( `Failed to set storage value: ${response.statusText}` ); } }, delete: async (key: string) => { if (!env.SPAWN_VARIANT_ID) { throw new Error("SPAWN_VARIANT_ID not set in environment"); } if (!env.SPAWN_SDK_API_KEY) { throw new Error("SPAWN_SDK_API_KEY not set in environment"); } if (!env.SPAWN_ROOM_ID) { throw new Error("SPAWN_ROOM_ID not set in environment"); } if (options?.storage) { await options.storage.delete(makeRoomKey(key, env.SPAWN_ROOM_ID)); return; } const response = await fetch( `${apiUrl}/api/sdk/v1/${env.SPAWN_VARIANT_ID}/storage/room/${encodeURIComponent(key)}`, { method: "DELETE", headers: { Authorization: `Bearer ${env.SPAWN_SDK_API_KEY}`, "X-Room-Id": env.SPAWN_ROOM_ID, }, } ); if (!response.ok) { throw new Error( `Failed to delete storage value: ${response.statusText}` ); } }, }, global: { get: async (key: string) => { if (!env.SPAWN_VARIANT_ID) { throw new Error("SPAWN_VARIANT_ID not set in environment"); } if (!env.SPAWN_SDK_API_KEY) { throw new Error("SPAWN_SDK_API_KEY not set in environment"); } if (options?.storage) { const value = await options.storage.get(makeGlobalKey(key)); if (value !== null) { return value; } } const response = await fetch( `${apiUrl}/api/sdk/v1/${env.SPAWN_VARIANT_ID}/storage/global/${encodeURIComponent(key)}`, { headers: { Authorization: `Bearer ${env.SPAWN_SDK_API_KEY}`, }, } ); if (response.status === 404) { return null; } const { value } = (await response.json()) as { value: any }; return value; }, set: async (key: string, value: any) => { if (!env.SPAWN_VARIANT_ID) { throw new Error("SPAWN_VARIANT_ID not set in environment"); } if (!env.SPAWN_SDK_API_KEY) { throw new Error("SPAWN_SDK_API_KEY not set in environment"); } if (options?.storage) { await options.storage.set(makeGlobalKey(key), value); return; } const response = await fetch( `${apiUrl}/api/sdk/v1/${env.SPAWN_VARIANT_ID}/storage/global/${encodeURIComponent(key)}`, { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${env.SPAWN_SDK_API_KEY}`, }, body: JSON.stringify({ value }), } ); if (!response.ok) { throw new Error( `Failed to set storage value: ${response.statusText}` ); } }, delete: async (key: string) => { if (!env.SPAWN_VARIANT_ID) { throw new Error("SPAWN_VARIANT_ID not set in environment"); } if (!env.SPAWN_SDK_API_KEY) { throw new Error("SPAWN_SDK_API_KEY not set in environment"); } if (options?.storage) { await options.storage.delete(makeGlobalKey(key)); return; } const response = await fetch( `${apiUrl}/api/sdk/v1/${env.SPAWN_VARIANT_ID}/storage/global/${encodeURIComponent(key)}`, { method: "DELETE", headers: { Authorization: `Bearer ${env.SPAWN_SDK_API_KEY}`, }, } ); if (!response.ok) { throw new Error( `Failed to delete storage value: ${response.statusText}` ); } }, }, user: (userId: string) => ({ get: async (key: string) => { if (!env.SPAWN_VARIANT_ID) { throw new Error("SPAWN_VARIANT_ID not set in environment"); } if (!env.SPAWN_SDK_API_KEY) { throw new Error("SPAWN_SDK_API_KEY not set in environment"); } if (options?.storage) { const value = await options.storage.get(makeUserKey(key, userId)); if (value !== null) { return value; } } const response = await fetch( `${apiUrl}/api/sdk/v1/${env.SPAWN_VARIANT_ID}/storage/user/${userId}/${encodeURIComponent(key)}`, { headers: { Authorization: `Bearer ${env.SPAWN_SDK_API_KEY}`, }, } ); if (response.status === 404) { return null; } if (!response.ok) { throw new Error( `Failed to get storage value: ${response.statusText}` ); } const { value } = (await response.json()) as { value: any }; return value; }, set: async (key: string, value: any) => { if (!env.SPAWN_VARIANT_ID) { throw new Error("SPAWN_VARIANT_ID not set in environment"); } if (!env.SPAWN_SDK_API_KEY) { throw new Error("SPAWN_SDK_API_KEY not set in environment"); } if (options?.storage) { await options.storage.set(makeUserKey(key, userId), value); return; } const response = await fetch( `${apiUrl}/api/sdk/v1/${env.SPAWN_VARIANT_ID}/storage/user/${userId}/${encodeURIComponent(key)}`, { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${env.SPAWN_SDK_API_KEY}`, }, body: JSON.stringify({ value }), } ); if (!response.ok) { throw new Error( `Failed to set storage value: ${response.statusText}` ); } }, delete: async (key: string) => { if (!env.SPAWN_VARIANT_ID) { throw new Error("SPAWN_VARIANT_ID not set in environment"); } if (!env.SPAWN_SDK_API_KEY) { throw new Error("SPAWN_SDK_API_KEY not set in environment"); } if (options?.storage) { await options.storage.delete(makeUserKey(key, userId)); return; } const response = await fetch( `${apiUrl}/api/sdk/v1/${env.SPAWN_VARIANT_ID}/storage/user/${userId}/${encodeURIComponent(key)}`, { method: "DELETE", headers: { Authorization: `Bearer ${env.SPAWN_SDK_API_KEY}`, }, } ); if (!response.ok) { throw new Error( `Failed to delete storage value: ${response.statusText}` ); } }, }), }, leaderboard: { submit: async (userId: string, leaderboardId: string, score: number) => { if (!env.SPAWN_VARIANT_ID) { throw new Error("SPAWN_VARIANT_ID not set in environment"); } if (!env.SPAWN_SDK_API_KEY) { throw new Error("SPAWN_SDK_API_KEY not set in environment"); } const response = await fetch( `${apiUrl}/api/sdk/v1/${env.SPAWN_VARIANT_ID}/leaderboard/bulk`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${env.SPAWN_SDK_API_KEY}`, }, body: JSON.stringify({ action: "submit", userId, leaderboardId, score, }), } ); if (!response.ok) { let errorMessage = `Failed to submit leaderboard score: ${response.statusText}`; try { const errorData = (await response.json()) as { error: string }; if (errorData.error) { errorMessage = `Failed to submit leaderboard score: ${errorData.error}`; } } catch { // If we can't parse the error response, use the default message } throw new Error(errorMessage); } }, getMultiple: async ( leaderboardIds: string[], options?: { period?: "today" | "week" | "month" | "all"; limit?: number; } ) => { if (!env.SPAWN_VARIANT_ID) { throw new Error("SPAWN_VARIANT_ID not set in environment"); } if (!env.SPAWN_SDK_API_KEY) { throw new Error("SPAWN_SDK_API_KEY not set in environment"); } const response = await fetch( `${apiUrl}/api/sdk/v1/${env.SPAWN_VARIANT_ID}/leaderboard/bulk`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${env.SPAWN_SDK_API_KEY}`, }, body: JSON.stringify({ action: "getMultiple", leaderboardIds, period: options?.period || "all", limit: options?.limit || 10, }), } ); if (!response.ok) { let errorMessage = `Failed to fetch leaderboards: ${response.statusText}`; try { const errorData = (await response.json()) as { error: string }; if (errorData.error) { errorMessage = `Failed to fetch leaderboards: ${errorData.error}`; } } catch { // If we can't parse the error response, use the default message } throw new Error(errorMessage); } const result = (await response.json()) as Record< string, LeaderboardEntry[] >; return result; }, }, inventory: { currencies: { credit: async (userId, currency, amount, options) => { if (!env.SPAWN_VARIANT_ID) { throw new Error("SPAWN_VARIANT_ID not set in environment"); } if (!env.SPAWN_SDK_API_KEY) { throw new Error("SPAWN_SDK_API_KEY not set in environment"); } const response = await fetch( `${apiUrl}/api/sdk/v1/${env.SPAWN_VARIANT_ID}/inventory/currencies/credit`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${env.SPAWN_SDK_API_KEY}`, }, body: JSON.stringify({ userId, currency, amount, idempotencyKey: options?.idempotencyKey, reason: options?.reason, }), } ); if (!response.ok) { const error = (await response.json()) as { error: string }; throw new Error( error.error || `Failed to credit currency: ${response.statusText}` ); } const result = (await response.json()) as { newBalance: number }; return { newBalance: result.newBalance }; }, debit: async (userId, currency, amount, options) => { if (!env.SPAWN_VARIANT_ID) { throw new Error("SPAWN_VARIANT_ID not set in environment"); } if (!env.SPAWN_SDK_API_KEY) { throw new Error("SPAWN_SDK_API_KEY not set in environment"); } const response = await fetch( `${apiUrl}/api/sdk/v1/${env.SPAWN_VARIANT_ID}/inventory/currencies/debit`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${env.SPAWN_SDK_API_KEY}`, }, body: JSON.stringify({ userId, currency, amount, idempotencyKey: options?.idempotencyKey, reason: options?.reason, }), } ); if (!response.ok) { const error = (await response.json()) as { error: string }; throw new Error( error.error || `Failed to debit currency: ${response.statusText}` ); } const result = (await response.json()) as { success: boolean; newBalance: number; }; return { success: result.success, newBalance: result.newBalance, }; }, exchange: async (userId, tx, options) => { if (!env.SPAWN_VARIANT_ID) { throw new Error("SPAWN_VARIANT_ID not set in environment"); } if (!env.SPAWN_SDK_API_KEY) { throw new Error("SPAWN_SDK_API_KEY not set in environment"); } const response = await fetch( `${apiUrl}/api/sdk/v1/${env.SPAWN_VARIANT_ID}/inventory/currencies/exchange`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${env.SPAWN_SDK_API_KEY}`, }, body: JSON.stringify({ userId, debit: tx.debit, credit: tx.credit, idempotencyKey: options?.idempotencyKey, reason: options?.reason, }), } ); if (!response.ok) { const error = (await response.json()) as { error: string }; throw new Error( error.error || `Failed to exchange currencies: ${response.statusText}` ); } const result = (await response.json()) as { success: boolean; balances: Record<string, number>; }; return { success: result.success, balances: result.balances, }; }, get: async (userId) => { if (!env.SPAWN_VARIANT_ID) { throw new Error("SPAWN_VARIANT_ID not set in environment"); } if (!env.SPAWN_SDK_API_KEY) { throw new Error("SPAWN_SDK_API_KEY not set in environment"); } const response = await fetch( `${apiUrl}/api/sdk/v1/${env.SPAWN_VARIANT_ID}/inventory/currencies?userId=${userId}`, { headers: { Authorization: `Bearer ${env.SPAWN_SDK_API_KEY}`, }, } ); if (!response.ok) { const error = (await response.json()) as { error: string }; throw new Error( error.error || `Failed to get balances: ${response.statusText}` ); } const result = (await response.json()) as { balances: Record<string, number>; }; return result.balances; }, }, items: { grant: async (userId, items, options) => { if (!env.SPAWN_VARIANT_ID) { throw new Error("SPAWN_VARIANT_ID not set in environment"); } if (!env.SPAWN_SDK_API_KEY) { throw new Error("SPAWN_SDK_API_KEY not set in environment"); } const response = await fetch( `${apiUrl}/api/sdk/v1/${env.SPAWN_VARIANT_ID}/inventory/items/grant`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${env.SPAWN_SDK_API_KEY}`, }, body: JSON.stringify({ userId, items, idempotencyKey: options?.idempotencyKey, reason: options?.reason, }), } ); if (!response.ok) { const error = (await response.json()) as { error: string }; throw new Error( error.error || `Failed to grant items: ${response.statusText}` ); } const result = (await response.json()) as { instanceIds: string[] }; return result.instanceIds; }, consume: async (userId, instanceId, options) => { if (!env.SPAWN_VARIANT_ID) { throw new Error("SPAWN_VARIANT_ID not set in environment"); } if (!env.SPAWN_SDK_API_KEY) { throw new Error("SPAWN_SDK_API_KEY not set in environment"); } const response = await fetch( `${apiUrl}/api/sdk/v1/${env.SPAWN_VARIANT_ID}/inventory/items/consume`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${env.SPAWN_SDK_API_KEY}`, }, body: JSON.stringify({ userId, instanceId, idempotencyKey: options?.idempotencyKey, reason: options?.reason, }), } ); if (!response.ok) { const error = (await response.json()) as { error: string }; throw new Error( error.error || `Failed to consume item: ${response.statusText}` ); } const result = (await response.json()) as { success: boolean }; return result.success; }, list: async (userId) => { if (!env.SPAWN_VARIANT_ID) { throw new Error("SPAWN_VARIANT_ID not set in environment"); } if (!env.SPAWN_SDK_API_KEY) { throw new Error("SPAWN_SDK_API_KEY not set in environment"); } const response = await fetch( `${apiUrl}/api/sdk/v1/${env.SPAWN_VARIANT_ID}/inventory/items?userId=${userId}`, { headers: { Authorization: `Bearer ${env.SPAWN_SDK_API_KEY}`, }, } ); if (!response.ok) { const error = (await response.json()) as { error: string }; throw new Error( error.error || `Failed to get inventory: ${response.statusText}` ); } const result = (await response.json()) as { items: InventoryItem[] }; return result.items as InventoryItem[]; }, }, }, }; return sdk; }