chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
+38
View File
@@ -0,0 +1,38 @@
{
"name": "@sim/runtime-secrets",
"version": "0.1.0",
"private": true,
"sideEffects": false,
"type": "module",
"license": "Apache-2.0",
"engines": {
"bun": ">=1.2.13",
"node": ">=20.0.0"
},
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
}
},
"scripts": {
"type-check": "tsc --noEmit",
"lint": "biome check --write --unsafe .",
"lint:check": "biome check .",
"format": "biome format --write .",
"format:check": "biome format .",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@aws-sdk/client-secrets-manager": "3.1032.0",
"@sim/logger": "workspace:*",
"@sim/utils": "workspace:*"
},
"devDependencies": {
"@sim/tsconfig": "workspace:*",
"@types/node": "24.2.1",
"typescript": "^7.0.2",
"vitest": "^4.1.0"
}
}
@@ -0,0 +1,91 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const { mockSend } = vi.hoisted(() => ({ mockSend: vi.fn() }))
vi.mock('@aws-sdk/client-secrets-manager', () => ({
SecretsManagerClient: class SecretsManagerClient {
send = mockSend
},
GetSecretValueCommand: class GetSecretValueCommand {
constructor(public input: unknown) {}
},
}))
vi.mock('@sim/logger', () => ({
createLogger: () => ({ info: vi.fn(), warn: vi.fn(), error: vi.fn() }),
}))
vi.mock('@sim/utils/helpers', () => ({
sleep: vi.fn().mockResolvedValue(undefined),
}))
import { loadRuntimeSecrets } from './index'
const TOUCHED = ['SIM_ENV_SECRET_ID', 'FOO', 'BAZ'] as const
describe('loadRuntimeSecrets', () => {
beforeEach(() => {
vi.clearAllMocks()
for (const key of TOUCHED) delete process.env[key]
})
afterEach(() => {
for (const key of TOUCHED) delete process.env[key]
})
it('no-ops when SIM_ENV_SECRET_ID is unset', async () => {
await loadRuntimeSecrets()
expect(mockSend).not.toHaveBeenCalled()
})
it('hydrates process.env from the parsed secret JSON', async () => {
process.env.SIM_ENV_SECRET_ID = '/test/sim/env-vars'
mockSend.mockResolvedValue({ SecretString: JSON.stringify({ FOO: 'bar', BAZ: 'qux' }) })
await loadRuntimeSecrets()
expect(process.env.FOO).toBe('bar')
expect(process.env.BAZ).toBe('qux')
})
it('never overwrites an already-set env var', async () => {
process.env.SIM_ENV_SECRET_ID = '/test/sim/env-vars'
process.env.FOO = 'existing'
mockSend.mockResolvedValue({ SecretString: JSON.stringify({ FOO: 'new', BAZ: 'qux' }) })
await loadRuntimeSecrets()
expect(process.env.FOO).toBe('existing')
expect(process.env.BAZ).toBe('qux')
})
it('throws when the secret is not valid JSON', async () => {
process.env.SIM_ENV_SECRET_ID = '/test/sim/env-vars'
mockSend.mockResolvedValue({ SecretString: 'not json' })
await expect(loadRuntimeSecrets()).rejects.toThrow(/not valid JSON/)
})
it('throws when the secret JSON is not an object', async () => {
process.env.SIM_ENV_SECRET_ID = '/test/sim/env-vars'
mockSend.mockResolvedValue({ SecretString: JSON.stringify(['a', 'b']) })
await expect(loadRuntimeSecrets()).rejects.toThrow(/must be a JSON object/)
})
it('throws immediately on a binary secret (no SecretString), without retrying', async () => {
process.env.SIM_ENV_SECRET_ID = '/test/sim/env-vars'
mockSend.mockResolvedValue({})
await expect(loadRuntimeSecrets()).rejects.toThrow(/binary secrets/)
expect(mockSend).toHaveBeenCalledTimes(1)
})
it('retries then throws when the fetch keeps failing', async () => {
process.env.SIM_ENV_SECRET_ID = '/test/sim/env-vars'
mockSend.mockRejectedValue(new Error('boom'))
await expect(loadRuntimeSecrets()).rejects.toThrow(/Failed to fetch runtime secrets/)
expect(mockSend).toHaveBeenCalledTimes(3)
})
})
+102
View File
@@ -0,0 +1,102 @@
import type { GetSecretValueCommandOutput } from '@aws-sdk/client-secrets-manager'
import { GetSecretValueCommand, SecretsManagerClient } from '@aws-sdk/client-secrets-manager'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { sleep } from '@sim/utils/helpers'
import { backoffWithJitter } from '@sim/utils/retry'
const logger = createLogger('RuntimeSecrets')
/** Plaintext env var (set in the ECS task definition) naming the secret to ingest. */
const SECRET_ID_ENV = 'SIM_ENV_SECRET_ID'
const MAX_ATTEMPTS = 3
/** Bounds each Secrets Manager request so a stalled response can't hang boot. */
const REQUEST_TIMEOUT_MS = 5000
/**
* Fetches the combined `/{env}/sim/env-vars` secret once at container boot and
* hydrates `process.env`, so secrets no longer have to be fanned out into the
* ECS task definition (which is approaching the 64 KB rendered-document limit).
*
* Must run before any application module that reads env at import time. No-ops
* when {@link SECRET_ID_ENV} is unset (local dev / self-hosted keep using their
* own env). Existing `process.env` keys are never overwritten, so explicit
* task-definition `environment` entries win. Throws on any fetch/parse failure
* so a misconfigured container crashes instead of booting without its config.
*/
export async function loadRuntimeSecrets(): Promise<void> {
const secretId = process.env[SECRET_ID_ENV]
if (!secretId) {
logger.info(`${SECRET_ID_ENV} not set; skipping runtime secret ingestion`)
return
}
const client = new SecretsManagerClient(
process.env.AWS_REGION ? { region: process.env.AWS_REGION } : {}
)
const secretString = await fetchSecretString(client, secretId)
const entries = parseSecretJson(secretString)
let loaded = 0
let skipped = 0
for (const [key, value] of Object.entries(entries)) {
if (key in process.env) {
skipped++
continue
}
process.env[key] = typeof value === 'string' ? value : JSON.stringify(value)
loaded++
}
logger.info('Runtime secrets ingested', { secretId, loaded, skipped })
}
async function fetchSecretString(client: SecretsManagerClient, secretId: string): Promise<string> {
const response = await sendWithRetry(client, secretId)
if (!response.SecretString) {
// Non-retriable: a binary secret will never become a string between attempts.
throw new Error('Secret has no SecretString (binary secrets are not supported)')
}
return response.SecretString
}
async function sendWithRetry(
client: SecretsManagerClient,
secretId: string
): Promise<GetSecretValueCommandOutput> {
let lastError: unknown
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
try {
return await client.send(new GetSecretValueCommand({ SecretId: secretId }), {
abortSignal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
})
} catch (error) {
lastError = error
if (attempt < MAX_ATTEMPTS) {
const delay = backoffWithJitter(attempt, null, { baseMs: 200, maxMs: 2000 })
logger.warn(
`Failed to fetch runtime secrets (attempt ${attempt}/${MAX_ATTEMPTS}), retrying`,
{ error: getErrorMessage(error) }
)
await sleep(delay)
}
}
}
throw new Error(`Failed to fetch runtime secrets from ${secretId}: ${getErrorMessage(lastError)}`)
}
function parseSecretJson(secretString: string): Record<string, unknown> {
let parsed: unknown
try {
parsed = JSON.parse(secretString)
} catch (error) {
throw new Error(`Runtime secret is not valid JSON: ${getErrorMessage(error)}`)
}
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
throw new Error('Runtime secret must be a JSON object of key/value pairs')
}
return parsed as Record<string, unknown>
}
+5
View File
@@ -0,0 +1,5 @@
{
"extends": "@sim/tsconfig/library.json",
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
@@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
environment: 'node',
},
})