@objectstack/core 17.0.0-rc.5 → 17.0.0-rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -6,7 +6,7 @@ import { ObjectLogger } from './logger.cjs';
6
6
  export { createLogger } from './logger.cjs';
7
7
  import * as QA from '@objectstack/spec/qa';
8
8
  import { KeyObject } from 'node:crypto';
9
- import { PluginCapability, PluginPermissions as PluginPermissions$1, PluginPermissionSet, ResourceType, PermissionAction, PluginPermission, SandboxConfig, KernelSecurityScanResult, KernelSecurityVulnerability, PluginHealthCheck, PluginHealthStatus as PluginHealthStatus$1, PluginHealthReport, HotReloadConfig, VersionConstraint, DependencyConflict, SemanticVersion, CompatibilityLevel } from '@objectstack/spec/kernel';
9
+ import { PluginCapability, PluginPermissions as PluginPermissions$1, PluginPermissionSet, ResourceType, PermissionAction, PluginPermission, SandboxConfig, KernelSecurityScanResult, KernelSecurityVulnerability, ExecutionContext, PluginHealthCheckParsed, PluginHealthStatus as PluginHealthStatus$1, PluginHealthReport, HotReloadConfigParsed, VersionConstraint, DependencyConflict, SemanticVersion, CompatibilityLevel } from '@objectstack/spec/kernel';
10
10
  import { AuthzPosture } from '@objectstack/spec/security';
11
11
  export { nextUtcCalendarDay, utcInstantMs } from '@objectstack/spec/data';
12
12
 
@@ -353,13 +353,18 @@ declare class ObjectKernel {
353
353
  * one bad handler must not amplify into leaked resources and unflushed
354
354
  * writes. Same reasoning, same wording, same `Hook handler failed:
355
355
  * kernel:shutdown` log line as `LiteKernel`'s dispatch site, which reaches
356
- * the shared isolating dispatcher `ObjectKernelBase.triggerHook` (#5257).
356
+ * the isolating dispatcher through `ObjectKernelBase.triggerHook` (#5257).
357
357
  *
358
- * `ObjectKernel` cannot call that dispatcher: it does not extend
358
+ * Until #5282 "same wording" was literally that the loop was typed out a
359
+ * second time here, because `ObjectKernel` does not extend
359
360
  * `ObjectKernelBase` (only `LiteKernel` does) and owns its own `hooks` map,
360
- * so the semantics are mirrored here rather than shared. One hook name
361
- * meaning two opposite things across the two kernels is exactly the bug
362
- * #5170/#5257 closed, so the pin for this one lives on both sides too.
361
+ * so the base's `protected triggerHook` is out of reach. The loop now lives
362
+ * in {@link dispatchHookIsolating}, which BOTH sides call: the storage is
363
+ * still two maps (deliberately unifying it was out of #5282's scope), but
364
+ * "isolating" is one implementation, so it can no longer drift on one
365
+ * kernel while the other keeps the old shape. That drift is exactly the bug
366
+ * #5170 / #5257 / #5274 each closed one hook at a time, and the paired-pin
367
+ * gate (`scripts/check-kernel-hook-pairs.mjs`) covers the residue.
363
368
  */
364
369
  private triggerShutdownHookIsolating;
365
370
  private performShutdown;
@@ -622,6 +627,11 @@ declare abstract class ObjectKernelBase {
622
627
  * (`kernel:ready`, `kernel:bootstrapped`, `kernel:listening`) use
623
628
  * {@link triggerHookOrThrow} (#5170, #5257).
624
629
  *
630
+ * The loop itself lives in {@link dispatchHookIsolating} — one
631
+ * implementation shared with `ObjectKernel`'s own `kernel:shutdown`
632
+ * dispatch, which cannot inherit this method (`ObjectKernel` does not
633
+ * extend this class) and used to hand-mirror it (#5282).
634
+ *
625
635
  * @param name - Hook name
626
636
  * @param args - Arguments to pass to handlers
627
637
  */
@@ -661,6 +671,10 @@ declare abstract class ObjectKernelBase {
661
671
  * default — and it is the reason this dispatcher is chosen per hook rather
662
672
  * than swapped in wholesale.
663
673
  *
674
+ * The loop itself lives in {@link dispatchHookPropagating} — the same
675
+ * function `PluginContext.trigger` runs on both kernels, so "propagating"
676
+ * means one thing repo-wide (#5282).
677
+ *
664
678
  * @param name - Hook name
665
679
  * @param args - Arguments to pass to handlers
666
680
  */
@@ -1831,6 +1845,275 @@ declare function resolveLocalizationContext(input: ResolveLocalizationInput): Pr
1831
1845
  currency?: string;
1832
1846
  }>;
1833
1847
 
1848
+ /**
1849
+ * ADR-0069 — authentication-policy session gate.
1850
+ *
1851
+ * Some auth policies (password expiry, enforced MFA) must block an
1852
+ * authenticated user from PROTECTED RESOURCES until they remediate, while
1853
+ * still letting them reach the auth endpoints (change-password, two-factor
1854
+ * enrollment, sign-out) and a few UI-bootstrap reads.
1855
+ *
1856
+ * The posture is computed ONCE, in the auth `customSession` enrichment, and
1857
+ * attached to the session user as `user.authGate = { code, message }`. The
1858
+ * transport seams (REST middleware, dispatcher) then call
1859
+ * {@link evaluateAuthGate} to decide whether THIS request is blocked. Keeping
1860
+ * the allow-list + decision in one pure function means the seams can never
1861
+ * drift on what is blocked.
1862
+ */
1863
+
1864
+ /**
1865
+ * The gate posture, DERIVED from its declaration on `ExecutionContextSchema`
1866
+ * (`packages/spec/src/kernel/execution-context.zod.ts`) rather than restated
1867
+ * here (#7280).
1868
+ *
1869
+ * It was a hand-written interface while the envelope field was undeclared, so
1870
+ * the two could have drifted with nothing to catch it — the exact class of
1871
+ * defect the closed entry field set (#6216) exists to make unrepresentable.
1872
+ * One declaration, one type.
1873
+ */
1874
+ type AuthGate = NonNullable<ExecutionContext['authGate']>;
1875
+ /**
1876
+ * Normalize the `authGate` a better-auth session user carries into the shape
1877
+ * `ExecutionContextSchema` declares — or `null` when there is no gate.
1878
+ *
1879
+ * The session user crosses an external boundary as `any`, so this is where the
1880
+ * declared contract is actually met: a gate naming no string `code` is not a
1881
+ * gate, and a missing/blank `message` is filled with the default rather than
1882
+ * riding onto the envelope (and into a `403` body) as `undefined`. Both
1883
+ * consumers normalize HERE, at the one producer, instead of tolerating a loose
1884
+ * shape downstream: {@link evaluateAuthGate} for the seams that decide per
1885
+ * path, and REST's `computeExecCtx` for the seam that lifts the posture onto
1886
+ * the execution context.
1887
+ */
1888
+ declare function normalizeAuthGate(sessionUser: any): AuthGate | null;
1889
+ /** True when `path` is exempt from the auth gate (auth + remediation + health). */
1890
+ declare function isAuthGateAllowlisted(rawPath: string | undefined | null): boolean;
1891
+ /**
1892
+ * Returns the active gate when `sessionUser` carries an `authGate` AND `path`
1893
+ * is not allow-listed; otherwise null. Anonymous users (no `authGate`) and
1894
+ * allow-listed paths always pass.
1895
+ */
1896
+ declare function evaluateAuthGate(sessionUser: any, path: string): AuthGate | null;
1897
+
1898
+ /**
1899
+ * assembleExecutionContext — the SINGLE assembly of an inbound request's
1900
+ * {@link ExecutionContext}, shared by every transport entry point.
1901
+ *
1902
+ * `resolveAuthzContext` (next door) already made AUTHORIZATION resolution
1903
+ * single-sourced. The step AFTER it — turning the resolved
1904
+ * {@link ResolvedAuthzContext} into the `ExecutionContext` envelope that
1905
+ * reaches enforcement — stayed hand-written per transport, and that duplication
1906
+ * produced a measured defect family:
1907
+ *
1908
+ * - **#6071 — field drift.** The REST copy never set `principalKind`, so every
1909
+ * enforcement judgment reading it (explain's guest⇒EXTERNAL floor, the
1910
+ * security plugin's agent baseline, the perf-disclosure gate) was silently
1911
+ * never-true on that face.
1912
+ * - **#6206 / #6551 — dropped fields.** The share-link copies omitted
1913
+ * `accessible_org_ids`, and the `group` posture's Layer 0 wall reads it
1914
+ * directly: real 403s for callers who should have been let through. Both
1915
+ * surfaces were since converted to pass the WHOLE envelope through.
1916
+ *
1917
+ * Both defects are the same shape: a field exists on `ExecutionContext`, one
1918
+ * copy carries it, another silently does not. This module makes that shape
1919
+ * unrepresentable by CLOSING the field set with a type
1920
+ * ({@link ExecutionContextEntryFields}) — every field a transport entry point
1921
+ * decides must be decided HERE, explicitly, and a new `ExecutionContext` field
1922
+ * fails to compile until it is either assembled or listed as
1923
+ * non-entry-resolved.
1924
+ *
1925
+ * ## Two named entries — the anonymous face is genuinely divergent (#6216)
1926
+ *
1927
+ * The maintainer ruling of 2026-08-08 on #6216 (Option A) settled the one
1928
+ * question that blocked convergence: what an anonymous request yields.
1929
+ *
1930
+ * - {@link assembleExecutionContext} — the DEFAULT, fail-closed entry. No
1931
+ * resolved principal → `undefined`, and the surface answers 401. This is the
1932
+ * REST face's contract, unchanged.
1933
+ * - {@link assembleExecutionContextOrGuest} — the EXPLICIT guest entry. No
1934
+ * resolved principal → a first-class guest envelope
1935
+ * (`principalKind: 'guest'`, `positions: ['guest']`), which the runtime /
1936
+ * MCP dispatcher has always produced and whose consumers are live
1937
+ * (`plugin-security/explain-engine.ts`: guest ⇒ `EXTERNAL` posture). A
1938
+ * surface adopts this entry ONLY when its product semantics serve anonymous
1939
+ * principals.
1940
+ *
1941
+ * Neither surface's runtime behaviour changes. What changes is that the
1942
+ * divergence is now NAMED API rather than drift — and the same is true of the
1943
+ * per-face values below (`accessToken`, `oauth`, `localization`): they are
1944
+ * REQUIRED inputs, so a face cannot silently omit one, and what a face chooses
1945
+ * to withhold it withholds on the record.
1946
+ *
1947
+ * Options B (guest everywhere — turns REST's anonymous 401s into authz-shaped
1948
+ * denials and makes anonymous-serving the DEFAULT posture of a new surface) and
1949
+ * C (no-ctx everywhere — deletes the guest principal, the `guest` position and
1950
+ * explain's `EXTERNAL` floor) were both considered and rejected: each breaks a
1951
+ * live consumer side.
1952
+ */
1953
+
1954
+ /**
1955
+ * `ExecutionContext` fields a transport ENTRY POINT does not resolve — they are
1956
+ * per-operation flags, engine internals, or attribution supplied further down
1957
+ * the stack. Listing one here is a deliberate, reviewable statement that no
1958
+ * request-identity resolution produces it; everything NOT listed is part of the
1959
+ * closed entry set below and must be assembled.
1960
+ *
1961
+ * - `actor` / `attributedUserId` — audit attribution, set by the host or the
1962
+ * hook layer (ADR-0014 D2, #4586), never by identity resolution.
1963
+ * - `rlsMembership` — engine-side RLS scoping cache.
1964
+ * - `transaction` / `traceId` — per-operation handles.
1965
+ * - `flowRunId`, `skipTriggers`, `skipAutomations`, `seedReplay`,
1966
+ * `skipStateMachine`, `preserveAudit` — per-write behaviour flags,
1967
+ * server-constructed at the call site.
1968
+ */
1969
+ type NonEntryExecutionContextField = 'actor' | 'attributedUserId' | 'rlsMembership' | 'transaction' | 'traceId' | 'flowRunId' | 'skipTriggers' | 'skipAutomations' | 'seedReplay' | 'skipStateMachine' | 'preserveAudit';
1970
+ /**
1971
+ * The CLOSED field set every transport entry point must decide. Derived from
1972
+ * `ExecutionContext` itself, so adding a field to `ExecutionContextSchema`
1973
+ * widens this union automatically — and the assembly below stops compiling
1974
+ * until the new field is either assembled or declared non-entry-resolved.
1975
+ */
1976
+ type EntryExecutionContextField = Exclude<keyof ExecutionContext, NonEntryExecutionContextField>;
1977
+ /**
1978
+ * One value per closed field. Every key is REQUIRED (`-?`) while the VALUE may
1979
+ * be `undefined` — the decision may not be omitted, only made explicitly. This
1980
+ * is the type that makes the #6071 drift class unrepresentable.
1981
+ */
1982
+ type ExecutionContextEntryFields = {
1983
+ [K in EntryExecutionContextField]-?: ExecutionContext[K];
1984
+ };
1985
+ /**
1986
+ * Emission order of the assembled envelope, and a second, independent
1987
+ * exhaustiveness bite: `satisfies` rejects a stale name, and
1988
+ * `_ENTRY_FIELDS_EXHAUSTIVE` below rejects a missing one.
1989
+ */
1990
+ declare const ENTRY_EXECUTION_CONTEXT_FIELDS: readonly ["positions", "permissions", "systemPermissions", "isSystem", "principalKind", "onBehalfOf", "audience", "userId", "tenantId", "email", "accessToken", "tabPermissions", "posture", "authGate", "org_user_ids", "accessible_org_ids", "oauthScopes", "timezone", "locale", "currency"];
1991
+ /**
1992
+ * OAuth 2.1 access-token provenance. Reaches the assembler from the `/mcp`
1993
+ * dispatch door ALONE (`acceptOAuthAccessToken`) — OAuth bearers carry coarse
1994
+ * tool-family scopes enforced at MCP tool dispatch, so honouring them on
1995
+ * another surface would bypass that scope model entirely.
1996
+ */
1997
+ interface OAuthTokenProvenance {
1998
+ /** The human `sub` the token was issued for. */
1999
+ userId: string;
2000
+ /** Granted scopes, surfaced on the envelope so MCP can narrow tool families. */
2001
+ scopes: string[];
2002
+ /**
2003
+ * The authorized client (`azp`). Present ⇒ this is an AI AGENT acting on
2004
+ * behalf of the human `userId`; absent ⇒ the token names no client and the
2005
+ * principal stays human (the scopes are still surfaced).
2006
+ */
2007
+ clientId?: string;
2008
+ /**
2009
+ * The agent's OWN permission CEILING, derived from {@link scopes} by the door
2010
+ * that speaks the OAuth scope vocabulary
2011
+ * (`scopesToAgentPermissionSets`, `@objectstack/spec/ai`).
2012
+ *
2013
+ * Interpreted THERE and not here on purpose: the scope vocabulary is
2014
+ * MCP-domain knowledge and `@objectstack/core` is the microkernel — it should
2015
+ * not acquire a dependency on the AI subdomain (concretely, every package
2016
+ * whose test config aliases `@objectstack/core` to its source would then have
2017
+ * to resolve `@objectstack/spec/ai` too, down to `driver-memory`). What the
2018
+ * ceiling REPLACES on the envelope is decided below, once, for every face —
2019
+ * and that is the part that drifted.
2020
+ */
2021
+ scopePermissions: string[];
2022
+ /**
2023
+ * Whether the token carries the user's consent to let this agent invoke
2024
+ * actions on their behalf — the `actions:execute` scope
2025
+ * (`MCP_OAUTH_SCOPE_ACTIONS`), evaluated at the same door for the same
2026
+ * reason.
2027
+ */
2028
+ delegatesActions: boolean;
2029
+ }
2030
+ /** Reference localization for an authenticated principal (@see resolveLocalizationContext). */
2031
+ interface EntryLocalization {
2032
+ timezone?: string;
2033
+ locale?: string;
2034
+ currency?: string;
2035
+ }
2036
+ /**
2037
+ * Everything the shared assembly needs. Every key is REQUIRED so a face cannot
2038
+ * silently omit one — a face that has no value for an input passes `undefined`
2039
+ * on the record, which is what turns the remaining divergences into named API.
2040
+ */
2041
+ interface ExecutionContextAssemblyInput {
2042
+ /** The shared authorization envelope (@see resolveAuthzContext). */
2043
+ authz: ResolvedAuthzContext;
2044
+ /**
2045
+ * OAuth access-token provenance, or `undefined` on a face that does not
2046
+ * accept one. Only the `/mcp` dispatch door passes a value; REST passes
2047
+ * `undefined` — which is why `principalKind: 'agent'`, `onBehalfOf` and
2048
+ * `oauthScopes` are not representable there.
2049
+ */
2050
+ oauth: OAuthTokenProvenance | undefined;
2051
+ /**
2052
+ * Resolved reference localization, or `undefined` when the face resolved
2053
+ * none (anonymous requests have no scope to resolve against).
2054
+ */
2055
+ localization: EntryLocalization | undefined;
2056
+ /**
2057
+ * The request's OWN locale preference (`Accept-Language`, `?locale`, …),
2058
+ * which wins over the workspace default; `undefined` when the caller
2059
+ * expresses none. Each face extracts it its own way — the PRECEDENCE lives
2060
+ * here so the two cannot disagree about it (#3957).
2061
+ */
2062
+ requestLocale: string | undefined;
2063
+ /**
2064
+ * The session bearer to carry on the envelope, surfaced to hooks as
2065
+ * `session.accessToken` (`objectql/engine.ts` `buildSession`,
2066
+ * `spec/data/hook.zod.ts`).
2067
+ *
2068
+ * A NAMED per-face divergence, preserved deliberately (#6216): the runtime /
2069
+ * MCP dispatcher passes `authz.accessToken`; the REST face has never carried
2070
+ * it and passes `undefined`, because widening a published hook surface to
2071
+ * expose the session token on a second transport is a product decision, not a
2072
+ * refactor. Being a required input, the choice is on the record at each face
2073
+ * instead of being an omission nobody can see.
2074
+ */
2075
+ accessToken: string | undefined;
2076
+ /**
2077
+ * [ADR-0069] The AUTHENTICATION-policy gate posture resolved for this
2078
+ * request's session (expired password / enforced MFA), or `undefined` when
2079
+ * the face resolves none — normalize a session user through
2080
+ * `normalizeAuthGate` rather than copying its `authGate` verbatim.
2081
+ *
2082
+ * A NAMED per-face divergence, on the same footing as {@link accessToken}
2083
+ * (#7280):
2084
+ *
2085
+ * - the **REST** face lifts it onto the envelope, because that is where its
2086
+ * consumer reads it (`RestServer.enforceAuth` → `403 { code, message }`);
2087
+ * - the **runtime / MCP dispatcher** passes `undefined`, because it enforces
2088
+ * the same ADR-0069 gate at its OWN seam (`HttpDispatcher.enforceAuthGate`
2089
+ * re-reads the session and calls `evaluateAuthGate` there) and never reads
2090
+ * `context.authGate` — carrying it would be a second, unread copy.
2091
+ *
2092
+ * Until #7280 declared it, this posture reached the envelope through an
2093
+ * `as any` spread AFTER assembly, which put it outside this closed set
2094
+ * entirely — the blind spot the set exists to remove.
2095
+ */
2096
+ authGate: AuthGate | undefined;
2097
+ }
2098
+ /**
2099
+ * The DEFAULT, fail-closed entry (#6216 Option A). An unauthenticated request
2100
+ * yields NO context — the surface answers 401. Every surface uses this one
2101
+ * unless serving anonymous principals is part of its product semantics.
2102
+ */
2103
+ declare function assembleExecutionContext(input: ExecutionContextAssemblyInput): ExecutionContext | undefined;
2104
+ /**
2105
+ * The EXPLICIT guest entry (#6216 Option A). An unauthenticated request becomes
2106
+ * a first-class guest principal — `principalKind: 'guest'`, `positions:
2107
+ * ['guest']` — which enforcement consumers read today
2108
+ * (`plugin-security/explain-engine.ts`: guest ⇒ `EXTERNAL` posture).
2109
+ *
2110
+ * Adopt this ONLY on a surface that genuinely serves anonymous principals: the
2111
+ * built-in `guest` position is the declared vocabulary for "what anonymous may
2112
+ * do", and handing a guest envelope to a surface that previously answered 401
2113
+ * converts an authentication failure into an authorization evaluation.
2114
+ */
2115
+ declare function assembleExecutionContextOrGuest(input: ExecutionContextAssemblyInput): ExecutionContext;
2116
+
1834
2117
  /**
1835
2118
  * ── The monotonic posture ladder (ADR-0095 D2/D3) ───────────────────────────
1836
2119
  *
@@ -1936,36 +2219,6 @@ interface LadderPrincipal {
1936
2219
  */
1937
2220
  declare function postureVisibleRows(posture: AuthzPosture, rows: readonly LadderRow[], principal: LadderPrincipal): LadderRow[];
1938
2221
 
1939
- /**
1940
- * ADR-0069 — authentication-policy session gate.
1941
- *
1942
- * Some auth policies (password expiry, enforced MFA) must block an
1943
- * authenticated user from PROTECTED RESOURCES until they remediate, while
1944
- * still letting them reach the auth endpoints (change-password, two-factor
1945
- * enrollment, sign-out) and a few UI-bootstrap reads.
1946
- *
1947
- * The posture is computed ONCE, in the auth `customSession` enrichment, and
1948
- * attached to the session user as `user.authGate = { code, message }`. The
1949
- * transport seams (REST middleware, dispatcher) then call
1950
- * {@link evaluateAuthGate} to decide whether THIS request is blocked. Keeping
1951
- * the allow-list + decision in one pure function means the seams can never
1952
- * drift on what is blocked.
1953
- */
1954
- interface AuthGate {
1955
- /** Stable machine code, e.g. `PASSWORD_EXPIRED` / `MFA_REQUIRED`. */
1956
- code: string;
1957
- /** Human-facing message. */
1958
- message: string;
1959
- }
1960
- /** True when `path` is exempt from the auth gate (auth + remediation + health). */
1961
- declare function isAuthGateAllowlisted(rawPath: string | undefined | null): boolean;
1962
- /**
1963
- * Returns the active gate when `sessionUser` carries an `authGate` AND `path`
1964
- * is not allow-listed; otherwise null. Anonymous users (no `authGate`) and
1965
- * allow-listed paths always pass.
1966
- */
1967
- declare function evaluateAuthGate(sessionUser: any, path: string): AuthGate | null;
1968
-
1969
2222
  /** HTTP status every seam returns for an anonymous-denied request. */
1970
2223
  declare const ANONYMOUS_DENY_STATUS: 401;
1971
2224
  /** Stable machine code (mirrors the REST `enforceAuth` seam). ADR-0112: SCREAMING, a `StandardErrorCode` member. */
@@ -2059,6 +2312,117 @@ declare function isGrantActive(row: GrantValidityWindow | null | undefined, nowM
2059
2312
  */
2060
2313
  declare function isGrantExpired(row: GrantValidityWindow | null | undefined, nowMs: number): boolean;
2061
2314
 
2315
+ /**
2316
+ * [#7284] The `__` operation-private-key convention — one owner, on the
2317
+ * CONSUMER side.
2318
+ *
2319
+ * `assemble-execution-context.ts` next door is the single place an
2320
+ * `ExecutionContext` is BUILT at a transport entry point (#6216). This file is
2321
+ * its counterpart at the other end: the single place one is stripped back down
2322
+ * before being forwarded to a question it was not resolved for.
2323
+ *
2324
+ * ## What a `__` key is
2325
+ *
2326
+ * plugin-security's middleware STAMPS keys onto the operation context, resolved
2327
+ * for the object of the operation IN FLIGHT. They are middleware-private
2328
+ * vocabulary, not fields of `ExecutionContext`, and every one of them is read as
2329
+ * a WIDENING input by whoever consumes it:
2330
+ *
2331
+ * - the ADR-0057 D1 access DEPTH the sharing owner-match expands to —
2332
+ * `__readScope` / `__writeScope`, plus the ADR-0090 D10 delegator halves
2333
+ * `__delegatorReadScope` / `__delegatorWriteScope`, stamped in place by
2334
+ * `security-plugin.ts` (`sc.__readScope = …`);
2335
+ * - the engine's internal privilege markers on the same channel —
2336
+ * `__expandRead` waives the object-level CRUD check for a lookup expansion,
2337
+ * `__referentialFieldClear` the referential-clear write.
2338
+ *
2339
+ * plugin-security is the PRODUCER of that vocabulary and would be the most
2340
+ * honest owner of the rule for consuming it, but none of the three consumers
2341
+ * depends on it and a string-prefix filter does not justify three new dependency
2342
+ * edges onto a plugin (the trade the filing card priced, #7284). `@objectstack/
2343
+ * spec` is fenced off by Prime Directive #2. `@objectstack/core` is the only
2344
+ * candidate every consumer already depends on, so the rule lives here and the
2345
+ * producer stays free of reverse edges.
2346
+ *
2347
+ * ## Why a consumer must drop them
2348
+ *
2349
+ * A caller's envelope carries a depth resolved for the object the middleware
2350
+ * last saw. A consumer that forwards that envelope to ask about a DIFFERENT
2351
+ * object applies one object's widening to another object's question — the exact
2352
+ * stale-scope leak `resolveWriteScopeForSharing` was extracted to prevent ("a
2353
+ * stale value can never leak in through a spread", `security-plugin.ts`).
2354
+ *
2355
+ * The leak is not hypothetical and does not require the consumer to be careless:
2356
+ * plugin-security only OVERWRITES `__readScope` when it actually resolves
2357
+ * permission sets for the new object (`if (permissionSets.length > 0)`), so a
2358
+ * stale depth SURVIVES into a question it was never resolved for whenever that
2359
+ * branch does not fire. A REST request that touched another object before
2360
+ * reaching, say, `/reports/:id/run` hands over an envelope the middleware has
2361
+ * already written into.
2362
+ *
2363
+ * Dropping is safe in the one direction that matters: the middleware re-stamps
2364
+ * the depth for THIS object when it resolves any set, so the only thing dropping
2365
+ * can do is leave the sharing owner-match at its narrowest (`own`) — the safe
2366
+ * direction.
2367
+ *
2368
+ * ## Why by PREFIX and never by a name list
2369
+ *
2370
+ * The `__` convention is what marks a key as belonging to the operation in
2371
+ * flight. A hand-maintained list of the six names above would go stale the day
2372
+ * the middleware stamps a seventh, and it would go stale SILENTLY — a forwarded
2373
+ * key nobody remembered to add reads exactly like a key that was meant to be
2374
+ * forwarded. The prefix is the contract; the names are its current membership.
2375
+ *
2376
+ * ⛔ The corollary, for whoever changes the middleware: a key that is
2377
+ * operation-private MUST carry the `__` prefix. Stamping one without it makes it
2378
+ * invisible to every consumer at once, and there is no compiler error.
2379
+ *
2380
+ * ## Known consumers
2381
+ *
2382
+ * `plugin-audit` (`comment-access-hooks.ts`, #7141), `service-storage`
2383
+ * (`attachment-access-hooks.ts`, #7145) and `plugin-reports`
2384
+ * (`report-service.ts`, #7204) — each forwarding a caller envelope to a gate or
2385
+ * a read that asks about a parent/target object rather than about the object the
2386
+ * middleware resolved for. Each of the three grew its own byte-equivalent copy
2387
+ * of this file by hand before #7284 gave the rule a home; `operation-private-
2388
+ * keys.pin.test.ts` is what now catches a fourth.
2389
+ */
2390
+
2391
+ /**
2392
+ * The prefix marking a key as private to the operation plugin-security has in
2393
+ * flight. See this module's header for why the convention is a prefix and not a
2394
+ * list of names.
2395
+ */
2396
+ declare const OPERATION_PRIVATE_KEY_PREFIX = "__";
2397
+ /**
2398
+ * The caller's execution envelope, minus the operation-private keys.
2399
+ *
2400
+ * A FRESH object every time, and that is load-bearing in BOTH directions:
2401
+ *
2402
+ * - outbound — a callee that stamps its own `__writeScope` onto what it
2403
+ * receives (which is exactly what plugin-security does before it calls the
2404
+ * sharing service) can never write back into the operation context the caller
2405
+ * was handed;
2406
+ * - inbound — the engine's middleware stamps a depth for the object it is about
2407
+ * to read onto whatever it is handed, so forwarding a caller's envelope BY
2408
+ * REFERENCE would write that depth back into the request context the route
2409
+ * goes on using.
2410
+ *
2411
+ * ⛔ Never `return exec;` on the "nothing to strip" path. The copy is the point,
2412
+ * not an optimisation to skip when the envelope happens to be clean — the two
2413
+ * hazards above are about the callee's future writes, not about the current
2414
+ * contents.
2415
+ *
2416
+ * Note what this deliberately does NOT do: it strips, it never SYNTHESISES a
2417
+ * depth for the new object. Absent depth leaves the sharing owner-match at its
2418
+ * narrowest (`own`), which is the safe direction and byte-for-byte what the
2419
+ * five-field projections these call sites replaced produced.
2420
+ *
2421
+ * @param exec the caller's envelope, as a bare record
2422
+ * @returns a new envelope carrying only the non-operation-private keys
2423
+ */
2424
+ declare function withoutOperationPrivateKeys(exec: Record<string, unknown>): ExecutionContext;
2425
+
2062
2426
  /**
2063
2427
  * Environment utilities for universal (Node/Browser) compatibility.
2064
2428
  */
@@ -2564,8 +2928,9 @@ declare function resolveFilterTokens<T>(filter: T, ctx?: FilterTokenResolutionCo
2564
2928
  * `current_user.organization_id`).
2565
2929
  *
2566
2930
  * Typed structurally, not as `ExecutionContext`, so both the parsed context
2567
- * (defaults applied) and the pre-parse `ExecutionContextInput` a caller holds
2568
- * mid-pipeline satisfy it. The three fields read here are optional in both.
2931
+ * (`ExecutionContextParsed`, defaults applied) and the pre-parse
2932
+ * `ExecutionContext` a caller holds mid-pipeline satisfy it. The three fields
2933
+ * read here are optional in both.
2569
2934
  */
2570
2935
  declare function filterTokenContextFrom(execCtx: ExecutionContextLike | undefined, now?: Date): FilterTokenResolutionContext;
2571
2936
 
@@ -2787,7 +3152,7 @@ declare class PluginHealthMonitor {
2787
3152
  /**
2788
3153
  * Register a plugin for health monitoring
2789
3154
  */
2790
- registerPlugin(pluginName: string, config: PluginHealthCheck): void;
3155
+ registerPlugin(pluginName: string, config: PluginHealthCheckParsed): void;
2791
3156
  /**
2792
3157
  * Start monitoring a plugin
2793
3158
  */
@@ -2862,7 +3227,7 @@ declare class PluginStateManager {
2862
3227
  /**
2863
3228
  * Save plugin state before reload
2864
3229
  */
2865
- saveState(pluginId: string, version: string, state: Record<string, any>, config: HotReloadConfig): Promise<string>;
3230
+ saveState(pluginId: string, version: string, state: Record<string, any>, config: HotReloadConfigParsed): Promise<string>;
2866
3231
  /**
2867
3232
  * Restore plugin state after reload
2868
3233
  */
@@ -2895,7 +3260,7 @@ declare class HotReloadManager {
2895
3260
  /**
2896
3261
  * Register a plugin for hot reload
2897
3262
  */
2898
- registerPlugin(pluginName: string, config: HotReloadConfig): void;
3263
+ registerPlugin(pluginName: string, config: HotReloadConfigParsed): void;
2899
3264
  /**
2900
3265
  * Start watching for changes (requires file system integration)
2901
3266
  */
@@ -3085,4 +3450,4 @@ declare class NamespaceResolver {
3085
3450
  private suggestAlternative;
3086
3451
  }
3087
3452
 
3088
- export { ANONYMOUS_DENY_BODY, ANONYMOUS_DENY_CODE, ANONYMOUS_DENY_MESSAGE, ANONYMOUS_DENY_STATUS, API_KEY_PREFIX, type AnonymousDenyInput, type ApiKeyPrincipal, type AuthGate, type BucketGranularity, type BulkWriteOptions, type BulkWriteRowResult, CORE_FALLBACK_FACTORIES, type CalendarParts, DependencyResolver, type EngineWithTransaction, type ExecutionContextLike, type FilterTokenResolutionContext, type GeneratedApiKey, type GrantValidityWindow, HotReloadManager, type InterruptedRun, type KernelState, type KeyInput, type LadderPrincipal, type LadderRow, LiteKernel, type MigrationChunk, type MigrationChunkContext, MigrationJournalRefusal, type MigrationPlan, type MigrationPlanProvider, MigrationPlanRegistry, type MigrationPlanStep, type MigrationRunResult, type NamespaceCheckResult, type NamespaceConflict, type NamespaceEntry, NamespaceResolver, ObjectKernel, ObjectKernelBase, type ObjectKernelConfig, ObjectLogger, type OrderablePlugin, POSTURE_INJECTION_RULE, POSTURE_LADDER, POSTURE_RANK, type ParsedSignature, type PermissionCheckResult$1 as PermissionCheckResult, type PermissionGrant, type Plugin, type PluginArtifactVerifyResult, PluginConfigValidator, type PluginContext, PluginHealthMonitor, type PluginHealthStatus, type PluginLoadResult, PluginLoader, type PluginMetadata, type PermissionCheckResult as PluginPermissionCheckResult, PluginPermissionEnforcer, PluginPermissionManager, type PluginPermissions, PluginSandboxRuntime, PluginSecurityScanner, type PluginSignatureConfig, PluginSignatureVerifier, type PluginStartupResult, type PostureEvidence, type PublisherVerifyResult, index as QA, type ResolveAuthzInput, type ResolveLocalizationInput, type ResolveUserAuthzGrantsOptions, type ResolvedAuthzContext, type ResourceUsage, type RetryOptions, type RunMigrationJournalOptions, SIGNATURE_ALG, type SandboxContext, type ScanTarget, SecurePluginContext, type SecurityIssue, SemanticVersionManager, type ServiceFactory, ServiceLifecycle, type ServiceRegistration, type SignatureVerificationResult, UnknownFilterTokenError, UnresolvedFilterTokenError, type UserAuthzGrants, type VersionCompatibility, assertInitServiceRequirements, bucketKeyToCalendarRange, buildPermissionsFromGrants, bulkWrite, calendarPartsInTz, calendarPartsInTzOrUtc, counterSignPayload, createMemoryCache, createMemoryI18n, createMemoryJob, createMemoryMetadata, createMemoryQueue, createPluginConfigValidator, createPluginPermissionEnforcer, deepMerge, defaultIsTransientError, derivePosture, describeInitOrderFault, engineCanRollBack, evaluateAuthGate, extractApiKey, filterTokenContextFrom, findInterruptedRuns, generateApiKey, generateEd25519KeyPair, getEnv, getMemoryUsage, hashApiKey, hashMigrationPlan, isAuthGateAllowlisted, isExpired, isGrantActive, isGrantExpired, isNode, parseScopes, parseSignature, planChunks, postureVisibleRows, readAuthoredTranslationLayer, readRunJournal, resolveApiKeyPrincipal, resolveAuthzContext, resolveFilterToken, resolveFilterTokens, resolveLocale, resolveLocalizationContext, resolvePluginOrder, resolveUserAuthzGrants, resumeMigrationJournal, runMigrationJournal, safeExit, shouldDenyAnonymous, signPayload, validateInitServiceContract, verifyPayload, verifyPlatformSignature, verifyPluginArtifact, verifyPublisherSignature, wireAuthoredTranslationSync, withTransientRetry, zonedDateStartToUtcMs };
3453
+ export { ANONYMOUS_DENY_BODY, ANONYMOUS_DENY_CODE, ANONYMOUS_DENY_MESSAGE, ANONYMOUS_DENY_STATUS, API_KEY_PREFIX, type AnonymousDenyInput, type ApiKeyPrincipal, type AuthGate, type BucketGranularity, type BulkWriteOptions, type BulkWriteRowResult, CORE_FALLBACK_FACTORIES, type CalendarParts, DependencyResolver, ENTRY_EXECUTION_CONTEXT_FIELDS, type EngineWithTransaction, type EntryExecutionContextField, type EntryLocalization, type ExecutionContextAssemblyInput, type ExecutionContextEntryFields, type ExecutionContextLike, type FilterTokenResolutionContext, type GeneratedApiKey, type GrantValidityWindow, HotReloadManager, type InterruptedRun, type KernelState, type KeyInput, type LadderPrincipal, type LadderRow, LiteKernel, type MigrationChunk, type MigrationChunkContext, MigrationJournalRefusal, type MigrationPlan, type MigrationPlanProvider, MigrationPlanRegistry, type MigrationPlanStep, type MigrationRunResult, type NamespaceCheckResult, type NamespaceConflict, type NamespaceEntry, NamespaceResolver, type OAuthTokenProvenance, OPERATION_PRIVATE_KEY_PREFIX, ObjectKernel, ObjectKernelBase, type ObjectKernelConfig, ObjectLogger, type OrderablePlugin, POSTURE_INJECTION_RULE, POSTURE_LADDER, POSTURE_RANK, type ParsedSignature, type PermissionCheckResult$1 as PermissionCheckResult, type PermissionGrant, type Plugin, type PluginArtifactVerifyResult, PluginConfigValidator, type PluginContext, PluginHealthMonitor, type PluginHealthStatus, type PluginLoadResult, PluginLoader, type PluginMetadata, type PermissionCheckResult as PluginPermissionCheckResult, PluginPermissionEnforcer, PluginPermissionManager, type PluginPermissions, PluginSandboxRuntime, PluginSecurityScanner, type PluginSignatureConfig, PluginSignatureVerifier, type PluginStartupResult, type PostureEvidence, type PublisherVerifyResult, index as QA, type ResolveAuthzInput, type ResolveLocalizationInput, type ResolveUserAuthzGrantsOptions, type ResolvedAuthzContext, type ResourceUsage, type RetryOptions, type RunMigrationJournalOptions, SIGNATURE_ALG, type SandboxContext, type ScanTarget, SecurePluginContext, type SecurityIssue, SemanticVersionManager, type ServiceFactory, ServiceLifecycle, type ServiceRegistration, type SignatureVerificationResult, UnknownFilterTokenError, UnresolvedFilterTokenError, type UserAuthzGrants, type VersionCompatibility, assembleExecutionContext, assembleExecutionContextOrGuest, assertInitServiceRequirements, bucketKeyToCalendarRange, buildPermissionsFromGrants, bulkWrite, calendarPartsInTz, calendarPartsInTzOrUtc, counterSignPayload, createMemoryCache, createMemoryI18n, createMemoryJob, createMemoryMetadata, createMemoryQueue, createPluginConfigValidator, createPluginPermissionEnforcer, deepMerge, defaultIsTransientError, derivePosture, describeInitOrderFault, engineCanRollBack, evaluateAuthGate, extractApiKey, filterTokenContextFrom, findInterruptedRuns, generateApiKey, generateEd25519KeyPair, getEnv, getMemoryUsage, hashApiKey, hashMigrationPlan, isAuthGateAllowlisted, isExpired, isGrantActive, isGrantExpired, isNode, normalizeAuthGate, parseScopes, parseSignature, planChunks, postureVisibleRows, readAuthoredTranslationLayer, readRunJournal, resolveApiKeyPrincipal, resolveAuthzContext, resolveFilterToken, resolveFilterTokens, resolveLocale, resolveLocalizationContext, resolvePluginOrder, resolveUserAuthzGrants, resumeMigrationJournal, runMigrationJournal, safeExit, shouldDenyAnonymous, signPayload, validateInitServiceContract, verifyPayload, verifyPlatformSignature, verifyPluginArtifact, verifyPublisherSignature, wireAuthoredTranslationSync, withTransientRetry, withoutOperationPrivateKeys, zonedDateStartToUtcMs };