spaps-sdk
Version:
Sweet Potato Authentication & Payment Service SDK - Zero-config client with built-in permission checking, role-based access control, and dayrate scheduling
1,213 lines (1,211 loc) • 54.3 kB
JavaScript
"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 __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
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/permissions.ts
var permissions_exports = {};
__export(permissions_exports, {
DEFAULT_ADMIN_ACCOUNTS: () => DEFAULT_ADMIN_ACCOUNTS,
PermissionChecker: () => PermissionChecker,
canAccessAdmin: () => canAccessAdmin,
createPermissionChecker: () => createPermissionChecker,
defaultPermissionChecker: () => defaultPermissionChecker,
getRoleAwareErrorMessage: () => getRoleAwareErrorMessage,
getUserDisplay: () => getUserDisplay,
getUserRole: () => getUserRole,
hasPermission: () => hasPermission,
isAdminAccount: () => isAdminAccount
});
function isAdminAccount(identifier, customAdmins = []) {
if (!identifier) return false;
const normalized = identifier.toLowerCase();
if (normalized === DEFAULT_ADMIN_ACCOUNTS.email.toLowerCase() || normalized === DEFAULT_ADMIN_ACCOUNTS.wallets.ethereum.toLowerCase() || normalized === DEFAULT_ADMIN_ACCOUNTS.wallets.solana.toLowerCase()) {
return true;
}
return customAdmins.some((admin) => {
if (typeof admin === "string") {
return admin.toLowerCase() === normalized;
}
if ("email" in admin && admin.email.toLowerCase() === normalized) {
return true;
}
if ("wallets" in admin) {
return Object.values(admin.wallets).some(
(wallet) => wallet.toLowerCase() === normalized
);
}
return false;
});
}
function getUserRole(identifier, customAdmins = []) {
if (!identifier) return "guest";
if (isAdminAccount(identifier, customAdmins)) {
return "admin";
}
return "user";
}
function hasPermission(user, requiredPermissions, customAdmins = []) {
if (!user) return false;
const identifier = user.email || user.wallet_address;
const userRole = getUserRole(identifier, customAdmins);
if (userRole === "admin") {
return true;
}
if (Array.isArray(requiredPermissions)) {
return requiredPermissions.every(
(permission) => user.permissions?.includes(permission)
);
}
return user.permissions?.includes(requiredPermissions) || false;
}
function canAccessAdmin(user, customAdmins = []) {
if (!user) {
return {
allowed: false,
reason: "Authentication required",
userRole: "guest",
requiredRole: "admin"
};
}
const identifier = user.email || user.wallet_address;
const userRole = getUserRole(identifier, customAdmins);
const isAdmin = userRole === "admin";
return {
allowed: isAdmin,
reason: isAdmin ? void 0 : "Admin privileges required",
userRole,
requiredRole: "admin"
};
}
function getRoleAwareErrorMessage(requiredRole, userRole, action = "perform this action") {
const messages = {
admin: {
user: `\u{1F512} Admin privileges required to ${action}. Please authenticate with an admin account.`,
guest: `\u{1F510} Authentication required. Please sign in with an admin account to ${action}.`
},
user: {
guest: `\u{1F510} Authentication required. Please sign in to ${action}.`
}
};
return messages[requiredRole]?.[userRole] || `Access denied. Required role: ${requiredRole}, current role: ${userRole}`;
}
function getUserDisplay(user, customAdmins = []) {
if (!user) {
return {
displayName: "Guest",
role: "guest",
badge: null,
isAdmin: false
};
}
const identifier = user.email || user.wallet_address;
const role = getUserRole(identifier, customAdmins);
const isAdmin = role === "admin";
return {
displayName: user.email || `${user.wallet_address?.slice(0, 6)}...${user.wallet_address?.slice(-4)}` || "User",
role,
badge: isAdmin ? "\u{1F451} Admin" : null,
isAdmin
};
}
function createPermissionChecker(customAdmins = []) {
return new PermissionChecker(customAdmins);
}
var DEFAULT_ADMIN_ACCOUNTS, PermissionChecker, defaultPermissionChecker;
var init_permissions = __esm({
"src/permissions.ts"() {
"use strict";
DEFAULT_ADMIN_ACCOUNTS = {
email: "buildooor@gmail.com",
wallets: {
ethereum: "0xa72bb7CeF1e4B2Cc144373d8dE0Add7CCc8DF4Ba",
solana: "HVEbdiYU3Rr34NHBSgKs7q8cvdTeZLqNL77Z1FB2vjLy"
}
};
PermissionChecker = class {
customAdmins;
constructor(customAdmins = []) {
this.customAdmins = customAdmins;
}
isAdmin(identifier) {
return isAdminAccount(identifier, this.customAdmins);
}
getRole(identifier) {
return getUserRole(identifier, this.customAdmins);
}
hasPermission(user, permissions) {
return hasPermission(user, permissions, this.customAdmins);
}
canAccessAdmin(user) {
return canAccessAdmin(user, this.customAdmins);
}
getErrorMessage(requiredRole, userRole, action) {
return getRoleAwareErrorMessage(requiredRole, userRole, action);
}
getUserDisplay(user) {
return getUserDisplay(user, this.customAdmins);
}
// Convenience methods
requiresAuth(user) {
return !user;
}
requiresAdmin(user) {
return !this.canAccessAdmin(user).allowed;
}
addCustomAdmin(admin) {
this.customAdmins.push(admin);
}
removeCustomAdmin(admin) {
this.customAdmins = this.customAdmins.filter((a) => a !== admin);
}
};
defaultPermissionChecker = new PermissionChecker();
}
});
// src/index.ts
var index_exports = {};
__export(index_exports, {
DEFAULT_ADMIN_ACCOUNTS: () => DEFAULT_ADMIN_ACCOUNTS,
PermissionChecker: () => PermissionChecker,
SPAPS: () => SPAPSClient,
SPAPSClient: () => SPAPSClient,
TokenManager: () => TokenManager,
WalletUtils: () => WalletUtils,
canAccessAdmin: () => canAccessAdmin,
createBrowserClient: () => createBrowserClient,
createPermissionChecker: () => createPermissionChecker,
createSecureMessageRequestSchema: () => import_spaps_types.createSecureMessageRequestSchema,
createServerClient: () => createServerClient,
default: () => index_default,
defaultPermissionChecker: () => defaultPermissionChecker,
detectKeyType: () => detectKeyType,
getRoleAwareErrorMessage: () => getRoleAwareErrorMessage,
getUserDisplay: () => getUserDisplay,
getUserRole: () => getUserRole,
hasPermission: () => hasPermission,
isAdminAccount: () => isAdminAccount,
secureMessageMetadataSchema: () => import_spaps_types.secureMessageMetadataSchema,
secureMessageSchema: () => import_spaps_types.secureMessageSchema,
verifyCryptoWebhookSignature: () => verifyCryptoWebhookSignature
});
module.exports = __toCommonJS(index_exports);
var import_crypto = __toESM(require("crypto"));
var import_axios = __toESM(require("axios"));
var import_spaps_types = require("spaps-types");
init_permissions();
if (typeof globalThis.fetch === "undefined") {
require("cross-fetch/polyfill");
}
var SPAPSClient = class {
client;
apiKey;
accessToken;
refreshToken;
_isLocalMode = false;
unwrapApiResponse(response, fallback) {
if (!response) {
throw new Error(fallback);
}
const payload = this.isAxiosResponse(response) ? response.data : this.isResponseLikeWithData(response) ? response.data : response;
if (this.isApiResponse(payload)) {
if (payload.success === false) {
throw new Error(payload.error?.message || fallback);
}
if (payload.data !== void 0) {
return payload.data;
}
return void 0;
}
return payload;
}
isAxiosResponse(value) {
if (!value || typeof value !== "object") {
return false;
}
const record = value;
return "data" in record && "status" in record;
}
isResponseLikeWithData(value) {
if (!value || typeof value !== "object") return false;
const record = value;
return "data" in record && !("success" in record);
}
isApiResponse(value) {
if (!value || typeof value !== "object") {
return false;
}
const record = value;
return "success" in record && typeof record.success === "boolean";
}
// Admin namespace for cleaner API
admin = {
createProduct: (productData) => this.createProduct(productData),
updateProduct: (productId, updates) => this.updateProduct(productId, updates),
deleteProduct: (productId) => this.deleteProduct(productId),
createPrice: (priceData) => this.createPrice(priceData),
syncProducts: () => this.syncProducts(),
getProducts: () => this.getProducts(),
triggerCryptoReconcile: (opts) => this.payments.crypto.reconcile(opts || {})
};
secureMessages = {
create: (payload) => this.createSecureMessage(payload),
list: () => this.listSecureMessages()
};
email = {
send: (options) => this.sendEmail(options),
listTemplates: () => this.listEmailTemplates(),
getTemplate: (templateKey) => this.getEmailTemplate(templateKey),
previewTemplate: (templateKey, context) => this.previewEmailTemplate(templateKey, context)
};
constructor(config = {}) {
const apiUrl = config.apiUrl || process.env.SPAPS_API_URL || process.env.NEXT_PUBLIC_SPAPS_API_URL;
const isBrowser = typeof window !== "undefined";
let effectiveApiKey;
if (config.publishableKey) {
effectiveApiKey = config.publishableKey;
} else if (config.secretKey) {
effectiveApiKey = config.secretKey;
if (isBrowser) {
console.warn("\u26A0\uFE0F SPAPS: Using secretKey in browser is not recommended. Use publishableKey instead.");
}
} else if (config.apiKey) {
effectiveApiKey = config.apiKey;
} else {
effectiveApiKey = process.env.SPAPS_API_KEY || process.env.NEXT_PUBLIC_SPAPS_API_KEY;
}
if (!apiUrl || apiUrl.includes("localhost") || apiUrl.includes("127.0.0.1")) {
this._isLocalMode = true;
this.client = import_axios.default.create({
baseURL: apiUrl || "http://localhost:3300",
timeout: config.timeout || 1e4,
headers: {
"Content-Type": "application/json"
}
});
} else {
if (!effectiveApiKey) {
console.warn("\u26A0\uFE0F SPAPS: No API key provided. Some features may not work.");
}
this.apiKey = effectiveApiKey;
this.client = import_axios.default.create({
baseURL: apiUrl,
timeout: config.timeout || 1e4,
headers: {
"Content-Type": "application/json",
...this.apiKey && { "X-API-Key": this.apiKey }
}
});
}
this.client.interceptors.request.use((config2) => {
if (this.accessToken && !config2.headers.Authorization) {
config2.headers.Authorization = `Bearer ${this.accessToken}`;
}
return config2;
});
this.client.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401 && this.refreshToken) {
try {
const { data } = await this.refresh(this.refreshToken);
this.accessToken = data.access_token;
this.refreshToken = data.refresh_token;
if (error.config) {
error.config.headers.Authorization = `Bearer ${this.accessToken}`;
return this.client.request(error.config);
}
} catch (refreshError) {
this.accessToken = void 0;
this.refreshToken = void 0;
}
}
return Promise.reject(error);
}
);
}
// -------- Helper methods (for tests and convenience) --------
/** Raw API request helper that returns an ApiResponse-like shape */
async request(method, url, data, requiresAuth = false) {
try {
const config = { method, url, data };
if (requiresAuth && this.accessToken) {
config.headers = { ...config.headers || {}, Authorization: `Bearer ${this.accessToken}` };
}
const res = await this.client.request(config);
return { success: true, data: res.data };
} catch (err) {
const message = err?.response?.data?.error?.message || err?.message || "Request failed";
return Promise.reject(new Error(message));
}
}
/** Health check helper that returns boolean */
async healthCheck() {
try {
const res = await this.client.get("/health");
return Boolean(res.data?.success);
} catch {
return false;
}
}
// Authentication Methods
async login(email, password) {
const response = await this.client.post("/api/auth/login", {
email,
password
});
this.accessToken = response.data.access_token;
this.refreshToken = response.data.refresh_token;
return response;
}
async register(email, password) {
const response = await this.client.post("/api/auth/register", {
email,
password
});
this.accessToken = response.data.access_token;
this.refreshToken = response.data.refresh_token;
return response;
}
async walletSignIn(walletAddress, signature, message, chainType = "solana") {
const response = await this.client.post("/api/auth/wallet-sign-in", {
wallet_address: walletAddress,
signature,
message,
chain_type: chainType
});
this.accessToken = response.data.access_token;
this.refreshToken = response.data.refresh_token;
return response;
}
async refresh(refreshToken) {
const response = await this.client.post("/api/auth/refresh", {
refresh_token: refreshToken || this.refreshToken
});
this.accessToken = response.data.access_token;
this.refreshToken = response.data.refresh_token;
return response;
}
async logout() {
await this.client.post("/api/auth/logout");
this.accessToken = void 0;
this.refreshToken = void 0;
}
async getUser() {
return this.client.get("/api/auth/user");
}
// -------- Facade namespaces (compat with previous SDK shape) --------
auth = {
/**
* Verify magic link token without mutating token state.
* Returns a simple success object from the API.
*/
getNonce: async (walletAddress) => {
const res = await this.client.post("/api/auth/nonce", { wallet_address: walletAddress });
const body = res.data;
if (body?.success === false) throw new Error(body?.error?.message || "Failed to generate nonce");
return body?.data ?? body;
},
signInWithWallet: async (req) => {
const res = await this.client.post("/api/auth/wallet-sign-in", req);
const body = res.data;
if (body?.success === false) throw new Error(body?.error?.message || "Wallet sign-in failed");
const data = body?.data ?? body;
this.accessToken = data.access_token;
this.refreshToken = data.refresh_token;
return data;
},
authenticateWallet: async (walletAddress, signFn, chainType, username) => {
const nonce = await this.auth.getNonce(walletAddress);
const signature = await signFn(nonce.message);
return this.auth.signInWithWallet({ wallet_address: walletAddress, signature, message: nonce.message, chain_type: chainType, username });
},
signInWithPassword: async (payload) => {
const res = await this.client.post("/api/auth/login", payload);
const body = res.data;
if (body?.success === false) throw new Error(body?.error?.message || "Login failed");
const data = body?.data ?? body;
this.accessToken = data.access_token;
this.refreshToken = data.refresh_token;
return data;
},
requestMagicLink: async (payload) => {
await this.client.post("/api/auth/magic-link", payload);
},
requestPasswordReset: async (payload) => {
await this.client.post("/api/auth/password-reset", payload);
},
confirmPasswordReset: async (payload) => {
await this.client.post("/api/auth/reset-password-confirm", payload);
},
/**
* Set or change password for authenticated users.
* For magic link / wallet users setting their first password, only new_password is required.
* For users changing an existing password, current_password is also required.
*/
setPassword: async (payload) => {
const res = await this.client.post("/api/auth/set-password", payload);
const body = res.data;
if (body?.success === false) throw new Error(body?.error?.message || "Set password failed");
return { message: body?.message || "Password updated successfully" };
},
register: async (payload) => {
const res = await this.client.post("/api/auth/register", payload);
const body = res.data;
if (body?.success === false) throw new Error(body?.error?.message || "Register failed");
const data = body?.data ?? body;
this.accessToken = data.access_token;
this.refreshToken = data.refresh_token;
return data;
},
/**
* Verify a magic link token and authenticate the user.
* Returns full auth response with tokens and auto-stores them.
*/
verifyMagicLink: async (payload) => {
const res = await this.client.post("/api/auth/verify-magic-link", {
token: payload.token,
type: payload.type || "magiclink"
});
const body = res.data;
if (body?.success === false) throw new Error(body?.error?.message || "Magic link verification failed");
const data = {
access_token: body.tokens?.access_token || body.access_token,
refresh_token: body.tokens?.refresh_token || body.refresh_token,
user: body.user
};
this.accessToken = data.access_token;
this.refreshToken = data.refresh_token;
return data;
},
solana: {
linkWallet: async (payload) => {
const res = await this.client.post("/api/auth/solana/link-wallet", payload);
return this.unwrapApiResponse(res, "Failed to link Solana wallet");
},
verifySignature: async (payload) => {
const res = await this.client.post("/api/auth/solana/verify-signature", payload);
return this.unwrapApiResponse(res, "Failed to verify Solana signature");
},
generateMessage: async (wallet_address) => {
const res = await this.client.get(`/api/auth/solana/generate-message/${wallet_address}`);
return this.unwrapApiResponse(res, "Failed to generate Solana auth message");
},
getWallets: async () => {
const res = await this.client.get("/api/auth/solana/wallets");
return this.unwrapApiResponse(res, "Failed to fetch Solana wallets");
},
networkInfo: async () => {
const res = await this.client.get("/api/auth/solana/network-info");
return this.unwrapApiResponse(res, "Failed to fetch Solana network info");
}
},
ethereum: {
linkWallet: async (payload) => {
const res = await this.client.post("/api/auth/ethereum/link-wallet", payload);
return this.unwrapApiResponse(res, "Failed to link Ethereum wallet");
},
verifySignature: async (payload) => {
const res = await this.client.post("/api/auth/ethereum/verify-signature", payload);
return this.unwrapApiResponse(res, "Failed to verify Ethereum signature");
},
verifyTypedData: async (payload) => {
const res = await this.client.post("/api/auth/ethereum/verify-typed-data", payload);
return this.unwrapApiResponse(res, "Failed to verify EIP-712 typed data");
},
generateMessage: async (wallet_address) => {
const res = await this.client.get(`/api/auth/ethereum/generate-message/${wallet_address}`);
return this.unwrapApiResponse(res, "Failed to generate Ethereum auth message");
},
generateTypedData: async (wallet_address) => {
const res = await this.client.get(`/api/auth/ethereum/generate-typed-data/${wallet_address}`);
return this.unwrapApiResponse(res, "Failed to generate Ethereum typed data");
},
getWallets: async () => {
const res = await this.client.get("/api/auth/ethereum/wallets");
return this.unwrapApiResponse(res, "Failed to fetch Ethereum wallets");
},
networkInfo: async () => {
const res = await this.client.get("/api/auth/ethereum/network-info");
return this.unwrapApiResponse(res, "Failed to fetch Ethereum network info");
},
balance: async (wallet_address) => {
const res = await this.client.get(`/api/auth/ethereum/balance/${wallet_address}`);
return this.unwrapApiResponse(res, "Failed to fetch Ethereum balance");
},
contractCheck: async (wallet_address, contract_address) => {
const res = await this.client.get(`/api/auth/ethereum/contract-check/${wallet_address}/${contract_address}`);
return this.unwrapApiResponse(res, "Failed to check contract");
}
},
refreshToken: async (refreshToken) => {
const res = await this.client.post("/api/auth/refresh", { refresh_token: refreshToken });
const body = res.data;
if (body?.success === false) throw new Error(body?.error?.message || "Token refresh failed");
const data = body?.data ?? body;
this.accessToken = data.access_token;
this.refreshToken = data.refresh_token;
return data;
},
/**
* Logout and clear tokens. Network errors are intentionally swallowed
* to avoid trapping users in a bad state during sign‑out flows.
*/
logout: async () => {
try {
await this.client.post("/api/auth/logout");
} catch {
} finally {
this.accessToken = void 0;
this.refreshToken = void 0;
}
},
getCurrentUser: async () => {
const res = await this.client.get("/api/auth/user");
const body = res.data;
if (body?.success === false) throw new Error(body?.error?.message || "Failed to get user profile");
return body?.data?.user ?? body?.user;
},
isAuthenticated: () => !!this.accessToken
};
payments = {
crypto: {
createInvoice: async (payload) => {
const body = {
asset: payload.asset,
network: payload.network,
amount: payload.amount
};
if (typeof payload.expiresInSeconds === "number") {
body.expires_in_seconds = payload.expiresInSeconds;
}
if (payload.beneficiary) {
body.beneficiary = payload.beneficiary;
}
if (payload.metadata) {
body.metadata = payload.metadata;
}
const res = await this.client.post("/api/payments/crypto/invoices", body);
const data = this.unwrapApiResponse(res, "Failed to create crypto invoice");
return data.invoice;
},
getInvoice: async (invoiceId) => {
const res = await this.client.get(`/api/payments/crypto/invoices/${invoiceId}`);
const data = this.unwrapApiResponse(res, "Failed to fetch crypto invoice");
return data.invoice;
},
getInvoiceStatus: async (invoiceId) => {
const res = await this.client.get(`/api/payments/crypto/invoices/${invoiceId}/status`);
return this.unwrapApiResponse(res, "Failed to fetch crypto invoice status");
},
reconcile: async (options = {}) => {
const headers = {};
if (options.reconToken) {
headers["X-Recon-Token"] = options.reconToken;
}
const payload = {};
if (options.cursor) {
payload.cursor = options.cursor;
}
const res = await this.client.post("/api/payments/crypto/reconcile", payload, { headers });
return this.unwrapApiResponse(res, "Failed to trigger crypto reconciliation");
}
},
createCheckoutSession: async (payload) => {
const headers = {};
if (this.accessToken) headers["Authorization"] = `Bearer ${this.accessToken}`;
const res = await this.client.post("/api/stripe/checkout-sessions", payload, { headers });
return this.unwrapApiResponse(res, "Failed to create checkout session");
},
createPaymentCheckout: async (params) => {
const payload = {
mode: "payment",
line_items: [{ price_id: params.price_id, quantity: params.quantity ?? 1 }],
success_url: params.success_url,
cancel_url: params.cancel_url,
require_legal_consent: params.require_legal_consent,
legal_consent_text: params.legal_consent_text
};
return this.payments.createCheckoutSession(payload);
},
createSubscriptionCheckout: async (params) => {
const payload = {
mode: "subscription",
line_items: [{ price_id: params.price_id, quantity: 1 }],
success_url: params.success_url,
cancel_url: params.cancel_url,
require_legal_consent: params.require_legal_consent,
legal_consent_text: params.legal_consent_text
};
if (params.trial_period_days) payload.subscription_data = { trial_period_days: params.trial_period_days };
return this.payments.createCheckoutSession(payload);
},
getCheckoutSession: async (sessionId) => {
const res = await this.client.get(`/api/stripe/checkout-sessions/${sessionId}`);
return this.unwrapApiResponse(res, "Failed to get checkout session");
},
listCheckoutSessions: async (query = {}) => {
const q = new URLSearchParams();
if (query.limit) q.append("limit", String(query.limit));
if (query.starting_after) q.append("starting_after", query.starting_after);
const res = await this.client.get(`/api/stripe/checkout-sessions${q.toString() ? `?${q.toString()}` : ""}`);
return this.unwrapApiResponse(res, "Failed to list checkout sessions");
},
expireCheckoutSession: async (sessionId) => {
const res = await this.client.post(`/api/stripe/checkout-sessions/${sessionId}/expire`);
return this.unwrapApiResponse(res, "Failed to expire checkout session");
},
listProducts: async (query = {}) => {
const q = new URLSearchParams();
if (query.category) q.append("category", query.category);
if (typeof query.active === "boolean") q.append("active", String(query.active));
if (query.limit) q.append("limit", String(query.limit));
if (query.starting_after) q.append("starting_after", query.starting_after);
const headers = {};
if (this.accessToken) headers["Authorization"] = `Bearer ${this.accessToken}`;
const res = await this.client.get(`/api/stripe/products${q.toString() ? `?${q.toString()}` : ""}`, { headers });
return this.unwrapApiResponse(res, "Failed to list products");
},
getProduct: async (productId) => {
const headers = {};
if (this.accessToken) headers["Authorization"] = `Bearer ${this.accessToken}`;
const res = await this.client.get(`/api/stripe/products/${productId}`, { headers });
return this.unwrapApiResponse(res, "Failed to get product");
},
createCustomerPortalSession: async (payload) => {
const headers = {};
if (this.accessToken) headers["Authorization"] = `Bearer ${this.accessToken}`;
const res = await this.client.post("/api/stripe/portal-session", payload, { headers });
return this.unwrapApiResponse(res, "Failed to create portal session");
},
// Guest checkout helpers
createGuestCheckoutSession: async (payload) => {
const headers = {};
if (this.accessToken) headers["Authorization"] = `Bearer ${this.accessToken}`;
const res = await this.client.post("/api/stripe/guest-checkout-sessions", payload, { headers });
return this.unwrapApiResponse(res, "Failed to create guest checkout session");
},
getGuestCheckoutSession: async (sessionId) => {
const headers = {};
if (this.accessToken) headers["Authorization"] = `Bearer ${this.accessToken}`;
const res = await this.client.get(`/api/stripe/guest-checkout-sessions/${sessionId}`, { headers });
return this.unwrapApiResponse(res, "Failed to get guest checkout session");
},
listGuestCheckoutSessions: async (query = {}) => {
const q = new URLSearchParams();
if (query.limit) q.append("limit", String(query.limit));
if (query.starting_after) q.append("starting_after", query.starting_after);
const headers = {};
if (this.accessToken) headers["Authorization"] = `Bearer ${this.accessToken}`;
const res = await this.client.get(`/api/stripe/guest-checkout-sessions${q.toString() ? `?${q.toString()}` : ""}`, { headers });
return this.unwrapApiResponse(res, "Failed to list guest checkout sessions");
},
convertGuestCheckoutSession: async (payload) => {
const headers = {};
if (this.accessToken) headers["Authorization"] = `Bearer ${this.accessToken}`;
const res = await this.client.post("/api/stripe/guest-checkout-sessions/convert", payload, { headers });
return this.unwrapApiResponse(res, "Failed to convert guest checkout session");
},
convertGuestCheckout: async (payload) => this.payments.convertGuestCheckoutSession(payload),
// Super-admin product helpers (admin token required)
listAllProductsSuperAdmin: async () => {
if (!this.accessToken) throw new Error("Authentication required. Please authenticate first.");
const res = await this.client.get("/api/stripe/products/super-admin/all", { headers: { Authorization: `Bearer ${this.accessToken}` } });
return this.unwrapApiResponse(res, "Failed to list all products (super admin)");
},
updateProductSuperAdmin: async (productId, updates) => {
if (!this.accessToken) throw new Error("Authentication required. Please authenticate first.");
const res = await this.client.put(`/api/stripe/products/super-admin/${productId}`, updates, { headers: { Authorization: `Bearer ${this.accessToken}` } });
return this.unwrapApiResponse(res, "Failed to update product (super admin)");
},
deleteProductSuperAdmin: async (productId) => {
if (!this.accessToken) throw new Error("Authentication required. Please authenticate first.");
const res = await this.client.delete(`/api/stripe/products/super-admin/${productId}`, { headers: { Authorization: `Bearer ${this.accessToken}` } });
return this.unwrapApiResponse(res, "Failed to archive product (super admin)");
},
createProductWithPrice: async (payload) => {
if (!this.accessToken) throw new Error("Authentication required. Please authenticate first.");
const res = await this.client.post("/api/stripe/products/with-price", payload, { headers: { Authorization: `Bearer ${this.accessToken}` } });
return this.unwrapApiResponse(res, "Failed to create product with price");
},
createProductWithPriceSuperAdmin: async (productId, payload) => {
if (!this.accessToken) throw new Error("Authentication required. Please authenticate first.");
const res = await this.client.post(`/api/stripe/products/super-admin/${productId}/with-price`, payload, { headers: { Authorization: `Bearer ${this.accessToken}` } });
return this.unwrapApiResponse(res, "Failed to create product with price (super admin)");
},
setDefaultPrice: async (productId, payload) => {
if (!this.accessToken) throw new Error("Authentication required. Please authenticate first.");
const res = await this.client.post(`/api/stripe/products/${productId}/default-price`, payload, { headers: { Authorization: `Bearer ${this.accessToken}` } });
return this.unwrapApiResponse(res, "Failed to set default price");
},
setDefaultPriceSuperAdmin: async (productId, payload) => {
if (!this.accessToken) throw new Error("Authentication required. Please authenticate first.");
const res = await this.client.put(`/api/stripe/products/super-admin/${productId}/default-price`, payload, { headers: { Authorization: `Bearer ${this.accessToken}` } });
return this.unwrapApiResponse(res, "Failed to set default price (super admin)");
},
createDefaultNewPrice: async (productId, payload) => {
if (!this.accessToken) throw new Error("Authentication required. Please authenticate first.");
const res = await this.client.post(`/api/stripe/products/${productId}/prices/default-new`, payload, { headers: { Authorization: `Bearer ${this.accessToken}` } });
return this.unwrapApiResponse(res, "Failed to create default price");
},
superAdminListAllProducts: async () => this.payments.listAllProductsSuperAdmin(),
superAdminUpdateProduct: async (productId, updates) => this.payments.updateProductSuperAdmin(productId, updates),
superAdminArchiveProduct: async (productId) => this.payments.deleteProductSuperAdmin(productId),
superAdminCreateProductWithPrice: async (applicationId, payload) => {
if (!this.accessToken) throw new Error("Authentication required. Please authenticate first.");
const res = await this.client.post(`/api/stripe/products/super-admin/${applicationId}/with-price`, payload, { headers: { Authorization: `Bearer ${this.accessToken}` } });
return this.unwrapApiResponse(res, "Failed to create product with price for application (super admin)");
},
superAdminCreatePriceAndSetDefault: async (productId, payload) => {
if (!this.accessToken) throw new Error("Authentication required. Please authenticate first.");
const res = await this.client.post(`/api/stripe/products/super-admin/${productId}/prices/default-new`, payload, { headers: { Authorization: `Bearer ${this.accessToken}` } });
return this.unwrapApiResponse(res, "Failed to create price and set default (super admin)");
},
superAdminSetDefaultPrice: async (productId, payload) => this.payments.setDefaultPriceSuperAdmin(productId, payload)
};
sessions = {
getCurrent: async () => {
const res = await this.client.get("/api/sessions/current", this.accessToken ? { headers: { Authorization: `Bearer ${this.accessToken}` } } : void 0);
return this.unwrapApiResponse(res, "Failed to get current session");
},
list: async (params = {}) => {
const q = new URLSearchParams();
if (params.limit) q.append("limit", String(params.limit));
if (params.starting_after) q.append("starting_after", params.starting_after);
const res = await this.client.get(`/api/sessions${q.toString() ? `?${q.toString()}` : ""}`, this.accessToken ? { headers: { Authorization: `Bearer ${this.accessToken}` } } : void 0);
return this.unwrapApiResponse(res, "Failed to list sessions");
},
validate: async () => {
const res = await this.client.post("/api/sessions/validate", {}, this.accessToken ? { headers: { Authorization: `Bearer ${this.accessToken}` } } : void 0);
return this.unwrapApiResponse(res, "Failed to validate session");
},
revoke: async (sessionId) => {
const res = await this.client.delete(`/api/sessions/${sessionId}`, this.accessToken ? { headers: { Authorization: `Bearer ${this.accessToken}` } } : void 0);
return this.unwrapApiResponse(res, "Failed to revoke session");
},
revokeAll: async () => {
const res = await this.client.delete("/api/sessions/all", this.accessToken ? { headers: { Authorization: `Bearer ${this.accessToken}` } } : void 0);
return this.unwrapApiResponse(res, "Failed to revoke all sessions");
},
touch: async () => {
const res = await this.client.post("/api/sessions/touch", {}, this.accessToken ? { headers: { Authorization: `Bearer ${this.accessToken}` } } : void 0);
return this.unwrapApiResponse(res, "Failed to touch session");
}
};
/**
* CFO (QuickBooks) namespace for managing multi-account connections
*/
cfo = {
/**
* Get all QuickBooks connections for the current user
*/
getConnections: async () => {
const res = await this.client.get("/api/cfo/user-connections", this.accessToken ? { headers: { Authorization: `Bearer ${this.accessToken}` } } : void 0);
return this.unwrapApiResponse(res, "Failed to get connections");
},
/**
* Get connection status (returns primary connection for backward compatibility)
*/
getStatus: async () => {
const res = await this.client.get("/api/cfo/user-status", this.accessToken ? { headers: { Authorization: `Bearer ${this.accessToken}` } } : void 0);
return this.unwrapApiResponse(res, "Failed to get status");
},
/**
* Start adding a new QuickBooks connection
* Returns auth URL to redirect user to QuickBooks OAuth
*/
addConnection: async () => {
const res = await this.client.post("/api/cfo/user-add-connection", {}, this.accessToken ? { headers: { Authorization: `Bearer ${this.accessToken}` } } : void 0);
return this.unwrapApiResponse(res, "Failed to start connection");
},
/**
* Disconnect a specific QuickBooks account
*/
disconnect: async (connectionId) => {
const res = await this.client.delete(`/api/cfo/user-connections/${connectionId}`, this.accessToken ? { headers: { Authorization: `Bearer ${this.accessToken}` } } : void 0);
return this.unwrapApiResponse(res, "Failed to disconnect");
}
};
/**
* DayRate (Dynamic Scheduling) namespace
* For booking half-day sessions with dynamic pricing
*/
dayrate = {
/**
* Get available slots with current pricing
* Public endpoint - no authentication required
*/
getAvailability: async () => {
const res = await this.client.get("/api/dayrate/availability");
return this.unwrapApiResponse(res, "Failed to get availability");
},
/**
* Create a single-slot booking and get Stripe checkout URL
* Returns a 10-minute reservation with checkout link
*/
createBooking: async (payload) => {
const res = await this.client.post("/api/dayrate/book", payload);
return this.unwrapApiResponse(res, "Failed to create booking");
},
/**
* Create a multi-slot booking and get Stripe checkout URL
* Reserves multiple slots with a single checkout session
*/
createMultiBooking: async (payload) => {
const res = await this.client.post("/api/dayrate/book-multi", payload);
return this.unwrapApiResponse(res, "Failed to create multi-booking");
}
};
// Stripe Methods
async createCheckoutSession(priceId, successUrl, cancelUrl) {
return this.client.post("/api/stripe/create-checkout-session", {
price_id: priceId,
success_url: successUrl,
cancel_url: cancelUrl
});
}
async getSubscription() {
return this.client.get("/api/stripe/subscription");
}
async cancelSubscription() {
await this.client.delete("/api/stripe/subscription");
}
// Usage Methods
async getUsageBalance() {
return this.client.get("/api/usage/balance");
}
async recordUsage(feature, amount) {
await this.client.post("/api/usage/record", {
feature,
amount
});
}
// Secure Messaging Methods
async createSecureMessage(payload) {
const response = await this.client.post("/api/secure-messages", payload);
return this.unwrapApiResponse(response, "Failed to create secure message");
}
async listSecureMessages() {
const response = await this.client.get("/api/secure-messages");
const payload = this.unwrapApiResponse(response, "Failed to list secure messages");
if (payload && Array.isArray(payload.messages)) {
return payload.messages;
}
return payload;
}
// Email Methods
/**
* Send an email using a template
*/
async sendEmail(options) {
const response = await this.client.post("/api/email/send", {
template_key: options.templateKey,
to: options.to,
context: options.context,
...options.userId && { user_id: options.userId }
});
return this.unwrapApiResponse(response, "Failed to send email");
}
/**
* List all email templates for the application
*/
async listEmailTemplates() {
const response = await this.client.get("/api/email/templates");
const payload = this.unwrapApiResponse(
response,
"Failed to list email templates"
);
if (payload && Array.isArray(payload.templates)) {
return payload.templates;
}
return payload;
}
/**
* Get a single email template by key
*/
async getEmailTemplate(templateKey) {
const response = await this.client.get(`/api/email/templates/${templateKey}`);
return this.unwrapApiResponse(response, "Failed to get email template");
}
/**
* Preview a rendered email template
*/
async previewEmailTemplate(templateKey, context) {
if (context) {
const response = await this.client.post(
`/api/email/templates/${templateKey}/preview`,
{ context }
);
return this.unwrapApiResponse(response, "Failed to preview email template");
} else {
const response = await this.client.get(`/api/email/templates/${templateKey}/preview`);
return this.unwrapApiResponse(response, "Failed to preview email template");
}
}
// Admin Methods (Require admin privileges)
/**
* Create a new Stripe product (Admin required)
*/
async createProduct(productData) {
if (!this.accessToken) {
throw new Error("Authentication required. Please authenticate first.");
}
return this.client.post("/api/stripe/products", productData, {
headers: {
"Authorization": `Bearer ${this.accessToken}`
}
});
}
/**
* Update an existing Stripe product (Admin required)
*/
async updateProduct(productId, updates) {
if (!this.accessToken) {
throw new Error("Authentication required. Please authenticate first.");
}
return this.client.put(`/api/stripe/products/${productId}`, updates, {
headers: {
"Authorization": `Bearer ${this.accessToken}`
}
});
}
/**
* Archive (soft delete) a Stripe product (Admin required)
*/
async deleteProduct(productId) {
if (!this.accessToken) {
throw new Error("Authentication required. Please authenticate first.");
}
return this.client.delete(`/api/stripe/products/${productId}`, {
headers: {
"Authorization": `Bearer ${this.accessToken}`
}
});
}
/**
* Create a new price for a product (Admin required)
*/
async createPrice(priceData) {
if (!this.accessToken) {
throw new Error("Authentication required. Please authenticate first.");
}
return this.client.post("/api/stripe/prices", priceData, {
headers: {
"Authorization": `Bearer ${this.accessToken}`
}
});
}
/**
* Sync all products from Stripe to local database (Super Admin required)
*/
async syncProducts() {
if (!this.accessToken) {
throw new Error("Authentication required. Please authenticate first.");
}
return this.client.post("/api/stripe/products/sync", {}, {
headers: {
"Authorization": `Bearer ${this.accessToken}`
}
});
}
/**
* Get products with admin metadata (if user is admin)
*/
async getProducts() {
const headers = {};
if (this.accessToken) {
headers["Authorization"] = `Bearer ${this.accessToken}`;
}
return this.client.get("/api/stripe/products", { headers });
}
// Utility Methods
isAuthenticated() {
return !!this.accessToken;
}
getAccessToken() {
return this.accessToken;
}
setAccessToken(token) {
this.accessToken = token;
}
setRefreshToken(token) {
this.refreshToken = token;
}
/**
* Restore tokens (for SSO/OAuth flows)
* Sets both access and refresh tokens
*/
restoreTokens(accessToken, refreshToken) {
this.accessToken = accessToken;
if (refreshToken) {
this.refreshToken = refreshToken;
}
}
isLocalMode() {
return this._isLocalMode;
}
/**
* Check if current user has admin privileges
* Note: This requires the user object from authentication
*/
isAdmin(user) {
if (!user) return false;
const identifier = user.email || user.wallet_address;
if (!identifier) return false;
const { isAdminAccount: isAdminAccount2 } = (init_permissions(), __toCommonJS(permissions_exports));
return isAdminAccount2(identifier);
}
async health() {
return this.client.get("/health");
}
};
function verifyCryptoWebhookSignature(options) {
const { body, signature, secret, toleranceSeconds = 300 } = options;
if (!signature) {
throw new Error("Missing webhook signature");
}
const parts = signature.split(",").reduce((acc, part) => {
const [key, value] = part.split("=");
if (key && value) acc[key.trim()] = value.trim();
return acc;
}, {});
const timestamp = parts["t"];
const expected = parts["v1"];
if (!timestamp || !expected) {
throw new Error("Invalid webhook signature format");
}
const rawBody = typeof body === "string" ? body : body instanceof Uint8Array ? Buffer.from(body).toString("utf8") : JSON.stringify(body ?? {});
const computed = import_crypto.default.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
const expectedBuffer = Buffer.from(expected, "hex");
const computedBuffer = Buffer.from(computed, "hex");
if (expectedBuffer.length !== computedBuffer.length || !import_crypto.default.timingSafeEqual(expectedBuffer, computedBuffer)) {
throw new Error("Invalid webhook signature");
}
const ts = Number(timestamp);
if (Number.isFinite(ts) && toleranceSeconds > 0) {
const ageSeconds = Math.abs(Date.now() / 1e3 - ts);
if (ageSeconds > toleranceSeconds) {
throw new Error("Webhook signature timestamp outside tolerance window");
}
}
return true;
}
var index_default = SPAPSClient;
var TokenManager = class _TokenManager {
static ACCESS_TOKEN_KEY = "sweet_potato_access_token";
static REFRESH_TOKEN_KEY = "sweet_potato_refresh_token";
static USER_KEY = "sweet_potato_user";
/**
* Cross-platform base64 decode (works in both Node.js and browser)
*/
static base64Decode(str) {
if (typeof Buffer !== "undefined") {
return Buffer.from(str, "base64").toString("utf8");
} else if (typeof atob !== "undefined") {
return atob(str);
}
throw new Error("No base64 decode method available");
}
static getStorage() {
if (typeof globalThis !== "undefined" && globalThis.localStorage) return globalThis.localStorage;
if (typeof globalThis !== "undefined" && globalThis.window?.localStorage) return globalThis.window.localStorage;
if (typeof global !== "undefined" && global.window?.localStorage) return global.window.localStorage;
return null;
}
static storeTokens(tokens) {
const s = _TokenManager.getStorage();
if (s) {
s.setItem(_TokenManager.ACCESS_TOKEN_KEY, tokens.access_token);
s.setItem(_TokenManager.REFRESH_TOKEN_KEY, tokens.refresh_token);
s.setItem(_TokenManager.USER_KEY, JSON.stringify(tokens.user));
}
}
static getAccessToken() {
const s = _TokenManager.getStorage();
return s ? s.getItem(_TokenManager.ACCESS_TOKEN_KEY) : null;
}
static getRefreshToken() {
const s = _TokenManager.getStorage();
return s ? s.getItem(_TokenManager.REFRESH_TOKEN_KEY) : null;
}
static getStoredUser() {
const s = _TokenManager.getStorage();
if (!s) return null;
const v = s.getItem(_TokenManager.USER_KEY);
return v ? JSON.parse(v) : null;
}
static clearTokens() {
const s = _TokenManager.getStorage();
if (s) {
s.removeItem(_TokenManager.ACCESS_TOKEN_KEY);
s.removeItem(_TokenManager.REFRESH_TOKEN_KEY);
s.removeItem(_TokenManager.USER_KEY);
}
}
static isTokenExpired(token) {
try {
const parts = token.split(".");
if (parts.length !== 3 || !parts[1]) return true;
const payload = JSON.parse(_TokenManager.base64Decode(parts[1]));
const now = Math.floor(Date.now() / 1e3);
return payload.exp < now;
} catch {
return true;
}
}
static async autoRefreshToken(sdk) {
const accessToken = _TokenManager.getAccessToken();
const refreshToken = _TokenManager.getRefreshToken();
if (!accessToken || !refreshToken) return false;
if (!_TokenManager.isTokenExpired(accessToken)) {
sdk.setAccessToken(accessToken);
return true;
}
try {
const newTokens = await sdk.auth.refreshToken(refreshToken);
_TokenManager.storeTokens(newTokens);
return true;
} catch {
_TokenManager.clearTokens();
return false;
}
}
/**
* Parse auth tokens from URL fragment (for OAuth callbacks)
* URL format: https://app.com/callback#access_token=xxx&refresh_token=xxx
*/
static parseTokensFromUrlFragment(url) {
let hash;
if (url) {
try {
hash = new URL(url).hash;
} catch {
return null;
}
} el