@spawnco/server
Version:
Server SDK
901 lines (808 loc) • 28.3 kB
text/typescript
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;
}