chore: import upstream snapshot with attribution
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
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
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
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
import { safeCompare } from '@sim/security/compare'
|
||||
import { sha256Hex } from '@sim/security/hash'
|
||||
import { hmacSha256Hex } from '@sim/security/hmac'
|
||||
import { normalizeEmail } from '@sim/utils/string'
|
||||
import type { NextResponse } from 'next/server'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import { isDev } from '@/lib/core/config/env-flags'
|
||||
|
||||
/**
|
||||
* Shared authentication utilities for deployed chat endpoints.
|
||||
* Handles token generation, validation, and auth cookies. CORS for these
|
||||
* endpoints lives in proxy.ts as the single source of truth.
|
||||
*/
|
||||
|
||||
function signPayload(payload: string): string {
|
||||
return hmacSha256Hex(payload, env.BETTER_AUTH_SECRET)
|
||||
}
|
||||
|
||||
function passwordSlot(encryptedPassword?: string | null): string {
|
||||
if (!encryptedPassword) return ''
|
||||
return sha256Hex(encryptedPassword).slice(0, 8)
|
||||
}
|
||||
|
||||
function generateAuthToken(
|
||||
deploymentId: string,
|
||||
type: string,
|
||||
encryptedPassword?: string | null
|
||||
): string {
|
||||
const payload = `${deploymentId}:${type}:${Date.now()}:${passwordSlot(encryptedPassword)}`
|
||||
const sig = signPayload(payload)
|
||||
return Buffer.from(`${payload}:${sig}`).toString('base64')
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an HMAC-signed authentication token for a chat deployment.
|
||||
* Includes a password-derived slot so changing the deployment password immediately
|
||||
* invalidates existing sessions.
|
||||
*/
|
||||
export function validateAuthToken(
|
||||
token: string,
|
||||
deploymentId: string,
|
||||
authType: string,
|
||||
encryptedPassword?: string | null
|
||||
): boolean {
|
||||
try {
|
||||
const decoded = Buffer.from(token, 'base64').toString()
|
||||
const lastColon = decoded.lastIndexOf(':')
|
||||
if (lastColon === -1) return false
|
||||
|
||||
const payload = decoded.slice(0, lastColon)
|
||||
const sig = decoded.slice(lastColon + 1)
|
||||
|
||||
const expectedSig = signPayload(payload)
|
||||
if (!safeCompare(sig, expectedSig)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const parts = payload.split(':')
|
||||
if (parts.length < 4) return false
|
||||
const [storedId, storedType, timestamp, storedPwSlot] = parts
|
||||
|
||||
if (storedId !== deploymentId) return false
|
||||
|
||||
// Bind the cookie to the auth type so a token minted under one mode (e.g. a
|
||||
// `public` share, which has an empty password slot) can't satisfy another
|
||||
// mode (e.g. `email` OTP) after the share's auth type is changed.
|
||||
if (storedType !== authType) return false
|
||||
|
||||
const expectedPwSlot = passwordSlot(encryptedPassword)
|
||||
if (storedPwSlot !== expectedPwSlot) return false
|
||||
|
||||
const createdAt = Number.parseInt(timestamp)
|
||||
const expireTime = 24 * 60 * 60 * 1000
|
||||
if (Date.now() - createdAt > expireTime) return false
|
||||
|
||||
return true
|
||||
} catch (_e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** The kind of deployed resource an auth cookie/token belongs to. */
|
||||
export type DeploymentAuthKind = 'chat' | 'file'
|
||||
|
||||
/** Canonical auth cookie name for a deployed resource (`{kind}_auth_{id}`). */
|
||||
export function deploymentAuthCookieName(cookiePrefix: DeploymentAuthKind, id: string): string {
|
||||
return `${cookiePrefix}_auth_${id}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an authentication cookie for a deployment
|
||||
*/
|
||||
export function setDeploymentAuthCookie(
|
||||
response: NextResponse,
|
||||
cookiePrefix: DeploymentAuthKind,
|
||||
deploymentId: string,
|
||||
authType: string,
|
||||
encryptedPassword?: string | null
|
||||
): void {
|
||||
const token = generateAuthToken(deploymentId, authType, encryptedPassword)
|
||||
response.cookies.set({
|
||||
name: deploymentAuthCookieName(cookiePrefix, deploymentId),
|
||||
value: token,
|
||||
httpOnly: true,
|
||||
secure: !isDev,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
maxAge: 60 * 60 * 24,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an email matches the allowed emails list (exact match or domain
|
||||
* match). Case-insensitive — email addresses are compared lowercased on both
|
||||
* sides, so callers don't need to normalize before calling.
|
||||
*/
|
||||
export function isEmailAllowed(email: string, allowedEmails: string[]): boolean {
|
||||
const normalizedEmail = normalizeEmail(email)
|
||||
const normalizedAllowed = allowedEmails.map(normalizeEmail)
|
||||
|
||||
if (normalizedAllowed.includes(normalizedEmail)) {
|
||||
return true
|
||||
}
|
||||
|
||||
const atIndex = normalizedEmail.indexOf('@')
|
||||
if (atIndex > 0) {
|
||||
const domain = normalizedEmail.substring(atIndex + 1)
|
||||
if (domain && normalizedAllowed.some((allowed) => allowed === `@${domain}`)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user