mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
* improvement(repo): restructuring to make realtime image narrower scoped * improvements * chore(repo): rebase fixes and quality improvements for realtime split Addresses merge-time issues and gaps from the realtime app split: - Retarget stale vi.mock paths to @sim/workflow-persistence/subblocks - Restore README branding, fix AGENTS.md script reference - Restore TSDoc on workflow-persistence subblocks helpers - Use toError() from @sim/utils/errors in save.ts - Add vitest config + local mocks so @sim/audit tests run standalone - Move socket.io-client to devDependencies in apps/realtime - Add missing package COPY steps to docker/app.Dockerfile - Add check:boundaries/check:realtime-prune scripts and wire into CI Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(security): consolidate crypto primitives into @sim/security Move general-purpose crypto primitives out of apps/sim into the @sim/security package so both apps/sim and apps/realtime can share them. @sim/security exports (all pure, dependency-free): ./compare safeCompare (constant-time HMAC-wrapped equality) ./encryption encrypt/decrypt (AES-256-GCM, iv:cipher:tag format) ./hash sha256Hex ./tokens generateSecureToken (base64url) Migrate apps/sim call sites to use these + @sim/utils helpers: crypto.randomUUID() -> generateId() from @sim/utils/id createHash('sha256').digest -> sha256Hex timingSafeEqual on hashed hex -> safeCompare new Promise(setTimeout) -> sleep from @sim/utils/helpers No behavior change: encryption format, digest output, and token length are preserved exactly. * refactor(copilot): use toError in remaining otel/finalize sites Replace the last two `error instanceof Error ? error : new Error(String(error))` patterns with toError from @sim/utils/errors. Completes the sweep of clean candidates — no behavior change. * refactor(security): consolidate HMAC-SHA256 primitives into @sim/security Adds hmacSha256Hex and hmacSha256Base64 to @sim/security/hmac and migrates 15 webhook providers plus 5 other hot paths (deployment token signing, outbound webhook requests, workspace notification delivery, notification test route, Shopify OAuth callback) off bare `createHmac` calls. Secret parameter accepts `string | Buffer` to cover base64-decoded Svix-style secrets (Resend) and MS Teams' HMAC scheme. AWS SigV4 signing in S3 and Textract tools intentionally retains direct `createHmac` usage — its multi-step key derivation chain doesn't fit a generic helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore(packages): post-audit test + packaging polish - Add safeCompare unit tests (identity, length mismatch, hex-nibble diff). - Add Buffer-secret cases to hmac tests to lock in Svix/MS-Teams contract. - Declare `reactflow` as a peerDependency on @sim/workflow-types — only used for type imports. - Add a barrel export to @sim/workflow-persistence for consumers that prefer package-level imports; subpath exports retained. - Document the data-field invariant in load.ts for loop/parallel subflow patching. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore(realtime): address PR review feedback - Remove redundant SOCKET_PORT=3002 env from Dockerfile runner stage (env.PORT already defaults to 3002 via zod schema). - Reorder PORT fallback so an explicitly-set SOCKET_PORT wins over the schema default for PORT; keeps SOCKET_PORT functional as an override instead of dead code. - Add dedicated type-check CI step for @sim/realtime so TS errors surface pre-deploy (the Dockerfile runs source TS via Bun and has no implicit build-time type check). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore(realtime): remove unused SOCKET_PORT env var SOCKET_PORT has lived in the socket server since the June 2025 refactor but was never actually set in any deploy config — docker-compose.prod, helm values/templates, .env.example, and docs all use PORT or the 3002 default exclusively. No self-hoster was ever pointed at SOCKET_PORT, so removing it is safe. Simplifies realtime port resolution to `env.PORT` (zod-validated with a 3002 default) and drops the orphaned sim-side schema entry. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Waleed Latif <walif6@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
69 lines
2.1 KiB
TypeScript
69 lines
2.1 KiB
TypeScript
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto'
|
|
|
|
/**
|
|
* AES-256-GCM encryption primitive. Produces a self-contained string in the
|
|
* format `iv:ciphertext:authTag` (all hex-encoded) that can be stored and
|
|
* later passed directly to {@link decrypt}.
|
|
*
|
|
* @param plaintext - UTF-8 string to encrypt
|
|
* @param key - 32-byte encryption key
|
|
*/
|
|
export async function encrypt(
|
|
plaintext: string,
|
|
key: Buffer
|
|
): Promise<{ encrypted: string; iv: string }> {
|
|
assertKey(key)
|
|
|
|
const iv = randomBytes(16)
|
|
const cipher = createCipheriv('aes-256-gcm', key, iv, { authTagLength: 16 })
|
|
let encrypted = cipher.update(plaintext, 'utf8', 'hex')
|
|
encrypted += cipher.final('hex')
|
|
|
|
const authTag = cipher.getAuthTag()
|
|
const ivHex = iv.toString('hex')
|
|
|
|
return {
|
|
encrypted: `${ivHex}:${encrypted}:${authTag.toString('hex')}`,
|
|
iv: ivHex,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* AES-256-GCM decryption primitive. Expects input produced by {@link encrypt}
|
|
* in the format `iv:ciphertext:authTag`. Throws when the format is malformed
|
|
* or when the GCM auth tag does not verify (tampered ciphertext, wrong key).
|
|
*/
|
|
export async function decrypt(encryptedValue: string, key: Buffer): Promise<{ decrypted: string }> {
|
|
assertKey(key)
|
|
|
|
const parts = encryptedValue.split(':')
|
|
if (parts.length < 3) {
|
|
throw new Error('Invalid encrypted value format. Expected "iv:encrypted:authTag"')
|
|
}
|
|
|
|
const ivHex = parts[0]
|
|
const authTagHex = parts[parts.length - 1]
|
|
const encrypted = parts.slice(1, -1).join(':')
|
|
|
|
if (!ivHex || !authTagHex) {
|
|
throw new Error('Invalid encrypted value format. Expected "iv:encrypted:authTag"')
|
|
}
|
|
|
|
const iv = Buffer.from(ivHex, 'hex')
|
|
const authTag = Buffer.from(authTagHex, 'hex')
|
|
|
|
const decipher = createDecipheriv('aes-256-gcm', key, iv, { authTagLength: 16 })
|
|
decipher.setAuthTag(authTag)
|
|
|
|
let decrypted = decipher.update(encrypted, 'hex', 'utf8')
|
|
decrypted += decipher.final('utf8')
|
|
|
|
return { decrypted }
|
|
}
|
|
|
|
function assertKey(key: Buffer): void {
|
|
if (key.length !== 32) {
|
|
throw new Error('Encryption key must be 32 bytes (256 bits)')
|
|
}
|
|
}
|