[TERMINAL · SKILLS]
> mounting /skills...
> indexing skill manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Use Cases/Build a Secure File Sharing Platform with Audit Trail

Build a Secure File Sharing Platform with Audit Trail

Build a self-hosted file sharing platform with end-to-end encryption, granular permissions, expiring links, and a complete audit trail for compliance — replacing Dropbox for a law firm handling sensitive documents.

Development#typescript#tsconfig#type-checking#type-safety#javascript-migration
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 7 skills

Avg quality 96/100·All SAFE
>

typescript

v1.0.0

Sets up and repairs TypeScript projects: picks tsconfig.json settings for a Node.js service, an npm library or a bundled web app, runs type-checking in CI, and replaces `any` and unsafe casts with types the compiler can verify. Use when someone asks to "set up TypeScript", "fix this tsconfig", "fix a TypeScript error", "upgrade to TypeScript 6 or 7", "migrate JavaScript to TypeScript", "publish types for a package", "run .ts files in Node" or "make this type-safe". Covers the changed defaults in TypeScript 6.0 and 7.0, module and moduleResolution, strict mode, project references, declaration output, narrowing, satisfies and common compiler errors.

93/100 quality
7.30× impact
SAFE· stale score
View skill
>

nextjs

v1.1.0

Assists with building production-grade React applications using Next.js 16. Use when working with the App Router, Server Components, Server Actions, the proxy file (formerly middleware), Cache Components, or deploying to Vercel or self-hosted environments. Trigger words: nextjs, next.js, app router, server components, server actions, use cache, proxy.ts, react framework, ssr, isr.

100/100 quality
5.29× impact
SAFE· stale score
View skill
>

postgresql

v1.1.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

100/100 quality
5.15× impact
SAFE· stale score
View skill
>

redis

v1.1.0

Redis is an in-memory data store used as a cache, session store, message broker and counter service. Use when adding a cache layer, storing sessions, rate limiting an API, running pub/sub or Streams with consumer groups, taking a distributed lock, building a leaderboard, or configuring Redis for production (persistence, maxmemory, eviction).

93/100 quality
1.79× impact
SAFE· stale score
View skill
>

zod

v1.1.0

Zod is a TypeScript-first schema library that validates data at runtime and infers static types from the same schema. Use when validating API request bodies, form input, environment variables or config files, parsing untrusted JSON, or migrating from Zod 3 to Zod 4. Covers z.object, safeParse, z.infer, coercion, discriminated unions, refinements, JSON Schema output and the v4 API changes.

93/100 quality
3.87× impact
SAFE· stale score
View skill
>

tailwindcss

v1.1.0

Tailwind CSS is a utility-first CSS framework: you style pages with small classes in the markup and ship only the CSS you use. Covers Tailwind v4 (CSS-first config with @theme, @import "tailwindcss", Vite and PostCSS plugins), responsive layouts, dark mode, custom animations, and the v3 to v4 renames. Use when someone asks to "style with Tailwind", "set up Tailwind v4", "upgrade Tailwind to v4", "add dark mode", or "fix Tailwind classes that stopped working".

93/100 quality
2.28× impact
SAFE· stale score
View skill
>

vitest

v1.1.0

Vitest is a test runner for JavaScript and TypeScript built on Vite, with a Jest-compatible API, built-in mocking, snapshots and code coverage. Use when a user asks to set up or run unit tests in a Vite or Node project, write tests with describe/it/expect, mock modules with vi.mock, add coverage thresholds, configure vitest.config.ts or test projects, migrate from Jest, or upgrade to Vitest 5.

100/100 quality
1.50× impact
SAFE· stale score
View skill
$

The Problem

A 50-person law firm shares case files with clients, opposing counsel, and courts via Dropbox. But their malpractice insurer flagged it as a compliance risk: Dropbox stores data on shared infrastructure, there's no audit trail of who accessed what, and a paralegal accidentally shared an entire case folder with the wrong client last quarter — a potential ethics violation. The firm needs a self-hosted platform where every file access is logged, links expire automatically, and documents can be watermarked per-viewer.

Step 1: File Storage with Encryption at Rest

typescript
// src/storage/encrypted-store.ts
import { createCipheriv, createDecipheriv, randomBytes, createHash } from 'crypto';
import { createReadStream, createWriteStream } from 'fs';
import { pipeline } from 'stream/promises';
import { join } from 'path';

const STORAGE_PATH = process.env.FILE_STORAGE_PATH ?? '/data/files';
const ENCRYPTION_KEY = Buffer.from(process.env.FILE_ENCRYPTION_KEY!, 'hex'); // 32 bytes

export async function storeFile(
  fileStream: NodeJS.ReadableStream,
  fileId: string,
  metadata: { filename: string; mimeType: string; uploadedBy: string }
): Promise<{ encryptedPath: string; iv: string; checksum: string; sizeBytes: number }> {
  const iv = randomBytes(16);
  const cipher = createCipheriv('aes-256-cbc', ENCRYPTION_KEY, iv);
  const hash = createHash('sha256');
  const encryptedPath = join(STORAGE_PATH, `${fileId}.enc`);
  const writeStream = createWriteStream(encryptedPath);

  let sizeBytes = 0;

  const transform = new (await import('stream')).Transform({
    transform(chunk, _, cb) {
      sizeBytes += chunk.length;
      hash.update(chunk);
      cb(null, chunk);
    },
  });

  await pipeline(fileStream, transform, cipher, writeStream);

  return {
    encryptedPath,
    iv: iv.toString('hex'),
    checksum: hash.digest('hex'),
    sizeBytes,
  };
}

export async function retrieveFile(
  fileId: string,
  iv: string
): Promise<NodeJS.ReadableStream> {
  const encryptedPath = join(STORAGE_PATH, `${fileId}.enc`);
  const decipher = createDecipheriv('aes-256-cbc', ENCRYPTION_KEY, Buffer.from(iv, 'hex'));
  const readStream = createReadStream(encryptedPath);
  return readStream.pipe(decipher);
}

Step 2: Granular Permissions and Expiring Links

typescript
// src/sharing/permissions.ts
import { z } from 'zod';
import { PrismaClient } from '@prisma/client';
import { randomBytes } from 'crypto';

const prisma = new PrismaClient();

export const SharePermission = z.object({
  fileId: z.string().uuid(),
  sharedWith: z.string().email(),
  permission: z.enum(['view', 'download', 'edit']),
  expiresAt: z.string().datetime().optional(),
  maxDownloads: z.number().int().positive().optional(),
  requirePassword: z.boolean().default(false),
  password: z.string().optional(),
  watermark: z.boolean().default(true),
  ipWhitelist: z.array(z.string().ip()).optional(),
});

export async function createShareLink(input: z.infer<typeof SharePermission>): Promise<{
  shareId: string;
  url: string;
}> {
  const token = randomBytes(32).toString('base64url');
  const passwordHash = input.password
    ? (await import('crypto')).createHash('sha256').update(input.password).digest('hex')
    : null;

  const share = await prisma.fileShare.create({
    data: {
      fileId: input.fileId,
      sharedWith: input.sharedWith,
      permission: input.permission,
      token,
      expiresAt: input.expiresAt ? new Date(input.expiresAt) : null,
      maxDownloads: input.maxDownloads,
      downloadCount: 0,
      passwordHash,
      watermark: input.watermark,
      ipWhitelist: input.ipWhitelist ?? [],
      active: true,
    },
  });

  return {
    shareId: share.id,
    url: `${process.env.BASE_URL}/share/${token}`,
  };
}

export async function validateAccess(
  token: string,
  clientIp: string,
  password?: string
): Promise<{ valid: boolean; share?: any; reason?: string }> {
  const share = await prisma.fileShare.findUnique({ where: { token } });

  if (!share || !share.active) return { valid: false, reason: 'Link not found or deactivated' };
  if (share.expiresAt && share.expiresAt < new Date()) return { valid: false, reason: 'Link expired' };
  if (share.maxDownloads && share.downloadCount >= share.maxDownloads)
    return { valid: false, reason: 'Download limit reached' };
  if (share.ipWhitelist.length > 0 && !share.ipWhitelist.includes(clientIp))
    return { valid: false, reason: 'IP not allowed' };
  if (share.passwordHash) {
    const hash = (await import('crypto')).createHash('sha256').update(password ?? '').digest('hex');
    if (hash !== share.passwordHash) return { valid: false, reason: 'Incorrect password' };
  }

  return { valid: true, share };
}

Step 3: Comprehensive Audit Trail

typescript
// src/audit/logger.ts
import { Pool } from 'pg';

const db = new Pool({ connectionString: process.env.DATABASE_URL });

export type AuditAction = 'upload' | 'download' | 'view' | 'share' | 'revoke'
  | 'delete' | 'rename' | 'move' | 'permission_change' | 'failed_access';

export async function logAuditEvent(event: {
  action: AuditAction;
  fileId: string;
  userId: string;
  clientIp: string;
  userAgent: string;
  metadata?: Record<string, unknown>;
}): Promise<void> {
  await db.query(`
    INSERT INTO audit_log (action, file_id, user_id, client_ip, user_agent, metadata, occurred_at)
    VALUES ($1, $2, $3, $4, $5, $6, NOW())
  `, [event.action, event.fileId, event.userId, event.clientIp, event.userAgent,
      JSON.stringify(event.metadata ?? {})]);
}

export async function getFileAuditTrail(fileId: string): Promise<any[]> {
  const { rows } = await db.query(`
    SELECT action, user_id, client_ip, metadata, occurred_at
    FROM audit_log WHERE file_id = $1
    ORDER BY occurred_at DESC LIMIT 500
  `, [fileId]);
  return rows;
}

export async function generateComplianceReport(dateRange: { from: Date; to: Date }): Promise<{
  totalEvents: number;
  byAction: Record<string, number>;
  failedAccess: number;
  uniqueUsers: number;
  externalAccess: number;
}> {
  const { rows } = await db.query(`
    SELECT action, COUNT(*) as cnt, COUNT(DISTINCT user_id) as users
    FROM audit_log WHERE occurred_at BETWEEN $1 AND $2
    GROUP BY action
  `, [dateRange.from, dateRange.to]);

  const byAction: Record<string, number> = {};
  let total = 0, users = 0, failed = 0;
  for (const r of rows) {
    byAction[r.action] = parseInt(r.cnt);
    total += parseInt(r.cnt);
    users = Math.max(users, parseInt(r.users));
    if (r.action === 'failed_access') failed = parseInt(r.cnt);
  }

  return { totalEvents: total, byAction, failedAccess: failed, uniqueUsers: users, externalAccess: 0 };
}

Results

  • Compliance audit passed — malpractice insurer approved the platform
  • Accidental sharing incidents: zero (was 2-3/quarter with Dropbox)
  • Audit trail: every file access logged with IP, timestamp, user agent
  • Expiring links: 100% of external shares auto-expire (default 7 days)
  • Access attempts blocked: 23 expired link attempts, 8 wrong-password attempts in first month
  • Watermarked downloads: every PDF downloaded by external parties has viewer-specific watermark
  • Self-hosted: all data on firm's own servers, zero third-party cloud exposure

Related use cases