Skip to main content

SDK Security

The DgiDgi SDK is designed with security as a first-class concern.

Token Security

In-Memory Only

Tokens are stored only in memory - never in localStorage, sessionStorage, or cookies accessible to JavaScript.

Platform App Note

The DgiDgi One Platform app uses a hybrid approach for OAuth flows:

  • Primary storage: In-memory (closure-based, not accessible from window)
  • OAuth redirect bridge: sessionStorage (cleared immediately after read)

This is necessary because OAuth redirects cause page reloads that clear in-memory state. The token is moved to memory immediately and sessionStorage is cleared.

// Internal implementation - tokens never touch storage
class TokenManager {
private accessToken: string | null = null; // Memory only
private expiresAt: number | null = null;

// No localStorage.setItem() or document.cookie
// Tokens exist only for the duration of the session
}

Why this matters:

  • XSS attacks cannot steal tokens from storage
  • Tokens are cleared on page refresh (intentional)
  • Session persistence via httpOnly cookies (server-managed)

Legacy Token Cleanup

The SDK automatically cleans up any legacy tokens from older versions:

// Automatically runs on client initialization
function clearLegacyTokens() {
if (typeof window === "undefined") return;
const legacyKeys = ["dgidgi_token", "access_token", "auth_token"];
legacyKeys.forEach(key => localStorage.removeItem(key));
}

Request Security

HTTPS Enforcement

// Production should always use HTTPS
const client = createClient({
baseURL: "https://api.dgidgi.one/api/v1",
});

Credentials Handling

All requests include credentials for cookie-based session management:

// Internal fetch configuration
fetch(url, {
credentials: "include", // Always include cookies
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`, // When available
},
});

No Manual Tenant Headers

Tenant context is derived from JWT claims - never send tenant ID in headers:

// CORRECT: Let the server derive tenant from JWT
await projects.list();

// WRONG: Never do this - potential security risk
await client.get("/projects", {
headers: { "X-Tenant-ID": tenantId },
});

Error Handling

Structured Errors

All errors follow a consistent format:

interface SDKError {
code: string; // Error code (e.g., "UNAUTHORIZED")
message: string; // Human-readable message
status?: number; // HTTP status code
requestId?: string; // For debugging/support
details?: unknown; // Additional context
}

Secure Error Messages

The SDK sanitizes error messages to prevent information leakage:

try {
await auth.login({ email, password });
} catch (error) {
// Error message is generic, not "user not found" vs "wrong password"
// Prevents user enumeration attacks
console.log(error.message); // "Invalid credentials"
}

Rate Limiting

The API implements rate limiting. The SDK handles this gracefully:

try {
await resource.action();
} catch (error) {
if (error.status === 429) {
// Rate limited - wait and retry
const retryAfter = error.details?.retryAfter || 60;
await new Promise(r => setTimeout(r, retryAfter * 1000));
await resource.action(); // Retry
}
}

Timeout Protection

All requests have configurable timeouts:

const client = createClient({
timeout: 120000, // 2 minutes default
});

// Per-request timeout
await client.get("/slow-endpoint", {
timeout: 300000, // 5 minutes for this request
});

Timeouts use AbortController for clean cancellation:

// Internal implementation
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);

try {
const response = await fetch(url, { signal: controller.signal });
} finally {
clearTimeout(timeoutId);
}

Secret Redaction

When logging or displaying errors, secrets are automatically redacted:

// If response accidentally contains secrets
const SECRET_PATTERNS = [
/api[_-]?key[=:]["']?([^\s"']+)/gi,
/bearer\s+[A-Za-z0-9+/=_-]+/gi,
/sk[-_][a-zA-Z0-9]{20,}/g, // Stripe keys
/ghp_[a-zA-Z0-9]{36}/g, // GitHub PAT
// ... more patterns
];

function redactSecrets(text: string): string {
let redacted = text;
SECRET_PATTERNS.forEach(pattern => {
redacted = redacted.replace(pattern, "[REDACTED]");
});
return redacted;
}

CORS Configuration

The API is configured with strict CORS policies:

// Server-side configuration
{
origin: [
"https://console.dgidgi.one",
"https://studio.dgidgi.one",
// Development origins only in dev mode
],
credentials: true,
methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
}

API Key Security

For server-to-server communication:

// API keys should be in environment variables
const apiKey = process.env.DGIDGI_API_KEY;

const client = createClient({
baseURL: "https://api.dgidgi.one/api/v1",
headers: {
"X-API-Key": apiKey,
},
});

Never expose API keys in:

  • Client-side code
  • Git repositories
  • Logs or error messages

Security Best Practices

1. Keep SDK Updated

npm update @dgidgi-one/sdk

2. Use Environment Variables

// .env (never commit)
VITE_API_URL=https://api.dgidgi.one/api/v1

// Usage
const client = createClient({
baseURL: import.meta.env.VITE_API_URL,
});

3. Handle Errors Securely

try {
await sensitiveOperation();
} catch (error) {
// Log request ID for debugging, not full error
console.error("Operation failed. Request ID:", error.requestId);

// Show generic message to user
showToast("Something went wrong. Please try again.");
}

4. Validate User Input

// Always validate before sending to API
import { z } from "zod";

const schema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
});

const validated = schema.parse(userInput);
await users.create(validated);

5. Use Content Security Policy

<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
connect-src 'self' https://api.dgidgi.one;
script-src 'self';
">

OAuth Security

State Parameter Protection

OAuth flows use a state parameter for CSRF protection:

// State is stored in memory/sessionStorage, not localStorage
// State is single-use (consumed after verification)
const state = generateSecureRandom();
setOAuthState(state); // Stored securely

// After callback
const savedState = consumeOAuthState(); // One-time read, then cleared
if (state !== savedState) {
throw new Error("Invalid state - potential CSRF attack");
}

OAuth Endpoint Security

OAuth endpoints follow OWASP best practices:

  • /oauth/{provider}/authorize: Uses POST (not GET) for state-changing operations
  • /oauth/{provider}/start: GET returns OAuth URL (read-only)
  • /oauth/{provider}/callback: Handles token exchange securely

Secrets Security

Reveal Policy

Secrets have a configurable reveal policy (default: "once"):

// Default behavior - secrets can only be revealed once
try {
const { value } = await secrets.tenant.reveal(secretId);
// Use value immediately
} catch (error) {
if (error.status === 403) {
// Policy violated - already revealed or disabled
console.log("Secret already revealed or reveal disabled");
}
}

Available policies (set via SECRETS_REVEAL_POLICY env var):

  • never: Reveal disabled entirely
  • once (default): First reveal succeeds, subsequent reveals blocked
  • allow: Unrestricted reveals (NOT recommended for production)

Billing Scope Protection

Billing write operations require billing:write scope:

// These endpoints require billing:write scope
await billing.redeemGiftCard(code); // 403 without scope
await billing.purchaseCredits(amount); // 403 without scope
await billing.createSubscription(plan); // 403 without scope
await billing.createPortalSession(); // 403 without scope

// Read-only endpoints only require authentication
await billing.getSummary(); // OK with any valid token

Reporting Security Issues

Found a security vulnerability? Please report it responsibly:

  1. DO NOT create a public GitHub issue
  2. Email security@dgidgi.one with details
  3. Include steps to reproduce
  4. Allow time for us to patch before disclosure