@@ -15,6 +15,9 @@ import type { IDataEngine, IObjectQLEngine } from '@objectstack/spec/contracts';
1515import type { PrimaryDatasourceVerdict } from '@objectstack/objectql' ;
1616import { readServiceSelfInfo , readChannelRoute , isSubscribableChannel , DispatcherErrorCode , resolveDiscoveryEnvironment } from '@objectstack/spec/api' ;
1717import type { ServiceInfo } from '@objectstack/spec/api' ;
18+ // [#20477] The caller's VETTED organization, read by the one helper `RestServer`
19+ // and the dispatcher's `/meta` doors share — imported, never restated.
20+ import { metaCallerOrganizationId } from '@objectstack/rest' ;
1821import { apiErrorResponse } from './error-envelope.js' ;
1922import { resolveRuntimeVersion } from './runtime-version.js' ;
2023import type { ExecutionContext } from '@objectstack/spec/kernel' ;
@@ -2181,51 +2184,45 @@ export class HttpDispatcher {
21812184 }
21822185
21832186 /**
2184- * Resolve `resolveActiveOrganizationId` — the ACTIVE ORGANIZATION id on the
2185- * request's auth session, i.e. `session.activeOrganizationId`, normalising
2186- * plain-object headers into a `Headers` instance first because the auth
2187- * API's `getSession` requires one. Returns `undefined` for anonymous calls,
2188- * when auth is not wired up, when the session names no active organization,
2189- * and on any failure reaching the auth service — the caller cannot
2190- * distinguish those four, by design.
2187+ * Resolve `resolveActiveOrganizationId` — the caller's VETTED active
2188+ * organization: the `tenantId` the identity step wrote onto
2189+ * `context.executionContext` (`resolveRequestScope` →
2190+ * `resolveExecutionContext` → `resolveAuthzContext`), ⛔ never the auth
2191+ * session's `activeOrganizationId` as stored.
21912192 *
2192- * ⚠️ Until this was corrected the block here described `resolveCallerUserId`
2193- * ("the calling user id from the request session"), a sibling deleted with
2194- * the multi-tenant `/cloud` control plane. TSDoc binds a block by POSITION,
2195- * so deleting the declaration under a docblock does not delete the docblock
2196- * — it silently re-points it at the next declaration down, which then ships
2197- * a description of a method that no longer exists. A rename sweep cannot
2198- * catch that, because a sweep reads the docblock OF the method it is
2199- * changing and this one read as if it already belonged there. Keep the
2200- * first line naming the method it documents, so the next re-point is
2201- * visible on sight rather than plausible.
2193+ * [#20477] Those are two different values. `resolveAuthzContext`
2194+ * (`@objectstack/core`) VETS the session claim: under a wall-enforcing
2195+ * tenancy posture, a claim naming an organization its owner no longer
2196+ * belongs to is DROPPED, and the request resolves with no active
2197+ * organization (maintainer ruling B on #15409, PR #15794). This source used
2198+ * to call the auth service's `getSession` and return the claim unread, so
2199+ * every door that asked it — the nine `/packages` doors — kept a removed
2200+ * member inside the organization they had left for the rest of the
2201+ * session: its commit history and export were served to them, and their
2202+ * publish-drafts, discard-drafts, revert, rollback, adopt-orphans,
2203+ * duplicate and uninstall ran inside it. Reading the vetted value HERE
2204+ * fixes every caller at once and adds no second vetting path. It is the
2205+ * value `RestServer` scopes a caller by (`ctx.tenantId`), read through the
2206+ * one helper the dispatcher's `/meta` doors already use (#20408), so no
2207+ * transport or domain can answer a different organization for one caller.
2208+ *
2209+ * An admitted API key's organization is its binding, so a key caller is
2210+ * answered that organization here; the session read found no session for a
2211+ * key and answered `undefined`.
2212+ *
2213+ * Returns `undefined` for an anonymous caller, a caller with no active
2214+ * organization, and one whose claim was dropped — one fact to every
2215+ * consumer. Every caller runs after `dispatch()`'s identity step: the
2216+ * `/packages` domain's anonymous-deny floor refuses a context carrying no
2217+ * resolved principal before any of them is reached.
2218+ *
2219+ * Keep the first line naming the method it documents: TSDoc binds a block
2220+ * by POSITION, so deleting a declaration from under its docblock silently
2221+ * re-points the block at the next declaration down (this one once described
2222+ * `resolveCallerUserId`, a sibling deleted with the `/cloud` control plane).
22022223 */
22032224 private async resolveActiveOrganizationId ( context : HttpProtocolContext ) : Promise < string | undefined > {
2204- try {
2205- // [#4127 FINDING, batch 5]
2206- // Third `.api` reader on the auth service; see the note above.
2207- const authService = await this . resolveService ( this . requestKernel ( context ) , CoreServiceName . enum . auth ) ;
2208- const rawHeaders = context . request ?. headers ;
2209- let headers : any = rawHeaders ;
2210- if ( rawHeaders && typeof rawHeaders === 'object' && typeof ( rawHeaders as any ) . get !== 'function' ) {
2211- try {
2212- const h = new Headers ( ) ;
2213- for ( const [ k , v ] of Object . entries ( rawHeaders as Record < string , any > ) ) {
2214- if ( v == null ) continue ;
2215- h . set ( k , Array . isArray ( v ) ? v . join ( ', ' ) : String ( v ) ) ;
2216- }
2217- headers = h ;
2218- } catch {
2219- headers = rawHeaders ;
2220- }
2221- }
2222- const apiObj = authService ?. auth ?. api ?? authService ?. api ;
2223- const sessionData = await apiObj ?. getSession ?. call ( apiObj , { headers } ) ;
2224- const oid = sessionData ?. session ?. activeOrganizationId ;
2225- return typeof oid === 'string' && oid . length > 0 ? oid : undefined ;
2226- } catch {
2227- return undefined ;
2228- }
2225+ return metaCallerOrganizationId ( context . executionContext ) ;
22292226 }
22302227
22312228 /** Thin delegate — body extracted to `./domains/ui.ts` (D11③ PR-3). */
0 commit comments