@@ -17,6 +17,7 @@ import { InjectRepository } from '@nestjs/typeorm'
1717import { Not , Repository , LessThan , In , JsonContains , FindOptionsWhere , ILike } from 'typeorm'
1818import { Sandbox } from '../entities/sandbox.entity'
1919import { SandboxFork } from '../entities/sandbox-fork.entity'
20+ import { SandboxMetadata } from '../entities/sandbox-metadata.entity'
2021import { CreateSandboxDto } from '../dto/create-sandbox.dto'
2122import { CreateSandboxSnapshotDto } from '../dto/create-sandbox-snapshot.dto'
2223import { ForkSandboxDto } from '../dto/fork-sandbox.dto'
@@ -153,6 +154,8 @@ export class SandboxService {
153154 private readonly dockerRegistryService : DockerRegistryService ,
154155 @InjectRepository ( SandboxFork )
155156 private readonly sandboxForkRepository : Repository < SandboxFork > ,
157+ @InjectRepository ( SandboxMetadata )
158+ private readonly sandboxMetadataRepository : Repository < SandboxMetadata > ,
156159 @Inject ( SANDBOX_SEARCH_ADAPTER )
157160 private readonly sandboxSearchAdapter : SandboxSearchAdapter ,
158161 ) { }
@@ -2615,6 +2618,51 @@ export class SandboxService {
26152618 } )
26162619 }
26172620
2621+ private async ensureMetadata ( sandboxId : string ) : Promise < SandboxMetadata > {
2622+ const existing = await this . sandboxMetadataRepository . findOne ( { where : { sandboxId } } )
2623+ if ( existing ) {
2624+ return existing
2625+ }
2626+
2627+ // ON CONFLICT do nothing to avoid race conditions on sandbox creation where multiple requests
2628+ // may attempt to create metadata for the same sandbox
2629+ await this . sandboxMetadataRepository
2630+ . createQueryBuilder ( )
2631+ . insert ( )
2632+ . into ( SandboxMetadata )
2633+ . values ( {
2634+ sandboxId,
2635+ signingKey : nanoid ( 32 ) ,
2636+ } )
2637+ . orIgnore ( )
2638+ . execute ( )
2639+
2640+ return this . sandboxMetadataRepository . findOneOrFail ( { where : { sandboxId } } )
2641+ }
2642+
2643+ async getSigningKey ( sandboxId : string ) : Promise < string > {
2644+ const cacheKey = `signing-key:${ sandboxId } `
2645+ const cached = await this . redis . get ( cacheKey )
2646+ if ( cached ) {
2647+ return cached
2648+ }
2649+
2650+ const metadata = await this . ensureMetadata ( sandboxId )
2651+ await this . redis . setex ( cacheKey , 300 , metadata . signingKey )
2652+ return metadata . signingKey
2653+ }
2654+
2655+ async rotateSigningKey ( sandboxId : string ) : Promise < string > {
2656+ await this . ensureMetadata ( sandboxId )
2657+ const newKey = nanoid ( 32 )
2658+ await this . sandboxMetadataRepository . update ( sandboxId , { signingKey : newKey } )
2659+ // Write-through (not delete): a concurrent getSigningKey that read the old key from the
2660+ // DB before this update could otherwise re-cache it after a plain delete, pinning a stale
2661+ // key for the full TTL. Overwriting with the new key closes that rotation race window.
2662+ await this . redis . setex ( `signing-key:${ sandboxId } ` , 300 , newKey )
2663+ return newKey
2664+ }
2665+
26182666 async updateLastActivityAt ( sandboxId : string , lastActivityAt : Date ) : Promise < void > {
26192667 await this . sandboxActivityService . updateLastActivityAt ( sandboxId , lastActivityAt )
26202668 }
0 commit comments