mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-03 15:43:11 +08:00
Add Chinese documentation for custom skills development and reference guide
- Created a new document for custom skills development (`custom.md`) detailing the structure, creation, implementation, and best practices for developing custom CCW skills. - Added an index document (`index.md`) summarizing all built-in skills, their categories, and usage examples. - Introduced a reference guide (`reference.md`) providing a quick reference for all 33 built-in CCW skills, including triggers and purposes.
This commit is contained in:
@@ -3,6 +3,7 @@ import { randomBytes } from 'crypto';
|
||||
export interface CsrfTokenManagerOptions {
|
||||
tokenTtlMs?: number;
|
||||
cleanupIntervalMs?: number;
|
||||
maxTokensPerSession?: number;
|
||||
}
|
||||
|
||||
type CsrfTokenRecord = {
|
||||
@@ -13,14 +14,20 @@ type CsrfTokenRecord = {
|
||||
|
||||
const DEFAULT_TOKEN_TTL_MS = 15 * 60 * 1000; // 15 minutes
|
||||
const DEFAULT_CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
||||
const DEFAULT_MAX_TOKENS_PER_SESSION = 5;
|
||||
|
||||
export class CsrfTokenManager {
|
||||
private readonly tokenTtlMs: number;
|
||||
private readonly records = new Map<string, CsrfTokenRecord>();
|
||||
private readonly maxTokensPerSession: number;
|
||||
// sessionId -> (token -> record) - supports multiple tokens per session
|
||||
private readonly sessionTokens = new Map<string, Map<string, CsrfTokenRecord>>();
|
||||
// Quick lookup: token -> sessionId for validation
|
||||
private readonly tokenToSession = new Map<string, string>();
|
||||
private readonly cleanupTimer: NodeJS.Timeout | null;
|
||||
|
||||
constructor(options: CsrfTokenManagerOptions = {}) {
|
||||
this.tokenTtlMs = options.tokenTtlMs ?? DEFAULT_TOKEN_TTL_MS;
|
||||
this.maxTokensPerSession = options.maxTokensPerSession ?? DEFAULT_MAX_TOKENS_PER_SESSION;
|
||||
|
||||
const cleanupIntervalMs = options.cleanupIntervalMs ?? DEFAULT_CLEANUP_INTERVAL_MS;
|
||||
if (cleanupIntervalMs > 0) {
|
||||
@@ -40,50 +47,137 @@ export class CsrfTokenManager {
|
||||
if (this.cleanupTimer) {
|
||||
clearInterval(this.cleanupTimer);
|
||||
}
|
||||
this.records.clear();
|
||||
this.sessionTokens.clear();
|
||||
this.tokenToSession.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a single CSRF token for a session
|
||||
*/
|
||||
generateToken(sessionId: string): string {
|
||||
const token = randomBytes(32).toString('hex');
|
||||
this.records.set(token, {
|
||||
sessionId,
|
||||
expiresAtMs: Date.now() + this.tokenTtlMs,
|
||||
used: false,
|
||||
});
|
||||
return token;
|
||||
const tokens = this.generateTokens(sessionId, 1);
|
||||
return tokens[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate multiple CSRF tokens for a session (pool pattern)
|
||||
* @param sessionId - Session identifier
|
||||
* @param count - Number of tokens to generate (max: maxTokensPerSession)
|
||||
* @returns Array of generated tokens
|
||||
*/
|
||||
generateTokens(sessionId: string, count: number): string[] {
|
||||
// Get or create session token map
|
||||
let sessionMap = this.sessionTokens.get(sessionId);
|
||||
if (!sessionMap) {
|
||||
sessionMap = new Map();
|
||||
this.sessionTokens.set(sessionId, sessionMap);
|
||||
}
|
||||
|
||||
// Limit count to max tokens per session
|
||||
const currentCount = sessionMap.size;
|
||||
const availableSlots = Math.max(0, this.maxTokensPerSession - currentCount);
|
||||
const tokensToGenerate = Math.min(count, availableSlots);
|
||||
|
||||
const tokens: string[] = [];
|
||||
const expiresAtMs = Date.now() + this.tokenTtlMs;
|
||||
|
||||
for (let i = 0; i < tokensToGenerate; i++) {
|
||||
const token = randomBytes(32).toString('hex');
|
||||
const record: CsrfTokenRecord = {
|
||||
sessionId,
|
||||
expiresAtMs,
|
||||
used: false,
|
||||
};
|
||||
sessionMap.set(token, record);
|
||||
this.tokenToSession.set(token, sessionId);
|
||||
tokens.push(token);
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a CSRF token against a session
|
||||
* Marks token as used (single-use) on successful validation
|
||||
*/
|
||||
validateToken(token: string, sessionId: string): boolean {
|
||||
const record = this.records.get(token);
|
||||
// Quick lookup: get session from token
|
||||
const tokenSessionId = this.tokenToSession.get(token);
|
||||
if (!tokenSessionId) return false;
|
||||
if (tokenSessionId !== sessionId) return false;
|
||||
|
||||
// Get session's token map
|
||||
const sessionMap = this.sessionTokens.get(sessionId);
|
||||
if (!sessionMap) return false;
|
||||
|
||||
// Get token record
|
||||
const record = sessionMap.get(token);
|
||||
if (!record) return false;
|
||||
if (record.used) return false;
|
||||
if (record.sessionId !== sessionId) return false;
|
||||
|
||||
// Check expiration
|
||||
if (Date.now() > record.expiresAtMs) {
|
||||
this.records.delete(token);
|
||||
this.removeToken(token, sessionId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mark as used (single-use enforcement)
|
||||
record.used = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a token from the pool
|
||||
*/
|
||||
private removeToken(token: string, sessionId: string): void {
|
||||
const sessionMap = this.sessionTokens.get(sessionId);
|
||||
if (sessionMap) {
|
||||
sessionMap.delete(token);
|
||||
// Clean up empty session maps
|
||||
if (sessionMap.size === 0) {
|
||||
this.sessionTokens.delete(sessionId);
|
||||
}
|
||||
}
|
||||
this.tokenToSession.delete(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of active tokens for a session
|
||||
*/
|
||||
getTokenCount(sessionId: string): number {
|
||||
const sessionMap = this.sessionTokens.get(sessionId);
|
||||
return sessionMap ? sessionMap.size : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total number of active tokens across all sessions
|
||||
*/
|
||||
getActiveTokenCount(): number {
|
||||
return this.tokenToSession.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up expired and used tokens
|
||||
*/
|
||||
cleanupExpiredTokens(nowMs: number = Date.now()): number {
|
||||
let removed = 0;
|
||||
|
||||
for (const [token, record] of this.records.entries()) {
|
||||
if (record.used || nowMs > record.expiresAtMs) {
|
||||
this.records.delete(token);
|
||||
removed += 1;
|
||||
for (const [sessionId, sessionMap] of this.sessionTokens.entries()) {
|
||||
for (const [token, record] of sessionMap.entries()) {
|
||||
if (record.used || nowMs > record.expiresAtMs) {
|
||||
sessionMap.delete(token);
|
||||
this.tokenToSession.delete(token);
|
||||
removed += 1;
|
||||
}
|
||||
}
|
||||
// Clean up empty session maps
|
||||
if (sessionMap.size === 0) {
|
||||
this.sessionTokens.delete(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
getActiveTokenCount(): number {
|
||||
return this.records.size;
|
||||
}
|
||||
}
|
||||
|
||||
let csrfManagerInstance: CsrfTokenManager | null = null;
|
||||
|
||||
@@ -79,17 +79,37 @@ function setCsrfCookie(res: ServerResponse, token: string, maxAgeSeconds: number
|
||||
}
|
||||
|
||||
export async function handleAuthRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
const { pathname, req, res } = ctx;
|
||||
const { pathname, req, res, url } = ctx;
|
||||
|
||||
if (pathname === '/api/csrf-token' && req.method === 'GET') {
|
||||
const sessionId = getOrCreateSessionId(req, res);
|
||||
const tokenManager = getCsrfTokenManager();
|
||||
const csrfToken = tokenManager.generateToken(sessionId);
|
||||
|
||||
res.setHeader('X-CSRF-Token', csrfToken);
|
||||
setCsrfCookie(res, csrfToken, 15 * 60);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
||||
res.end(JSON.stringify({ csrfToken }));
|
||||
// Check for count parameter (pool pattern)
|
||||
const countParam = url.searchParams.get('count');
|
||||
const count = countParam ? Math.min(Math.max(1, parseInt(countParam, 10) || 1), 10) : 1;
|
||||
|
||||
if (count === 1) {
|
||||
// Single token response (existing behavior)
|
||||
const csrfToken = tokenManager.generateToken(sessionId);
|
||||
res.setHeader('X-CSRF-Token', csrfToken);
|
||||
setCsrfCookie(res, csrfToken, 15 * 60);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
||||
res.end(JSON.stringify({ csrfToken }));
|
||||
} else {
|
||||
// Batch token response (pool pattern)
|
||||
const tokens = tokenManager.generateTokens(sessionId, count);
|
||||
const firstToken = tokens[0];
|
||||
|
||||
// Set header and cookie with first token for compatibility
|
||||
res.setHeader('X-CSRF-Token', firstToken);
|
||||
setCsrfCookie(res, firstToken, 15 * 60);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
||||
res.end(JSON.stringify({
|
||||
tokens,
|
||||
expiresIn: 15 * 60, // seconds
|
||||
}));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user