Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions packages/metadata-protocol/src/protocol-authoring-posture-probes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* [#25467 cut 26] The runtime authoring gate's deployment-posture probes, from
* `ObjectStackProtocolImplementation`, moved out of `protocol.ts` whole: the
* four reads of THIS deployment that `assertRuntimeAuthoringRules` hands the
* gate on every write.
*
* - `orgWallEnforced` — does the configured tenancy posture wall
* organizations (#6285)? Unreadable reads as walled.
* - `tenancyPostureInForce` — the posture the `tenancy` service reports in
* force, for the public-form intake advisory (#21476).
* - `rlsMembershipResolverKeys` — the membership keys the mounted RLS
* resolver declares.
* - `hierarchyResolverMounted` — is a hierarchy resolver mounted (#26188)?
*
* A MOVE, not a redesign. Each function below is the body of the
* `ObjectStackProtocolImplementation` method of the same name as it stood,
* re-indented one level and otherwise unedited: the same service keys, the
* same `try`s and the same answers when a read throws.
*
* ## How the moved bodies reach the class
*
* The shape #23050 set for `rest-server.ts` (its `data-action-routes.ts`
* header carries the full reasoning), as `protocol-batch-data.ts` applies it.
* The class keeps each method — the published `dist/*.d.ts` lists its name,
* docblock and declared signature — and its body delegates here. Three read
* the constructor-supplied `getServicesRegistry` field, handed over as a
* getter ({@link AuthoringPostureProbesHost}), as
* `protocol-metadata-service-read.ts` hands that field over, so each reads the
* registry at call time — a resolver registered after this protocol is
* assembled is seen by the next write. `orgWallEnforced` reads no member, so
* its stub calls the plain function. The callers — the closures in
* `assertRuntimeAuthoringRules`'s stub — keep calling the class methods.
*
* What the bodies read from elsewhere, they import from where `protocol.ts`
* imported it; four of those names had no other reader there and left its
* imports, with the comments that explained them. Nothing re-exports this
* module, so the published surface does not move.
*/

import type { TenancyPosture } from '@objectstack/spec/security';
// [#6285] ADR-0105 D1's authority on "does this deployment wall organizations?".
// `resolveMultiOrgEnabled()` is DEMOTED and its own doc comment says answering
// this question with it is a bug (cloud#1020, #5233) — so the posture, and only
// the posture, is what the runtime authoring gate is told.
import { postureEnforcesWall } from '@objectstack/spec/security';
import { resolveTenancyPosture } from '@objectstack/types';
import {
// [#21476] The posture IN FORCE, read off the `tenancy` service the one way
// the anonymous form doors read it — the runtime authoring gate's input for
// its public-form intake advisory (see `tenancyPostureInForce()`).
anonymousFormIntakePosture,
} from '@objectstack/metadata-core';
import { RLS_MEMBERSHIP_RESOLVER_SERVICE } from '@objectstack/spec/contracts';
import { HIERARCHY_SCOPE_RESOLVER_SERVICE } from './runtime-authoring-gate.js';
import type { ObjectStackProtocolImplementation } from './protocol.js';

/**
* The `ObjectStackProtocolImplementation` members the functions below read —
* every one of them, and no other; each function's `this` is the `Pick` of
* the members it reads. Each is typed AS the class member (an indexed access
* on the class), so a change to a member's type on the class is a change
* here too, and naming a member the class does not have is a compile error.
*/
export interface AuthoringPostureProbesHost {
readonly getServicesRegistry: ObjectStackProtocolImplementation['getServicesRegistry'];
}

/** The body of `ObjectStackProtocolImplementation#orgWallEnforced`; the method's docblock there is its contract. */
export function orgWallEnforced(): boolean {
try {
return postureEnforcesWall(resolveTenancyPosture());
} catch {
return true;
}
}

/** The body of `ObjectStackProtocolImplementation#tenancyPostureInForce`; the method's docblock there is its contract. */
export function tenancyPostureInForce(this: Pick<AuthoringPostureProbesHost, 'getServicesRegistry'>): TenancyPosture | undefined {
try {
return anonymousFormIntakePosture(this.getServicesRegistry?.().get('tenancy'));
} catch {
return undefined;
}
}

/** The body of `ObjectStackProtocolImplementation#rlsMembershipResolverKeys`; the method's docblock there is its contract. */
export function rlsMembershipResolverKeys(this: Pick<AuthoringPostureProbesHost, 'getServicesRegistry'>): readonly string[] | undefined {
const registry = this.getServicesRegistry?.();
if (!registry) return undefined;
let resolver: unknown;
try {
resolver = registry.get(RLS_MEMBERSHIP_RESOLVER_SERVICE);
} catch {
return undefined;
}
if (resolver === undefined || resolver === null) return [];
const keys = (resolver as { keys?: unknown }).keys;
if (!Array.isArray(keys) || !keys.every((k) => typeof k === 'string')) return undefined;
return keys as readonly string[];
}

/** The body of `ObjectStackProtocolImplementation#hierarchyResolverMounted`; the method's docblock there is its contract. */
export function hierarchyResolverMounted(this: Pick<AuthoringPostureProbesHost, 'getServicesRegistry'>): boolean | undefined {
const registry = this.getServicesRegistry?.();
if (!registry) return undefined;
try {
const resolver = registry.get(HIERARCHY_SCOPE_RESOLVER_SERVICE);
return resolver !== undefined && resolver !== null;
} catch {
return undefined;
}
}
65 changes: 24 additions & 41 deletions packages/metadata-protocol/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@ import type {
} from '@objectstack/spec/api';
import { IDataEngine, engineCanRollBack, recordNotFoundError } from '@objectstack/core';
import { declaredHttpStatus, declaredUserMessage, isSandboxOrigin, resolveTenancyPosture, resolveThrownHttpError, sandboxBusinessMessage } from '@objectstack/types';
// [#6285] ADR-0105 D1's authority on "does this deployment wall organizations?".
// `resolveMultiOrgEnabled()` is DEMOTED and its own doc comment says answering
// this question with it is a bug (cloud#1020, #5233) — so the posture, and only
// the posture, is what the runtime authoring gate is told.
import { judgePermissionSetOwnerBinding, postureEnforcesWall, type TenancyPosture } from '@objectstack/spec/security';
import { judgePermissionSetOwnerBinding, type TenancyPosture } from '@objectstack/spec/security';
// [commit 376c70f98] The derived `version` this file's `getDiscovery()` serves as the
// `DiscoverySchema` "System Identity" field — never a literal again.
import { resolveDiscoveryVersion } from './discovery-version.js';
Expand All @@ -27,7 +23,6 @@ import { stripUnauthorableProperties } from './unauthorable-nodes.js';
// admits anything. See the module header for the predicate and its controls.
import { markErasedAuthoringInput } from './erased-authoring-mark.js';
import {
HIERARCHY_SCOPE_RESOLVER_SERVICE,
SDUI_MANIFEST_SERVICE,
isUsableSduiManifest,
stampHtmlPageRequires,
Expand Down Expand Up @@ -105,10 +100,6 @@ import {
// {@link ObjectStackProtocolImplementation.getMetaItemLayered}'s code-layer
// fallback so a hydrated row is never answered as the code layer.
isTenantAuthored,
// [#21476] The posture IN FORCE, read off the `tenancy` service the one way
// the anonymous form doors read it — the runtime authoring gate's input for
// its public-form intake advisory (see `tenancyPostureInForce()`).
anonymousFormIntakePosture,
// [ADR-0131 D6/D7] A legacy organization's rows, off the physical column,
// on a database the ceremony has not migrated — see migrateStoredMetadata.
readLegacyOrganizationRows,
Expand Down Expand Up @@ -167,7 +158,7 @@ import { PLURAL_TO_SINGULAR, SINGULAR_TO_PLURAL, canonicalMetaUrlType, metaUrlSp
// depends on `@objectstack/service-cluster`; a bridge plugin there hands the
// live transport in through `attachMetadataMutationPubSub`.
import type { IFieldFileService, IPubSub } from '@objectstack/spec/contracts';
import { FIELD_FILE_SERVICE, RLS_MEMBERSHIP_RESOLVER_SERVICE } from '@objectstack/spec/contracts';
import { FIELD_FILE_SERVICE } from '@objectstack/spec/contracts';
import { applyConversionsToFlow, type ConversionNotice, type ConversionTodoNotice } from '@objectstack/spec';
import { type FormView, type I18nLabel, isAggregatedViewContainer, expandViewContainer } from '@objectstack/spec/ui';
// [commit ece4dad31] Emitted-specifier pin. This module's inferred public declarations
Expand Down Expand Up @@ -420,6 +411,8 @@ import { listLiveMetadataTypes } from './protocol-live-metadata-types.js';
import { saveConflict } from './protocol-save-conflict.js';
// [#25467 cut 25] The post-persistence mutation fan-out: projector, local listeners, cluster publish, publish listeners.
import { emitMetaItemPublished, notifyMutationListenersLocal, publishMetadataMutation, runMutationProjector } from './protocol-mutation-fan-out.js';
// [#25467 cut 26] The runtime authoring gate's deployment-posture probes: org wall, tenancy, RLS keys, hierarchy.
import { hierarchyResolverMounted, orgWallEnforced, rlsMembershipResolverKeys, tenancyPostureInForce } from './protocol-authoring-posture-probes.js';

/**
* Canonical Zod schema per metadata type lives in
Expand Down Expand Up @@ -7460,11 +7453,8 @@ export class ObjectStackProtocolImplementation implements
* refusal combination to be turned away.
*/
private orgWallEnforced(): boolean {
try {
return postureEnforcesWall(resolveTenancyPosture());
} catch {
return true;
}
// [#25467 cut 26] The body moved to `protocol-authoring-posture-probes.ts`; it reads no member.
return orgWallEnforced();
}

/**
Expand Down Expand Up @@ -7494,11 +7484,12 @@ export class ObjectStackProtocolImplementation implements
* reports none, and the advisory is simply not raised.
*/
private tenancyPostureInForce(): TenancyPosture | undefined {
try {
return anonymousFormIntakePosture(this.getServicesRegistry?.().get('tenancy'));
} catch {
return undefined;
}
// [#25467 cut 26] The body moved to `protocol-authoring-posture-probes.ts` (see `AuthoringPostureProbesHost`).
const readGetServicesRegistry = () => this.getServicesRegistry;
const host: ThisParameterType<typeof tenancyPostureInForce> = {
get getServicesRegistry() { return readGetServicesRegistry(); },
};
return tenancyPostureInForce.call(host);
}

/**
Expand All @@ -7516,18 +7507,12 @@ export class ObjectStackProtocolImplementation implements
* wrong ones.
*/
private rlsMembershipResolverKeys(): readonly string[] | undefined {
const registry = this.getServicesRegistry?.();
if (!registry) return undefined;
let resolver: unknown;
try {
resolver = registry.get(RLS_MEMBERSHIP_RESOLVER_SERVICE);
} catch {
return undefined;
}
if (resolver === undefined || resolver === null) return [];
const keys = (resolver as { keys?: unknown }).keys;
if (!Array.isArray(keys) || !keys.every((k) => typeof k === 'string')) return undefined;
return keys as readonly string[];
// [#25467 cut 26] The body moved to `protocol-authoring-posture-probes.ts` (see `AuthoringPostureProbesHost`).
const readGetServicesRegistry = () => this.getServicesRegistry;
const host: ThisParameterType<typeof rlsMembershipResolverKeys> = {
get getServicesRegistry() { return readGetServicesRegistry(); },
};
return rlsMembershipResolverKeys.call(host);
}

/**
Expand All @@ -7544,14 +7529,12 @@ export class ObjectStackProtocolImplementation implements
* throws.
*/
private hierarchyResolverMounted(): boolean | undefined {
const registry = this.getServicesRegistry?.();
if (!registry) return undefined;
try {
const resolver = registry.get(HIERARCHY_SCOPE_RESOLVER_SERVICE);
return resolver !== undefined && resolver !== null;
} catch {
return undefined;
}
// [#25467 cut 26] The body moved to `protocol-authoring-posture-probes.ts` (see `AuthoringPostureProbesHost`).
const readGetServicesRegistry = () => this.getServicesRegistry;
const host: ThisParameterType<typeof hierarchyResolverMounted> = {
get getServicesRegistry() { return readGetServicesRegistry(); },
};
return hierarchyResolverMounted.call(host);
}

/**
Expand Down
Loading