@spawnco/server
Version:
Server SDK
843 lines (839 loc) • 28.9 kB
JavaScript
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
});
;