@dulcetlabs/suglink
v0.3.0
Published
Social login with multi-chain wallet creation (Solana & Sui) - Non-custodial Web3 onboarding made simple
Maintainers
Readme
SugLink
DULCET LABS INTERNAL PACKAGE
⚠️ IMPORTANT: PERMISSION REQUIRED FOR USE ⚠️
This software requires explicit written permission from Dulcet Labs before any commercial use, including but not limited to production deployment, incorporation into commercial products, or use by competing businesses.
SugLink is an internal Dulcet Labs library that enables easy onboarding and multi-chain wallet creation using existing social accounts (Google, TikTok, and Snapchat). It provides a seamless way to generate deterministic keypairs for Solana and Sui blockchains based on social accounts.
Features
- Multi-Chain Support: Generate wallets for Solana and Sui blockchains from the same social login
- App-Specific Wallet Isolation: Each app gets unique wallets for the same user via
appIdparameter - Chain Selection: Choose
"solana","sui", or"all"chains during login - Universal Compatibility: Works seamlessly in both Node.js and browser environments using Web Crypto API and native crypto modules
- Social Login Integration: Full Google OAuth support, with TikTok and Snapchat in development
- Deterministic Keypair Generation: Generates blockchain keypairs based on social credentials using universal crypto implementations
- Backward Compatibility: Existing Solana-only implementations continue to work without changes
- Secure OAuth Flow Handling: Manages OAuth securely with CSRF protection and environment variable support
- TypeScript Support: Includes TypeScript definitions for better development experience
- Dual Module Support: Compatible with both ESM and CommonJS
- Browser Ready: Automatically detects environment and uses appropriate crypto APIs (Web Crypto API in browsers, Node.js crypto in server)
Universal Compatibility
SugLink works in all JavaScript environments:
✅ Node.js Applications
- Uses native Node.js
cryptomodule for maximum performance - Environment variables supported via
dotenv - Full OAuth server implementation
✅ Browser Applications
- Uses Web Crypto API for secure operations
- Works with React, Vue, Angular, vanilla JavaScript
- Compatible with all modern bundlers (Webpack, Vite, Rollup)
- No Node.js dependencies in browser builds
✅ React Native
- Universal crypto implementation ensures compatibility
- Same API across all platforms
🔧 Environment Detection
SugLink automatically detects the runtime environment and uses the appropriate crypto implementation:
- Browsers: Web Crypto API (
window.crypto.subtle) - Node.js: Native crypto module (
require('crypto')) - Buffer: Universal Buffer polyfill for browser compatibility
Installation
Install the package using npm, yarn, or pnpm:
# Using npm
npm install @dulcetlabs/suglink @solana/web3.js
# For Sui support (optional)
npm install @mysten/sui
# Using yarn
yarn add @dulcetlabs/suglink @solana/web3.js @mysten/sui
# Using pnpm
pnpm add @dulcetlabs/suglink @solana/web3.js @mysten/suiBrowser Setup Notes
For browser applications, modern bundlers automatically handle the universal compatibility:
- Webpack: Automatically polyfills Node.js modules
- Vite: Built-in support for universal packages
- Create React App: Works out of the box
- Next.js: Full compatibility in both SSR and client-side
No additional configuration required! SugLink automatically detects the browser environment and uses Web Crypto API.
Setup
Environment Variables
Create a .env file in your project root and add your OAuth credentials:
# Google OAuth Configuration
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
GOOGLE_REDIRECT_URI=your_redirect_uri
# TikTok OAuth Configuration
TIKTOK_CLIENT_ID=your_tiktok_client_id
TIKTOK_CLIENT_SECRET=your_tiktok_client_secret
TIKTOK_REDIRECT_URI=your_redirect_uri
# Snapchat OAuth Configuration
SNAPCHAT_CLIENT_ID=your_snapchat_client_id
SNAPCHAT_CLIENT_SECRET=your_snapchat_client_secret
SNAPCHAT_REDIRECT_URI=your_redirect_uriLoading Environment Variables
Ensure that the dotenv package is used to load these variables:
import * as dotenv from "dotenv";
// Load environment variables from .env file
dotenv.config();Usage
React Application Example
import React, { useState, useEffect } from "react";
import { SugLink } from "@dulcetlabs/suglink";
const App = () => {
const [publicKey, setPublicKey] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const sugLink = new SugLink();
// Handle OAuth callback
useEffect(() => {
const handleCallback = async () => {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get("code");
const state = urlParams.get("state");
const expectedState = localStorage.getItem("oauth_state");
if (code && state && expectedState) {
try {
setIsLoading(true);
await sugLink.handleCallback("google", code, state, expectedState);
setPublicKey(sugLink.getPublicKey());
localStorage.removeItem("oauth_state");
// Clean up URL
window.history.replaceState(
{},
document.title,
window.location.pathname
);
} catch (error) {
console.error("Login failed:", error);
} finally {
setIsLoading(false);
}
}
};
handleCallback();
}, []);
const handleLogin = async (platform: "google") => {
try {
const state = await sugLink.generateState();
localStorage.setItem("oauth_state", state);
const authUrl = sugLink.getAuthUrl(platform, state);
window.location.href = authUrl;
} catch (error) {
console.error("Failed to initiate login:", error);
}
};
const handleLogout = () => {
sugLink.logout();
setPublicKey(null);
};
if (isLoading) return <div>Logging in...</div>;
return (
<div>
{!publicKey ? (
<button onClick={() => handleLogin("google")}>Login with Google</button>
) : (
<div>
<p>Public Key: {publicKey}</p>
<button onClick={handleLogout}>Logout</button>
</div>
)}
</div>
);
};
export default App;Multi-Chain Support Example
import React, { useState } from "react";
import { SugLink } from "@dulcetlabs/suglink";
import { Keypair } from "@solana/web3.js";
const MultiChainApp = () => {
const [wallets, setWallets] = useState<{
solana?: { publicKey: string; privateKey: Uint8Array };
sui?: { address: string; privateKey: string };
}>({});
const [selectedChain, setSelectedChain] = useState<"solana" | "sui" | "all">(
"all"
);
const sugLink = new SugLink();
const handleMultiChainLogin = async () => {
try {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get("code");
if (!code) {
// Redirect to OAuth
const state = await sugLink.generateState();
localStorage.setItem("oauth_state", state);
const authUrl = sugLink.getAuthUrl("google", state);
window.location.href = authUrl;
return;
}
// Handle OAuth callback with multi-chain support
const result = await sugLink.login("google", code, selectedChain);
if (typeof result === "object" && "solana" in result) {
// Multi-chain result
const walletData: typeof wallets = {};
if (result.solana) {
walletData.solana = {
publicKey: result.solana.publicKey,
privateKey: result.solana.privateKey,
};
}
if (result.sui) {
walletData.sui = {
address: result.sui.address,
privateKey: result.sui.privateKey,
};
}
setWallets(walletData);
} else {
// Legacy Solana-only result
const keypair = result as Keypair;
setWallets({
solana: {
publicKey: keypair.publicKey.toString(),
privateKey: keypair.secretKey,
},
});
}
} catch (error) {
console.error("Multi-chain login failed:", error);
}
};
return (
<div>
<h2>Multi-Chain Wallet Generator</h2>
<div>
<label>
<input
type="radio"
value="solana"
checked={selectedChain === "solana"}
onChange={(e) => setSelectedChain(e.target.value as "solana")}
/>
Solana Only
</label>
<label>
<input
type="radio"
value="sui"
checked={selectedChain === "sui"}
onChange={(e) => setSelectedChain(e.target.value as "sui")}
/>
Sui Only
</label>
<label>
<input
type="radio"
value="all"
checked={selectedChain === "all"}
onChange={(e) => setSelectedChain(e.target.value as "all")}
/>
Both Chains
</label>
</div>
<button onClick={handleMultiChainLogin}>
Generate{" "}
{selectedChain === "all" ? "Multi-Chain" : selectedChain.toUpperCase()}{" "}
Wallet
</button>
{wallets.solana && (
<div>
<h3>Solana Wallet</h3>
<p>
<strong>Public Key:</strong> {wallets.solana.publicKey}
</p>
<p>
<strong>Private Key:</strong> {wallets.solana.privateKey.length}{" "}
bytes
</p>
</div>
)}
{wallets.sui && (
<div>
<h3>Sui Wallet</h3>
<p>
<strong>Address:</strong> {wallets.sui.address}
</p>
<p>
<strong>Private Key:</strong> {wallets.sui.privateKey}
</p>
</div>
)}
</div>
);
};
export default MultiChainApp;App-Specific Wallet Isolation
Problem: By default, the same user gets the same wallet across all apps using SugLink, which creates security and UX issues.
Solution: Use the appId parameter to generate unique wallets per app while maintaining deterministic wallets within each app.
import { SugLink } from "@dulcetlabs/suglink";
const sugLink = new SugLink();
// Each app should have a unique identifier
const ecommerceConfig = {
clientId: "your-google-client-id",
clientSecret: "your-google-client-secret",
redirectUri: "http://localhost:3000/callback",
appId: "ecommerce-platform-v1" // 🔑 Unique app identifier
};
const gamingConfig = {
clientId: "your-google-client-id",
clientSecret: "your-google-client-secret",
redirectUri: "http://localhost:3000/callback",
appId: "gaming-platform-v1" // 🔑 Different app = different wallets
};
// Same user, different apps = different wallets
const ecommerceWallet = await sugLink.login("google", authCode, "solana", ecommerceConfig);
const gamingWallet = await sugLink.login("google", authCode, "solana", gamingConfig);
console.log("E-commerce wallet:", ecommerceWallet.solana?.publicKey);
console.log("Gaming wallet:", gamingWallet.solana?.publicKey);
// ✅ These will be different addresses
// Same app, same user = same wallet (deterministic)
const ecommerceWallet2 = await sugLink.login("google", authCode, "solana", ecommerceConfig);
console.log("Same app again:", ecommerceWallet2.solana?.publicKey);
// ✅ This will match ecommerceWallet.solana?.publicKeyMulti-Chain App Isolation
// DeFi app gets unique wallets across both chains
const deFiConfig = {
clientId: "defi-client-id",
clientSecret: "defi-client-secret",
redirectUri: "http://localhost:3000/callback",
appId: "defi-protocol-v2"
};
// NFT marketplace gets different wallets for same user
const nftConfig = {
clientId: "nft-client-id",
clientSecret: "nft-client-secret",
redirectUri: "http://localhost:3000/callback",
appId: "nft-marketplace-v1"
};
const deFiWallets = await sugLink.login("google", authCode, "all", deFiConfig);
const nftWallets = await sugLink.login("google", authCode, "all", nftConfig);
// Different apps = different wallets on both chains
console.log("DeFi Solana:", deFiWallets.solana?.publicKey);
console.log("DeFi Sui:", deFiWallets.sui?.address);
console.log("NFT Solana:", nftWallets.solana?.publicKey); // Different from DeFi
console.log("NFT Sui:", nftWallets.sui?.address); // Different from DeFiAppId Best Practices
// ✅ Good appId examples
const configs = {
production: { appId: "myapp-prod-v1" },
staging: { appId: "myapp-staging-v1" },
development: { appId: "myapp-dev-v1" },
// Different products
ecommerce: { appId: "company-ecommerce-v1" },
gaming: { appId: "company-gaming-v1" },
// Versioning for migrations
legacy: { appId: "myapp-v1" },
current: { appId: "myapp-v2" }
};
// ❌ Avoid these patterns
const badConfigs = {
noAppId: {}, // Same wallet across all apps
dynamic: { appId: `user-${userId}` }, // Different per user
timestamp: { appId: Date.now().toString() } // Changes every time
};Backward Compatibility
// Existing code without appId continues to work
const legacyConfig = {
clientId: "client-id",
clientSecret: "client-secret",
redirectUri: "http://localhost:3000/callback"
// No appId = uses legacy behavior
};
const wallet = await sugLink.login("google", authCode, "solana", legacyConfig);
// ✅ Works exactly as beforeTypeScript Application Example
import { SugLink } from "@dulcetlabs/suglink";
const sugLink = new SugLink();
// Step 1: Redirect user to OAuth provider
async function initiateLogin(platform: "google") {
const state = await sugLink.generateState();
// Store state for later verification
sessionStorage.setItem("oauth_state", state);
const authUrl = sugLink.getAuthUrl(platform, state);
window.location.href = authUrl;
}
// Step 2: Handle OAuth callback
async function handleOAuthCallback() {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get("code");
const receivedState = urlParams.get("state");
const expectedState = sessionStorage.getItem("oauth_state");
if (!code || !receivedState || !expectedState) {
throw new Error("Missing OAuth parameters");
}
try {
await sugLink.handleCallback("google", code, receivedState, expectedState);
const publicKey = sugLink.getPublicKey();
console.log("Public Key:", publicKey);
// Clean up
sessionStorage.removeItem("oauth_state");
return publicKey;
} catch (error) {
console.error("Error during login:", error);
throw error;
}
}
function logout() {
sugLink.logout();
console.log("Logged out successfully");
}Plain HTML/JavaScript Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SugLink Example</title>
<script type="module">
import { SugLink } from "./node_modules/@dulcetlabs/suglink/dist/esm/index.js";
const sugLink = new SugLink();
async function handleLogin(platform, code) {
try {
await sugLink.login(platform, code);
const publicKey = sugLink.getPublicKey();
document.getElementById(
"publicKey"
).innerText = `Public Key: ${publicKey}`;
} catch (error) {
console.error("Login failed:", error);
}
}
window.handleLogin = handleLogin;
</script>
</head>
<body>
<button onclick="handleLogin('google', 'auth_code')">
Login with Google
</button>
<button onclick="handleLogin('tiktok', 'auth_code')">
Login with TikTok
</button>
<button onclick="handleLogin('snapchat', 'auth_code')">
Login with Snapchat
</button>
<p id="publicKey"></p>
</body>
</html>Direct OAuth Configuration
You can also set the OAuth configuration directly in your code:
import { SugLink } from "@dulcetlabs/suglink";
const sugLink = new SugLink();
const customConfig = {
clientId: "your_custom_client_id",
clientSecret: "your_custom_client_secret",
redirectUri: "your_custom_redirect_uri",
};
async function handleLogin(
platform: "google" | "tiktok" | "snapchat",
code: string
) {
try {
const keypair = await sugLink.login(platform, code, customConfig);
console.log("Keypair generated:", keypair.publicKey.toString());
} catch (error) {
console.error("Login failed:", error);
}
}Login Modes
SugLink supports 4 different login modes to accommodate different use cases and ensure backward compatibility:
1. 🔄 Legacy Mode (Backward Compatible)
// Existing code continues to work unchanged
const keypair = await sugLink.login("google", code);
const keypair = await sugLink.login("google", code, config);- Returns:
Keypair(Solana only) - Use Case: Existing applications that only need Solana
- Migration: No code changes required
2. 🟡 Solana Only Mode (New API)
// Explicitly request Solana using new structured API
const result = await sugLink.login("google", code, "solana");
console.log(result.solana?.publicKey); // Solana wallet details
console.log(result.sui); // undefined- Returns:
{ solana: WalletInfo, sui?: undefined } - Use Case: Solana-only apps using the new structured response
- Migration: Update to use structured response format
3. 🔵 Sui Only Mode
// Generate only Sui wallet
const result = await sugLink.login("google", code, "sui");
console.log(result.sui?.address); // Sui wallet details
console.log(result.solana); // undefined- Returns:
{ solana?: undefined, sui: WalletInfo } - Use Case: Sui-only applications
- Migration: New applications targeting Sui blockchain
4. 🌈 Multi-Chain Mode (Both Chains)
// Generate both Solana and Sui wallets from same social login
const result = await sugLink.login("google", code, "all");
console.log(result.solana?.publicKey); // Solana wallet
console.log(result.sui?.address); // Sui wallet- Returns:
{ solana: WalletInfo, sui: WalletInfo } - Use Case: Multi-chain applications supporting both blockchains
- Migration: Full multi-chain capability
🔧 Mode Detection
SugLink automatically detects which mode to use based on parameters:
// Legacy mode: 2 or 3 parameters, third is config object
await sugLink.login("google", code);
await sugLink.login("google", code, { clientId: "..." });
// Multi-chain mode: 3+ parameters, third is chain selector string
await sugLink.login("google", code, "solana");
await sugLink.login("google", code, "sui", config);
await sugLink.login("google", code, "all", config);API Reference
SugLink Class
Constructor
new SugLink();Methods
getAuthUrl(platform, state?, config?): Generates OAuth authorization URL for the specified platform.platform: 'google' (TikTok and Snapchat coming soon)state: Optional state parameter for CSRF protection (recommended)config: Optional OAuth configuration object- Returns: string (authorization URL)
generateState(): Generates a secure random state parameter for CSRF protection.- Returns: Promise (32-character hex string)
login(platform, code, config?): [Legacy] Logs in using social platform credentials and generates a Solana keypair.platform: 'google' (TikTok and Snapchat coming soon)code: OAuth authorization codeconfig: Optional OAuth configuration object- Returns: Promise
login(platform, code, chain, config?): [Multi-Chain] Logs in and generates keypair(s) for specified blockchain(s).platform: 'google' (TikTok and Snapchat coming soon)code: OAuth authorization codechain: 'solana' | 'sui' | 'all' - which blockchain(s) to generate wallets forconfig: Optional OAuth configuration object- Returns: Promise where MultiChainResult contains:
solana?:{ keypair: Keypair, publicKey: string, privateKey: Uint8Array }sui?:{ keypair: SuiKeypairLike, address: string, privateKey: string }
handleCallback(platform, code, receivedState?, expectedState?, config?): Handles OAuth callback with state validation.platform: 'google' (TikTok and Snapchat coming soon)code: OAuth authorization code from callbackreceivedState: State parameter from OAuth callbackexpectedState: Expected state value for CSRF protectionconfig: Optional OAuth configuration object- Returns: Promise (legacy Solana-only)
Solana Methods
getKeypair(): Returns the current Solana keypair.- Returns: Keypair
- Throws: Error if no keypair available
getPublicKey(): Returns the Solana public key as a base-58 string.- Returns: string
- Throws: Error if no keypair available
getPrivateKey(): Returns the Solana private key as a Uint8Array.- Returns: Uint8Array
- Throws: Error if no keypair available
Sui Methods
getSuiKeypair(): Returns the current Sui keypair (if generated via multi-chain login).- Returns: SuiKeypairLike
- Throws: Error if Sui keypair hasn't been generated
getSuiAddress(): Returns the Sui address as a 0x-prefixed string.- Returns: string (e.g., "0x4779c70ab11c82f7724c63be8868663e210a79cc3f430e5dbc4ae4793761323d")
- Throws: Error if Sui keypair hasn't been generated
getSuiPrivateKey(): Returns the Sui private key in Bech32 format.- Returns: string (e.g., "suiprivkey1qp9757x3gu64cfyxquyx73t7h7nzcmhk7nr55zwzv0whfsa4yffcvfntxfe")
- Throws: Error if Sui keypair hasn't been generated
General Methods
logout(): Clears all current keypairs, effectively logging out.- Returns: void
SuiUtils Class
Utility class for Sui blockchain operations, available as a standalone import:
import { SuiUtils } from "@dulcetlabs/suglink";Static Methods
createKeypairFromSeed(seed): Creates a Sui Ed25519 keypair from seed.seed: Buffer (32+ bytes, will use first 32 bytes)- Returns: Promise
getSuiAddress(keypair): Extracts Sui address from keypair.keypair: SuiKeypairLike- Returns: string (0x-prefixed address)
getSuiPrivateKeyBytes(keypair): Extracts raw private key bytes.keypair: SuiKeypairLike- Returns: Uint8Array (32 bytes for Ed25519)
exportSuiKeypair(keypair): Exports keypair in official Sui format.keypair: SuiKeypairLike- Returns:
{ privateKey: string, schema: string }
Example Usage
import { SuiUtils } from "@dulcetlabs/suglink";
import { Buffer } from "buffer";
// Create keypair from seed
const seed = Buffer.from("your_32_byte_seed_here");
const keypair = await SuiUtils.createKeypairFromSeed(seed);
// Get address and private key
const address = SuiUtils.getSuiAddress(keypair);
const { privateKey } = SuiUtils.exportSuiKeypair(keypair);
console.log("Address:", address);
console.log("Private Key:", privateKey);Security Considerations
- Never expose your OAuth client secrets in client-side code.
- Always use environment variables for sensitive configuration.
- Implement proper OAuth flow with state verification.
- Store keypairs securely and never expose private keys.
- Use HTTPS for all OAuth redirects.
- Implement proper session management.
License
This package is provided under the MIT License with Commons Clause.
Usage Requirements
- Permission Required: You MUST obtain explicit written permission from Dulcet Labs before using the software in production environments or incorporating it into commercial products.
Support
For support, please open an issue in the GitHub repository or contact the maintainers.
Platform Support
Social Login Platforms
SugLink currently supports:
- Google ✅ (Fully implemented with ID token verification)
- TikTok 🚧 (In development - OAuth endpoints configured)
- Snapchat 🚧 (In development - OAuth endpoints configured)
Each platform requires its own OAuth credentials and proper setup in their respective developer consoles.
Blockchain Support
SugLink supports multiple blockchains:
- Solana ✅ (Fully implemented with Ed25519 keypairs)
- Sui ✅ (Fully implemented with Ed25519 keypairs and proper address derivation)
Both blockchains use the same deterministic seed generation, ensuring that the same social login produces consistent wallet addresses across chains.
What's New in v0.1.0
� Multi-Chain Support
- Sui Blockchain Integration: Generate Sui wallets alongside Solana wallets from the same social login
- Chain Selection: Choose
"solana","sui", or"all"during login - Backward Compatibility: Existing Solana-only code continues to work without changes
- SuiUtils Class: Standalone utility class for Sui keypair operations
🔧 Multi-Chain API
// Generate wallets for both chains
const result = await sugLink.login("google", code, "all");
console.log("Solana:", result.solana?.publicKey);
console.log("Sui:", result.sui?.address);
// Chain-specific generation
const solanaOnly = await sugLink.login("google", code, "solana");
const suiOnly = await sugLink.login("google", code, "sui");
// Legacy support (still works)
const keypair = await sugLink.login("google", code); // Returns Solana Keypair💎 Sui Integration Features
- Ed25519 Keypairs: Uses official Sui SDK Ed25519 implementation
- Proper Address Format: 0x-prefixed 64-character addresses
- Bech32 Private Keys: suiprivkey1... format for compatibility
- Deterministic Generation: Same social login = same wallet addresses across chains
�🌐 Universal Compatibility
- Browser Support: Full compatibility with all modern browsers using Web Crypto API
- Node.js Support: Optimized performance using native Node.js crypto module
- Automatic Detection: Seamlessly switches between environments without configuration
🔧 Breaking Changes
generateState()is now async: Updated to use secure random generation across all environments// Old (v0.0.x) const state = sugLink.generateState(); // New (v0.1.0+) const state = await sugLink.generateState();
🚀 Technical Improvements
- Universal Crypto: Uses Web Crypto API in browsers, Node.js crypto in server environments
- Buffer Polyfill: Automatic Buffer support for browser compatibility
- Enhanced Security: Cryptographically secure random generation in all environments
- Optional Dependencies: Sui SDK is optional - only required when using Sui features
Potential Improvements
- Enhanced Key Generation: Add salt to the
buildTimeKeygeneration process for additional security. - Network Security: Add timeout settings for axios POST requests to prevent hanging connections.
- Type Safety and Validation: Implement TypeScript strict null checks for better type safety.
- Keypair Verification: Add methods to verify keypair generation correctness.
- Error Handling: Enhance error messages with more detailed information.
These improvements are planned for future releases. Contributions are welcome!
