@softlock/sdk
Version:
Official Softlock SDK for access key validation and management
694 lines (548 loc) • 16 kB
Markdown
# Softlock SDK
Official SDK for integrating Softlock access key validation into your applications.
## Installation
```bash
npm install @softlock/sdk
# or
yarn add @softlock/sdk
# or
pnpm add @softlock/sdk
```
## Architecture
The SDK is split into client and server modules to ensure compatibility with Next.js App Router and React Server Components:
- **`@softlock/sdk/client`** - React hooks and components (client-side only)
- **`@softlock/sdk/server`** - Validation utilities and middleware (server-safe)
- **`@softlock/sdk`** - Core client and server-safe utilities
## Quick Start
### Server-Side Validation
```typescript
// Server components, API routes, middleware
import { initSoftlock, validateKey } from "@softlock/sdk/server";
// Initialize with your tenant ID
initSoftlock({
tenantId: "your-tenant-id", // Your Discord server ID or tenant UUID
baseUrl: "https://your-softlock-instance.com", // Optional: custom instance
cacheTtl: 300000, // Optional: cache TTL in ms (default: 5 minutes)
debug: true, // Optional: enable debug logging
});
async function checkAccess(userKey: string) {
const result = await validateKey(userKey);
if (result.valid) {
console.log("Access granted!", result.key);
} else {
console.log("Access denied:", result.error);
}
}
```
### Client-Side Usage
```typescript
// Client components only
import { initSoftlock } from "@softlock/sdk/client";
// Initialize on client-side (typically in a provider or root component)
initSoftlock({
tenantId: "your-tenant-id",
baseUrl: "https://your-softlock-instance.com",
});
```
## React Integration
### Using Hooks
```tsx
"use client"; // Required for client components
import { useAccessKey } from "@softlock/sdk/client";
import { useState } from "react";
function AccessChecker() {
const { result, loading, error, validate } = useAccessKey();
const [keyInput, setKeyInput] = useState("");
const handleValidate = () => {
validate(keyInput);
};
return (
<div>
<input
value={keyInput}
onChange={(e) => setKeyInput(e.target.value)}
placeholder="Enter your access key"
/>
<button onClick={handleValidate} disabled={loading}>
{loading ? "Validating..." : "Validate"}
</button>
{result && <div>{result.valid ? "✅ Valid" : "❌ Invalid"}</div>}
</div>
);
}
```
### Using Components
```tsx
"use client"; // Required for client components
import {
AccessKeyValidator,
AccessGuard,
AccessStatus,
} from "@softlock/sdk/client";
import { useState } from "react";
function App() {
const [userKey, setUserKey] = useState("");
return (
<div>
{/* Built-in validator component */}
<AccessKeyValidator
placeholder="Enter your access key..."
onValidation={(result) => {
if (result.valid) {
setUserKey(result.key?.key_value || "");
}
}}
autoValidate={true}
/>
{/* Protected content */}
<AccessGuard
keyValue={userKey}
fallback={<div>You need a valid access key to see this content.</div>}
>
<h1>Secret Content!</h1>
<p>This content is only visible to users with valid access keys.</p>
</AccessGuard>
{/* Show access status */}
<AccessStatus
keyValue={userKey}
showDetails={true}
refreshInterval={60000} // Refresh every minute
/>
</div>
);
}
```
## Middleware Protection
### Express.js
```javascript
import express from "express";
import { createExpressMiddleware, initSoftlock } from "@softlock/sdk/server";
const app = express();
// Initialize Softlock first
initSoftlock({ tenantId: "your-tenant-id" });
// Protect routes
const softlockAuth = createExpressMiddleware({
extractKey: (req) => req.headers["x-api-key"], // Custom extraction
onUnauthorized: (req, res) => {
res.status(403).json({ error: "Access denied" });
},
});
app.get("/protected", softlockAuth, (req, res) => {
// Access granted! User data available in req.softlock
res.json({
message: "Hello!",
user: req.softlock.user,
});
});
```
### Next.js API Routes
```typescript
import { withSoftlockAuth } from "@softlock/sdk/server";
async function handler(req: NextApiRequest, res: NextApiResponse) {
// This handler only runs if access key is valid
// User data available in req.softlock
res.json({
message: "Protected data",
user: req.softlock?.user,
});
}
export default withSoftlockAuth(handler);
```
### Next.js Middleware
#### Basic Protection
```typescript
// middleware.ts
import { NextRequest } from "next/server";
import { createNextMiddleware } from "@softlock/sdk/server";
export async function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith("/protected")) {
return createNextMiddleware()(request);
}
}
export const config = {
matcher: "/protected/:path*",
};
```
#### Advanced Protection with Redirects
```typescript
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
import { createNextMiddleware, initSoftlock } from "@softlock/sdk/server";
initSoftlock({
tenantId: "your-tenant-id",
baseUrl: "https://your-softlock-instance.com",
debug: true,
});
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Skip beta-access page to avoid redirect loops
if (pathname === "/beta-access") {
return NextResponse.next();
}
// Check if user has valid access key
const validKey = request.cookies.get("softlock_valid_key")?.value;
// If no valid key, redirect to beta access page
if (!validKey) {
return NextResponse.redirect(new URL("/beta-access", request.url));
}
// Use Softlock middleware for additional validation
return createNextMiddleware({
extractKey: (req) => {
return req.cookies.get("softlock_valid_key")?.value || "";
},
})(request);
}
export const config = {
matcher: ["/dashboard/:path*", "/premium/:path*"],
};
```
## Advanced Usage
### Custom Client Configuration
```typescript
// Server-side
import { SoftlockClient } from "@softlock/sdk/server";
const client = new SoftlockClient({
tenantId: "your-tenant-id",
baseUrl: "https://api.yourapp.com",
apiKey: "your-api-key", // For server-side validation
cacheTtl: 600000, // 10 minutes
debug: false,
});
// Use client directly
const result = await client.validateKey("ak_...");
```
### Access Management Hook
```tsx
"use client";
import { useAccessKey, useUserAccess } from "@softlock/sdk/client";
import { useState, useEffect } from "react";
function AccessManager() {
const [userKey, setUserKey] = useState("");
const { result: keyResult } = useAccessKey(userKey, { autoValidate: true });
const { user, loading, error, refreshAccess } = useUserAccess();
useEffect(() => {
if (keyResult?.valid) {
// Store valid key
localStorage.setItem("access_key", userKey);
}
}, [keyResult, userKey]);
return (
<div>
<input
value={userKey}
onChange={(e) => setUserKey(e.target.value)}
placeholder="Access key"
/>
{keyResult?.valid && (
<div>
<h3>Access Granted</h3>
<p>User: {user?.discord_tag}</p>
<button onClick={refreshAccess}>Refresh Access</button>
</div>
)}
</div>
);
}
```
## Next.js App Router Integration
### Provider Setup
```tsx
// app/providers.tsx
"use client";
import { initSoftlock } from "@softlock/sdk/client";
import { useEffect } from "react";
export function SoftlockProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
initSoftlock({
tenantId: process.env.NEXT_PUBLIC_TENANT_ID!,
baseUrl: process.env.NEXT_PUBLIC_SOFTLOCK_URL,
debug: process.env.NODE_ENV === "development",
});
}, []);
return <>{children}</>;
}
```
```tsx
// app/layout.tsx
import { SoftlockProvider } from "./providers";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<SoftlockProvider>{children}</SoftlockProvider>
</body>
</html>
);
}
```
### Server Components with Validation
```tsx
// app/protected/page.tsx
import { validateKey } from "@softlock/sdk/server";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
export default async function ProtectedPage() {
const cookieStore = cookies();
const accessKey = cookieStore.get("access_key")?.value;
if (!accessKey) {
redirect("/beta-access");
}
const result = await validateKey(accessKey);
if (!result.valid) {
redirect("/beta-access");
}
return (
<div>
<h1>Protected Content</h1>
<p>Welcome, {result.key?.discord_tag}!</p>
</div>
);
}
```
import { useUserAccess } from "@softlock/sdk";
function UserDashboard({ userId }: { userId: string }) {
const { hasAccess, loading, checkAccess, revokeAccess } =
useUserAccess(userId);
return (
<div>
{loading && <div>Checking access...</div>}
{hasAccess === true && (
<div>
<h2>Welcome! You have access.</h2>
<button onClick={revokeAccess}>Revoke Access</button>
</div>
)}
{hasAccess === false && (
<div>
<h2>Access Required</h2>
<button onClick={() => checkAccess("ak_...")}>
Check Access Key
</button>
</div>
)}
</div>
);
}
````
### Access Guard with Redirect
```tsx
import { AccessGuard } from "@softlock/sdk";
function ProtectedPage() {
return (
<AccessGuard
keyValue={getUserKey()} // Your function to get user's key
redirectUrl="/login"
onUnauthorized={() => {
console.log("User tried to access protected content");
// Track analytics, show notification, etc.
}}
loadingComponent={<div>Verifying access...</div>}
>
<YourProtectedContent />
</AccessGuard>
);
}
````
## Accessing Discord User Information
The SDK provides multiple ways to retrieve the Discord ID and other user information associated with an access key:
### 1. From Validation Result
```typescript
import { validateKey } from "@softlock/sdk";
async function getUserInfo(accessKey: string) {
const result = await validateKey(accessKey);
if (result.valid && result.key) {
const discordId = result.key.discord_user_id;
const discordTag = result.key.discord_tag; // username#discriminator
console.log("Discord User ID:", discordId);
console.log("Discord Tag:", discordTag);
return {
discordId,
discordTag,
keyId: result.key.id,
status: result.key.status,
};
}
return null;
}
```
### 2. In React Components (Hooks)
```tsx
import { useAccessKey } from "@softlock/sdk";
function UserProfile() {
const { result } = useAccessKey();
if (result?.valid && result.key) {
return (
<div>
<h3>User Information</h3>
<p>Discord ID: {result.key.discord_user_id}</p>
<p>Discord Tag: {result.key.discord_tag}</p>
<p>Key Status: {result.key.status}</p>
<p>
Key Created: {new Date(result.key.created_at).toLocaleDateString()}
</p>
</div>
);
}
return <div>No valid access key found</div>;
}
```
### 3. In API Routes/Middleware
```typescript
// Express.js
app.get("/user-info", softlockAuth, (req, res) => {
const discordId = req.softlock?.user?.discordId;
const discordTag = req.softlock?.user?.discordTag;
res.json({
discordId,
discordTag,
keyData: req.softlock?.key,
});
});
// Next.js API Route
export default withSoftlockAuth((req, res) => {
const userInfo = {
discordId: req.softlock?.user?.discordId,
discordTag: req.softlock?.user?.discordTag,
keyId: req.softlock?.key?.id,
};
res.json({ user: userInfo });
});
```
### 4. In Next.js Middleware (Headers)
```typescript
// middleware.ts
export async function middleware(request: NextRequest) {
// ... validation logic ...
if (validationResult.valid && validationResult.key) {
const requestHeaders = new Headers(request.headers);
// Add Discord ID to headers for downstream use
requestHeaders.set(
"x-discord-user-id",
validationResult.key.discord_user_id || ""
);
requestHeaders.set("x-discord-tag", validationResult.key.discord_tag || "");
return NextResponse.next({
request: {
headers: requestHeaders,
},
});
}
}
// In your page/component, access via headers
export async function getServerSideProps({ req }) {
const discordId = req.headers["x-discord-user-id"];
const discordTag = req.headers["x-discord-tag"];
return {
props: { discordId, discordTag },
};
}
```
### 5. Using the Client Directly
```typescript
import { SoftlockClient } from "@softlock/sdk";
const client = new SoftlockClient({
tenantId: "your-tenant-id",
});
async function lookupUser(accessKey: string) {
try {
const validation = await client.validateKey(accessKey);
if (validation.valid && validation.key) {
return {
discordId: validation.key.discord_user_id,
discordTag: validation.key.discord_tag,
keyDetails: {
id: validation.key.id,
status: validation.key.status,
createdAt: validation.key.created_at,
expiresAt: validation.key.expires_at,
lastUsed: validation.key.used_at,
},
};
}
} catch (error) {
console.error("Failed to validate key:", error);
}
return null;
}
```
## API Reference
### Configuration
```typescript
interface SoftlockConfig {
tenantId: string; // Required: Your tenant/server ID
baseUrl?: string; // Optional: Custom API base URL
apiKey?: string; // Optional: API key for server-side
cacheTtl?: number; // Optional: Cache TTL in ms
debug?: boolean; // Optional: Enable debug logging
}
```
### Validation Result
```typescript
interface ValidationResult {
valid: boolean; // Whether the key is valid
key?: AccessKey; // Key details if valid
error?: string; // Error message if invalid
cached?: boolean; // Whether result came from cache
}
```
### Access Key
```typescript
interface AccessKey {
id: string; // Unique key ID
key_value: string; // The actual key value
status: "active" | "revoked" | "expired";
discord_user_id?: string; // Associated Discord user
discord_tag?: string; // Discord username#discriminator
created_at: string; // ISO timestamp
expires_at?: string; // ISO timestamp (if expires)
used_at?: string; // ISO timestamp (when first used)
}
```
## Error Handling
The SDK provides specific error types for better error handling:
```typescript
import {
SoftlockError,
ValidationError,
NetworkError,
ConfigurationError,
} from "@softlock/sdk";
try {
const result = await validateKey("invalid-key");
} catch (error) {
if (error instanceof ValidationError) {
console.log("Key validation failed:", error.message);
} else if (error instanceof NetworkError) {
console.log("Network issue:", error.message);
} else if (error instanceof ConfigurationError) {
console.log("Configuration issue:", error.message);
}
}
```
## Best Practices
1. **Initialize Early**: Call `initSoftlock()` at the top level of your app
2. **Cache Wisely**: Use appropriate cache TTL based on your security needs
3. **Handle Errors**: Always handle validation errors gracefully
4. **Secure Keys**: Never expose API keys in client-side code
5. **Monitor Usage**: Use debug mode during development
## TypeScript Support
The SDK is written in TypeScript and provides full type safety:
```typescript
import type {
ValidationResult,
AccessKey,
SoftlockConfig,
} from "@softlock/sdk";
// All types are exported and ready to use
const config: SoftlockConfig = {
tenantId: "your-tenant-id",
};
```
## Contributing
See the [main repository](https://github.com/eXt-Ra/softlock) for contribution guidelines.
## License
MIT