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.
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.ai/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.ai",
"https://studio.dgidgi.ai",
// 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.ai/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/sdk
2. Use Environment Variables
// .env (never commit)
VITE_API_URL=https://api.dgidgi.ai/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.ai;
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 entirelyonce(default): First reveal succeeds, subsequent reveals blockedallow: 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:
- DO NOT create a public GitHub issue
- Email security@dgidgi.ai with details
- Include steps to reproduce
- Allow time for us to patch before disclosure