@objectstack/core 17.0.0 → 17.1.0

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.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Logger, LifecycleEventName, IServiceRegistry, AudienceBindingSuggestionFilter, IObjectQLEngine } from '@objectstack/spec/contracts';
2
- export { EngineSchemaRegistryView, EngineTransactionInfo, EngineTransactionOptions, IDataDriver, IDataEngine, IHttpRequest, IHttpResponse, IHttpServer, IObjectQLEngine, Logger, Middleware, RouteHandler } from '@objectstack/spec/contracts';
2
+ export { EngineSchemaRegistryView, EngineTransactionInfo, EngineTransactionOptions, HttpResponseObservation, HttpResponseObserver, IDataDriver, IDataEngine, IHttpRequest, IHttpResponse, IHttpServer, IObjectQLEngine, Logger, Middleware, RouteHandler, UNMATCHED_ROUTE_PATTERN } from '@objectstack/spec/contracts';
3
3
  import { z } from 'zod';
4
4
  import { LoggerConfig, MigrationOnCrashPolicy, MigrationJournalEvent } from '@objectstack/spec/system';
5
5
  import { ObjectLogger } from './logger.js';
@@ -7,7 +7,7 @@ export { createLogger } from './logger.js';
7
7
  import * as QA from '@objectstack/spec/qa';
8
8
  import { KeyObject } from 'node:crypto';
9
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
- import { AuthzPosture } from '@objectstack/spec/security';
10
+ import { TenancyPosture, AuthzPosture } from '@objectstack/spec/security';
11
11
  export { nextUtcCalendarDay, utcInstantMs } from '@objectstack/spec/data';
12
12
 
13
13
  /**
@@ -222,6 +222,14 @@ declare class ObjectKernel {
222
222
  constructor(config?: ObjectKernelConfig);
223
223
  /**
224
224
  * Register a plugin with enhanced validation
225
+ *
226
+ * Duplicate names OVERWRITE, with one `warn` naming both versions — the
227
+ * declared contract in `plugin-registration.ts`, applied identically by
228
+ * `LiteKernel.use()` (#9864, maintainer ruling 2026-08-19). The overwrite
229
+ * itself is unchanged: it is what lets an app config's `plugins` entry
230
+ * supersede a plugin the CLI auto-registered earlier in the same boot
231
+ * (#9863). What changes is that it is no longer silent, and no longer
232
+ * disagrees with the other kernel.
225
233
  */
226
234
  use(plugin: Plugin): Promise<this>;
227
235
  /**
@@ -811,6 +819,17 @@ declare class LiteKernel extends ObjectKernelBase {
811
819
  /**
812
820
  * Register a plugin
813
821
  * @param plugin - Plugin instance
822
+ *
823
+ * Duplicate names OVERWRITE, with one `warn` naming both versions — the
824
+ * declared contract in `plugin-registration.ts`, applied identically by
825
+ * `ObjectKernel.use()` (#9864, maintainer ruling 2026-08-19).
826
+ *
827
+ * This method used to `throw` `[Kernel] Plugin '<name>' already
828
+ * registered` here while `ObjectKernel` overwrote silently, so one input
829
+ * had two meanings depending on which kernel was running — and the kernel
830
+ * that runs in production was the silent one. The ruling converged them on
831
+ * the behaviour that already works (an app config superseding a plugin the
832
+ * CLI auto-registered, #9863) and made it audible rather than removing it.
814
833
  */
815
834
  use(plugin: Plugin): this;
816
835
  /**
@@ -882,11 +901,67 @@ declare class TestRunner {
882
901
  declare class HttpTestAdapter implements TestExecutionAdapter {
883
902
  private baseUrl;
884
903
  private authToken?;
904
+ /**
905
+ * The single discovery probe of a run, memoised as the in-flight promise so
906
+ * concurrent record actions share one request rather than racing N.
907
+ *
908
+ * `os test` builds ONE adapter for the whole run (`packages/cli/src/commands/
909
+ * test.ts`) and hands it to every suite, so instance scope IS run scope.
910
+ */
911
+ private mountPromise?;
885
912
  constructor(baseUrl: string, authToken?: string | undefined);
886
- /** `{baseUrl}{apiBasePath}{dataPrefix}/{object}` the collection URL. */
913
+ /** The resolved data mount; probes at most once per adapter. */
914
+ private dataMount;
915
+ /**
916
+ * Ask the server where it serves the Data Protocol, and fall back to the
917
+ * convention — loudly — when it cannot say.
918
+ *
919
+ * ## [#7983] What the probe recovers, measured rather than assumed
920
+ *
921
+ * `@objectstack/client` answers the same question through discovery
922
+ * (`getRoute`, `packages/client/src/index.ts`), and this follows it: prefer
923
+ * the server's own `routes.data`, fall back to the convention. Measured on a
924
+ * booted stack (REST generator + dispatcher bridge, three configs):
925
+ *
926
+ * | deployment | `{apiBase}/discovery` | serves |
927
+ * |--------------------------------|-----------------------|---------------|
928
+ * | stock | 200 `/api/v1/data` | `/api/v1/data`|
929
+ * | `crud.dataPrefix: '/objects'` | 200 `/api/v1/objects` | `/api/v1/objects` |
930
+ * | `api.apiPath: '/api/2026-01'` | **404** | `/api/2026-01/data` |
931
+ *
932
+ * So the probe closes the `dataPrefix` row exactly: `RestServer`'s discovery
933
+ * handler substitutes the configured prefix into `routes.data`, and reading
934
+ * it is strictly better than recomputing it here. The `apiPath` row it cannot
935
+ * close, and the reason is structural rather than an oversight — `apiPath`
936
+ * moves the base that discovery itself is mounted under, so the document that
937
+ * would name the new mount is behind the very prefix we are missing.
938
+ *
939
+ * ⛔ And the one discovery document at a FIXED path does not rescue it:
940
+ * `/.well-known/objectstack` is mounted at the site root by the dispatcher
941
+ * bridge, but its `routes.data` is the DISPATCHER's own `${prefix}/data` —
942
+ * measured as `/api/v1/data` under all three configs above, including the two
943
+ * where the server serves elsewhere. Falling back to it would turn "we could
944
+ * not resolve the mount" into "discovery told us `/api/v1/data`": the same
945
+ * 404, now with a false provenance attached. Not probed, deliberately.
946
+ *
947
+ * Hence: one probe, then a diagnostic that NAMES the mount, the evidence and
948
+ * the remedy. `api_call` takes the path it is given and is unaffected either
949
+ * way — it stays the escape hatch for a host this cannot reach.
950
+ */
951
+ private resolveDataMount;
952
+ /** `{baseUrl}{dataMount}/{object}` — the collection URL. */
887
953
  private collectionUrl;
888
954
  /** `{collection}/{id}` — the single-record URL. */
889
955
  private recordUrl;
956
+ /**
957
+ * The provenance clause appended to a failed record action's error.
958
+ *
959
+ * The card this closes is about a 404 that reads like the author's own URL
960
+ * mistake; the mount is the one fact that distinguishes the two, so it rides
961
+ * on the failure itself rather than only on a warning printed earlier in the
962
+ * transcript.
963
+ */
964
+ private mountNote;
890
965
  execute(action: QA.TestAction, _context: Record<string, unknown>): Promise<unknown>;
891
966
  private createRecord;
892
967
  private updateRecord;
@@ -1712,9 +1787,74 @@ declare function isExpired(value: unknown, nowMs: number): boolean;
1712
1787
  /** The principal resolved from a valid `sys_api_key`. */
1713
1788
  interface ApiKeyPrincipal {
1714
1789
  userId: string;
1790
+ /**
1791
+ * The organization this key authenticates INTO — read from the row's
1792
+ * `active_organization_id` and adopted by `resolveAuthzContext` as the
1793
+ * request's active organization (`ExecutionContext.tenantId`), which is what
1794
+ * lets the ADR-0105 Layer 0 wall match. `undefined` for a key minted before
1795
+ * #8287, or one minted under the `single` posture where there is no
1796
+ * organization to inherit.
1797
+ */
1715
1798
  tenantId?: string;
1716
1799
  scopes: string[];
1717
1800
  }
1801
+ /**
1802
+ * [#8287] Why a key was refused. Distinct from "no key present" and from "this
1803
+ * key is unknown/revoked/expired": a refusal means the credential is real and
1804
+ * intact but cannot be admitted under this deployment's tenancy posture.
1805
+ */
1806
+ type ApiKeyRefusalReason = 'organization_required' | 'organization_membership_ended';
1807
+ /**
1808
+ * The verdict on an inbound API key. Three outcomes, deliberately distinct:
1809
+ *
1810
+ * - `none` — no key header, or a key that is unknown / revoked / expired /
1811
+ * owner-less. Indistinguishable by design (never tell a prober which), and
1812
+ * the caller MAY fall through to the session path exactly as before.
1813
+ * - `admitted` — a usable principal.
1814
+ * - `refused` — a real, intact key the posture cannot admit. The caller must
1815
+ * NOT fall through to the session path: falling through would be more
1816
+ * permissive than today's behaviour (an API key already outranks a session),
1817
+ * and the whole point of the refusal is that it is LOUD at call time.
1818
+ */
1819
+ type ApiKeyAdmission = {
1820
+ outcome: 'none';
1821
+ } | {
1822
+ outcome: 'admitted';
1823
+ principal: ApiKeyPrincipal;
1824
+ } | {
1825
+ outcome: 'refused';
1826
+ reason: ApiKeyRefusalReason;
1827
+ message: string;
1828
+ };
1829
+ /**
1830
+ * The shape of the kernel's `tenancy` service this module reads a posture from.
1831
+ * Structural on purpose: `@objectstack/core` must not depend on the plugin that
1832
+ * provides it, and an embedding without that plugin simply supplies nothing.
1833
+ */
1834
+ interface TenancyPostureSource {
1835
+ posture?: string;
1836
+ isolationActive?: boolean;
1837
+ }
1838
+ /**
1839
+ * [#8287] Resolve the EFFECTIVE tenancy posture from the kernel's `tenancy`
1840
+ * service — the same reconciliation `plugin-security` performs before handing a
1841
+ * posture to `computeTenantLayer0Filter`, so the wall and the API-key admission
1842
+ * can never disagree about which posture is in force.
1843
+ *
1844
+ * ⚠️ Deliberately NOT `resolveTenancyPosture()` from `@objectstack/types`, which
1845
+ * reads `OS_TENANCY_POSTURE` directly. That answers what the operator ASKED
1846
+ * for, not what is ENFORCED: under ADR-0093 D4/D5 a deployment that requests
1847
+ * `isolated` without the enterprise `@objectstack/organizations` runtime
1848
+ * resolves to `single` and runs with NO organization wall. Reading the env
1849
+ * there would refuse org-less API keys on a deployment whose wall is not even
1850
+ * active — breaking working automation to enforce a boundary that does not
1851
+ * exist. The `tenancy` service is the one place that already knows the
1852
+ * difference.
1853
+ *
1854
+ * Returns `undefined` when no service is available, which callers must treat as
1855
+ * "no posture-conditional refusal" — see {@link resolveApiKeyAdmission}.
1856
+ */
1857
+ declare function effectiveTenancyPosture(tenancy: TenancyPostureSource | undefined | null): TenancyPosture | undefined;
1718
1858
  /**
1719
1859
  * Verify an inbound API key against `sys_api_key` and resolve its principal.
1720
1860
  * This is the ONE verify path shared by the dispatcher/MCP and REST surfaces.
@@ -1726,7 +1866,21 @@ interface ApiKeyPrincipal {
1726
1866
  * @param headers Request headers (Web `Headers` or a plain object).
1727
1867
  * @param nowMs Clock for expiry checks (injectable for tests).
1728
1868
  */
1729
- declare function resolveApiKeyPrincipal(ql: any, headers: any, nowMs?: number): Promise<ApiKeyPrincipal | undefined>;
1869
+ declare function resolveApiKeyPrincipal(ql: any, headers: any, nowMs?: number, tenancyPosture?: TenancyPosture): Promise<ApiKeyPrincipal | undefined>;
1870
+ /**
1871
+ * [#8287] The full verdict behind {@link resolveApiKeyPrincipal} — same lookup,
1872
+ * but it distinguishes a POSTURE REFUSAL from "no principal".
1873
+ *
1874
+ * `resolveApiKeyPrincipal` collapses `refused` into `undefined` so every
1875
+ * existing caller keeps working and keeps failing closed; a caller that can
1876
+ * report WHY (the shared `resolveAuthzContext`) uses this instead.
1877
+ *
1878
+ * The only refusal decided here is the org-less one, because it needs nothing
1879
+ * but the row and the posture. The ex-member refusal needs the caller's
1880
+ * membership set and is decided in `resolveAuthzContext`, where that set is
1881
+ * already resolved.
1882
+ */
1883
+ declare function resolveApiKeyAdmission(ql: any, headers: any, nowMs?: number, tenancyPosture?: TenancyPosture): Promise<ApiKeyAdmission>;
1730
1884
 
1731
1885
  /** The transport-agnostic authorization envelope produced from a request. */
1732
1886
  interface ResolvedAuthzContext {
@@ -1758,6 +1912,24 @@ interface ResolvedAuthzContext {
1758
1912
  * anonymous requests carry no rung.
1759
1913
  */
1760
1914
  posture?: AuthzPosture;
1915
+ /**
1916
+ * [#8287] Set when an inbound API key was REFUSED — a real, intact
1917
+ * credential this deployment's tenancy posture cannot admit. The context is
1918
+ * otherwise EMPTY (no `userId`), so every transport already fails it closed
1919
+ * to 401 with no change; this field only lets a transport that wants to say
1920
+ * WHY do so, instead of answering the operator with a bare "unauthenticated"
1921
+ * for a key they can see is neither revoked nor expired.
1922
+ *
1923
+ * ⚠️ `reason` is NOT an `error.code`. The wire vocabulary is closed
1924
+ * (ADR-0112: `StandardErrorCode ∪ ERROR_CODE_LEDGER`, both in `packages/spec`)
1925
+ * and a refused credential's standard member is `UNAUTHENTICATED`. This is a
1926
+ * diagnostic discriminator for the message, deliberately lowercase so it can
1927
+ * never be mistaken for one.
1928
+ */
1929
+ authRefusal?: {
1930
+ reason: ApiKeyRefusalReason;
1931
+ message: string;
1932
+ };
1761
1933
  }
1762
1934
  interface ResolveAuthzInput {
1763
1935
  /** Data engine (ObjectQL) exposing `find(object, { where, limit, context })`. */
@@ -1772,6 +1944,18 @@ interface ResolveAuthzInput {
1772
1944
  getSession?: (headers: any) => Promise<any> | any;
1773
1945
  /** Clock injection for API-key expiry (tests). */
1774
1946
  nowMs?: number;
1947
+ /**
1948
+ * [#8287] The deployment's EFFECTIVE tenancy posture, as resolved from the
1949
+ * kernel's `tenancy` service (`effectiveTenancyPosture`) — never from
1950
+ * `OS_TENANCY_POSTURE`, which reports what was requested rather than what is
1951
+ * enforced (ADR-0093 D4/D5).
1952
+ *
1953
+ * Supplied by the transport because this resolver is deliberately
1954
+ * kernel-agnostic. OMITTING it disables the two posture-conditional API-key
1955
+ * refusals and leaves behaviour exactly as it was — so an unwired caller is
1956
+ * never made WORSE, only less strict.
1957
+ */
1958
+ tenancyPosture?: TenancyPosture;
1775
1959
  }
1776
1960
  /**
1777
1961
  * Resolve the authorization context for an inbound request. Always resolves —
@@ -2230,12 +2414,16 @@ declare const ANONYMOUS_DENY_CODE: "UNAUTHENTICATED";
2230
2414
  /** Human-facing message. */
2231
2415
  declare const ANONYMOUS_DENY_MESSAGE = "Authentication is required to access this endpoint.";
2232
2416
  /**
2233
- * The **REST seam's** 401 body — flat `{ error, message }`. NOT the platform's
2234
- * only one; see the two-envelope table below before you reuse this shape.
2417
+ * The **REST seam's** 401 body — flat `{ error, code, message }`. NOT the
2418
+ * platform's only one; see the two-envelope table below before you reuse this
2419
+ * shape.
2235
2420
  *
2236
- * Exactly one consumer writes it: `@objectstack/rest`'s `enforceAuth`
2237
- * (`rest-server.ts` — `res.status(ANONYMOUS_DENY_STATUS).json(ANONYMOUS_DENY_BODY)`),
2238
- * which owns the `/data/*` and `/meta` surfaces.
2421
+ * Two consumers write it verbatim, both flat-family seams: `@objectstack/rest`'s
2422
+ * `enforceAuth` (`rest-server.ts` —
2423
+ * `res.status(ANONYMOUS_DENY_STATUS).json(ANONYMOUS_DENY_BODY)`), which owns the
2424
+ * `/data/*` and `/meta` surfaces, and `@objectstack/runtime`'s
2425
+ * `mountRouteOnServer` (`dispatcher-plugin.ts` — the endpoint-route 401 arm,
2426
+ * #9823), which answers declared routes mounted on the HTTP server.
2239
2427
  *
2240
2428
  * ## Two live envelopes, one denial (#5632)
2241
2429
  *
@@ -2244,8 +2432,11 @@ declare const ANONYMOUS_DENY_MESSAGE = "Authentication is required to access thi
2244
2432
  * {@link ANONYMOUS_DENY_MESSAGE}). What differs is the **wrapper**:
2245
2433
  *
2246
2434
  * - **REST seam** — `@objectstack/rest` `enforceAuth`, this constant, verbatim:
2247
- * `{ error: 'UNAUTHENTICATED', message: '…' }`. The code is the value of the
2248
- * top-level `error` key; there is no `success` key and no nesting.
2435
+ * `{ error: 'UNAUTHENTICATED', code: 'UNAUTHENTICATED', message: '…' }`.
2436
+ * The machine code lives in the top-level `code` key the same documented
2437
+ * key every other REST error family answers (#9487, maintainer-ruled
2438
+ * ADDITIVE: `error` keeps carrying the code value it always has, so no
2439
+ * existing reader breaks). There is no `success` key and no nesting.
2249
2440
  * - **Dispatcher seams** — the five runtime domains `domains/ai.ts`,
2250
2441
  * `domains/meta.ts`, `domains/security.ts`, `domains/actions.ts` and
2251
2442
  * `domains/automation.ts` do NOT use this constant. Each calls
@@ -2257,7 +2448,10 @@ declare const ANONYMOUS_DENY_MESSAGE = "Authentication is required to access thi
2257
2448
  * (#4007) records the flat and wrapped envelopes as the two live ones, and
2258
2449
  * assigns retiring one of them to the envelope-convergence line (#3843 family).
2259
2450
  * Converging them is a breaking wire change; it is not this module's to make,
2260
- * and this constant must not be read as if it had already happened.
2451
+ * and this constant must not be read as if it had already happened. The #9487
2452
+ * `code` key does NOT settle that question either way (ADR-0112 D5 stays
2453
+ * open): it aligns the flat family to the `{ error, code }` shape the other
2454
+ * flat REST error families already answer, without moving or removing a key.
2261
2455
  *
2262
2456
  * ## Reading this from a consumer (human or AI author)
2263
2457
  *
@@ -2274,6 +2468,7 @@ declare const ANONYMOUS_DENY_MESSAGE = "Authentication is required to access thi
2274
2468
  */
2275
2469
  declare const ANONYMOUS_DENY_BODY: {
2276
2470
  readonly error: "UNAUTHENTICATED";
2471
+ readonly code: "UNAUTHENTICATED";
2277
2472
  readonly message: "Authentication is required to access this endpoint.";
2278
2473
  };
2279
2474
  interface AnonymousDenyInput {
@@ -2316,6 +2511,114 @@ declare function isGrantActive(row: GrantValidityWindow | null | undefined, nowM
2316
2511
  */
2317
2512
  declare function isGrantExpired(row: GrantValidityWindow | null | undefined, nowMs: number): boolean;
2318
2513
 
2514
+ /** A catalogue row that may carry the `active` flag (`sys_permission_set`, `sys_position`). */
2515
+ interface ActivatableRow {
2516
+ active?: unknown;
2517
+ }
2518
+ /**
2519
+ * True unless the row carries an `active` column that is explicitly OFF.
2520
+ *
2521
+ * The ONE predicate every reader of `sys_permission_set.active` /
2522
+ * `sys_position.active` uses, so the resolver that enforces the flag and the
2523
+ * break-glass guard that simulates a write to it can never disagree about what
2524
+ * "deactivated" means.
2525
+ */
2526
+ declare function isRowActive(row: ActivatableRow | null | undefined): boolean;
2527
+
2528
+ /**
2529
+ * ADMIN_STANDING_SURFACE — what `resolveAuthzContext` READS when it decides
2530
+ * who is an administrator, declared beside the resolver that reads it.
2531
+ *
2532
+ * ## Why this file exists (#8734)
2533
+ *
2534
+ * `plugin-auth`'s break-glass guard (`last-admin-guard.ts`, ADR-0024 D5.2)
2535
+ * decides whether a pending write can empty the administrator population by
2536
+ * testing the payload against three standing-key lists — `MEMBER_STANDING_KEYS`,
2537
+ * `GRANT_STANDING_KEYS`, `PERMISSION_SET_STANDING_KEYS`. Those lists are not an
2538
+ * independent design artifact: they are a CACHE of the columns this resolver
2539
+ * consumes. A payload touching none of them is skipped without any reads, so a
2540
+ * column this resolver starts reading and the guard's list omits is a write
2541
+ * class the guard silently stops judging — the one write class that can lock an
2542
+ * installation out of its own administration, with no in-product recovery.
2543
+ *
2544
+ * Nothing bound the two together. The correspondence was carried by a comment,
2545
+ * and it had already gone false once: #6084 wrote, beside the list, that
2546
+ * everything a permission-set write touches other than `name` — naming `active`
2547
+ * explicitly — is invisible to "who is an administrator". That was true when
2548
+ * written. #8613 made `active` a resolution-time predicate (a DEACTIVATED
2549
+ * `admin_full_access` set confers nothing, §6b below), and the sentence became
2550
+ * false. It was caught by one agent reading the comment closely enough to
2551
+ * notice it contradicted the code being written. Nothing mechanical would have
2552
+ * caught it: the guard's own tests stay green, because the guard is simply never
2553
+ * consulted for that write.
2554
+ *
2555
+ * ## What this file is, and what it is NOT
2556
+ *
2557
+ * It is a MEASUREMENT, not a wish. Its column lists are asserted equal to what
2558
+ * the resolver actually reads at runtime, by
2559
+ * `admin-standing-surface.test.ts`, which drives the real
2560
+ * `resolveAuthzContext` over a recording engine and collects every property
2561
+ * access and every `where` key per table. That is deliberate: a hand-written
2562
+ * list of "columns the derivation reads" is the same artifact as the comment
2563
+ * that went stale, one indirection along. Observation is also the only reading
2564
+ * that survives the derivation moving INTO a helper — `active` is read by
2565
+ * `isRowActive(ps)` and the window bounds by `isGrantActive(row, now)`, neither
2566
+ * of which names a column at the resolver's own call site.
2567
+ *
2568
+ * It is NOT a projection the resolver consumes. `ql.find` here returns whole
2569
+ * rows and the reads are ordinary property accesses on untyped rows, so nothing
2570
+ * in this file can FORCE the resolver to read only what it declares. The force
2571
+ * comes from the observation test: add a read, and this declaration is red
2572
+ * until it is updated; update this declaration, and `plugin-auth`'s
2573
+ * correspondence test is red until every new column is either in a standing-key
2574
+ * list or explicitly excluded with a reason.
2575
+ *
2576
+ * ## Reading the entries
2577
+ *
2578
+ * Every table this resolution path reads is listed — including the ones that
2579
+ * CANNOT confer administrator standing, each with the reason it cannot. That is
2580
+ * the table-level half of the same guarantee: a resolver that starts deriving
2581
+ * administrator standing from a new table would otherwise be invisible to a
2582
+ * column-set comparison, because the new table appears in neither side's list.
2583
+ */
2584
+ /** How a table this resolver reads relates to "who is an administrator". */
2585
+ interface AdminStandingTable {
2586
+ /**
2587
+ * `derives` — a write to this table can change the administrator population,
2588
+ * so `last-admin-guard.ts` must carry a standing-key list for it.
2589
+ * `reads-only` — this resolver reads the table for something else entirely.
2590
+ */
2591
+ readonly role: 'derives' | 'reads-only';
2592
+ /** Why the row above is the right classification. Prose, but pinned to a measured table. */
2593
+ readonly reason: string;
2594
+ /**
2595
+ * Every column this resolver reads on the table — property accesses and
2596
+ * `where` keys alike, in every spelling it actually touches. Declared for
2597
+ * `derives` tables only; asserted equal to the observed set.
2598
+ */
2599
+ readonly columns?: readonly string[];
2600
+ }
2601
+ /**
2602
+ * The measured read surface of the administrator derivation.
2603
+ *
2604
+ * Scope, stated so the gate cannot be read as claiming more than it measures:
2605
+ * this is the SESSION/user-id resolution path — `resolveAuthzContext` with a
2606
+ * principal, and therefore all of `resolveUserAuthzGrants`. The API-key
2607
+ * ADMISSION path (`resolveApiKeyAdmission`) is outside it on purpose: it
2608
+ * authenticates a principal and seeds `permissions` with the key's scopes, and
2609
+ * confers no administrator standing of its own — `hasPlatformAdminGrant` (§6b)
2610
+ * is set only from a `sys_permission_set` row reached through an UNSCOPED
2611
+ * `sys_user_permission_set` grant, never from a scope string.
2612
+ */
2613
+ declare const ADMIN_STANDING_SURFACE: Readonly<Record<string, AdminStandingTable>>;
2614
+ /** The tables a write to which can change who is an administrator. */
2615
+ declare function adminStandingTables(): string[];
2616
+ /**
2617
+ * The columns this resolver reads on `table`, or `undefined` when the table is
2618
+ * not part of the administrator derivation.
2619
+ */
2620
+ declare function adminStandingColumns(table: string): readonly string[] | undefined;
2621
+
2319
2622
  /**
2320
2623
  * [#7678] The `?status=` vocabulary of the audience-binding suggestion list
2321
2624
  * (ADR-0090 D5/D9) — ONE owner for a rule that had exactly one implementation
@@ -2512,6 +2815,20 @@ interface CalendarParts {
2512
2815
  month: number;
2513
2816
  day: number;
2514
2817
  }
2818
+ /**
2819
+ * A wall clock as a human writes it — calendar day plus an optional
2820
+ * time-of-day, with **no zone attached**. `2026-08-01 06:00:00` is this shape:
2821
+ * it names a reading on a clock, and only a reference timezone turns it into an
2822
+ * instant. Omitted time components default to 0, so {@link CalendarParts} alone
2823
+ * is midnight.
2824
+ */
2825
+ interface WallClockParts extends CalendarParts {
2826
+ /** 0-23. */
2827
+ hour?: number;
2828
+ minute?: number;
2829
+ second?: number;
2830
+ millisecond?: number;
2831
+ }
2515
2832
  /**
2516
2833
  * The year/month/day an instant falls on in `tz`. Throws if `tz` is not a
2517
2834
  * valid IANA zone (callers treat that as a fall-through to UTC).
@@ -2536,8 +2853,52 @@ declare function calendarPartsInTzOrUtc(d: Date, tz?: string): CalendarParts;
2536
2853
  *
2537
2854
  * Used by date-bucket drill ranges (#1752): a `datetime` field buckets on the
2538
2855
  * reference-tz calendar, so its bucket boundary is that tz's midnight instant.
2856
+ *
2857
+ * Date-only by contract: a `YYYY-MM-DD HH:mm:ss` argument is still `NaN` here.
2858
+ * Callers holding a wall clock with a time-of-day want
2859
+ * {@link zonedWallClockToUtcMs}, which this delegates its zone arithmetic to.
2539
2860
  */
2540
2861
  declare function zonedDateStartToUtcMs(ymd: string, tz?: string): number;
2862
+ /**
2863
+ * The UTC instant (epoch ms) at which a **wall clock** reading happens in
2864
+ * reference timezone `tz` — the general inverse of {@link calendarPartsInTz},
2865
+ * of which {@link zonedDateStartToUtcMs} is the midnight special case.
2866
+ *
2867
+ * `2026-08-01 06:00:00` in `Asia/Shanghai` is `2026-07-31T22:00:00Z`: a
2868
+ * different day, month and quarter. That gap is why this direction exists as a
2869
+ * shared primitive at all — bulk import (#8485) reads offset-free spreadsheet
2870
+ * cells, which are wall clocks and nothing more, and `new Date(cell)` resolves
2871
+ * them against the **process** `TZ`, i.e. a host setting rather than the
2872
+ * tenant's configured zone.
2873
+ *
2874
+ * DST-safe: the zone offset is read from the platform tz database via
2875
+ * `Intl.DateTimeFormat` (never hand-computed), and a two-pass resolution settles
2876
+ * the case where the offset differs side-to-side of the target instant. Two
2877
+ * wall clocks are not a bijection with instants, and this function resolves
2878
+ * both degenerate cases to the **earlier candidate instant** — in both, the
2879
+ * final pass reads the offset on the DST side of the transition (measured, not
2880
+ * merely intended — `datetime.test.ts` pins both):
2881
+ * - a clock reading the zone **skips** (spring forward: `02:30` on a US
2882
+ * spring-forward day) settles on the *post*-transition offset (EDT, −04),
2883
+ * which places the instant just **before** the gap: it reads `01:30` EST
2884
+ * locally, not `03:30` EDT. Note this is the opposite of Temporal's
2885
+ * `'compatible'` disambiguation, which pushes a gap reading forward;
2886
+ * - a clock reading that happens **twice** (fall back: `01:30` on a US
2887
+ * fall-back day) resolves to its first occurrence, the one still on the
2888
+ * pre-transition DST offset (EDT, −04).
2889
+ *
2890
+ * A spreadsheet cell naming a wall clock that its zone never had is ambiguous
2891
+ * by construction; what matters for an import is that the answer is
2892
+ * deterministic and host-independent, which both branches above are.
2893
+ *
2894
+ * FALLBACK — an unset, `'UTC'`, or invalid `tz` reads the wall clock **as UTC**,
2895
+ * never as the process-local clock. Every caller of this family already degrades
2896
+ * that way ({@link zonedDateStartToUtcMs}, and the export renderer's cell path),
2897
+ * and a host `TZ` fallback would reintroduce exactly the deployment-dependent
2898
+ * instant this primitive exists to remove. A parts object that produces an
2899
+ * invalid date (`NaN` components) returns `NaN`, as `Date.UTC` does.
2900
+ */
2901
+ declare function zonedWallClockToUtcMs(parts: WallClockParts, tz?: string): number;
2541
2902
 
2542
2903
  /**
2543
2904
  * Granularity of a canonical date-bucket key. Mirrors `@objectstack/spec`'s
@@ -3006,6 +3367,34 @@ declare function resolveFilterTokens<T>(filter: T, ctx?: FilterTokenResolutionCo
3006
3367
  */
3007
3368
  declare function filterTokenContextFrom(execCtx: ExecutionContextLike | undefined, now?: Date): FilterTokenResolutionContext;
3008
3369
 
3370
+ /** Which temporal storage rule a declared field takes. */
3371
+ type TemporalComparandKind = 'datetime' | 'date' | 'time';
3372
+ /**
3373
+ * The kind a declared field's `type` takes, or `null` for every non-temporal
3374
+ * field.
3375
+ *
3376
+ * The same three-way split `driver-memory`'s `indexTemporalFields` and
3377
+ * `SqlDriver.temporalFieldKind` make, so the door and the drivers cannot
3378
+ * disagree about which fields are temporal at all.
3379
+ */
3380
+ declare function temporalComparandKind(fieldType: unknown): TemporalComparandKind | null;
3381
+ /**
3382
+ * Is `value` a comparand that a `kind` column's storage rule cannot read?
3383
+ *
3384
+ * `true` ONLY for a non-empty, non-placeholder STRING that the kind's rule
3385
+ * would hand back unchanged. Everything else — a number, a `Date`, `null`, a
3386
+ * `{ $field }` reference, filter structure, the empty string, a `{token}` —
3387
+ * answers `false`, each for a reason recorded in the module note or below.
3388
+ *
3389
+ * A `{placeholder}` is stepped around rather than judged because it is another
3390
+ * layer's vocabulary and that layer already refuses the unknown ones loudly
3391
+ * (`FILTER_TOKEN_UNKNOWN` / 400, with the resolvable tokens listed). Both doors
3392
+ * that call this run BEFORE token resolution, so judging a placeholder here
3393
+ * would refuse `{30_days_ago}` — the platform's own correct spelling, and the
3394
+ * positive control this fix is pinned against.
3395
+ */
3396
+ declare function isUninterpretableTemporalComparand(kind: TemporalComparandKind, value: unknown): boolean;
3397
+
3009
3398
  /**
3010
3399
  * [#4435] The 404 a single-record operation answers when the id names no row.
3011
3400
  *
@@ -3630,4 +4019,4 @@ declare class NamespaceResolver {
3630
4019
  private suggestAlternative;
3631
4020
  }
3632
4021
 
3633
- export { ANONYMOUS_DENY_BODY, ANONYMOUS_DENY_CODE, ANONYMOUS_DENY_MESSAGE, ANONYMOUS_DENY_STATUS, API_KEY_PREFIX, AUDIENCE_BINDING_SUGGESTION_STATUSES, AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES, type AnonymousDenyInput, type ApiKeyPrincipal, type AudienceBindingSuggestionStatus, 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, assertMetadataRegisterContract, bucketKeyToCalendarRange, buildPermissionsFromGrants, bulkWrite, calendarPartsInTz, calendarPartsInTzOrUtc, canonicalMetadataServiceType, collectInternalWriteResponseFields, counterSignPayload, createMemoryCache, createMemoryI18n, createMemoryJob, createMemoryMetadata, createMemoryQueue, createPluginConfigValidator, createPluginPermissionEnforcer, deepMerge, defaultIsTransientError, derivePosture, describeInitOrderFault, engineCanRollBack, evaluateAuthGate, extractApiKey, filterTokenContextFrom, findInterruptedRuns, generateApiKey, generateEd25519KeyPair, getEnv, getMemoryUsage, hashApiKey, hashMigrationPlan, isAudienceBindingSuggestionStatus, isAuthGateAllowlisted, isExpired, isGrantActive, isGrantExpired, isNode, normalizeAuthGate, omitInternalFieldsFromWriteResponse, parseScopes, parseSignature, planChunks, postureVisibleRows, readAuthoredTranslationLayer, readRunJournal, recordNotFoundError, resolveApiKeyPrincipal, resolveAuthzContext, resolveFilterToken, resolveFilterTokens, resolveLocale, resolveLocalizationContext, resolvePluginOrder, resolveUserAuthzGrants, resumeMigrationJournal, runMigrationJournal, safeExit, shouldDenyAnonymous, signPayload, unknownAudienceBindingSuggestionStatusMessage, validateInitServiceContract, verifyPayload, verifyPlatformSignature, verifyPluginArtifact, verifyPublisherSignature, wireAuthoredTranslationSync, withTransientRetry, withoutOperationPrivateKeys, zonedDateStartToUtcMs };
4022
+ export { ADMIN_STANDING_SURFACE, ANONYMOUS_DENY_BODY, ANONYMOUS_DENY_CODE, ANONYMOUS_DENY_MESSAGE, ANONYMOUS_DENY_STATUS, API_KEY_PREFIX, AUDIENCE_BINDING_SUGGESTION_STATUSES, AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES, type ActivatableRow, type AdminStandingTable, type AnonymousDenyInput, type ApiKeyAdmission, type ApiKeyPrincipal, type ApiKeyRefusalReason, type AudienceBindingSuggestionStatus, 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, type TemporalComparandKind, type TenancyPostureSource, UnknownFilterTokenError, UnresolvedFilterTokenError, type UserAuthzGrants, type VersionCompatibility, type WallClockParts, adminStandingColumns, adminStandingTables, assembleExecutionContext, assembleExecutionContextOrGuest, assertInitServiceRequirements, assertMetadataRegisterContract, bucketKeyToCalendarRange, buildPermissionsFromGrants, bulkWrite, calendarPartsInTz, calendarPartsInTzOrUtc, canonicalMetadataServiceType, collectInternalWriteResponseFields, counterSignPayload, createMemoryCache, createMemoryI18n, createMemoryJob, createMemoryMetadata, createMemoryQueue, createPluginConfigValidator, createPluginPermissionEnforcer, deepMerge, defaultIsTransientError, derivePosture, describeInitOrderFault, effectiveTenancyPosture, engineCanRollBack, evaluateAuthGate, extractApiKey, filterTokenContextFrom, findInterruptedRuns, generateApiKey, generateEd25519KeyPair, getEnv, getMemoryUsage, hashApiKey, hashMigrationPlan, isAudienceBindingSuggestionStatus, isAuthGateAllowlisted, isExpired, isGrantActive, isGrantExpired, isNode, isRowActive, isUninterpretableTemporalComparand, normalizeAuthGate, omitInternalFieldsFromWriteResponse, parseScopes, parseSignature, planChunks, postureVisibleRows, readAuthoredTranslationLayer, readRunJournal, recordNotFoundError, resolveApiKeyAdmission, resolveApiKeyPrincipal, resolveAuthzContext, resolveFilterToken, resolveFilterTokens, resolveLocale, resolveLocalizationContext, resolvePluginOrder, resolveUserAuthzGrants, resumeMigrationJournal, runMigrationJournal, safeExit, shouldDenyAnonymous, signPayload, temporalComparandKind, unknownAudienceBindingSuggestionStatusMessage, validateInitServiceContract, verifyPayload, verifyPlatformSignature, verifyPluginArtifact, verifyPublisherSignature, wireAuthoredTranslationSync, withTransientRetry, withoutOperationPrivateKeys, zonedDateStartToUtcMs, zonedWallClockToUtcMs };