@objectstack/core 17.0.0 → 17.2.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.cjs CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ ADMIN_STANDING_SURFACE: () => ADMIN_STANDING_SURFACE,
33
34
  ANONYMOUS_DENY_BODY: () => ANONYMOUS_DENY_BODY,
34
35
  ANONYMOUS_DENY_CODE: () => ANONYMOUS_DENY_CODE,
35
36
  ANONYMOUS_DENY_MESSAGE: () => ANONYMOUS_DENY_MESSAGE,
@@ -65,8 +66,11 @@ __export(index_exports, {
65
66
  SecurePluginContext: () => SecurePluginContext,
66
67
  SemanticVersionManager: () => SemanticVersionManager,
67
68
  ServiceLifecycle: () => ServiceLifecycle,
69
+ UNMATCHED_ROUTE_PATTERN: () => import_contracts.UNMATCHED_ROUTE_PATTERN,
68
70
  UnknownFilterTokenError: () => UnknownFilterTokenError,
69
71
  UnresolvedFilterTokenError: () => UnresolvedFilterTokenError,
72
+ adminStandingColumns: () => adminStandingColumns,
73
+ adminStandingTables: () => adminStandingTables,
70
74
  assembleExecutionContext: () => assembleExecutionContext,
71
75
  assembleExecutionContextOrGuest: () => assembleExecutionContextOrGuest,
72
76
  assertInitServiceRequirements: () => assertInitServiceRequirements,
@@ -91,6 +95,7 @@ __export(index_exports, {
91
95
  defaultIsTransientError: () => defaultIsTransientError,
92
96
  derivePosture: () => derivePosture,
93
97
  describeInitOrderFault: () => describeInitOrderFault,
98
+ effectiveTenancyPosture: () => effectiveTenancyPosture,
94
99
  engineCanRollBack: () => engineCanRollBack,
95
100
  evaluateAuthGate: () => evaluateAuthGate,
96
101
  extractApiKey: () => extractApiKey,
@@ -108,6 +113,8 @@ __export(index_exports, {
108
113
  isGrantActive: () => isGrantActive,
109
114
  isGrantExpired: () => isGrantExpired,
110
115
  isNode: () => isNode,
116
+ isRowActive: () => isRowActive,
117
+ isUninterpretableTemporalComparand: () => isUninterpretableTemporalComparand,
111
118
  nextUtcCalendarDay: () => import_data.nextUtcCalendarDay,
112
119
  normalizeAuthGate: () => normalizeAuthGate,
113
120
  omitInternalFieldsFromWriteResponse: () => omitInternalFieldsFromWriteResponse,
@@ -118,6 +125,7 @@ __export(index_exports, {
118
125
  readAuthoredTranslationLayer: () => readAuthoredTranslationLayer,
119
126
  readRunJournal: () => readRunJournal,
120
127
  recordNotFoundError: () => recordNotFoundError,
128
+ resolveApiKeyAdmission: () => resolveApiKeyAdmission,
121
129
  resolveApiKeyPrincipal: () => resolveApiKeyPrincipal,
122
130
  resolveAuthzContext: () => resolveAuthzContext,
123
131
  resolveFilterToken: () => resolveFilterToken,
@@ -131,6 +139,7 @@ __export(index_exports, {
131
139
  safeExit: () => safeExit,
132
140
  shouldDenyAnonymous: () => shouldDenyAnonymous,
133
141
  signPayload: () => signPayload,
142
+ temporalComparandKind: () => temporalComparandKind,
134
143
  unknownAudienceBindingSuggestionStatusMessage: () => unknownAudienceBindingSuggestionStatusMessage,
135
144
  utcInstantMs: () => import_data.utcInstantMs,
136
145
  validateInitServiceContract: () => validateInitServiceContract,
@@ -141,7 +150,8 @@ __export(index_exports, {
141
150
  wireAuthoredTranslationSync: () => wireAuthoredTranslationSync,
142
151
  withTransientRetry: () => withTransientRetry,
143
152
  withoutOperationPrivateKeys: () => withoutOperationPrivateKeys,
144
- zonedDateStartToUtcMs: () => zonedDateStartToUtcMs
153
+ zonedDateStartToUtcMs: () => zonedDateStartToUtcMs,
154
+ zonedWallClockToUtcMs: () => zonedWallClockToUtcMs
145
155
  });
146
156
  module.exports = __toCommonJS(index_exports);
147
157
 
@@ -1484,35 +1494,6 @@ function createMemoryQueue() {
1484
1494
  };
1485
1495
  }
1486
1496
 
1487
- // src/fallbacks/memory-job.ts
1488
- function createMemoryJob() {
1489
- const jobs = /* @__PURE__ */ new Map();
1490
- return {
1491
- __serviceInfo: {
1492
- status: "degraded",
1493
- handlerReady: false,
1494
- message: "In-process job registry \u2014 trigger() runs handlers, but scheduled jobs never fire on their own (no timer). Register a job plugin (e.g. Agenda) for real scheduling."
1495
- },
1496
- _serviceName: "job",
1497
- async schedule(name, schedule, handler) {
1498
- jobs.set(name, { schedule, handler });
1499
- },
1500
- async cancel(name) {
1501
- jobs.delete(name);
1502
- },
1503
- async trigger(name, data) {
1504
- const job = jobs.get(name);
1505
- if (job?.handler) await job.handler({ jobId: name, data });
1506
- },
1507
- async getExecutions() {
1508
- return [];
1509
- },
1510
- async listJobs() {
1511
- return [...jobs.keys()];
1512
- }
1513
- };
1514
- }
1515
-
1516
1497
  // src/fallbacks/memory-i18n.ts
1517
1498
  var import_system = require("@objectstack/spec/system");
1518
1499
  function deepMerge(target, source) {
@@ -1740,6 +1721,35 @@ function createMemoryMetadata() {
1740
1721
  };
1741
1722
  }
1742
1723
 
1724
+ // src/fallbacks/memory-job.ts
1725
+ function createMemoryJob() {
1726
+ const jobs = /* @__PURE__ */ new Map();
1727
+ return {
1728
+ __serviceInfo: {
1729
+ status: "degraded",
1730
+ handlerReady: false,
1731
+ message: "In-process job registry \u2014 trigger() runs handlers, but scheduled jobs never fire on their own (no timer). Register a job plugin (e.g. Agenda) for real scheduling."
1732
+ },
1733
+ _serviceName: "job",
1734
+ async schedule(name, schedule, handler) {
1735
+ jobs.set(name, { schedule, handler });
1736
+ },
1737
+ async cancel(name) {
1738
+ jobs.delete(name);
1739
+ },
1740
+ async trigger(name, data) {
1741
+ const job = jobs.get(name);
1742
+ if (job?.handler) await job.handler({ jobId: name, data });
1743
+ },
1744
+ async getExecutions() {
1745
+ return [];
1746
+ },
1747
+ async listJobs() {
1748
+ return [...jobs.keys()];
1749
+ }
1750
+ };
1751
+ }
1752
+
1743
1753
  // src/fallbacks/authored-translation-sync.ts
1744
1754
  var import_system2 = require("@objectstack/spec/system");
1745
1755
  var OWNER_PROP = "__authoredTranslationSyncOwner";
@@ -1872,10 +1882,71 @@ var CORE_FALLBACK_FACTORIES = {
1872
1882
  metadata: createMemoryMetadata,
1873
1883
  cache: createMemoryCache,
1874
1884
  queue: createMemoryQueue,
1875
- job: createMemoryJob,
1876
1885
  i18n: createMemoryI18n
1877
1886
  };
1878
1887
 
1888
+ // src/plugin-registration.ts
1889
+ function versionLabel(plugin) {
1890
+ return plugin.version ? `v${plugin.version}` : "unversioned";
1891
+ }
1892
+ function describeSupersededRegistration(previous, next) {
1893
+ return `Plugin superseded: '${next.name}' \u2014 the later registration (${versionLabel(next)}) REPLACED the earlier one (${versionLabel(previous)}). Only the later instance is initialized and started; the earlier one is discarded without ever running init(). Duplicate registration by name is last-one-wins on both kernels by declared contract (#9864) \u2014 register the plugin once if that is not what you meant.`;
1894
+ }
1895
+ function registerPluginByName(registry, plugin, logger) {
1896
+ const previous = registry.get(plugin.name);
1897
+ if (previous !== void 0) {
1898
+ logger.warn(describeSupersededRegistration(previous, plugin), {
1899
+ plugin: plugin.name,
1900
+ supersededVersion: previous.version,
1901
+ supersedingVersion: plugin.version
1902
+ });
1903
+ }
1904
+ registry.set(plugin.name, plugin);
1905
+ return previous;
1906
+ }
1907
+
1908
+ // src/timeout-guard.ts
1909
+ var TimeoutGuard = class {
1910
+ constructor(timeoutMs, createTimeoutError) {
1911
+ /**
1912
+ * Settles `expiry` without a value. `Promise<never>` has no resolvable
1913
+ * value in the type system, but settling it is the entire point: it is
1914
+ * only ever called from `reclaim()`, i.e. after the race it guarded has
1915
+ * already been decided, so the resolution is discarded by construction and
1916
+ * can never become a race winner. The cast localises that argument here
1917
+ * rather than pushing a lie into every caller's return type.
1918
+ */
1919
+ this.settleExpiry = () => {
1920
+ };
1921
+ this.expiry = new Promise((resolve, reject) => {
1922
+ this.settleExpiry = resolve;
1923
+ this.timer = setTimeout(() => reject(createTimeoutError()), timeoutMs);
1924
+ });
1925
+ }
1926
+ /**
1927
+ * Reclaim the guard once the race it protects has been decided. Both
1928
+ * halves, always: the timer is cleared so it cannot fire against a
1929
+ * lifecycle phase that is already over, and `expiry` is settled so neither
1930
+ * it nor the race's reaction on it is retained.
1931
+ *
1932
+ * Idempotent — `clearTimeout` on a cleared handle and a second resolve on
1933
+ * a settled promise are both no-ops.
1934
+ */
1935
+ reclaim() {
1936
+ clearTimeout(this.timer);
1937
+ this.timer = void 0;
1938
+ this.settleExpiry();
1939
+ }
1940
+ };
1941
+ async function raceWithTimeout(operation, timeoutMs, createTimeoutError) {
1942
+ const guard = new TimeoutGuard(timeoutMs, createTimeoutError);
1943
+ try {
1944
+ return await Promise.race([operation, guard.expiry]);
1945
+ } finally {
1946
+ guard.reclaim();
1947
+ }
1948
+ }
1949
+
1879
1950
  // src/kernel.ts
1880
1951
  var ObjectKernel = class {
1881
1952
  constructor(config = {}) {
@@ -1960,6 +2031,14 @@ var ObjectKernel = class {
1960
2031
  }
1961
2032
  /**
1962
2033
  * Register a plugin with enhanced validation
2034
+ *
2035
+ * Duplicate names OVERWRITE, with one `warn` naming both versions — the
2036
+ * declared contract in `plugin-registration.ts`, applied identically by
2037
+ * `LiteKernel.use()` (#9864, maintainer ruling 2026-08-19). The overwrite
2038
+ * itself is unchanged: it is what lets an app config's `plugins` entry
2039
+ * supersede a plugin the CLI auto-registered earlier in the same boot
2040
+ * (#9863). What changes is that it is no longer silent, and no longer
2041
+ * disagrees with the other kernel.
1963
2042
  */
1964
2043
  async use(plugin) {
1965
2044
  if (this.state !== "idle") {
@@ -1970,11 +2049,13 @@ var ObjectKernel = class {
1970
2049
  throw new Error(`Failed to load plugin: ${plugin.name} - ${result.error?.message}`);
1971
2050
  }
1972
2051
  const pluginMeta = result.plugin;
1973
- this.plugins.set(pluginMeta.name, pluginMeta);
1974
- this.logger.info(`Plugin registered: ${pluginMeta.name}@${pluginMeta.version}`, {
1975
- plugin: pluginMeta.name,
1976
- version: pluginMeta.version
1977
- });
2052
+ const superseded = registerPluginByName(this.plugins, pluginMeta, this.logger);
2053
+ if (superseded === void 0) {
2054
+ this.logger.info(`Plugin registered: ${pluginMeta.name}@${pluginMeta.version}`, {
2055
+ plugin: pluginMeta.name,
2056
+ version: pluginMeta.version
2057
+ });
2058
+ }
1978
2059
  return this;
1979
2060
  }
1980
2061
  /**
@@ -2136,14 +2217,11 @@ var ObjectKernel = class {
2136
2217
  this.logger.info("Graceful shutdown started");
2137
2218
  const shutdownTimeoutError = new Error("Shutdown timeout exceeded");
2138
2219
  try {
2139
- const shutdownPromise = this.performShutdown();
2140
- const timeoutPromise = new Promise((_, reject) => {
2141
- const t = setTimeout(() => {
2142
- reject(shutdownTimeoutError);
2143
- }, this.config.shutdownTimeout);
2144
- if (t.unref) t.unref();
2145
- });
2146
- await Promise.race([shutdownPromise, timeoutPromise]);
2220
+ await raceWithTimeout(
2221
+ this.performShutdown(),
2222
+ this.config.shutdownTimeout,
2223
+ () => shutdownTimeoutError
2224
+ );
2147
2225
  this.state = "stopped";
2148
2226
  this.logger.info("\u2705 Graceful shutdown complete");
2149
2227
  } catch (error) {
@@ -2261,25 +2339,17 @@ var ObjectKernel = class {
2261
2339
  * as well: if the hook never settles and nothing else keeps the loop alive,
2262
2340
  * Node exits before the timer can fire and the timeout is never reported.
2263
2341
  * The guard has to stay ref'd exactly as long as the race is undecided,
2264
- * which is what `clearTimeout` in a `finally` expresses.
2342
+ * which is what clearing on settle expresses.
2265
2343
  *
2266
- * `operation` is widened to `T | PromiseLike<T>` because the Plugin
2267
- * contract permits a synchronous hook (`init`/`start` return
2268
- * `void | Promise<void>`); such a hook wins the race immediately and the
2269
- * guard is reclaimed on the same turn.
2344
+ * Clearing the timer was only half of it, though (#10604): the promise the
2345
+ * race still holds a reaction on has to SETTLE, or it and that reaction are
2346
+ * retained past the end of the run two leaking promises per boot, which
2347
+ * is what `vitest --detectAsyncLeaks` names here. Both halves now live in
2348
+ * `TimeoutGuard.reclaim()`, shared with `shutdown()`, so the two sites
2349
+ * cannot drift into doing one half each again.
2270
2350
  */
2271
2351
  async raceStartupTimeout(operation, timeout, message) {
2272
- let guard;
2273
- const timeoutPromise = new Promise((_, reject) => {
2274
- guard = setTimeout(() => {
2275
- reject(new Error(message));
2276
- }, timeout);
2277
- });
2278
- try {
2279
- return await Promise.race([operation, timeoutPromise]);
2280
- } finally {
2281
- clearTimeout(guard);
2282
- }
2352
+ return raceWithTimeout(operation, timeout, () => new Error(message));
2283
2353
  }
2284
2354
  /**
2285
2355
  * Whether a service is resolvable on this kernel right now — direct
@@ -2452,14 +2522,21 @@ var LiteKernel = class extends ObjectKernelBase {
2452
2522
  /**
2453
2523
  * Register a plugin
2454
2524
  * @param plugin - Plugin instance
2525
+ *
2526
+ * Duplicate names OVERWRITE, with one `warn` naming both versions — the
2527
+ * declared contract in `plugin-registration.ts`, applied identically by
2528
+ * `ObjectKernel.use()` (#9864, maintainer ruling 2026-08-19).
2529
+ *
2530
+ * This method used to `throw` `[Kernel] Plugin '<name>' already
2531
+ * registered` here while `ObjectKernel` overwrote silently, so one input
2532
+ * had two meanings depending on which kernel was running — and the kernel
2533
+ * that runs in production was the silent one. The ruling converged them on
2534
+ * the behaviour that already works (an app config superseding a plugin the
2535
+ * CLI auto-registered, #9863) and made it audible rather than removing it.
2455
2536
  */
2456
2537
  use(plugin) {
2457
2538
  this.validateIdle();
2458
- const pluginName = plugin.name;
2459
- if (this.plugins.has(pluginName)) {
2460
- throw new Error(`[Kernel] Plugin '${pluginName}' already registered`);
2461
- }
2462
- this.plugins.set(pluginName, plugin);
2539
+ registerPluginByName(this.plugins, plugin, this.logger);
2463
2540
  return this;
2464
2541
  }
2465
2542
  /**
@@ -2709,27 +2786,117 @@ var TestRunner = class {
2709
2786
 
2710
2787
  // src/qa/http-adapter.ts
2711
2788
  var import_api = require("@objectstack/spec/api");
2712
- var dataPathCache;
2713
- function defaultDataPath() {
2714
- if (dataPathCache === void 0) {
2789
+ var conventionCache;
2790
+ function conventionMounts() {
2791
+ if (conventionCache === void 0) {
2715
2792
  const api = import_api.RestApiConfigSchema.parse({});
2716
2793
  const crud = import_api.CrudEndpointsConfigSchema.parse({});
2717
- dataPathCache = `${api.apiPath ?? `${api.basePath}/${api.version}`}${crud.dataPrefix}`;
2794
+ const apiBase = api.apiPath ?? `${api.basePath}/${api.version}`;
2795
+ conventionCache = { apiBase, dataPath: `${apiBase}${crud.dataPrefix}` };
2718
2796
  }
2719
- return dataPathCache;
2797
+ return conventionCache;
2720
2798
  }
2721
2799
  var HttpTestAdapter = class {
2722
2800
  constructor(baseUrl, authToken) {
2723
2801
  this.baseUrl = baseUrl;
2724
2802
  this.authToken = authToken;
2725
2803
  }
2726
- /** `{baseUrl}{apiBasePath}{dataPrefix}/{object}` the collection URL. */
2727
- collectionUrl(objectName) {
2728
- return `${this.baseUrl}${defaultDataPath()}/${encodeURIComponent(objectName)}`;
2804
+ /** The resolved data mount; probes at most once per adapter. */
2805
+ dataMount() {
2806
+ if (this.mountPromise === void 0) {
2807
+ this.mountPromise = this.resolveDataMount();
2808
+ }
2809
+ return this.mountPromise;
2810
+ }
2811
+ /**
2812
+ * Ask the server where it serves the Data Protocol, and fall back to the
2813
+ * convention — loudly — when it cannot say.
2814
+ *
2815
+ * ## [#7983] What the probe recovers, measured rather than assumed
2816
+ *
2817
+ * `@objectstack/client` answers the same question through discovery
2818
+ * (`getRoute`, `packages/client/src/index.ts`), and this follows it: prefer
2819
+ * the server's own `routes.data`, fall back to the convention. Measured on a
2820
+ * booted stack (REST generator + dispatcher bridge, three configs):
2821
+ *
2822
+ * | deployment | `{apiBase}/discovery` | serves |
2823
+ * |--------------------------------|-----------------------|---------------|
2824
+ * | stock | 200 `/api/v1/data` | `/api/v1/data`|
2825
+ * | `crud.dataPrefix: '/objects'` | 200 `/api/v1/objects` | `/api/v1/objects` |
2826
+ * | `api.apiPath: '/api/2026-01'` | **404** | `/api/2026-01/data` |
2827
+ *
2828
+ * So the probe closes the `dataPrefix` row exactly: `RestServer`'s discovery
2829
+ * handler substitutes the configured prefix into `routes.data`, and reading
2830
+ * it is strictly better than recomputing it here. The `apiPath` row it cannot
2831
+ * close, and the reason is structural rather than an oversight — `apiPath`
2832
+ * moves the base that discovery itself is mounted under, so the document that
2833
+ * would name the new mount is behind the very prefix we are missing.
2834
+ *
2835
+ * ⛔ And the one discovery document at a FIXED path does not rescue it:
2836
+ * `/.well-known/objectstack` is mounted at the site root by the dispatcher
2837
+ * bridge, but its `routes.data` is the DISPATCHER's own `${prefix}/data` —
2838
+ * measured as `/api/v1/data` under all three configs above, including the two
2839
+ * where the server serves elsewhere. Falling back to it would turn "we could
2840
+ * not resolve the mount" into "discovery told us `/api/v1/data`": the same
2841
+ * 404, now with a false provenance attached. Not probed, deliberately.
2842
+ *
2843
+ * Hence: one probe, then a diagnostic that NAMES the mount, the evidence and
2844
+ * the remedy. `api_call` takes the path it is given and is unaffected either
2845
+ * way — it stays the escape hatch for a host this cannot reach.
2846
+ */
2847
+ async resolveDataMount() {
2848
+ const { apiBase, dataPath } = conventionMounts();
2849
+ const probeUrl = `${this.baseUrl}${apiBase}/discovery`;
2850
+ let why;
2851
+ try {
2852
+ const headers = {};
2853
+ if (this.authToken) {
2854
+ headers["Authorization"] = `Bearer ${this.authToken}`;
2855
+ }
2856
+ const response = await fetch(probeUrl, { method: "GET", headers });
2857
+ if (response.ok) {
2858
+ const body = await response.json();
2859
+ const doc = body && typeof body === "object" && "routes" in body ? body : body?.data;
2860
+ const advertised = doc?.routes?.data;
2861
+ if (typeof advertised === "string" && advertised.length > 0) {
2862
+ return {
2863
+ path: advertised,
2864
+ source: "discovery",
2865
+ why: `GET ${probeUrl} advertised routes.data`
2866
+ };
2867
+ }
2868
+ why = `GET ${probeUrl} answered ${response.status} but carried no routes.data`;
2869
+ } else {
2870
+ why = `GET ${probeUrl} answered ${response.status}`;
2871
+ }
2872
+ } catch (error) {
2873
+ why = `GET ${probeUrl} could not be reached (${error.message})`;
2874
+ }
2875
+ const mount = { path: dataPath, source: "convention", why };
2876
+ console.warn(
2877
+ `[HttpTestAdapter] Data Protocol mount NOT resolved from discovery. Record actions (create_record, read_record, update_record, delete_record, query_records) will address ${this.baseUrl}${dataPath}, the convention declared by RestApiConfigSchema (apiPath ?? basePath/version) + CrudEndpointsConfigSchema.dataPrefix. Evidence: ${why}. A deployment that sets crud.dataPrefix is normally recovered by this probe; one that sets api.apiPath moves discovery itself out from under it, and no fixed-path document reports the REST mount \u2014 write those steps as api_call, which takes the path you give it.`
2878
+ );
2879
+ return mount;
2880
+ }
2881
+ /** `{baseUrl}{dataMount}/{object}` — the collection URL. */
2882
+ collectionUrl(mount, objectName) {
2883
+ return `${this.baseUrl}${mount.path}/${encodeURIComponent(objectName)}`;
2729
2884
  }
2730
2885
  /** `{collection}/{id}` — the single-record URL. */
2731
- recordUrl(objectName, id) {
2732
- return `${this.collectionUrl(objectName)}/${encodeURIComponent(String(id))}`;
2886
+ recordUrl(mount, objectName, id) {
2887
+ return `${this.collectionUrl(mount, objectName)}/${encodeURIComponent(String(id))}`;
2888
+ }
2889
+ /**
2890
+ * The provenance clause appended to a failed record action's error.
2891
+ *
2892
+ * The card this closes is about a 404 that reads like the author's own URL
2893
+ * mistake; the mount is the one fact that distinguishes the two, so it rides
2894
+ * on the failure itself rather than only on a warning printed earlier in the
2895
+ * transcript.
2896
+ */
2897
+ mountNote(mount) {
2898
+ const how = mount.source === "discovery" ? "resolved from discovery" : "assumed by convention \u2014 the server was not able to state it";
2899
+ return `record actions addressed ${this.baseUrl}${mount.path} (data mount ${how}: ${mount.why})`;
2733
2900
  }
2734
2901
  async execute(action, _context) {
2735
2902
  const headers = {
@@ -2762,48 +2929,53 @@ var HttpTestAdapter = class {
2762
2929
  }
2763
2930
  }
2764
2931
  async createRecord(objectName, data, headers) {
2765
- const response = await fetch(this.collectionUrl(objectName), {
2932
+ const mount = await this.dataMount();
2933
+ const response = await fetch(this.collectionUrl(mount, objectName), {
2766
2934
  method: "POST",
2767
2935
  headers,
2768
2936
  body: JSON.stringify(data)
2769
2937
  });
2770
- return this.handleResponse(response);
2938
+ return this.handleResponse(response, this.mountNote(mount));
2771
2939
  }
2772
2940
  async updateRecord(objectName, data, headers) {
2773
2941
  const { id, ...fields } = data;
2774
2942
  if (!id) throw new Error("Update record requires id in payload");
2775
- const response = await fetch(this.recordUrl(objectName, id), {
2943
+ const mount = await this.dataMount();
2944
+ const response = await fetch(this.recordUrl(mount, objectName, id), {
2776
2945
  method: "PATCH",
2777
2946
  headers,
2778
2947
  body: JSON.stringify(fields)
2779
2948
  });
2780
- return this.handleResponse(response);
2949
+ return this.handleResponse(response, this.mountNote(mount));
2781
2950
  }
2782
2951
  async deleteRecord(objectName, data, headers) {
2783
2952
  const id = data.id;
2784
2953
  if (!id) throw new Error("Delete record requires id in payload");
2785
- const response = await fetch(this.recordUrl(objectName, id), {
2954
+ const mount = await this.dataMount();
2955
+ const response = await fetch(this.recordUrl(mount, objectName, id), {
2786
2956
  method: "DELETE",
2787
2957
  headers
2788
2958
  });
2789
- return this.handleResponse(response);
2959
+ return this.handleResponse(response, this.mountNote(mount));
2790
2960
  }
2791
2961
  async readRecord(objectName, data, headers) {
2792
2962
  const id = data.id;
2793
2963
  if (!id) throw new Error("Read record requires id in payload");
2794
- const response = await fetch(this.recordUrl(objectName, id), {
2964
+ const mount = await this.dataMount();
2965
+ const response = await fetch(this.recordUrl(mount, objectName, id), {
2795
2966
  method: "GET",
2796
2967
  headers
2797
2968
  });
2798
- return this.handleResponse(response);
2969
+ return this.handleResponse(response, this.mountNote(mount));
2799
2970
  }
2800
2971
  async queryRecords(objectName, data, headers) {
2801
- const response = await fetch(`${this.collectionUrl(objectName)}/query`, {
2972
+ const mount = await this.dataMount();
2973
+ const response = await fetch(`${this.collectionUrl(mount, objectName)}/query`, {
2802
2974
  method: "POST",
2803
2975
  headers,
2804
2976
  body: JSON.stringify(data)
2805
2977
  });
2806
- return this.handleResponse(response);
2978
+ return this.handleResponse(response, this.mountNote(mount));
2807
2979
  }
2808
2980
  async rawApiCall(endpoint, data, headers) {
2809
2981
  const method = data.method || "GET";
@@ -2816,10 +2988,13 @@ var HttpTestAdapter = class {
2816
2988
  });
2817
2989
  return this.handleResponse(response);
2818
2990
  }
2819
- async handleResponse(response) {
2991
+ async handleResponse(response, mountNote) {
2820
2992
  if (!response.ok) {
2821
2993
  const text = await response.text();
2822
- throw new Error(`HTTP Error ${response.status}: ${text}`);
2994
+ const wrongUrlShaped = response.status === 404 || response.status === 405;
2995
+ throw new Error(
2996
+ `HTTP Error ${response.status}: ${text}${wrongUrlShaped && mountNote ? ` \u2014 ${mountNote}` : ""}`
2997
+ );
2823
2998
  }
2824
2999
  const contentType = response.headers.get("content-type");
2825
3000
  if (contentType && contentType.includes("application/json")) {
@@ -4161,6 +4336,7 @@ var PluginSecurityScanner = class {
4161
4336
 
4162
4337
  // src/security/api-key.ts
4163
4338
  var import_node_crypto2 = require("crypto");
4339
+ var import_security = require("@objectstack/spec/security");
4164
4340
  var API_KEY_PREFIX = "osk_";
4165
4341
  var API_KEY_ENTROPY_BYTES = 32;
4166
4342
  var VISIBLE_PREFIX_LEN = 12;
@@ -4214,10 +4390,18 @@ function isExpired(value, nowMs) {
4214
4390
  if (Number.isNaN(ms)) return false;
4215
4391
  return ms <= nowMs;
4216
4392
  }
4217
- async function resolveApiKeyPrincipal(ql, headers, nowMs = Date.now()) {
4393
+ function effectiveTenancyPosture(tenancy) {
4394
+ if (!tenancy) return void 0;
4395
+ return (0, import_security.normalizeTenancyPosture)(tenancy.posture) ?? (tenancy.isolationActive ? "isolated" : "single");
4396
+ }
4397
+ async function resolveApiKeyPrincipal(ql, headers, nowMs = Date.now(), tenancyPosture) {
4398
+ const admission = await resolveApiKeyAdmission(ql, headers, nowMs, tenancyPosture);
4399
+ return admission.outcome === "admitted" ? admission.principal : void 0;
4400
+ }
4401
+ async function resolveApiKeyAdmission(ql, headers, nowMs = Date.now(), tenancyPosture) {
4218
4402
  const apiKey = extractApiKey(headers);
4219
- if (!apiKey) return void 0;
4220
- if (!ql || typeof ql.find !== "function") return void 0;
4403
+ if (!apiKey) return { outcome: "none" };
4404
+ if (!ql || typeof ql.find !== "function") return { outcome: "none" };
4221
4405
  let rows;
4222
4406
  try {
4223
4407
  rows = await ql.find("sys_api_key", {
@@ -4226,19 +4410,29 @@ async function resolveApiKeyPrincipal(ql, headers, nowMs = Date.now()) {
4226
4410
  context: { isSystem: true }
4227
4411
  });
4228
4412
  } catch {
4229
- return void 0;
4413
+ return { outcome: "none" };
4230
4414
  }
4231
4415
  if (rows && rows.value) rows = rows.value;
4232
4416
  const row = Array.isArray(rows) ? rows[0] : void 0;
4233
- if (!row || row.revoked === true) return void 0;
4417
+ if (!row || row.revoked === true) return { outcome: "none" };
4234
4418
  const expiresAt = row.expires_at ?? row.expiresAt;
4235
- if (isExpired(expiresAt, nowMs)) return void 0;
4419
+ if (isExpired(expiresAt, nowMs)) return { outcome: "none" };
4236
4420
  const userId = row.user_id ?? row.userId;
4237
- if (!userId || typeof userId !== "string") return void 0;
4421
+ if (!userId || typeof userId !== "string") return { outcome: "none" };
4422
+ const tenantId = typeof row.active_organization_id === "string" && row.active_organization_id ? row.active_organization_id : void 0;
4423
+ if (!tenantId && tenancyPosture) {
4424
+ const posture = tenancyPosture;
4425
+ if ((0, import_security.postureEnforcesWall)(posture) && !(0, import_security.postureUsesUnionScope)(posture)) {
4426
+ return {
4427
+ outcome: "refused",
4428
+ reason: "organization_required",
4429
+ message: "This API key carries no organization and cannot be used under the `isolated` tenancy posture, where every organization-scoped read is walled to an active organization. Mint a replacement key \u2014 new keys inherit the minter\u2019s active organization."
4430
+ };
4431
+ }
4432
+ }
4238
4433
  return {
4239
- userId,
4240
- tenantId: row.organization_id ?? row.organizationId ?? void 0,
4241
- scopes: parseScopes(row.scopes)
4434
+ outcome: "admitted",
4435
+ principal: { userId, tenantId, scopes: parseScopes(row.scopes) }
4242
4436
  };
4243
4437
  }
4244
4438
  function readHeader(headers, name) {
@@ -4266,6 +4460,7 @@ function safeJsonParse(s, fallback) {
4266
4460
 
4267
4461
  // src/security/resolve-authz-context.ts
4268
4462
  var import_spec = require("@objectstack/spec");
4463
+ var import_security2 = require("@objectstack/spec/security");
4269
4464
 
4270
4465
  // src/security/grant-validity.ts
4271
4466
  function toEpochMs(value) {
@@ -4348,6 +4543,15 @@ function postureVisibleRows(posture, rows, principal) {
4348
4543
  }
4349
4544
  }
4350
4545
 
4546
+ // src/security/row-active.ts
4547
+ var DEACTIVATED_VALUES = [false, 0, "0", "false"];
4548
+ function isRowActive(row) {
4549
+ if (!row) return false;
4550
+ const value = row.active;
4551
+ if (value === void 0 || value === null) return true;
4552
+ return !DEACTIVATED_VALUES.includes(value);
4553
+ }
4554
+
4351
4555
  // src/security/resolve-authz-context.ts
4352
4556
  function safeJsonParse2(s, fallback) {
4353
4557
  try {
@@ -4356,10 +4560,11 @@ function safeJsonParse2(s, fallback) {
4356
4560
  return fallback;
4357
4561
  }
4358
4562
  }
4359
- async function tryFind(ql, object, where, limit = 100) {
4563
+ async function tryFind(ql, object, where, limit = 100, organizationId) {
4360
4564
  if (!ql || typeof ql.find !== "function") return [];
4361
4565
  try {
4362
- let rows = await ql.find(object, { where, limit, context: { isSystem: true } });
4566
+ const context = organizationId ? { isSystem: true, tenantId: organizationId } : { isSystem: true };
4567
+ let rows = await ql.find(object, { where, limit, context });
4363
4568
  if (rows && rows.value) rows = rows.value;
4364
4569
  return Array.isArray(rows) ? rows : [];
4365
4570
  } catch {
@@ -4377,7 +4582,12 @@ async function resolveAuthzContext(input) {
4377
4582
  };
4378
4583
  let userId;
4379
4584
  let tenantId;
4380
- const keyPrincipal = await resolveApiKeyPrincipal(ql, headers, input.nowMs);
4585
+ const admission = await resolveApiKeyAdmission(ql, headers, input.nowMs, input.tenancyPosture);
4586
+ if (admission.outcome === "refused") {
4587
+ ctx.authRefusal = { reason: admission.reason, message: admission.message };
4588
+ return ctx;
4589
+ }
4590
+ const keyPrincipal = admission.outcome === "admitted" ? admission.principal : void 0;
4381
4591
  if (keyPrincipal) {
4382
4592
  userId = keyPrincipal.userId;
4383
4593
  tenantId = keyPrincipal.tenantId;
@@ -4405,6 +4615,22 @@ async function resolveAuthzContext(input) {
4405
4615
  seedPermissions: ctx.permissions,
4406
4616
  seedEmail: ctx.email
4407
4617
  });
4618
+ if (keyPrincipal?.tenantId && input.tenancyPosture) {
4619
+ const posture = input.tenancyPosture;
4620
+ if ((0, import_security2.postureEnforcesWall)(posture) && !grants.accessible_org_ids.includes(keyPrincipal.tenantId)) {
4621
+ return {
4622
+ positions: [],
4623
+ permissions: [],
4624
+ systemPermissions: [],
4625
+ org_user_ids: [],
4626
+ accessible_org_ids: [],
4627
+ authRefusal: {
4628
+ reason: "organization_membership_ended",
4629
+ message: "This API key authenticates into an organization its owner is no longer a member of. The key was not revoked \u2014 the membership that backed it ended."
4630
+ }
4631
+ };
4632
+ }
4633
+ }
4408
4634
  ctx.positions = grants.positions;
4409
4635
  ctx.permissions = grants.permissions;
4410
4636
  ctx.systemPermissions = grants.systemPermissions;
@@ -4436,12 +4662,19 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4436
4662
  }
4437
4663
  return userRow;
4438
4664
  };
4665
+ const needsUserRow = !grants.email || !grants.permissions.includes("ai_seat");
4666
+ const [, members, userPositionRows, orgMembersLeg, upsRowsAll] = await Promise.all([
4667
+ needsUserRow ? getUserRow() : Promise.resolve(void 0),
4668
+ tryFind(ql, "sys_member", { user_id: userId }, 200),
4669
+ tryFind(ql, "sys_user_position", { user_id: userId }, 200),
4670
+ tenantId ? tryFind(ql, "sys_member", { organization_id: tenantId }, 1e3) : Promise.resolve([]),
4671
+ tryFind(ql, "sys_user_permission_set", { user_id: userId }, 100)
4672
+ ]);
4439
4673
  if (!grants.email) {
4440
4674
  const u = await getUserRow();
4441
4675
  if (u?.email) grants.email = String(u.email);
4442
4676
  }
4443
4677
  const nowMs = opts.nowMs ?? Date.now();
4444
- const members = await tryFind(ql, "sys_member", { user_id: userId }, 200);
4445
4678
  const accessibleOrgIds = /* @__PURE__ */ new Set();
4446
4679
  for (const m of members) {
4447
4680
  if (!isGrantActive(m, nowMs)) continue;
@@ -4449,7 +4682,9 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4449
4682
  if (typeof org === "string" && org) accessibleOrgIds.add(org);
4450
4683
  }
4451
4684
  grants.accessible_org_ids = Array.from(accessibleOrgIds);
4452
- const activeMembers = tenantId ? members.filter((m) => (m.organization_id ?? m.organizationId) === tenantId) : members;
4685
+ const activeMembers = members.filter(
4686
+ (m) => isGrantActive(m, nowMs) && (!tenantId || (m.organization_id ?? m.organizationId) === tenantId)
4687
+ );
4453
4688
  for (const m of activeMembers) {
4454
4689
  if (m.role && typeof m.role === "string") {
4455
4690
  for (const raw of m.role.split(",").map((s) => s.trim()).filter(Boolean)) {
@@ -4458,7 +4693,6 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4458
4693
  }
4459
4694
  }
4460
4695
  }
4461
- const userPositionRows = await tryFind(ql, "sys_user_position", { user_id: userId }, 200);
4462
4696
  for (const ur of userPositionRows) {
4463
4697
  const org = ur.organization_id ?? null;
4464
4698
  if (org && tenantId && org !== tenantId) continue;
@@ -4467,14 +4701,13 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4467
4701
  if (typeof r === "string" && r && !grants.positions.includes(r)) grants.positions.push(r);
4468
4702
  }
4469
4703
  if (tenantId) {
4470
- const orgMembers = await tryFind(ql, "sys_member", { organization_id: tenantId }, 1e3);
4704
+ const orgMembers = orgMembersLeg;
4471
4705
  const ids = new Set(
4472
4706
  orgMembers.map((m) => m.user_id ?? m.userId).filter((v) => typeof v === "string" && v.length > 0)
4473
4707
  );
4474
4708
  ids.add(userId);
4475
4709
  grants.org_user_ids = Array.from(ids);
4476
4710
  }
4477
- const upsRowsAll = await tryFind(ql, "sys_user_permission_set", { user_id: userId }, 100);
4478
4711
  const upsRows = upsRowsAll.filter((r) => isGrantActive(r, nowMs));
4479
4712
  const psIds = new Set(
4480
4713
  upsRows.filter((r) => {
@@ -4488,8 +4721,14 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4488
4721
  let hasPlatformAdminGrant = false;
4489
4722
  if (!grants.positions.includes("everyone")) grants.positions.push("everyone");
4490
4723
  if (grants.positions.length > 0) {
4491
- const positionRows = await tryFind(ql, "sys_position", { name: { $in: grants.positions } }, 100);
4492
- const positionIds = positionRows.map((r) => r.id).filter(Boolean);
4724
+ const positionRows = await tryFind(ql, "sys_position", { name: { $in: grants.positions } }, 200, tenantId);
4725
+ const deactivatedNames = new Set(
4726
+ positionRows.filter((r) => !isRowActive(r)).map((r) => r.name).filter(Boolean)
4727
+ );
4728
+ if (deactivatedNames.size > 0) {
4729
+ grants.positions = grants.positions.filter((n) => !deactivatedNames.has(n));
4730
+ }
4731
+ const positionIds = positionRows.filter((r) => isRowActive(r)).map((r) => r.id).filter(Boolean);
4493
4732
  if (positionIds.length > 0) {
4494
4733
  const rpsRows = await tryFind(ql, "sys_position_permission_set", { position_id: { $in: positionIds } }, 500);
4495
4734
  for (const r of rpsRows) {
@@ -4499,7 +4738,8 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4499
4738
  }
4500
4739
  }
4501
4740
  if (psIds.size > 0) {
4502
- const psRows = await tryFind(ql, "sys_permission_set", { id: { $in: Array.from(psIds) } }, 500);
4741
+ const psRowsAll = await tryFind(ql, "sys_permission_set", { id: { $in: Array.from(psIds) } }, 500);
4742
+ const psRows = psRowsAll.filter((r) => isRowActive(r));
4503
4743
  const tabRank = { hidden: 0, default_off: 1, default_on: 2, visible: 3 };
4504
4744
  const mergedTabs = {};
4505
4745
  for (const ps of psRows) {
@@ -4559,34 +4799,89 @@ function coerceCurrency(value) {
4559
4799
  const s = typeof value === "string" ? value.trim().toUpperCase() : "";
4560
4800
  return /^[A-Z]{3}$/.test(s) ? s : void 0;
4561
4801
  }
4802
+ var LOCALIZATION_FAILURE_CACHE_TTL_MS = 3e4;
4803
+ var localizationFailureCache = /* @__PURE__ */ new WeakMap();
4562
4804
  async function resolveLocalizationContext(input) {
4805
+ const { ql, tenantId, userId } = input;
4806
+ const cacheKey = `${tenantId ?? ""}|${userId ?? ""}`;
4807
+ if (ql && typeof ql === "object") {
4808
+ const hit = localizationFailureCache.get(ql)?.get(cacheKey);
4809
+ if (hit && hit.expiresAt > Date.now()) return hit.value;
4810
+ }
4811
+ const { value, failed } = await resolveLocalizationContextUncached(input);
4812
+ if (failed && ql && typeof ql === "object") {
4813
+ const bucket = localizationFailureCache.get(ql) ?? /* @__PURE__ */ new Map();
4814
+ bucket.set(cacheKey, { value, expiresAt: Date.now() + LOCALIZATION_FAILURE_CACHE_TTL_MS });
4815
+ localizationFailureCache.set(ql, bucket);
4816
+ }
4817
+ return value;
4818
+ }
4819
+ async function resolveLocalizationContextUncached(input) {
4563
4820
  const { ql, settings, tenantId, userId } = input;
4821
+ let failed = false;
4564
4822
  try {
4565
4823
  if (settings && typeof settings.get === "function") {
4566
4824
  const sctx = { tenantId, userId };
4567
- const [tzRes, localeRes, currencyRes] = await Promise.all([
4568
- settings.get("localization", "timezone", sctx).catch(() => void 0),
4569
- settings.get("localization", "locale", sctx).catch(() => void 0),
4570
- settings.get("localization", "currency", sctx).catch(() => void 0)
4571
- ]);
4825
+ let tzRes;
4826
+ let localeRes;
4827
+ let currencyRes;
4828
+ if (typeof settings.getMany === "function") {
4829
+ try {
4830
+ const many = await settings.getMany("localization", ["timezone", "locale", "currency"], sctx);
4831
+ tzRes = many.timezone;
4832
+ localeRes = many.locale;
4833
+ currencyRes = many.currency;
4834
+ } catch {
4835
+ failed = true;
4836
+ }
4837
+ } else {
4838
+ [tzRes, localeRes, currencyRes] = await Promise.all([
4839
+ settings.get("localization", "timezone", sctx).catch(() => {
4840
+ failed = true;
4841
+ return void 0;
4842
+ }),
4843
+ settings.get("localization", "locale", sctx).catch(() => {
4844
+ failed = true;
4845
+ return void 0;
4846
+ }),
4847
+ settings.get("localization", "currency", sctx).catch(() => {
4848
+ failed = true;
4849
+ return void 0;
4850
+ })
4851
+ ]);
4852
+ }
4572
4853
  const tz = coerceTimeZone(tzRes?.value);
4573
4854
  const locale = coerceLocale(localeRes?.value);
4574
4855
  const currency = coerceCurrency(currencyRes?.value);
4575
- if (tz || locale || currency) return { timezone: tz ?? "UTC", locale: locale ?? "en-US", currency };
4856
+ if (tz || locale || currency) {
4857
+ return { value: { timezone: tz ?? "UTC", locale: locale ?? "en-US", currency }, failed: false };
4858
+ }
4576
4859
  }
4577
4860
  } catch {
4861
+ failed = true;
4862
+ }
4863
+ let rows = [];
4864
+ if (ql && typeof ql.find === "function") {
4865
+ try {
4866
+ let result = await ql.find("sys_setting", {
4867
+ where: { namespace: "localization", key: { $in: ["timezone", "locale", "currency"] }, scope: "tenant" },
4868
+ limit: 10,
4869
+ context: { isSystem: true }
4870
+ });
4871
+ if (result && result.value) result = result.value;
4872
+ rows = Array.isArray(result) ? result : [];
4873
+ } catch {
4874
+ failed = true;
4875
+ }
4578
4876
  }
4579
- const rows = await tryFind(
4580
- ql,
4581
- "sys_setting",
4582
- { namespace: "localization", key: { $in: ["timezone", "locale", "currency"] }, scope: "tenant" },
4583
- 10
4584
- );
4585
4877
  const valueOf = (k) => rows.find((r) => r.key === k)?.value;
4586
4878
  return {
4587
- timezone: coerceTimeZone(valueOf("timezone")) ?? "UTC",
4588
- locale: coerceLocale(valueOf("locale")) ?? "en-US",
4589
- currency: coerceCurrency(valueOf("currency"))
4879
+ value: {
4880
+ timezone: coerceTimeZone(valueOf("timezone")) ?? "UTC",
4881
+ locale: coerceLocale(valueOf("locale")) ?? "en-US",
4882
+ currency: coerceCurrency(valueOf("currency"))
4883
+ },
4884
+ failed
4590
4885
  };
4591
4886
  }
4592
4887
 
@@ -4730,6 +5025,7 @@ var ANONYMOUS_DENY_CODE = "UNAUTHENTICATED";
4730
5025
  var ANONYMOUS_DENY_MESSAGE = "Authentication is required to access this endpoint.";
4731
5026
  var ANONYMOUS_DENY_BODY = {
4732
5027
  error: ANONYMOUS_DENY_CODE,
5028
+ code: ANONYMOUS_DENY_CODE,
4733
5029
  message: ANONYMOUS_DENY_MESSAGE
4734
5030
  };
4735
5031
  function shouldDenyAnonymous(input) {
@@ -4743,6 +5039,76 @@ function shouldDenyAnonymous(input) {
4743
5039
  return true;
4744
5040
  }
4745
5041
 
5042
+ // src/security/admin-standing-surface.ts
5043
+ var ADMIN_STANDING_SURFACE = {
5044
+ sys_permission_set: {
5045
+ role: "derives",
5046
+ reason: "The row `platform_admin` is resolved BY NAME from (\xA76b). Renaming it, deleting it or switching it off (ADR-0049 `active`, read here since #8613) un-makes every platform admin at once, with no identity table touched.",
5047
+ columns: [
5048
+ "id",
5049
+ "name",
5050
+ "active",
5051
+ "system_permissions",
5052
+ "systemPermissions",
5053
+ "tab_permissions",
5054
+ "tabPermissions"
5055
+ ]
5056
+ },
5057
+ sys_user_permission_set: {
5058
+ role: "derives",
5059
+ reason: "The grant that makes a user a platform admin: an UNSCOPED, in-window (ADR-0091) grant of `admin_full_access` (\xA76). Re-pointing it, scoping it to an organization or moving it out of its window revokes the standing while leaving the row in place.",
5060
+ columns: [
5061
+ "user_id",
5062
+ "permission_set_id",
5063
+ "permissionSetId",
5064
+ "organization_id",
5065
+ "organizationId",
5066
+ "valid_from",
5067
+ "validFrom",
5068
+ "valid_until",
5069
+ "validUntil"
5070
+ ]
5071
+ },
5072
+ sys_member: {
5073
+ role: "derives",
5074
+ reason: "Organization owner/admin standing (\xA73). The graded `role` is projected into `positions` here and separately drives the `organization_admin` capability grant, which is what the posture ladder reads; the break-glass guard counts the same rows one step earlier, by grade (ADR-0108). Either way a downgrade of the last graded membership is a write that can empty the administrator population.",
5075
+ columns: [
5076
+ "user_id",
5077
+ "userId",
5078
+ "organization_id",
5079
+ "organizationId",
5080
+ "role",
5081
+ "valid_from",
5082
+ "validFrom",
5083
+ "valid_until",
5084
+ "validUntil"
5085
+ ]
5086
+ },
5087
+ sys_user: {
5088
+ role: "reads-only",
5089
+ reason: "Read for the `current_user.email` RLS fallback and the ADR-0024 `ai_seat` synthesis (\xA77). Neither confers administrator standing. The guard does watch this table, but for the ban/delete WRITE SHAPES \u2014 `banned` is never read here, so it is not a derivation column and carries no standing-key list."
5090
+ },
5091
+ sys_user_position: {
5092
+ role: "reads-only",
5093
+ reason: "ADR-0057 D4 platform-RBAC position assignments (\xA74). A position can carry permission sets (see `sys_position_permission_set`) but never platform-admin standing \u2014 \xA76b requires the set to be reached through an unscoped USER grant (`unscopedUserPsIds`), so a position-bound `admin_full_access` resolves the set name into `permissions` and leaves `hasPlatformAdminGrant` false."
5094
+ },
5095
+ sys_position: {
5096
+ role: "reads-only",
5097
+ reason: "Read to drop DEACTIVATED positions (ADR-0049, \xA76a). Same reason as `sys_user_position`: the position path cannot reach `hasPlatformAdminGrant`."
5098
+ },
5099
+ sys_position_permission_set: {
5100
+ role: "reads-only",
5101
+ reason: "Position-bound permission sets (\xA76a). Contributes ids to `psIds` \u2014 and therefore names to `permissions` \u2014 but not to `unscopedUserPsIds`, which is the set \xA76b tests for platform-admin standing."
5102
+ }
5103
+ };
5104
+ function adminStandingTables() {
5105
+ return Object.entries(ADMIN_STANDING_SURFACE).filter(([, t]) => t.role === "derives").map(([name]) => name).sort();
5106
+ }
5107
+ function adminStandingColumns(table) {
5108
+ const entry = ADMIN_STANDING_SURFACE[table];
5109
+ return entry?.role === "derives" ? entry.columns : void 0;
5110
+ }
5111
+
4746
5112
  // src/security/audience-binding-suggestion-status.ts
4747
5113
  var AUDIENCE_BINDING_SUGGESTION_STATUSES = {
4748
5114
  pending: true,
@@ -4793,10 +5159,26 @@ function calendarPartsInTzOrUtc(d, tz) {
4793
5159
  }
4794
5160
  function zonedDateStartToUtcMs(ymd2, tz) {
4795
5161
  const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ymd2);
4796
- const wallAsUtc = m ? Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])) : NaN;
5162
+ if (!m) return NaN;
5163
+ return zonedWallClockToUtcMs(
5164
+ { year: Number(m[1]), month: Number(m[2]), day: Number(m[3]) },
5165
+ tz
5166
+ );
5167
+ }
5168
+ function zonedWallClockToUtcMs(parts, tz) {
5169
+ const wallAsUtc = Date.UTC(
5170
+ parts.year,
5171
+ parts.month - 1,
5172
+ parts.day,
5173
+ parts.hour ?? 0,
5174
+ parts.minute ?? 0,
5175
+ parts.second ?? 0,
5176
+ parts.millisecond ?? 0
5177
+ );
4797
5178
  if (!tz || tz === "UTC" || Number.isNaN(wallAsUtc)) return wallAsUtc;
4798
5179
  try {
4799
5180
  const offsetAt = (t) => {
5181
+ const whole = Math.floor(t / 1e3) * 1e3;
4800
5182
  const p = new Intl.DateTimeFormat("en-US", {
4801
5183
  timeZone: tz,
4802
5184
  hourCycle: "h23",
@@ -4806,9 +5188,9 @@ function zonedDateStartToUtcMs(ymd2, tz) {
4806
5188
  hour: "2-digit",
4807
5189
  minute: "2-digit",
4808
5190
  second: "2-digit"
4809
- }).formatToParts(new Date(t));
5191
+ }).formatToParts(new Date(whole));
4810
5192
  const g = (k) => Number(p.find((x) => x.type === k)?.value);
4811
- return Date.UTC(g("year"), g("month") - 1, g("day"), g("hour"), g("minute"), g("second")) - t;
5193
+ return Date.UTC(g("year"), g("month") - 1, g("day"), g("hour"), g("minute"), g("second")) - whole;
4812
5194
  };
4813
5195
  const off1 = offsetAt(wallAsUtc - offsetAt(wallAsUtc));
4814
5196
  return wallAsUtc - off1;
@@ -5619,6 +6001,37 @@ function filterTokenContextFrom(execCtx, now) {
5619
6001
  };
5620
6002
  }
5621
6003
 
6004
+ // src/utils/temporal-comparand.ts
6005
+ var import_data3 = require("@objectstack/spec/data");
6006
+ function temporalComparandKind(fieldType) {
6007
+ if (fieldType === "datetime") return "datetime";
6008
+ if (fieldType === "date") return "date";
6009
+ if (fieldType === "time") return "time";
6010
+ return null;
6011
+ }
6012
+ function readsAsInstant(s) {
6013
+ if (/^-?\d+$/.test(s)) return Number.isFinite(new Date(Number(s)).getTime());
6014
+ const iso = /^\d{4}-\d{2}-\d{2}$/.test(s) ? `${s}T00:00:00.000Z` : /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(:\d{2}(\.\d+)?)?$/.test(s) ? `${s.replace(" ", "T")}Z` : s;
6015
+ return Number.isFinite(Date.parse(iso));
6016
+ }
6017
+ function readsAsCalendarDay(s) {
6018
+ return /^\d{4}-\d{2}-\d{2}/.test(s);
6019
+ }
6020
+ function readsAsWallClock(s) {
6021
+ const m = /^(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?$/.exec(s);
6022
+ if (!m) return false;
6023
+ return Number(m[1]) <= 23 && Number(m[2]) <= 59 && Number(m[3] ?? "0") <= 59;
6024
+ }
6025
+ function isUninterpretableTemporalComparand(kind, value) {
6026
+ if (typeof value !== "string") return false;
6027
+ const s = value.trim();
6028
+ if (s === "") return false;
6029
+ if ((0, import_data3.classifyFilterToken)(value) !== null) return false;
6030
+ if (kind === "datetime") return !readsAsInstant(s);
6031
+ if (kind === "date") return !readsAsCalendarDay(s);
6032
+ return !(readsAsWallClock(s) || readsAsInstant(s));
6033
+ }
6034
+
5622
6035
  // src/utils/record-not-found.ts
5623
6036
  function recordNotFoundError(object, id) {
5624
6037
  const err = new Error(`Record ${id} not found in ${object}`);
@@ -6604,8 +7017,12 @@ var NamespaceResolver = class {
6604
7017
  return `${shortName}_${ns}`;
6605
7018
  }
6606
7019
  };
7020
+
7021
+ // src/index.ts
7022
+ var import_contracts = require("@objectstack/spec/contracts");
6607
7023
  // Annotate the CommonJS export names for ESM import in node:
6608
7024
  0 && (module.exports = {
7025
+ ADMIN_STANDING_SURFACE,
6609
7026
  ANONYMOUS_DENY_BODY,
6610
7027
  ANONYMOUS_DENY_CODE,
6611
7028
  ANONYMOUS_DENY_MESSAGE,
@@ -6641,8 +7058,11 @@ var NamespaceResolver = class {
6641
7058
  SecurePluginContext,
6642
7059
  SemanticVersionManager,
6643
7060
  ServiceLifecycle,
7061
+ UNMATCHED_ROUTE_PATTERN,
6644
7062
  UnknownFilterTokenError,
6645
7063
  UnresolvedFilterTokenError,
7064
+ adminStandingColumns,
7065
+ adminStandingTables,
6646
7066
  assembleExecutionContext,
6647
7067
  assembleExecutionContextOrGuest,
6648
7068
  assertInitServiceRequirements,
@@ -6667,6 +7087,7 @@ var NamespaceResolver = class {
6667
7087
  defaultIsTransientError,
6668
7088
  derivePosture,
6669
7089
  describeInitOrderFault,
7090
+ effectiveTenancyPosture,
6670
7091
  engineCanRollBack,
6671
7092
  evaluateAuthGate,
6672
7093
  extractApiKey,
@@ -6684,6 +7105,8 @@ var NamespaceResolver = class {
6684
7105
  isGrantActive,
6685
7106
  isGrantExpired,
6686
7107
  isNode,
7108
+ isRowActive,
7109
+ isUninterpretableTemporalComparand,
6687
7110
  nextUtcCalendarDay,
6688
7111
  normalizeAuthGate,
6689
7112
  omitInternalFieldsFromWriteResponse,
@@ -6694,6 +7117,7 @@ var NamespaceResolver = class {
6694
7117
  readAuthoredTranslationLayer,
6695
7118
  readRunJournal,
6696
7119
  recordNotFoundError,
7120
+ resolveApiKeyAdmission,
6697
7121
  resolveApiKeyPrincipal,
6698
7122
  resolveAuthzContext,
6699
7123
  resolveFilterToken,
@@ -6707,6 +7131,7 @@ var NamespaceResolver = class {
6707
7131
  safeExit,
6708
7132
  shouldDenyAnonymous,
6709
7133
  signPayload,
7134
+ temporalComparandKind,
6710
7135
  unknownAudienceBindingSuggestionStatusMessage,
6711
7136
  utcInstantMs,
6712
7137
  validateInitServiceContract,
@@ -6717,6 +7142,7 @@ var NamespaceResolver = class {
6717
7142
  wireAuthoredTranslationSync,
6718
7143
  withTransientRetry,
6719
7144
  withoutOperationPrivateKeys,
6720
- zonedDateStartToUtcMs
7145
+ zonedDateStartToUtcMs,
7146
+ zonedWallClockToUtcMs
6721
7147
  });
6722
7148
  //# sourceMappingURL=index.cjs.map