npm package: @softlock/sdk
Description: Official Softlock SDK for access key validation and management
Author: Softlock Team
Latest version: 0.0.11

# 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