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
13 changes: 13 additions & 0 deletions .changeset/30053-hand-series-private-docblocks-slice-2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@objectstack/objectql': patch
'@objectstack/spec': patch
---

refactor(objectql): the private docblocks of the last 28 hand-series stubs in `engine.ts` move into their hand modules, onto the functions that hold their bodies; no behaviour or public declaration changes

Clause-②: no
Metadata-protocol: none

- The second and last slice of the `engine.ts` codemod's `--hand-docs` pass: 10 hand modules, 858 docblock lines. With the first slice, every private hand-series stub that had a docblock now keeps it beside its body; public and protected docblocks stay on the class. `engine.ts` goes from 12,618 to 11,752 lines.
- The liveness ledger's `query.groupBy` row now cites `engine-delete-and-aggregate.ts#aggregate`, where the body lives, because `engine.ts` no longer names the key.
- Nothing to do on upgrade: the exported names, the public `.d.ts` surface and what every method does are the same.
47 changes: 46 additions & 1 deletion packages/objectql/src/engine-count-aggregate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@
* `rejectCredentialAggregation` was not that package's first load. The
* published build bundles both files into one, where the cycle does not
* exist.
*
* [#30053 codemod] A PRIVATE method's docblock has since moved here,
* onto its function (`--hand-docs` of `scripts/codemod-split-engine.mjs`),
* and the class keeps none for it; a public or protected method's docblock
* stays on the class, the contract, and its function keeps the placeholder.
*/

import type { EngineAggregateOptions, EngineCountOptions, QueryAST } from '@objectstack/spec/data';
Expand Down Expand Up @@ -162,7 +167,47 @@ export async function count(
return opCtx.result as number;
}

/** The body of `ObjectQL#rejectCredentialAggregation`; the method's docblock there is its contract. */
/**
* Fail-closed guard (ADR-0100 / #3171 / #7922): refuse to aggregate over a
* field whose value is withheld on the generic read path. Such fields reach
* this guard through **two independent collectors**, and it needs both:
*
* - {@link collectCredentialFields} — keyed by field TYPE (`secret` /
* `password`). A GROUP BY / MIN / MAX / array_agg over such a column would
* surface the stored `secret:<id>` ref or the password value.
* - {@link collectInternalReadFields} — keyed by the `internal: true` FLAG
* (#7728). ADR-0100's third credential channel is a one-way hash living in
* an ordinary `text` column, which no type-keyed collector can ever reach;
* the flag is that channel's opt-in declaration. Without this half the
* guard had the same type-vs-flag blind spot #7728 fixed on the read path:
* a flagged column was omitted from `find`/`findOne` yet freely groupable
* here, so the flag's promise ("never returned on the generic data path")
* stopped at the edge of `aggregate()`.
*
* Post-hoc masking is not available on this path — the value is already a
* group key by the time there is a row, and masking group keys corrupts the
* result. So we reject instead.
*
* Neither collector carries a `managedBy` exemption, so the union does not
* acquire one, deliberately. Read-masking exempts `password` on better-auth
* objects so login reads still see the stored value; *aggregating* a
* credential is never legitimate, least of all on an identity table, where it
* is an inference oracle over hashes.
*
* Only the two output-bearing positions on `EngineAggregateOptions` carry
* field names: `aggregations[].field` (skip COUNT(*) — undefined or '*') and
* `groupBy[]` (a string, or a `{ field }` bucket object).
*
* The refusal carries the ADR-0112 envelope — `INVALID_FIELD` / 400, located
* at the object and the field(s) named. It used to be a bare `Error` with no
* `code` or `status`, so a 400-class author refusal reached the data door
* 500-shaped (`INTERNAL_ERROR`, an undeclared fault). The code matches the
* analytics door's own refusal of an `internal: true` member and the engine's
* structured-JSON group-by refusal: the verdict is about the NAME being a
* field that is never returned on a generic data exit, which `INVALID_FIELD`
* is the catalog member for. The envelope now binds the `secret` / `password`
* case too, which shared the bare `Error`.
*/
export function rejectCredentialAggregation(
this: Pick<CountAggregateHost, '_registry'>,
object: string,
Expand Down
159 changes: 153 additions & 6 deletions packages/objectql/src/engine-driver-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@
* This module imports the engine only as a TYPE, so there is no module cycle
* at run time, and nothing re-exports it, so the published surface does not
* move.
*
* [#30053 codemod] A PRIVATE method's docblock has since moved here,
* onto its function (`--hand-docs` of `scripts/codemod-split-engine.mjs`),
* and the class keeps none for it; a public or protected method's docblock
* stays on the class, the contract, and its function keeps the placeholder.
*/

import { DriverOptionsSchema, isTenancyDisabled } from '@objectstack/spec/data';
Expand Down Expand Up @@ -189,7 +194,96 @@ export function mergeDriverOptionsIntoHookView(
return addsNothing ? callerBag : { ...bag, ...driverOptions };
}

/** The body of `ObjectQL#buildDriverOptions`; the method's docblock there is its contract. */
/**
* Build the DriverOptions blob passed to every IDataDriver call.
*
* # The tenant read scope is the wall's verdict (ADR-0131 D8)
*
* The driver's native tenant scope — `tenantId` (equality) and `tenantIds`
* (the `group` union), which `applyTenantScope` compiles — is the Layer 0
* verdict projected onto `DriverOptions`, never a second derivation beside
* it. One function decides ({@link computeTenantLayer0Verdict},
* `@objectstack/core`), and the engine reads it from one of two places:
*
* - **the verdict the wall RECORDED on this operation**
* ({@link OperationContext.tenantLayer0Verdict}, passed in as
* `scope.recordedVerdict`) — present where the security layer composed its
* wall onto the predicate: a read, or a predicate write. It already carries
* what only that layer can know (the exempt `PLATFORM_ADMIN` on a
* posture-permitting object, the verified platform owner's read, the
* delegator's intersection), so the driver reaches exactly the rows Layer 0
* admits;
* - **otherwise, the same function over the engine's own facts** — the
* posture in force ({@link resolveEnginePosture}), the context's active
* organization and access set, the object's tenancy — with no admin
* exemption, since the engine holds no permission sets. That covers a
* system context, a by-id write, and a composition with no security layer
* mounted.
*
* Projected: `none` (the `single` posture; a non-tenant, tenancy-disabled or
* federated object; an exempt platform admin) → no scope; `organization` →
* `tenantId`; `organizations` → `tenantIds`, with the active organization as
* `tenantId` (the driver keys its scope on `tenantId` being present); `deny`
* → the active organization if there is one — the narrowest scope a driver
* can express, and where the wall ran its deny sentinel is already in the
* predicate. Under `single` the driver therefore no longer narrows a read to
* the active organization: the posture is one tenant, and isolation inside it
* was never the driver's to add (ADR-0131 D11).
*
* # The write target is not a read scope
*
* An insert (`scope.writeTarget`) keeps the active organization as
* `tenantId` in EVERY posture — `injectTenantOnInsert` stamps the row with
* it, the counter scopes by it, and an upsert's identity probe refuses a
* merge into another organization's row by it — and the `group` membership
* set beside it, unchanged.
*
* # Withheld for two object postures
*
* 1. **`tenancy.enabled: false`** (ADR-0066 platform-global posture, e.g.
* `sys_license`): stamping the caller's active-org tenantId there would
* org-scope a global catalog at the driver, and its NULL-org rows would
* vanish for authenticated org-context reads while anonymous reads still
* see them (#3249).
*
* 2. **`external != null`** — a federated object (ADR-0015), whose schema
* is owned by the REMOTE database (#7738). The driver turns `tenantId`
* into `organization_id = :tenant`, and
* against a remote table that carries no such column that is a SQL error
* on Postgres/MySQL — or worse on SQLite, whose quoted-identifier
* fallback reinterprets the unresolvable identifier as the string
* literal `'organization_id'`, makes both disjuncts constant-false, and
* answers **0 rows with HTTP 200**: a correctly-bound external object
* silently reads empty.
*
* Note the reason is NOT "the remote happens to lack the column". The
* column the driver detects is the platform's OWN: `applySystemFields`
* (`resolveInjectedSystemColumns`) injects `organization_id` into every
* object it registers, with no `external` branch, and
* `SqlDriver.registerExternalObject` is DDL-free by design and runs no
* introspection — so it computes the tenant column from the platform's
* field set, never from the remote's. On a federated object
* `organization_id`'s presence is therefore always the injection and
* never evidence about the remote, which leaves the engine no ground on
* which to scope by it. Tenant isolation for federated data belongs to
* the remote and to the layers above (RBAC/RLS, the datasource binding),
* not to a predicate the platform guesses onto someone else's table.
*
* # Declared keys only
*
* The result holds the keys `DriverOptionsSchema` declares and nothing else
* (`pickDeclaredDriverOptions`, `engine-driver-options.ts`). `base` is
* usually a caller's whole option bag — the query, the write-observability
* callbacks and the other engine-level keys ride in it — and only its
* declared keys are kept: an explicitly-passed `base.tenantId` /
* `base.transaction` / `base.bypassTenantAudit` is deliberate caller intent
* and still wins, under every verdict; everything else in the bag is the
* engine's, never a driver's. That is why a verb hands its driver THIS
* object and keeps `input.options` as the hooks' additive view
* (`mergeDriverOptionsIntoHookView`). System / isSystem callers may still
* cross tenants by clearing `tenantId` themselves on the resulting object;
* this helper does not mask the system path.
*/
export function buildDriverOptions(
this: Pick<DriverOptionsHost, 'isClosedUnitHandle' | 'txStore' | 'transactionCoversDriverFor' | '_registry' | 'driverWriteTarget' | 'driverReadScope' | 'wallExemptedOperation'>,
object: string,
Expand Down Expand Up @@ -315,7 +409,16 @@ export function buildDriverOptions(
return opts;
}

/** The body of `ObjectQL#driverReadScope`; the method's docblock there is its contract. */
/**
* [ADR-0131 D8] The driver's native read scope for one operation — the Layer
* 0 verdict projected onto `DriverOptions` (see {@link buildDriverOptions}).
* `undefined` is "no scope"; the caller has already excluded a
* tenancy-disabled or federated object.
*
* A recorded verdict that does not parse as `TenantLayer0VerdictSchema`
* reads as absent — never as an organization — and the engine then decides
* over its own facts, which carry no admin exemption: fail toward the wall.
*/
export function driverReadScope(
this: Pick<DriverOptionsHost, 'engineLayer0Verdict'>,
execCtx: ExecutionContext | undefined,
Expand All @@ -339,7 +442,11 @@ export function driverReadScope(
}
}

/** The body of `ObjectQL#engineLayer0Verdict`; the method's docblock there is its contract. */
/**
* The Layer 0 verdict over the engine's OWN facts — what {@link driverReadScope}
* falls back to where no wall was recorded. The one ladder,
* `computeTenantLayer0Verdict`, with the exemption's facts withheld.
*/
export function engineLayer0Verdict(
this: Pick<DriverOptionsHost, 'resolveEnginePosture'>,
execCtx: ExecutionContext | undefined,
Expand All @@ -362,7 +469,22 @@ export function engineLayer0Verdict(
});
}

/** The body of `ObjectQL#wallExemptedOperation`; the method's docblock there is its contract. */
/**
* [ADR-0131 D8 · 8c-1] Did the wall's EXEMPTION lift this operation's scope?
* True when the recorded verdict is `none` while the same ladder over the
* engine's own facts — every rung but the exemption — would have walled it.
* That is plugin-security's own `crossedByPlatformAdmin` test (the ladder
* asked a second time with only the exemption withheld), with the engine's
* existing fallback ({@link engineLayer0Verdict}, what an unrecorded
* operation is scoped by) as the second asking: no rung is computed beside
* the one ladder, and a `single` posture answers `none` both ways, so it is
* no crossing. The #12974 owner bypass records `none` on READS only, where
* the declaration is inert.
*
* Absence is not exemption: a by-id write, a system context or a stack with
* no security layer records nothing, and answers false. A recorded value
* that does not parse answers false too — junk never lifts anything.
*/
export function wallExemptedOperation(
this: Pick<DriverOptionsHost, 'engineLayer0Verdict'>,
execCtx: ExecutionContext | undefined,
Expand All @@ -378,7 +500,12 @@ export function wallExemptedOperation(
return this.engineLayer0Verdict(execCtx, activeOrganization).kind !== 'none';
}

/** The body of `ObjectQL#driverWriteTarget`; the method's docblock there is its contract. */
/**
* The write target of an insert or upsert: the active organization, in every
* posture, with the `group` membership set beside it — the scope the
* driver's insert readback and upsert identity probe narrow by (ADR-0105 D5:
* the active organization is where a write lands).
*/
export function driverWriteTarget(
this: Pick<DriverOptionsHost, 'resolveEnginePosture'>,
execCtx: ExecutionContext | undefined,
Expand All @@ -389,7 +516,27 @@ export function driverWriteTarget(
return { tenantId: execCtx.tenantId as string, ...(tenantIds ? { tenantIds: [...tenantIds] } : {}) };
}

/** The body of `ObjectQL#resolveEnginePosture`; the method's docblock there is its contract. */
/**
* [#8844] The tenancy posture IN FORCE for this engine, by the same two-step
* `app-plugin.organizationWallActive` uses and for the same reason: the
* injected provider is the `tenancy` service's answer (what is actually
* ENFORCED, degraded boots included), and the env resolution is what the
* operator ASKED for — the best fact available to a lean embedding that
* mounts no SecurityPlugin.
*
* ⛔ Never `resolveMultiOrgEnabled()`: ADR-0105 D1 demoted that boolean to a
* back-compat INPUT of `resolveTenancyPosture()`, so it reads `false` on a
* deployment configured the documented way and would put a fully walled
* install on the single-tenant branch below — the third recurrence of that
* shape is what `app-plugin` documents.
*
* Read live per call, never cached: nothing here may freeze a verdict a later
* boot phase can still change. `resolveTenancyPosture()` THROWS on an
* unrecognized `OS_TENANCY_POSTURE` (deliberately — a typo'd posture must not
* silently remove the wall); a write is not the place that refusal belongs,
* and an unreadable posture is exactly the case where guessing `single` would
* be worst, so it degrades to the walled reading and the write is refused.
*/
export function resolveEnginePosture(
this: Pick<DriverOptionsHost, 'tenancyPostureProvider'>,
): TenancyPosture {
Expand Down
Loading
Loading