@@ -600,14 +600,21 @@ export class RestServer {
600600 return result ;
601601 }
602602
603- private async resolveProtocol ( environmentId ?: string , req ?: any ) : Promise < ObjectStackProtocol > {
604- if ( environmentId === 'platform' ) return this . protocol ;
605- if ( ! environmentId && req && this . envRegistry && this . kernelManager ) {
603+ /**
604+ * Resolve the environment a request targets: explicit id → tenant hostname
605+ * → `X-Environment-Id` header → single-project default. Returns undefined
606+ * for control-plane requests. Shared by every per-environment service
607+ * resolution (protocol, analytics, …) so they can never disagree about
608+ * which kernel a request belongs to.
609+ */
610+ private async resolveRequestEnvironmentId ( environmentId ?: string , req ?: any ) : Promise < string | undefined > {
611+ if ( environmentId ) return environmentId ;
612+ if ( req && this . envRegistry && this . kernelManager ) {
606613 const host = this . extractHostname ( req ) ;
607614 if ( host ) {
608615 try {
609616 const result = await this . resolveHostnameCached ( host ) ;
610- if ( result ?. environmentId ) environmentId = result . environmentId ;
617+ if ( result ?. environmentId ) return result . environmentId ;
611618 } catch {
612619 // fall through to next strategy
613620 }
@@ -618,12 +625,12 @@ export class RestServer {
618625 // serving multiple compiled bundles via OS_PROJECT_ARTIFACTS).
619626 // We validate the id through the env registry to avoid
620627 // routing to a non-existent kernel.
621- if ( ! environmentId && typeof this . envRegistry . resolveById === 'function' ) {
628+ if ( typeof this . envRegistry . resolveById === 'function' ) {
622629 const headerVal = this . extractProjectIdHeader ( req ) ;
623630 if ( headerVal ) {
624631 try {
625632 const driver = await this . envRegistry . resolveById ( headerVal ) ;
626- if ( driver ) environmentId = headerVal ;
633+ if ( driver ) return headerVal ;
627634 } catch {
628635 // fall through to default fallback
629636 }
@@ -635,14 +642,20 @@ export class RestServer {
635642 // (no `/projects/<id>` prefix, no hostname mapping, no header)
636643 // resolve to the lone project's kernel rather than the control
637644 // plane.
638- if ( ! environmentId && this . defaultEnvironmentIdProvider ) {
645+ if ( this . defaultEnvironmentIdProvider ) {
639646 try {
640647 const def = this . defaultEnvironmentIdProvider ( ) ;
641- if ( def ) environmentId = def ;
648+ if ( def ) return def ;
642649 } catch { /* fall through */ }
643650 }
644- if ( ! environmentId || ! this . kernelManager ) return this . protocol ;
645- const kernel = await this . kernelManager . getOrCreate ( environmentId ) ;
651+ return undefined ;
652+ }
653+
654+ private async resolveProtocol ( environmentId ?: string , req ?: any ) : Promise < ObjectStackProtocol > {
655+ if ( environmentId === 'platform' ) return this . protocol ;
656+ const envId = await this . resolveRequestEnvironmentId ( environmentId , req ) ;
657+ if ( ! envId || ! this . kernelManager ) return this . protocol ;
658+ const kernel = await this . kernelManager . getOrCreate ( envId ) ;
646659 return kernel . getServiceAsync < ObjectStackProtocol > ( 'protocol' ) ;
647660 }
648661
@@ -3589,7 +3602,21 @@ export class RestServer {
35893602 */
35903603 private registerAnalyticsEndpoints ( basePath : string ) : void {
35913604 const isScoped = basePath . includes ( '/environments/:environmentId' ) ;
3592- const resolveService = async ( environmentId ?: string ) => {
3605+ // Resolve the ENVIRONMENT's analytics service first — its strategy
3606+ // bridges are bound to the env kernel's own data engine. The host
3607+ // provider (whose 'data' is the host kernel's engine) is only a
3608+ // fallback: serving a tenant's dataset query from the host engine
3609+ // reads the WRONG database and silently aggregates over nothing
3610+ // (the staging "Total Spend: 0 on a populated table" incident).
3611+ const resolveService = async ( environmentId ?: string , req ?: any ) => {
3612+ try {
3613+ const envId = await this . resolveRequestEnvironmentId ( environmentId , req ) ;
3614+ if ( envId && envId !== 'platform' && this . kernelManager ) {
3615+ const kernel = await this . kernelManager . getOrCreate ( envId ) ;
3616+ const svc = await kernel . getServiceAsync < any > ( 'analytics' ) . catch ( ( ) => undefined ) ;
3617+ if ( svc ) return svc ;
3618+ }
3619+ } catch { /* fall back to the host service */ }
35933620 if ( ! this . analyticsServiceProvider ) return undefined ;
35943621 try { return await this . analyticsServiceProvider ( environmentId ) ; }
35953622 catch { return undefined ; }
@@ -3604,7 +3631,7 @@ export class RestServer {
36043631 const context = await this . resolveExecCtx ( environmentId , req ) ;
36053632 if ( this . enforceAuth ( req , res , context ) ) return ;
36063633
3607- const svc = await resolveService ( environmentId ) ;
3634+ const svc = await resolveService ( environmentId , req ) ;
36083635 if ( ! svc || typeof svc . queryDataset !== 'function' ) {
36093636 return res . status ( 501 ) . json ( {
36103637 code : 'NOT_IMPLEMENTED' ,
0 commit comments