UNPKG

@spawnco/server

Version:

Server SDK

843 lines (839 loc) 28.9 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { TokenVerifier: () => TokenVerifier, createSDK: () => createSDK }); module.exports = __toCommonJS(index_exports); // src/token-verifier.ts var import_jsonwebtoken = __toESM(require("jsonwebtoken")); var import_jwk_to_pem = __toESM(require("jwk-to-pem")); var TokenVerifier = class { // 1 hour constructor(jwksUrl) { this.cache = /* @__PURE__ */ new Map(); this.CACHE_DURATION = 36e5; this.jwksUrl = jwksUrl || `${process.env.SPAWN_API_URL || "https://kiln.spawn.com"}/api/.well-known/jwks.json`; } async verify(token) { const decoded = import_jsonwebtoken.default.decode(token, { complete: true }); if (!decoded || typeof decoded === "string") { throw new Error("Invalid token format"); } const kid = decoded.header.kid; if (!kid) { throw new Error("Token missing key ID"); } const publicKey = await this.getPublicKey(kid); return import_jsonwebtoken.default.verify(token, publicKey, { algorithms: ["RS256"] }); } async getPublicKey(kid) { const cached = this.cache.get(kid); if (cached && Date.now() - cached.fetchedAt < this.CACHE_DURATION) { return cached.key; } const response = await fetch(this.jwksUrl); if (!response.ok) { throw new Error( `Failed to fetch JWKS: ${response.status} ${response.statusText}` ); } const jwks = await response.json(); const jwk = jwks.keys.find((k) => k.kid === kid); if (!jwk) { throw new Error(`Key ${kid} not found in JWKS`); } const pem = (0, import_jwk_to_pem.default)(jwk); this.cache.set(kid, { key: pem, fetchedAt: Date.now() }); return pem; } // Clear cache on rotation clearCache() { this.cache.clear(); } }; // src/create-sdk.ts function createSDK(env, options) { 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) => `${env.SPAWN_VARIANT_ID}:global:${key}`; const makeRoomKey = (key, roomId) => `${env.SPAWN_VARIANT_ID}:room:${roomId}:${key}`; const makeUserKey = (key, userId) => `${env.SPAWN_VARIANT_ID}:user:${userId}:${key}`; const sdk = { 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(); return config; }, 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(); return version; }, effectiveVersion: async () => { const configVersion = env.SPAWN_CONFIG_VERSION; if (configVersion && configVersion !== "live") { return String(configVersion); } if (!env.SPAWN_VARIANT_ID) { return "1"; } if (!env.SPAWN_SDK_API_KEY) { console.warn("SPAWN_SDK_API_KEY not set, using default version 1"); return "1"; } 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(); return String(liveConfig?.version || 1); } } catch (error) { console.error("Failed to fetch live config version:", error); } return "1"; } }, sparks: { verifyPurchase: async ({ purchaseId, price }) => { 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(); return success && transaction.amountPaid === price; } }, user: { token: { verify: async (token) => { try { const verified = await tokenVerifier.verify( token ); const now = Math.floor(Date.now() / 1e3); if (verified.exp <= now) { return { valid: false }; } const user = { id: verified.sub, username: verified.username || verified.email || "Anonymous", avatarUrl: void 0, // Not included in token isGuest: verified.isGuest }; return { valid: true, user, expires: new Date(verified.exp * 1e3), payload: verified }; } catch (error) { console.error("Token verification failed:", error); return { valid: false }; } } } }, chat: { checkIsMuted: 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}/chat/mute-status?userId=${encodeURIComponent( userId )}`, { headers: { Authorization: `Bearer ${env.SPAWN_SDK_API_KEY}` } } ); if (!response.ok) { return false; } const result = await response.json(); return Boolean(result.isMuted); } }, storage: { room: { get: async (key) => { 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 value2 = await options.storage.get( makeRoomKey(key, env.SPAWN_ROOM_ID) ); if (value2 !== null) { return value2; } } 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(); return value; }, set: async (key, value) => { 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) => { 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) => { 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 value2 = await options.storage.get(makeGlobalKey(key)); if (value2 !== null) { return value2; } } 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(); return value; }, set: async (key, value) => { 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) => { 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) => ({ get: async (key) => { 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 value2 = await options.storage.get(makeUserKey(key, userId)); if (value2 !== null) { return value2; } } 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(); return value; }, set: async (key, value) => { 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) => { 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, leaderboardId, score) => { 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(); if (errorData.error) { errorMessage = `Failed to submit leaderboard score: ${errorData.error}`; } } catch { } throw new Error(errorMessage); } }, getMultiple: async (leaderboardIds, options2) => { 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: options2?.period || "all", limit: options2?.limit || 10 }) } ); if (!response.ok) { let errorMessage = `Failed to fetch leaderboards: ${response.statusText}`; try { const errorData = await response.json(); if (errorData.error) { errorMessage = `Failed to fetch leaderboards: ${errorData.error}`; } } catch { } throw new Error(errorMessage); } const result = await response.json(); return result; } }, inventory: { currencies: { credit: async (userId, currency, amount, options2) => { 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: options2?.idempotencyKey, reason: options2?.reason }) } ); if (!response.ok) { const error = await response.json(); throw new Error( error.error || `Failed to credit currency: ${response.statusText}` ); } const result = await response.json(); return { newBalance: result.newBalance }; }, debit: async (userId, currency, amount, options2) => { 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: options2?.idempotencyKey, reason: options2?.reason }) } ); if (!response.ok) { const error = await response.json(); throw new Error( error.error || `Failed to debit currency: ${response.statusText}` ); } const result = await response.json(); return { success: result.success, newBalance: result.newBalance }; }, exchange: async (userId, tx, options2) => { 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: options2?.idempotencyKey, reason: options2?.reason }) } ); if (!response.ok) { const error = await response.json(); throw new Error( error.error || `Failed to exchange currencies: ${response.statusText}` ); } const result = await response.json(); 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(); throw new Error( error.error || `Failed to get balances: ${response.statusText}` ); } const result = await response.json(); return result.balances; } }, items: { grant: async (userId, items, options2) => { 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: options2?.idempotencyKey, reason: options2?.reason }) } ); if (!response.ok) { const error = await response.json(); throw new Error( error.error || `Failed to grant items: ${response.statusText}` ); } const result = await response.json(); return result.instanceIds; }, consume: async (userId, instanceId, options2) => { 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: options2?.idempotencyKey, reason: options2?.reason }) } ); if (!response.ok) { const error = await response.json(); throw new Error( error.error || `Failed to consume item: ${response.statusText}` ); } const result = await response.json(); 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(); throw new Error( error.error || `Failed to get inventory: ${response.statusText}` ); } const result = await response.json(); return result.items; } } } }; return sdk; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { TokenVerifier, createSDK });