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>
75 lines
2.5 KiB
TypeScript
75 lines
2.5 KiB
TypeScript
import { randomBytes } from 'node:crypto'
|
|
import { describe, expect, it } from 'vitest'
|
|
import { decrypt, encrypt } from './encryption'
|
|
|
|
const KEY = Buffer.from('0'.repeat(64), 'hex')
|
|
|
|
describe('encrypt', () => {
|
|
it('returns iv:ciphertext:authTag and a 32-char hex IV', async () => {
|
|
const result = await encrypt('secret', KEY)
|
|
expect(result.encrypted.split(':')).toHaveLength(3)
|
|
expect(result.iv).toHaveLength(32)
|
|
})
|
|
|
|
it('produces distinct ciphertexts for the same input', async () => {
|
|
const a = await encrypt('same', KEY)
|
|
const b = await encrypt('same', KEY)
|
|
expect(a.encrypted).not.toBe(b.encrypted)
|
|
})
|
|
|
|
it('rejects keys that are not 32 bytes', async () => {
|
|
await expect(encrypt('x', Buffer.alloc(16))).rejects.toThrow(/32 bytes/)
|
|
})
|
|
})
|
|
|
|
describe('decrypt', () => {
|
|
it('round-trips arbitrary UTF-8 input', async () => {
|
|
const plaintext = 'Hello, !"#$%&\'()*+,-./0123456789:;<=>?@'
|
|
const { encrypted } = await encrypt(plaintext, KEY)
|
|
const { decrypted } = await decrypt(encrypted, KEY)
|
|
expect(decrypted).toBe(plaintext)
|
|
})
|
|
|
|
it('round-trips empty strings', async () => {
|
|
const { encrypted } = await encrypt('', KEY)
|
|
const { decrypted } = await decrypt(encrypted, KEY)
|
|
expect(decrypted).toBe('')
|
|
})
|
|
|
|
it('round-trips long inputs', async () => {
|
|
const plaintext = 'a'.repeat(10_000)
|
|
const { encrypted } = await encrypt(plaintext, KEY)
|
|
const { decrypted } = await decrypt(encrypted, KEY)
|
|
expect(decrypted).toBe(plaintext)
|
|
})
|
|
|
|
it('throws on malformed input', async () => {
|
|
await expect(decrypt('invalid', KEY)).rejects.toThrow(
|
|
'Invalid encrypted value format. Expected "iv:encrypted:authTag"'
|
|
)
|
|
await expect(decrypt('part1:part2', KEY)).rejects.toThrow(
|
|
'Invalid encrypted value format. Expected "iv:encrypted:authTag"'
|
|
)
|
|
})
|
|
|
|
it('throws when ciphertext is tampered', async () => {
|
|
const { encrypted } = await encrypt('original', KEY)
|
|
const parts = encrypted.split(':')
|
|
parts[1] = `deadbeef${parts[1].slice(8)}`
|
|
await expect(decrypt(parts.join(':'), KEY)).rejects.toThrow()
|
|
})
|
|
|
|
it('throws when auth tag is tampered', async () => {
|
|
const { encrypted } = await encrypt('original', KEY)
|
|
const parts = encrypted.split(':')
|
|
parts[2] = '0'.repeat(32)
|
|
await expect(decrypt(parts.join(':'), KEY)).rejects.toThrow()
|
|
})
|
|
|
|
it('throws when decrypted with a different key', async () => {
|
|
const { encrypted } = await encrypt('original', KEY)
|
|
const otherKey = randomBytes(32)
|
|
await expect(decrypt(encrypted, otherKey)).rejects.toThrow()
|
|
})
|
|
})
|