UNPKG

better-near-auth

Version:

Sign in with NEAR (SIWN) plugin for Better Auth

1,665 lines 57.1 kB
import { APIError, createAuthEndpoint, createAuthMiddleware, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
import { setSessionCookie } from "better-auth/cookies";
import { BetterAuthError } from "@better-auth/core/error";
import { InMemoryKeyStore, Near, decodeSignedDelegateAction, generateKey, generateNonce, parseKey, verifyNep413Signature } from "near-kit";
import { base58, hex } from "@scure/base";
import z$1, { z } from "zod";
import { AccountIdSchema } from "near-kit/schemas";
//#region src/profile.ts
const FALLBACK_URL = "https://ipfs.near.social/ipfs/bafkreidn5fb2oygegqaldx7ycdmhu4owcrmoxd7ekbzfmeakkobz2ja7qy";
function getNetworkFromAccountId(accountId) {
	return accountId.endsWith(".testnet") ? "testnet" : "mainnet";
}
function getImageUrl(image, fallback) {
	if (image?.url) return image.url;
	if (image?.ipfs_cid) return `https://ipfs.near.social/ipfs/${image.ipfs_cid}`;
	return fallback || FALLBACK_URL;
}
async function defaultGetProfile(accountId, apiKey) {
	const network = getNetworkFromAccountId(accountId);
	try {
		const kvUrl = network === "testnet" ? "https://kv.test.fastnear.com" : "https://kv.main.fastnear.com";
		const effectiveApiKey = apiKey || process.env.FASTNEAR_API_KEY;
		const headers = { "Content-Type": "application/json" };
		if (effectiveApiKey) headers["Authorization"] = `Bearer ${effectiveApiKey}`;
		const response = await fetch(`${kvUrl}/v0/latest/social.near/${accountId}/profile/**`, { headers });
		if (response.ok) {
			const entry = (await response.json())?.entries?.[0];
			if (entry?.value) try {
				const profile = typeof entry.value === "string" ? JSON.parse(entry.value) : entry.value;
				if (profile?.name || profile?.description || profile?.image) return {
					name: profile.name,
					description: profile.description,
					image: profile.image,
					backgroundImage: profile.backgroundImage,
					linktree: profile.linktree
				};
			} catch {}
		}
		const apiBase = {
			mainnet: "https://api.near.social",
			testnet: "https://test.api.near.social"
		}[network];
		const keys = [`${accountId}/profile/**`];
		const fallbackResponse = await fetch(`${apiBase}/get`, {
			method: "POST",
			headers: { "Content-Type": "application/json" },
			body: JSON.stringify({ keys })
		});
		if (!fallbackResponse.ok) throw new Error(`HTTP error! status: ${fallbackResponse.status}`);
		const profile = (await fallbackResponse.json())?.[accountId]?.profile;
		if (profile) return {
			name: profile.name,
			description: profile.description,
			image: profile.image,
			backgroundImage: profile.backgroundImage,
			linktree: profile.linktree
		};
		return null;
	} catch (error) {
		return null;
	}
}
//#endregion
//#region src/schema.ts
const schema = {
	nearAccount: { fields: {
		userId: {
			type: "string",
			references: {
				model: "user",
				field: "id"
			},
			required: true
		},
		accountId: {
			type: "string",
			required: true
		},
		network: {
			type: "string",
			required: true
		},
		publicKey: {
			type: "string",
			required: true
		},
		isPrimary: {
			type: "boolean",
			defaultValue: false
		},
		createdAt: {
			type: "date",
			required: true
		}
	} },
	relayedTransaction: { fields: {
		userId: {
			type: "string",
			references: {
				model: "user",
				field: "id"
			}
		},
		txHash: {
			type: "string",
			required: true
		},
		senderId: {
			type: "string",
			required: true
		},
		receiverId: {
			type: "string",
			required: true
		},
		network: {
			type: "string",
			required: true
		},
		status: {
			type: "string",
			required: true
		},
		gasUsed: { type: "string" },
		createdAt: {
			type: "date",
			required: true
		},
		updatedAt: { type: "date" }
	} },
	relayerKey: { fields: {
		accountId: {
			type: "string",
			required: true
		},
		encryptedPrivateKey: {
			type: "string",
			required: true
		},
		iv: {
			type: "string",
			required: true
		},
		publicKey: {
			type: "string",
			required: true
		},
		network: {
			type: "string",
			required: true
		},
		createdAt: {
			type: "date",
			required: true
		},
		lastUsedAt: { type: "date" }
	} }
};
//#endregion
//#region src/types.ts
const socialImageSchema = z.object({
	url: z.string().optional(),
	ipfs_cid: z.string().optional()
});
const profileSchema = z.object({
	name: z.string().optional(),
	description: z.string().optional(),
	image: socialImageSchema.optional(),
	backgroundImage: socialImageSchema.optional(),
	linktree: z.record(z.string(), z.string()).optional()
});
const signedMessageSchema = z.object({
	accountId: z.string(),
	publicKey: z.string(),
	signature: z.string(),
	state: z.string().optional()
});
const LinkAccountRequest = z.object({
	signedMessage: signedMessageSchema,
	message: z.string(),
	recipient: z.string(),
	nonce: z.string(),
	accountId: AccountIdSchema,
	callbackUrl: z.string().optional()
});
const SetPrimaryAccountRequest = z.object({
	accountId: AccountIdSchema,
	network: z.enum(["mainnet", "testnet"]).optional()
});
const NonceRequest = z.object({
	accountId: AccountIdSchema,
	networkId: z.union([z.literal("mainnet"), z.literal("testnet")])
});
const VerifyRequest = z.object({
	signedMessage: signedMessageSchema,
	message: z.string(),
	recipient: z.string(),
	nonce: z.string(),
	accountId: AccountIdSchema,
	callbackUrl: z.string().optional()
});
const RelayRequest = z.object({ payload: z.string() });
const RelayResponse = z.object({
	txHash: z.string(),
	status: z.enum([
		"pending",
		"completed",
		"failed"
	])
});
const RelayStatusResponse = z.object({
	status: z.enum([
		"pending",
		"completed",
		"failed"
	]),
	gasUsed: z.string().optional(),
	outcome: z.unknown().optional()
});
const ViewContractRequest = z.object({
	contractId: z.string(),
	methodName: z.string(),
	args: z.record(z.string(), z.any()).optional()
});
const NonceResponse = z.object({ nonce: z.string() });
const VerifyResponse = z.object({
	token: z.string(),
	success: z.literal(true),
	user: z.object({
		id: z.string(),
		accountId: AccountIdSchema,
		network: z.union([z.literal("mainnet"), z.literal("testnet")])
	})
});
const ProfileResponse = profileSchema.nullable();
const ViewContractResponse = z.object({ result: z.unknown() });
const ProfileRequest = z.object({ accountId: AccountIdSchema.optional() });
const RelayedTransactionSchema = z.object({
	id: z.string(),
	userId: z.string(),
	txHash: z.string(),
	senderId: z.string(),
	receiverId: z.string(),
	network: z.string(),
	status: z.string(),
	gasUsed: z.string().optional(),
	createdAt: z.string(),
	updatedAt: z.string().optional()
});
const RelayHistoryResponse = z.object({ transactions: z.array(RelayedTransactionSchema) });
const relayerConfigSchema = z.object({
	accountId: z.string().optional(),
	privateKey: z.string().optional(),
	whitelistedContracts: z.array(z.string()).optional(),
	maxGasPerTransaction: z.string().regex(/^\d+$/, "must be a non-negative integer string of gas units").optional(),
	maxDepositPerTransaction: z.string().regex(/^\d+$/, "must be a non-negative integer string of yoctoNEAR").optional()
}).strict();
const relayerDualNetworkConfigSchema = z.object({
	mainnet: relayerConfigSchema.optional(),
	testnet: relayerConfigSchema.optional()
}).strict();
const GetRelayerInfoRequest = z.object({ network: z.enum(["mainnet", "testnet"]).optional() });
const SUB_ACCOUNT_LABEL_REGEX = /^([a-z\d]+[-_])*[a-z\d]+$/;
const SubAccountNameSchema = z.string().min(2, "Sub-account name must be at least 2 characters").max(64, "Sub-account name must be at most 64 characters").regex(SUB_ACCOUNT_LABEL_REGEX, "Sub-account name must contain only lowercase alphanumeric characters, hyphens, and underscores (no leading/trailing hyphens or underscores)");
const CreateSubAccountRequest = z.object({
	subAccountName: SubAccountNameSchema,
	network: z.enum(["mainnet", "testnet"]).optional(),
	publicKey: z.string()
});
const CreateSubAccountResponse = z.object({
	success: z.literal(true),
	accountId: z.string(),
	network: z.enum(["mainnet", "testnet"]),
	publicKey: z.string(),
	message: z.string()
});
const CheckSubAccountAvailabilityRequest = z.object({
	subAccountName: SubAccountNameSchema,
	network: z.enum(["mainnet", "testnet"]).optional()
});
const CheckSubAccountAvailabilityResponse = z.object({
	available: z.boolean(),
	accountId: z.string(),
	parentAccount: z.string().optional(),
	reason: z.enum([
		"taken",
		"invalid",
		"too-long",
		"not-configured"
	]).optional()
});
//#endregion
//#region src/utils.ts
function bytesToBase64(bytes) {
	return btoa(String.fromCharCode(...bytes));
}
function base64ToBytes(base64) {
	return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
}
function bytesToHex(bytes) {
	return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
}
async function deriveAesKey(secret) {
	const keyMaterial = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HKDF" }, false, ["deriveKey"]);
	return crypto.subtle.deriveKey({
		name: "HKDF",
		hash: "SHA-256",
		salt: new TextEncoder().encode("better-near-auth-relayer"),
		info: new Uint8Array(0)
	}, keyMaterial, {
		name: "AES-GCM",
		length: 256
	}, false, ["encrypt", "decrypt"]);
}
async function encryptPrivateKey(privateKey, secret) {
	const aesKey = await deriveAesKey(secret);
	const iv = crypto.getRandomValues(new Uint8Array(12));
	const encrypted = await crypto.subtle.encrypt({
		name: "AES-GCM",
		iv
	}, aesKey, privateKey);
	return {
		encrypted: bytesToBase64(new Uint8Array(encrypted)),
		iv: bytesToBase64(iv)
	};
}
async function decryptPrivateKey(encrypted, iv, secret) {
	const aesKey = await deriveAesKey(secret);
	const decrypted = await crypto.subtle.decrypt({
		name: "AES-GCM",
		iv: base64ToBytes(iv)
	}, aesKey, base64ToBytes(encrypted));
	return new Uint8Array(decrypted);
}
//#endregion
//#region src/index.ts
async function hashNonce(nonce) {
	const data = new TextEncoder().encode(hex.encode(nonce));
	const hashBuffer = await crypto.subtle.digest("SHA-256", data);
	return hex.encode(new Uint8Array(hashBuffer));
}
function deriveEmail(accountId, recipient) {
	if (accountId.endsWith(".near")) {
		const localPart = accountId.slice(0, -5);
		if (!localPart.includes(".")) return `${localPart}@near.email`;
	}
	return `temp-${crypto.randomUUID().slice(0, 8)}@${recipient}`;
}
function nearAccountKey(account) {
	return `${account.accountId}:${account.network}`;
}
function getCreatedAtTime(account) {
	return account.createdAt instanceof Date ? account.createdAt.getTime() : new Date(account.createdAt).getTime();
}
function buildListAccountsResponse(nearAccounts) {
	const activeAccount = nearAccounts.find((account) => account.isPrimary) ?? nearAccounts[0] ?? null;
	const activeKey = activeAccount ? nearAccountKey(activeAccount) : null;
	const accounts = nearAccounts.map((account) => {
		const isActive = activeKey === nearAccountKey(account);
		return {
			...account,
			providerId: "siwn",
			isActive,
			isAvailable: !isActive
		};
	}).sort((a, b) => {
		if (a.isActive !== b.isActive) return a.isActive ? -1 : 1;
		return getCreatedAtTime(a) - getCreatedAtTime(b);
	});
	const listedActiveAccount = accounts.find((account) => account.isActive) ?? null;
	return {
		accounts,
		activeAccount: listedActiveAccount ? { ...listedActiveAccount } : null,
		availableAccounts: accounts.filter((account) => account.isAvailable).map((account) => ({ ...account }))
	};
}
function parseRelayerConfig(input) {
	if (input == null) return void 0;
	if (typeof input !== "object") throw new BetterAuthError("Invalid relayer config: expected an object with 'mainnet'/'testnet' keys or top-level relayer fields.");
	const keys = Object.keys(input);
	const flatKeys = Object.keys(relayerConfigSchema.shape);
	const hasNetworkKey = keys.includes("mainnet") || keys.includes("testnet");
	const hasFlatKey = flatKeys.some((k) => keys.includes(k));
	if (hasNetworkKey && hasFlatKey) throw new BetterAuthError(`Invalid relayer config: cannot mix per-network keys (mainnet/testnet) with top-level fields (${flatKeys.join(", ")}). Use either a flat RelayerConfig or a RelayerDualNetworkConfig.`);
	const result = (hasNetworkKey ? relayerDualNetworkConfigSchema : relayerConfigSchema).safeParse(input);
	if (!result.success) throw new BetterAuthError(`Invalid relayer config: ${result.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ")}`);
	if (hasNetworkKey) return {
		kind: "dual",
		config: result.data
	};
	return {
		kind: "flat",
		config: result.data
	};
}
function createNear(network, headers, rpcUrl, keyStore) {
	const config = { headers };
	if (rpcUrl) config.network = {
		rpcUrl,
		networkId: network
	};
	else config.network = network;
	if (keyStore) config.keyStore = keyStore;
	return new Near(config);
}
async function initRelayer(networkConfig, network, adapter, secret, apiKey, rpcUrl) {
	if (!networkConfig) return null;
	const headers = {};
	if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
	if (networkConfig.accountId && networkConfig.privateKey) {
		const near = createNear(network, headers, rpcUrl, new InMemoryKeyStore({ [networkConfig.accountId]: networkConfig.privateKey }));
		const explicitPublicKey = parseKey(networkConfig.privateKey);
		return {
			near,
			accountId: networkConfig.accountId,
			network,
			mode: "explicit",
			publicKey: explicitPublicKey.publicKey.toString(),
			whitelistedContracts: networkConfig.whitelistedContracts,
			maxGasPerTransaction: networkConfig.maxGasPerTransaction,
			maxDepositPerTransaction: networkConfig.maxDepositPerTransaction
		};
	}
	if (networkConfig.accountId && !networkConfig.privateKey) console.warn(`[siwn] Relayer accountId "${networkConfig.accountId}" is set for ${network} but no privateKey was provided. Falling back to ephemeral mode. Set the relayer privateKey in your SIWN plugin options to use explicit mode.`);
	const existing = await adapter.findOne({
		model: "relayerKey",
		where: [{
			field: "network",
			operator: "eq",
			value: network
		}]
	});
	if (existing) {
		if (!secret) throw new Error("BETTER_AUTH_SECRET required for relayer key decryption");
		const privateKeyBytes = await decryptPrivateKey(existing.encryptedPrivateKey, existing.iv, secret);
		const keyPair = parseKey(`ed25519:${base58.encode(privateKeyBytes)}`);
		const accountId = bytesToHex(keyPair.publicKey.data);
		console.log(`[siwn] Relayer recovered: ${accountId} (${network})`);
		const keyStore = new InMemoryKeyStore();
		await keyStore.add(accountId, keyPair);
		return {
			near: createNear(network, headers, rpcUrl, keyStore),
			accountId,
			network,
			mode: "ephemeral",
			publicKey: keyPair.publicKey.toString(),
			whitelistedContracts: networkConfig.whitelistedContracts,
			maxGasPerTransaction: networkConfig.maxGasPerTransaction,
			maxDepositPerTransaction: networkConfig.maxDepositPerTransaction,
			createdAt: existing.createdAt,
			lastUsedAt: existing.lastUsedAt
		};
	}
	const keyPair = generateKey();
	if (!secret) throw new Error("BETTER_AUTH_SECRET required for relayer key encryption");
	const privateKeyBytes = keyPair.secretKey.startsWith("ed25519:") ? base58.decode(keyPair.secretKey.slice(8)) : new Uint8Array(0);
	const publicKeyBase58 = keyPair.publicKey.toString().replace("ed25519:", "");
	const accountId = bytesToHex(keyPair.publicKey.data);
	const createdAt = /* @__PURE__ */ new Date();
	const { encrypted, iv } = await encryptPrivateKey(privateKeyBytes, secret);
	await adapter.create({
		model: "relayerKey",
		data: {
			accountId,
			encryptedPrivateKey: encrypted,
			iv,
			publicKey: `ed25519:${publicKeyBase58}`,
			network,
			createdAt,
			lastUsedAt: createdAt
		}
	});
	console.log(`[siwn] Relayer created in EPHEMERAL mode: ${accountId} (${network})`);
	console.log(`[siwn] Fund this account with NEAR to enable gasless relay`);
	console.log(`[siwn] Private key is encrypted in DB — persists across restarts`);
	const keyStore = new InMemoryKeyStore();
	await keyStore.add(accountId, keyPair);
	return {
		near: createNear(network, headers, rpcUrl, keyStore),
		accountId,
		network,
		mode: "ephemeral",
		publicKey: keyPair.publicKey.toString(),
		whitelistedContracts: networkConfig.whitelistedContracts,
		maxGasPerTransaction: networkConfig.maxGasPerTransaction,
		maxDepositPerTransaction: networkConfig.maxDepositPerTransaction,
		createdAt,
		lastUsedAt: createdAt
	};
}
async function relayOnChain(payload, relayerState) {
	const userAction = decodeSignedDelegateAction(payload);
	return { txHash: (await relayerState.near.transaction(relayerState.accountId).signedDelegateAction(userAction).send({ waitUntil: "EXECUTED" })).transaction.hash };
}
async function defaultValidateLimitedAccessKey(accountId, publicKey, recipient, near) {
	const key = await near.getAccessKey(accountId, publicKey);
	if (!key) return false;
	if (key.permission === "FullAccess") return true;
	if ("FunctionCall" in key.permission) return key.permission.FunctionCall.receiver_id === recipient;
	return false;
}
function isImplicitAccount(accountId) {
	return /^[0-9a-f]{64}$/.test(accountId);
}
function resolveParentAccount(subAccountCfg, parentKey, rState) {
	const parent = subAccountCfg?.parentAccount ?? rState?.accountId;
	if (!parent || isImplicitAccount(parent)) return {
		parentAccount: null,
		subAccountAvailable: false
	};
	if (!rState && !parentKey) return {
		parentAccount: null,
		subAccountAvailable: false
	};
	return {
		parentAccount: parent,
		subAccountAvailable: true
	};
}
const siwn = (options) => {
	if (!options.recipient && !options.recipients) throw new Error("Either 'recipient' or 'recipients' must be provided to siwn plugin");
	const relayerConfig = parseRelayerConfig(options.relayer);
	const apiKey = options.apiKey;
	const getRecipient = (network) => {
		if (options.recipients) return options.recipients[network];
		return options.recipient;
	};
	const getSupportedNetworks = () => {
		if (options.recipients) return ["mainnet", "testnet"];
		if (options.recipient) return [getNetworkFromAccountId(options.recipient)];
		return ["mainnet"];
	};
	const getRelayerConfig = (network) => {
		if (!relayerConfig) return void 0;
		return relayerConfig.kind === "dual" ? relayerConfig.config[network] : relayerConfig.config;
	};
	const getRpcUrl = (network) => {
		if (!options.rpcUrl) return void 0;
		if (typeof options.rpcUrl === "object") return options.rpcUrl[network];
		return options.rpcUrl;
	};
	const getSubAccountConfig = (network) => {
		if (!options.subAccount) return void 0;
		if ("mainnet" in options.subAccount && typeof options.subAccount.mainnet === "object") return options.subAccount[network];
		return options.subAccount;
	};
	const getParentKey = (network) => {
		if (!options.secrets?.parentKey) return void 0;
		if (typeof options.secrets.parentKey === "object") return options.secrets.parentKey[network];
		return options.secrets.parentKey;
	};
	const getRawParentKey = (network) => {
		const fromSecrets = getParentKey(network);
		if (fromSecrets) return fromSecrets;
		const relayerCfg = getRelayerConfig(network);
		const subAccountCfg = getSubAccountConfig(network);
		if (!relayerCfg || !relayerCfg.accountId || !relayerCfg.privateKey) return void 0;
		const parent = subAccountCfg?.parentAccount ?? relayerCfg.accountId;
		if (parent && parent === relayerCfg.accountId) return relayerCfg.privateKey;
	};
	const primaryNetwork = getSupportedNetworks()[0];
	const relayerStates = /* @__PURE__ */ new Map();
	const relayerInitPromises = /* @__PURE__ */ new Map();
	const headers = {};
	if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
	const ensureRelayer = async (adapter, secret, network) => {
		if (relayerStates.has(network)) return relayerStates.get(network) ?? null;
		const existingInit = relayerInitPromises.get(network);
		if (existingInit) return existingInit;
		const relayerCfg = getRelayerConfig(network);
		const initPromise = initRelayer(relayerCfg, network, adapter, secret, apiKey, getRpcUrl(network)).then((state) => {
			relayerStates.set(network, state);
			relayerInitPromises.delete(network);
			if (state) {
				console.log(`[siwn] Relayer initialized: ${state.accountId} (${network}, ${state.mode})`);
				if (state.mode === "ephemeral") {
					console.log(`[siwn] Fund this account with NEAR to enable gasless relay`);
					console.log(`[siwn] Private key is encrypted in DB — persists across restarts`);
				}
			} else if (relayerCfg) console.warn(`[siwn] Relayer config provided but not initialized for ${network}. Check BETTER_AUTH_SECRET for ephemeral mode.`);
			return state;
		}).catch((error) => {
			relayerInitPromises.delete(network);
			console.error(`[siwn] Failed to initialize relayer for ${network}:`, error);
			throw error;
		});
		relayerInitPromises.set(network, initPromise);
		return initPromise;
	};
	const getNear = (network) => {
		const networkRpcUrl = getRpcUrl(network);
		if (networkRpcUrl) return new Near({
			network: {
				rpcUrl: networkRpcUrl,
				networkId: network
			},
			headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : void 0
		});
		return new Near({
			network,
			headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : void 0
		});
	};
	return {
		id: "siwn",
		schema,
		init: (ctx) => {
			if (!relayerConfig) return;
			for (const network of getSupportedNetworks()) setImmediate(() => {
				ensureRelayer(ctx.adapter, ctx.secret, network).catch((err) => {
					console.error(`[siwn] Relayer init failed for ${network}: ${err instanceof Error ? err.message : String(err)}`);
				});
			});
		},
		hooks: { after: [{
			matcher: (context) => context.path === "/auth/session" && context.method === "GET",
			handler: createAuthMiddleware(async (ctx) => {
				const session = ctx.context.session;
				if (session) {
					const nearAccount = await ctx.context.adapter.findOne({
						model: "nearAccount",
						where: [{
							field: "userId",
							operator: "eq",
							value: session.user.id
						}, {
							field: "isPrimary",
							operator: "eq",
							value: true
						}]
					});
					if (nearAccount) ctx.context.session = {
						...session,
						user: {
							...session.user,
							nearAccount
						}
					};
				}
				return { context: ctx };
			})
		}] },
		endpoints: {
			linkNearAccount: createAuthEndpoint("/near/link-account", {
				method: "POST",
				body: LinkAccountRequest,
				use: [sessionMiddleware],
				requireRequest: true
			}, async (ctx) => {
				const { signedMessage, message, recipient, nonce, accountId, callbackUrl } = ctx.body;
				const network = getNetworkFromAccountId(accountId);
				const session = ctx.context.session;
				if (!session) throw new APIError("UNAUTHORIZED", {
					message: "Must be logged in to link NEAR account",
					status: 401
				});
				try {
					const near = getNear(network);
					if (!await verifyNep413Signature(signedMessage, {
						message,
						recipient,
						nonce: hex.decode(nonce),
						callbackUrl
					}, {
						near,
						maxAge: 900 * 1e3
					})) throw new APIError("UNAUTHORIZED", {
						message: "Unauthorized: Invalid signature",
						status: 401
					});
					if (signedMessage.accountId !== accountId) throw new APIError("UNAUTHORIZED", {
						message: "Unauthorized: Account ID mismatch",
						status: 401
					});
					const publicKey = signedMessage.publicKey;
					if (!options.requireFullAccessKey && options.validateLimitedAccessKey) {
						if (!await options.validateLimitedAccessKey({
							accountId,
							publicKey,
							recipient: options.recipient
						})) throw new APIError("UNAUTHORIZED", {
							message: "Unauthorized: Invalid function call access key",
							status: 401
						});
					}
					if (await ctx.context.adapter.findOne({
						model: "nearAccount",
						where: [{
							field: "accountId",
							operator: "eq",
							value: accountId
						}, {
							field: "network",
							operator: "eq",
							value: network
						}]
					})) throw new APIError("BAD_REQUEST", {
						message: "This NEAR account is already linked to another user",
						status: 400
					});
					const existingPrimaryAccount = await ctx.context.adapter.findOne({
						model: "nearAccount",
						where: [{
							field: "userId",
							operator: "eq",
							value: session.user.id
						}, {
							field: "isPrimary",
							operator: "eq",
							value: true
						}]
					});
					await ctx.context.adapter.create({
						model: "nearAccount",
						data: {
							userId: session.user.id,
							accountId,
							network,
							publicKey,
							isPrimary: !existingPrimaryAccount,
							createdAt: /* @__PURE__ */ new Date()
						}
					});
					await ctx.context.internalAdapter.createAccount({
						userId: session.user.id,
						providerId: "siwn",
						accountId: `${accountId}:${network}`,
						createdAt: /* @__PURE__ */ new Date(),
						updatedAt: /* @__PURE__ */ new Date()
					});
					return ctx.json({
						success: true,
						accountId,
						network,
						message: "NEAR account successfully linked"
					});
				} catch (error) {
					if (error instanceof APIError) throw error;
					throw new APIError("UNAUTHORIZED", {
						message: "Something went wrong. Please try again later.",
						error: error instanceof Error ? error.message : "Unknown error",
						status: 401
					});
				}
			}),
			unlinkNearAccount: createAuthEndpoint("/near/unlink-account", {
				method: "POST",
				body: z$1.object({
					accountId: z$1.string(),
					network: z$1.enum(["mainnet", "testnet"]).optional()
				}),
				use: [sessionMiddleware]
			}, async (ctx) => {
				const { accountId, network: providedNetwork } = ctx.body;
				const session = ctx.context.session;
				if (!session) throw new APIError("UNAUTHORIZED", {
					message: "Must be logged in to unlink NEAR account",
					status: 401
				});
				const network = providedNetwork || getNetworkFromAccountId(accountId);
				const nearAccount = await ctx.context.adapter.findOne({
					model: "nearAccount",
					where: [
						{
							field: "userId",
							operator: "eq",
							value: session.user.id
						},
						{
							field: "accountId",
							operator: "eq",
							value: accountId
						},
						{
							field: "network",
							operator: "eq",
							value: network
						}
					]
				});
				if (!nearAccount) throw new APIError("NOT_FOUND", {
					message: "NEAR account not found or not linked to your user",
					status: 404
				});
				if ((await ctx.context.adapter.findMany({
					model: "account",
					where: [{
						field: "userId",
						operator: "eq",
						value: session.user.id
					}]
				})).length <= 1) throw new APIError("BAD_REQUEST", {
					message: "Cannot unlink last authentication method. Link another account first.",
					status: 400
				});
				if (nearAccount.isPrimary) {
					const otherNearAccounts = await ctx.context.adapter.findMany({
						model: "nearAccount",
						where: [{
							field: "userId",
							operator: "eq",
							value: session.user.id
						}, {
							field: "accountId",
							operator: "ne",
							value: accountId
						}]
					});
					if (otherNearAccounts.length > 0) await ctx.context.adapter.update({
						model: "nearAccount",
						where: [{
							field: "id",
							operator: "eq",
							value: otherNearAccounts[0].id
						}],
						update: { isPrimary: true }
					});
				}
				await ctx.context.adapter.delete({
					model: "nearAccount",
					where: [
						{
							field: "userId",
							operator: "eq",
							value: session.user.id
						},
						{
							field: "accountId",
							operator: "eq",
							value: accountId
						},
						{
							field: "network",
							operator: "eq",
							value: network
						}
					]
				});
				const accountToDelete = await ctx.context.adapter.findOne({
					model: "account",
					where: [
						{
							field: "userId",
							operator: "eq",
							value: session.user.id
						},
						{
							field: "providerId",
							operator: "eq",
							value: "siwn"
						},
						{
							field: "accountId",
							operator: "eq",
							value: `${accountId}:${network}`
						}
					]
				});
				if (accountToDelete) await ctx.context.internalAdapter.deleteAccount(accountToDelete.id);
				return ctx.json({
					success: true,
					accountId,
					network,
					message: "NEAR account successfully unlinked"
				});
			}),
			listNearAccounts: createAuthEndpoint("/near/list-accounts", {
				method: "GET",
				use: [sessionMiddleware]
			}, async (ctx) => {
				const session = ctx.context.session;
				const nearAccounts = await ctx.context.adapter.findMany({
					model: "nearAccount",
					where: [{
						field: "userId",
						operator: "eq",
						value: session.user.id
					}]
				});
				return ctx.json(buildListAccountsResponse(nearAccounts));
			}),
			setPrimaryNearAccount: createAuthEndpoint("/near/set-primary-account", {
				method: "POST",
				body: SetPrimaryAccountRequest,
				use: [sessionMiddleware]
			}, async (ctx) => {
				const { accountId, network: providedNetwork } = ctx.body;
				const session = ctx.context.session;
				const network = providedNetwork || getNetworkFromAccountId(accountId);
				const targetAccount = await ctx.context.adapter.findOne({
					model: "nearAccount",
					where: [
						{
							field: "userId",
							operator: "eq",
							value: session.user.id
						},
						{
							field: "accountId",
							operator: "eq",
							value: accountId
						},
						{
							field: "network",
							operator: "eq",
							value: network
						}
					]
				});
				if (!targetAccount) throw new APIError("NOT_FOUND", {
					message: "NEAR account not found or not linked to your user",
					status: 404
				});
				const nearAccounts = await ctx.context.adapter.findMany({
					model: "nearAccount",
					where: [{
						field: "userId",
						operator: "eq",
						value: session.user.id
					}]
				});
				await Promise.all(nearAccounts.map((account) => ctx.context.adapter.update({
					model: "nearAccount",
					where: [{
						field: "id",
						operator: "eq",
						value: account.id
					}],
					update: { isPrimary: nearAccountKey(account) === nearAccountKey(targetAccount) }
				})));
				const updatedNearAccounts = await ctx.context.adapter.findMany({
					model: "nearAccount",
					where: [{
						field: "userId",
						operator: "eq",
						value: session.user.id
					}]
				});
				return ctx.json({
					success: true,
					accountId,
					network,
					message: "Primary NEAR account updated",
					...buildListAccountsResponse(updatedNearAccounts)
				});
			}),
			getSiwnNonce: createAuthEndpoint("/near/nonce", {
				method: "POST",
				body: NonceRequest
			}, async (ctx) => {
				const { accountId, networkId } = ctx.body;
				const network = getNetworkFromAccountId(accountId);
				if (networkId !== network) throw new APIError("BAD_REQUEST", {
					message: "Network ID mismatch with account ID",
					status: 400
				});
				if (!await getNear(network).accountExists(accountId)) throw new APIError("BAD_REQUEST", {
					message: "Account does not exist on-chain",
					status: 400
				});
				const nonce = options.getNonce ? await options.getNonce() : generateNonce();
				const nonceString = hex.encode(nonce);
				await ctx.context.internalAdapter.createVerificationValue({
					identifier: `siwn:${accountId}:${network}`,
					value: nonceString,
					expiresAt: new Date(Date.now() + 900 * 1e3)
				});
				return ctx.json(NonceResponse.parse({ nonce: nonceString }));
			}),
			getSiwnProfile: createAuthEndpoint("/near/profile", {
				method: "POST",
				body: ProfileRequest
			}, async (ctx) => {
				const { accountId } = ctx.body;
				let targetAccountId = accountId;
				if (!targetAccountId) {
					const session = (await getSessionFromCtx(ctx))?.session;
					if (!session) throw new APIError("UNAUTHORIZED", {
						message: "Session required when no accountId provided",
						status: 401
					});
					const nearAccount = await ctx.context.adapter.findOne({
						model: "nearAccount",
						where: [{
							field: "userId",
							operator: "eq",
							value: session.user.id
						}, {
							field: "isPrimary",
							operator: "eq",
							value: true
						}]
					});
					if (!nearAccount) throw new APIError("NOT_FOUND", {
						message: "No NEAR account found for user",
						status: 404
					});
					targetAccountId = nearAccount.accountId;
				}
				const profile = await (options.getProfile || ((id) => defaultGetProfile(id, apiKey)))(targetAccountId);
				return ctx.json(ProfileResponse.parse(profile));
			}),
			verifySiwnMessage: createAuthEndpoint("/near/verify", {
				method: "POST",
				body: VerifyRequest,
				requireRequest: true
			}, async (ctx) => {
				const { signedMessage, message, recipient, nonce, accountId, callbackUrl } = ctx.body;
				const network = getNetworkFromAccountId(accountId);
				try {
					const near = getNear(network);
					const nonceBytes = hex.decode(nonce);
					if (!await verifyNep413Signature(signedMessage, {
						message,
						recipient,
						nonce: nonceBytes,
						callbackUrl
					}, {
						near,
						maxAge: 900 * 1e3
					})) throw new APIError("UNAUTHORIZED", {
						message: "Unauthorized: Invalid signature",
						status: 401
					});
					if (signedMessage.accountId !== accountId) throw new APIError("UNAUTHORIZED", {
						message: "Unauthorized: Account ID mismatch",
						status: 401
					});
					const publicKey = signedMessage.publicKey;
					const nonceHash = await hashNonce(nonceBytes);
					if (await ctx.context.internalAdapter.findVerificationValue(`siwn-nonce:${nonceHash}`)) throw new APIError("UNAUTHORIZED", {
						message: "Unauthorized: Nonce already used (replay attack detected)",
						status: 401,
						code: "UNAUTHORIZED_NONCE_REPLAY"
					});
					await ctx.context.internalAdapter.createVerificationValue({
						identifier: `siwn-nonce:${nonceHash}`,
						value: "used",
						expiresAt: new Date(Date.now() + 900 * 1e3)
					});
					if (!options.requireFullAccessKey) {
						if (!await (options.validateLimitedAccessKey || ((args) => defaultValidateLimitedAccessKey(args.accountId, args.publicKey, args.recipient || getRecipient(network), near)))({
							accountId,
							publicKey,
							recipient: options.recipient
						})) throw new APIError("UNAUTHORIZED", {
							message: "Unauthorized: Invalid function call access key",
							status: 401
						});
					}
					let user = null;
					const existingNearAccount = await ctx.context.adapter.findOne({
						model: "nearAccount",
						where: [{
							field: "accountId",
							operator: "eq",
							value: accountId
						}, {
							field: "network",
							operator: "eq",
							value: network
						}]
					});
					if (existingNearAccount) user = await ctx.context.adapter.findOne({
						model: "user",
						where: [{
							field: "id",
							operator: "eq",
							value: existingNearAccount.userId
						}]
					});
					else {
						const anyNearAccount = await ctx.context.adapter.findOne({
							model: "nearAccount",
							where: [{
								field: "accountId",
								operator: "eq",
								value: accountId
							}]
						});
						if (anyNearAccount) user = await ctx.context.adapter.findOne({
							model: "user",
							where: [{
								field: "id",
								operator: "eq",
								value: anyNearAccount.userId
							}]
						});
					}
					if (!user) {
						const userEmail = deriveEmail(accountId, getRecipient(network));
						const profile = await (options.getProfile || ((id) => defaultGetProfile(id, apiKey)))(accountId);
						user = await ctx.context.internalAdapter.createUser({
							name: profile?.name ?? accountId,
							email: userEmail,
							image: profile?.image ? getImageUrl(profile.image) : ""
						});
						await ctx.context.adapter.create({
							model: "nearAccount",
							data: {
								userId: user.id,
								accountId,
								network,
								publicKey,
								isPrimary: true,
								createdAt: /* @__PURE__ */ new Date()
							}
						});
						await ctx.context.internalAdapter.createAccount({
							userId: user.id,
							providerId: "siwn",
							accountId: `${accountId}:${network}`,
							createdAt: /* @__PURE__ */ new Date(),
							updatedAt: /* @__PURE__ */ new Date()
						});
					} else if (!existingNearAccount) {
						await ctx.context.adapter.create({
							model: "nearAccount",
							data: {
								userId: user.id,
								accountId,
								network,
								publicKey,
								isPrimary: false,
								createdAt: /* @__PURE__ */ new Date()
							}
						});
						await ctx.context.internalAdapter.createAccount({
							userId: user.id,
							providerId: "siwn",
							accountId: `${accountId}:${network}`,
							createdAt: /* @__PURE__ */ new Date(),
							updatedAt: /* @__PURE__ */ new Date()
						});
					}
					await ensureRelayer(ctx.context.adapter, ctx.context.secret, network);
					const session = await ctx.context.internalAdapter.createSession(user.id);
					if (!session) throw new APIError("INTERNAL_SERVER_ERROR", {
						message: "Internal Server Error",
						status: 500
					});
					await setSessionCookie(ctx, {
						session,
						user
					});
					return ctx.json(VerifyResponse.parse({
						token: session.token,
						success: true,
						user: {
							id: user.id,
							accountId,
							network
						}
					}));
				} catch (error) {
					if (error instanceof APIError) throw error;
					const msg = error instanceof Error ? error.message : "Unknown error";
					const at = error instanceof Error && error.stack ? error.stack.split("\n").slice(1, 3).map((s) => s.trim()).join(" <- ") : "";
					console.error(`[siwn] Verify error: ${msg}${at ? ` (${at})` : ""}`);
					throw new APIError("UNAUTHORIZED", {
						message: "Something went wrong. Please try again later.",
						error: msg,
						status: 401
					});
				}
			}),
			relayNearTransaction: createAuthEndpoint("/near/relay", {
				method: "POST",
				body: RelayRequest,
				use: [sessionMiddleware]
			}, async (ctx) => {
				const { payload } = ctx.body;
				const session = ctx.context.session;
				if (!session) throw new APIError("UNAUTHORIZED", {
					message: "Must be authenticated to relay transactions",
					status: 401
				});
				const nearAccount = await ctx.context.adapter.findOne({
					model: "nearAccount",
					where: [{
						field: "userId",
						operator: "eq",
						value: session.user.id
					}, {
						field: "isPrimary",
						operator: "eq",
						value: true
					}]
				});
				if (!nearAccount) throw new APIError("UNAUTHORIZED", {
					message: "No NEAR account linked to session",
					status: 401
				});
				const network = nearAccount.network;
				const rState = await ensureRelayer(ctx.context.adapter, ctx.context.secret, network);
				if (!rState) throw new APIError("SERVICE_UNAVAILABLE", {
					message: "Relayer not configured",
					status: 503
				});
				try {
					const delegateAction = decodeSignedDelegateAction(payload).signedDelegate.delegateAction;
					if (delegateAction.senderId !== nearAccount.accountId) throw new APIError("UNAUTHORIZED", {
						message: "Delegate action sender does not match session account",
						status: 401
					});
					const relayerConfigForNetwork = getRelayerConfig(network);
					const whitelistedContracts = relayerConfigForNetwork?.whitelistedContracts;
					if (whitelistedContracts?.length) {
						if (!whitelistedContracts.includes(delegateAction.receiverId)) throw new APIError("FORBIDDEN", {
							message: `Contract ${delegateAction.receiverId} is not whitelisted for relay`,
							status: 403
						});
					}
					const maxGasPerTransaction = relayerConfigForNetwork?.maxGasPerTransaction;
					if (maxGasPerTransaction) {
						const totalGas = delegateAction.actions.reduce((sum, a) => {
							return sum + ("functionCall" in a ? a.functionCall.gas : 0n);
						}, 0n);
						if (totalGas > BigInt(maxGasPerTransaction)) throw new APIError("BAD_REQUEST", {
							message: `Transaction gas (${totalGas}) exceeds relayer limit (${maxGasPerTransaction})`,
							status: 400
						});
					}
					const maxDepositPerTransaction = relayerConfigForNetwork?.maxDepositPerTransaction;
					if (maxDepositPerTransaction) {
						const totalDeposit = delegateAction.actions.reduce((sum, a) => {
							if ("functionCall" in a) return sum + a.functionCall.deposit;
							if ("transfer" in a) return sum + a.transfer.deposit;
							return sum;
						}, 0n);
						if (totalDeposit > BigInt(maxDepositPerTransaction)) throw new APIError("BAD_REQUEST", {
							message: `Transaction deposit (${totalDeposit}) exceeds relayer limit (${maxDepositPerTransaction})`,
							status: 400
						});
					}
					const result = await relayOnChain(payload, rState);
					await ctx.context.adapter.create({
						model: "relayedTransaction",
						data: {
							userId: session.user.id,
							txHash: result.txHash,
							senderId: delegateAction.senderId,
							receiverId: delegateAction.receiverId,
							network,
							status: "pending",
							createdAt: /* @__PURE__ */ new Date()
						}
					});
					if (rState.mode === "ephemeral") await ctx.context.adapter.update({
						model: "relayerKey",
						where: [{
							field: "network",
							operator: "eq",
							value: network
						}],
						update: { lastUsedAt: /* @__PURE__ */ new Date() }
					});
					return ctx.json(RelayResponse.parse({
						txHash: result.txHash,
						status: "pending"
					}));
				} catch (error) {
					if (error instanceof APIError) throw error;
					throw new APIError("INTERNAL_SERVER_ERROR", {
						message: error instanceof Error ? error.message : "Relay failed",
						status: 500
					});
				}
			}),
			getRelayStatus: createAuthEndpoint("/near/relay-status/:txHash", {
				method: "GET",
				use: [sessionMiddleware]
			}, async (ctx) => {
				const txHash = ctx.params?.txHash;
				if (!txHash) throw new APIError("BAD_REQUEST", {
					message: "Transaction hash required",
					status: 400
				});
				const session = ctx.context.session;
				const relayedTx = await ctx.context.adapter.findOne({
					model: "relayedTransaction",
					where: [{
						field: "txHash",
						operator: "eq",
						value: txHash
					}, {
						field: "userId",
						operator: "eq",
						value: session.user.id
					}]
				});
				if (!relayedTx) throw new APIError("NOT_FOUND", {
					message: "Transaction not found or not owned by this user",
					status: 404
				});
				const network = relayedTx.network;
				const senderId = relayedTx.senderId;
				try {
					const txResult = await getNear(network).getTransactionStatus(txHash, senderId);
					const txStatus = txResult.status;
					if (txStatus && typeof txStatus === "object") {
						const hasSuccess = "SuccessValue" in txStatus || "SuccessReceiptId" in txStatus;
						const hasFailure = "Failure" in txStatus;
						const status = hasSuccess ? "completed" : hasFailure ? "failed" : "pending";
						if (status !== "pending") await ctx.context.adapter.update({
							model: "relayedTransaction",
							where: [{
								field: "txHash",
								operator: "eq",
								value: txHash
							}],
							update: { status }
						});
						const gasUsed = txResult.transaction_outcome?.outcome?.gas_burnt?.toString();
						return ctx.json(RelayStatusResponse.parse({
							status,
							gasUsed,
							outcome: txResult
						}));
					}
					return ctx.json(RelayStatusResponse.parse({ status: "pending" }));
				} catch (error) {
					return ctx.json(RelayStatusResponse.parse({ status: "pending" }));
				}
			}),
			getRelayerInfo: createAuthEndpoint("/near/relayer-info", {
				method: "POST",
				body: GetRelayerInfoRequest,
				use: [sessionMiddleware]
			}, async (ctx) => {
				const { network: requestedNetwork } = ctx.body;
				const targetNetwork = requestedNetwork ?? primaryNetwork;
				let rState = null;
				try {
					rState = await ensureRelayer(ctx.context.adapter, ctx.context.secret, targetNetwork);
				} catch (error) {
					const message = error instanceof Error ? error.message : String(error);
					console.error(`[siwn] getRelayerInfo: relayer init failed for ${targetNetwork}: ${message}`);
					return ctx.json({
						enabled: false,
						error: message,
						subAccountAvailable: false
					});
				}
				if (!rState) return ctx.json({
					enabled: false,
					subAccountAvailable: false
				});
				const { parentAccount, subAccountAvailable } = resolveParentAccount(getSubAccountConfig(targetNetwork), getParentKey(targetNetwork), rState);
				const near = getNear(rState.network);
				let account;
				try {
					account = await near.getAccount(rState.accountId);
				} catch {
					account = {
						balance: "0",
						available: "0",
						staked: "0",
						storageUsage: "0",
						storageBytes: 0,
						hasContract: false,
						codeHash: ""
					};
				}
				return ctx.json({
					enabled: true,
					accountId: rState.accountId,
					mode: rState.mode,
					network: rState.network,
					publicKey: rState.publicKey,
					balance: account.balance,
					available: account.available,
					staked: account.staked,
					storageUsage: account.storageUsage,
					storageBytes: account.storageBytes,
					hasContract: account.hasContract,
					hasKey: true,
					createdAt: rState.createdAt,
					lastUsedAt: rState.lastUsedAt,
					parentAccount: parentAccount ?? void 0,
					subAccountAvailable
				});
			}),
			getRelayHistory: createAuthEndpoint("/near/relay-history", {
				method: "GET",
				use: [sessionMiddleware]
			}, async (ctx) => {
				const session = ctx.context.session;
				let transactions = [];
				try {
					transactions = await ctx.context.adapter.findMany({
						model: "relayedTransaction",
						where: [{
							field: "userId",
							operator: "eq",
							value: session.user.id
						}]
					}) || [];
				} catch (err) {
					console.error("relay-history findMany error:", err);
				}
				const sorted = transactions.sort((a, b) => {
					const aTime = a.createdAt instanceof Date ? a.createdAt.getTime() : new Date(a.createdAt ?? 0).getTime();
					return (b.createdAt instanceof Date ? b.createdAt.getTime() : new Date(b.createdAt ?? 0).getTime()) - aTime;
				});
				return ctx.json({ transactions: sorted.map((tx) => ({
					id: String(tx.id ?? ""),
					userId: String(tx.userId ?? ""),
					txHash: String(tx.txHash ?? ""),
					senderId: String(tx.senderId ?? ""),
					receiverId: String(tx.receiverId ?? ""),
					network: String(tx.network ?? "mainnet"),
					status: String(tx.status ?? "pending"),
					gasUsed: tx.gasUsed ? String(tx.gasUsed) : void 0,
					createdAt: tx.createdAt instanceof Date ? tx.createdAt.toISOString() : tx.createdAt ? String(tx.createdAt) : (/* @__PURE__ */ new Date()).toISOString(),
					updatedAt: tx.updatedAt instanceof Date ? tx.updatedAt.toISOString() : tx.updatedAt ? String(tx.updatedAt) : void 0
				})) });
			}),
			viewContract: createAuthEndpoint("/near/view", {
				method: "POST",
				body: ViewContractRequest,
				use: [sessionMiddleware]
			}, async (ctx) => {
				const { contractId, methodName, args } = ctx.body;
				const session = ctx.context.session;
				const near = getNear((await ctx.context.adapter.findOne({
					model: "nearAccount",
					where: [{
						field: "userId",
						operator: "eq",
						value: session.user.id
					}, {
						field: "isPrimary",
						operator: "eq",
						value: true
					}]
				}))?.network || primaryNetwork);
				let result;
				try {
					result = await near.view(contractId, methodName, args ?? {});
				} catch (error) {
					if (error instanceof APIError) throw error;
					throw new APIError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Unknown error" });
				}
				return ctx.json(ViewContractResponse.parse({ result }));
			}),
			createSubAccount: createAuthEndpoint("/near/create-sub-account", {
				method: "POST",
				body: CreateSubAccountRequest,
				use: [sessionMiddleware]
			}, async (ctx) => {
				const { subAccountName, network: providedNetwork, publicKey } = ctx.body;
				const session = ctx.context.session;
				if (!session) throw new APIError("UNAUTHORIZED", {
					message: "Must be authenticated to create a sub-account",
					status: 401
				});
				const nearAccount = await ctx.context.adapter.findOne({
					model: "nearAccount",
					where: [{
						field: "userId",
						operator: "eq",
						value: session.user.id
					}, {
						field: "isPrimary",
						operator: "eq",
						value: true
					}]
				});
				const network = providedNetwork || nearAccount?.network || primaryNetwork;
				const subAccountCfg = getSubAccountConfig(network);
				const userAccountId = nearAccount?.accountId ?? "";
				const parentKey = getParentKey(network);
				const rState = await ensureRelayer(ctx.context.adapter, ctx.context.secret, network);
				const { parentAccount, subAccountAvailable } = resolveParentAccount(subAccountCfg, parentKey, rState);
				if (!subAccountAvailable || !parentAccount) {
					const configuredParent = subAccountCfg?.parentAccount;
					let msg;
					if (configuredParent && !parentKey) msg = `Sub-account creation unavailable on ${network}: parent key not configured for ${configuredParent}. Set NEAR_SUB_ACCOUNT_PARENT_KEY_${network.toUpperCase()} in your environment, or use an explicit relayer with a named account.`;
					else if (!configuredParent) msg = `Sub-account creation unavailable on ${network}: no parent account configured. Set subAccount.${network}.parentAccount in your SIWN plugin options, or use an explicit relayer with a named account.`;
					else msg = `Sub-account creation unavailable on ${network}: requires a named parent account with a parent key, or an explicit relayer with a named account.`;
					throw new APIError("SERVICE_UNAVAILABLE", {
						message: msg,
						status: 503
					});
				}
				const effectiveParentKey = getRawParentKey(network);
				if (parentAccount !== rState?.accountId && !effectiveParentKey) throw new APIError("SERVICE_UNAVAILABLE", {
					message: "Sub-account parent differs from relayer account. Provide secrets.parentKey to sign as the parent account.",
					status: 503
				});
				const newAccountId = `${subAccountName}.${parentAccount}`;
				const minDeposit = subAccountCfg?.minDeposit || "0.1 NEAR";
				const near = getNear(network);
				if (await near.accountExists(newAccountId)) throw new APIError("CONFLICT", {
					message: `Account ${newAccountId} already exists on ${network}`,
					status: 409
				});
				const txCtx = {
					newAccountId,
					parentAccount,
					userPublicKey: publicKey,
					userAccountId,
					userId: session.user.id,
					network
				};
				const signingNear = rState?.near ?? near;
				let txBuilder = signingNear.transaction(parentAccount).createAccount(newAccountId).addKey(publicKey, { type: "fullAccess" });
				if (subAccountCfg?.parentHasFullAccess) {
					const parentPublicKey = parentAccount === rState?.accountId ? rState.publicKey : parseKey(effectiveParentKey).publicKey.toString();
					txBuilder = txBuilder.addKey(parentPublicKey, { type: "fullAccess" });
				}
				if (subAccountCfg?.addRelayerFCAK !== false && subAccountCfg?.relayerFCAK && rState) {
					const fcakPermission = {
						type: "functionCall",
						receiverId: subAccountCfg.relayerFCAK.receiverId
					};
					if (subAccountCfg.relayerFCAK.methodNames) fcakPermission.methodNames = subAccountCfg.relayerFCAK.methodNames;
					if (subAccountCfg.relayerFCAK.allowance) fcakPermission.allowance = subAccountCfg.relayerFCAK.allowance;
					txBuilder.addKey(rState.publicKey, fcakPermission);
				}
				txBuilder.transfer(newAccountId, minDeposit);
				if (subAccountCfg?.deploy) {
					if ("wasm" in subAccountCfg.deploy) txBuilder = txBuilder.deployContract(newAccountId, subAccountCfg.deploy.wasm);
					else if ("fromPublished" in subAccountCfg.deploy) txBuilder = txBuilder.deployFromPublished(subAccountCfg.deploy.fromPublished);
				}
				if (subAccountCfg?.init) {
					const initArgs = typeof subAccountCfg.init.args === "function" ? subAccountCfg.init.args(txCtx) : subAccountCfg.init.args;
					txBuilder = txBuilder.functionCall(newAccountId, subAccountCfg.init.methodName, initArgs);
				}
				if (subAccountCfg?.extendTx) txBuilder = subAccountCfg.extendTx(txBuilder, txCtx);
				if (effectiveParentKey) txBuilder = txBuilder.signWith(effectiveParentKey);
				const cleanupDb = async () => {
					try {
						const existing = await ctx.context.adapter.findOne({
							model: "nearAccount",
							where: [{
								field: "accountId",
								operator: "eq",
								value: newAccountId
							}]
						});
						if (existing) await ctx.context.adapter.delete({
							model: "nearAccount",
							where: [{
								field: "id",
								operator: "eq",
								value: existing.id
							}]
						});
					} catch {}
					try {
						await ctx.context.internalAdapter.deleteAccount(`${newAccountId}:${network}`);
					} catch {}
				};
				const lifecycleCtx = {
					...txCtx,
					near: signingNear
				};
				try {
					await txBuilder.send({ waitUntil: "EXECUTED" });
				} catch (error) {
					throw new APIError("INTERNAL_SERVER_ERROR", {
						message: `Failed to create sub-account: ${error instanceof Error ? error.message : "Unknown error"}`,
						status: 500
					});
				}
				try {
					await ctx.context.adapter.create({
						model: "nearAccount",
						data: {
							userId: session.user.id,
							accountId: newAccountId,
							network,
							publicKey,
							isPrimary: false,
							createdAt: /* @__PURE__ */ new Date()
						}
					});
					await ctx.context.internalAdapter.createAccount({
						userId: session.user.id,
						providerId: "siwn",
						accountId: `${newAccountId}:${network}`,
						createdAt: /* @__PURE__ */ new Date(),
						updatedAt: /* @__PURE__ */ new Date()
					});
					if (subAccountCfg?.onCreated) await subAccountCfg.onCreated(lifecycleCtx);
				} catch (error) {
					await cleanupDb();
					if (subAccountCfg?.onRollback) try {
						await subAccountCfg.onRollback(lifecycleCtx);
					} catch {}
					else try {
						const key = getRawParentKey(network);
						if (key) await getNear(network).transaction(newAccountId).deleteAccount({ beneficiary: parentAccount }).signWith(key).send({ waitUntil: "EXECUTED" });
					} catch {}
					throw new APIError("INTERNAL_SERVER_ERROR", {
						message: `Sub-account created but post-creation failed: ${error instanceof Error ? error.message : "Unknown error"}`,
						status: 500
					});
				}
				return ctx.json(CreateSubAccountResponse.parse({
					success: true,
					accountId: newAccountId,
					network,
					publicKey,
					message: `Sub-account ${newAccountId} created on ${network}`
				}));
			}),
			checkSubAccountAvailability: createAuthEndpoint("/near/check-sub-account-availability", {
				method: "POST",
				body: CheckSubAccountAvailabilityRequest,
				use: [sessionMiddleware]
			}, async (ctx) => {
				const { subAccountName, network: providedNetwork } = ctx.body;
				const session = ctx.context.session;
				if (!session) throw new APIError("UNAUTHORIZED", {
					message: "Must be authenticated to check sub-account availability",
					status: 401
				});
				const nearAccount = await ctx.context.adapter.findOne({
					model: "nearAccount",
					where: [{
						field: "userId",
						operator: "eq",
						value: session.user.id
					}, {
						field: "isPrimary",
						operator: "eq",
						value: true
					}]
				});
				if (!nearAccount) throw new APIError("UNAUTHORIZED", {
					message: "Must have a linked NEAR wallet to check sub-account availability",
					status: 401
				});
				const network = providedNetwork || nearAccount.network || primaryNetwork;
				const parentAccount = getSubAccountConfig(network)?.parentAccount ?? null;
				if (!parentAccount || isImplicitAccount(parentAccount)) return ctx.json(CheckSubAccountAvailabilityResponse.parse({
					available: false,
					accountId: "",
					reason: "not-configured"
				}));
				const accountId = `${subAccountName}.${parentAccount}`;
				if (accountId.length > 64) return ctx.json(CheckSubAccountAvailabilityResponse.parse({
					available: false,
					accountId,
					parentAccount,
					reason: "too-long"
				}));
				const exists = await getNear(network).accountExists(accountId);
				return ctx.json(CheckSubAccountAvailabilityResponse.parse({
					available: !exists,
					accountId,
					parentAccount,
					reason: exists ? "taken" : void 0
				}));
			})
		}
	};
};
//#endregion
export { CheckSubAccountAvailabilityRequest, CheckSubAccountAvailabilityResponse, CreateSubAccountRequest, CreateSubAccountResponse, GetRelayerInfoRequest, LinkAccountRequest, NonceRequest, NonceResponse, ProfileRequest, ProfileResponse, RelayHistoryResponse, RelayRequest, RelayResponse, RelayStatusResponse, RelayedTransactionSchema, SUB_ACCOUNT_LABEL_REGEX, SetPrimaryAccountRequest, SubAccountNameSchema, VerifyRequest, VerifyResponse, ViewContractRequest, ViewContractResponse, parseRelayerConfig, profileSchema, relayerConfigSchema, relayerDualNetworkConfigSchema, siwn, socialImageSchema };

//# sourceMappingURL=index.js.map