@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.js CHANGED
@@ -1355,35 +1355,6 @@ function createMemoryQueue() {
1355
1355
  };
1356
1356
  }
1357
1357
 
1358
- // src/fallbacks/memory-job.ts
1359
- function createMemoryJob() {
1360
- const jobs = /* @__PURE__ */ new Map();
1361
- return {
1362
- __serviceInfo: {
1363
- status: "degraded",
1364
- handlerReady: false,
1365
- 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."
1366
- },
1367
- _serviceName: "job",
1368
- async schedule(name, schedule, handler) {
1369
- jobs.set(name, { schedule, handler });
1370
- },
1371
- async cancel(name) {
1372
- jobs.delete(name);
1373
- },
1374
- async trigger(name, data) {
1375
- const job = jobs.get(name);
1376
- if (job?.handler) await job.handler({ jobId: name, data });
1377
- },
1378
- async getExecutions() {
1379
- return [];
1380
- },
1381
- async listJobs() {
1382
- return [...jobs.keys()];
1383
- }
1384
- };
1385
- }
1386
-
1387
1358
  // src/fallbacks/memory-i18n.ts
1388
1359
  import { normalizeSupportedLocales } from "@objectstack/spec/system";
1389
1360
  function deepMerge(target, source) {
@@ -1611,6 +1582,35 @@ function createMemoryMetadata() {
1611
1582
  };
1612
1583
  }
1613
1584
 
1585
+ // src/fallbacks/memory-job.ts
1586
+ function createMemoryJob() {
1587
+ const jobs = /* @__PURE__ */ new Map();
1588
+ return {
1589
+ __serviceInfo: {
1590
+ status: "degraded",
1591
+ handlerReady: false,
1592
+ 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."
1593
+ },
1594
+ _serviceName: "job",
1595
+ async schedule(name, schedule, handler) {
1596
+ jobs.set(name, { schedule, handler });
1597
+ },
1598
+ async cancel(name) {
1599
+ jobs.delete(name);
1600
+ },
1601
+ async trigger(name, data) {
1602
+ const job = jobs.get(name);
1603
+ if (job?.handler) await job.handler({ jobId: name, data });
1604
+ },
1605
+ async getExecutions() {
1606
+ return [];
1607
+ },
1608
+ async listJobs() {
1609
+ return [...jobs.keys()];
1610
+ }
1611
+ };
1612
+ }
1613
+
1614
1614
  // src/fallbacks/authored-translation-sync.ts
1615
1615
  import { LEGACY_OBJECT_FIRST_KEYS } from "@objectstack/spec/system";
1616
1616
  var OWNER_PROP = "__authoredTranslationSyncOwner";
@@ -1743,10 +1743,71 @@ var CORE_FALLBACK_FACTORIES = {
1743
1743
  metadata: createMemoryMetadata,
1744
1744
  cache: createMemoryCache,
1745
1745
  queue: createMemoryQueue,
1746
- job: createMemoryJob,
1747
1746
  i18n: createMemoryI18n
1748
1747
  };
1749
1748
 
1749
+ // src/plugin-registration.ts
1750
+ function versionLabel(plugin) {
1751
+ return plugin.version ? `v${plugin.version}` : "unversioned";
1752
+ }
1753
+ function describeSupersededRegistration(previous, next) {
1754
+ 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.`;
1755
+ }
1756
+ function registerPluginByName(registry, plugin, logger) {
1757
+ const previous = registry.get(plugin.name);
1758
+ if (previous !== void 0) {
1759
+ logger.warn(describeSupersededRegistration(previous, plugin), {
1760
+ plugin: plugin.name,
1761
+ supersededVersion: previous.version,
1762
+ supersedingVersion: plugin.version
1763
+ });
1764
+ }
1765
+ registry.set(plugin.name, plugin);
1766
+ return previous;
1767
+ }
1768
+
1769
+ // src/timeout-guard.ts
1770
+ var TimeoutGuard = class {
1771
+ constructor(timeoutMs, createTimeoutError) {
1772
+ /**
1773
+ * Settles `expiry` without a value. `Promise<never>` has no resolvable
1774
+ * value in the type system, but settling it is the entire point: it is
1775
+ * only ever called from `reclaim()`, i.e. after the race it guarded has
1776
+ * already been decided, so the resolution is discarded by construction and
1777
+ * can never become a race winner. The cast localises that argument here
1778
+ * rather than pushing a lie into every caller's return type.
1779
+ */
1780
+ this.settleExpiry = () => {
1781
+ };
1782
+ this.expiry = new Promise((resolve, reject) => {
1783
+ this.settleExpiry = resolve;
1784
+ this.timer = setTimeout(() => reject(createTimeoutError()), timeoutMs);
1785
+ });
1786
+ }
1787
+ /**
1788
+ * Reclaim the guard once the race it protects has been decided. Both
1789
+ * halves, always: the timer is cleared so it cannot fire against a
1790
+ * lifecycle phase that is already over, and `expiry` is settled so neither
1791
+ * it nor the race's reaction on it is retained.
1792
+ *
1793
+ * Idempotent — `clearTimeout` on a cleared handle and a second resolve on
1794
+ * a settled promise are both no-ops.
1795
+ */
1796
+ reclaim() {
1797
+ clearTimeout(this.timer);
1798
+ this.timer = void 0;
1799
+ this.settleExpiry();
1800
+ }
1801
+ };
1802
+ async function raceWithTimeout(operation, timeoutMs, createTimeoutError) {
1803
+ const guard = new TimeoutGuard(timeoutMs, createTimeoutError);
1804
+ try {
1805
+ return await Promise.race([operation, guard.expiry]);
1806
+ } finally {
1807
+ guard.reclaim();
1808
+ }
1809
+ }
1810
+
1750
1811
  // src/kernel.ts
1751
1812
  var ObjectKernel = class {
1752
1813
  constructor(config = {}) {
@@ -1831,6 +1892,14 @@ var ObjectKernel = class {
1831
1892
  }
1832
1893
  /**
1833
1894
  * Register a plugin with enhanced validation
1895
+ *
1896
+ * Duplicate names OVERWRITE, with one `warn` naming both versions — the
1897
+ * declared contract in `plugin-registration.ts`, applied identically by
1898
+ * `LiteKernel.use()` (#9864, maintainer ruling 2026-08-19). The overwrite
1899
+ * itself is unchanged: it is what lets an app config's `plugins` entry
1900
+ * supersede a plugin the CLI auto-registered earlier in the same boot
1901
+ * (#9863). What changes is that it is no longer silent, and no longer
1902
+ * disagrees with the other kernel.
1834
1903
  */
1835
1904
  async use(plugin) {
1836
1905
  if (this.state !== "idle") {
@@ -1841,11 +1910,13 @@ var ObjectKernel = class {
1841
1910
  throw new Error(`Failed to load plugin: ${plugin.name} - ${result.error?.message}`);
1842
1911
  }
1843
1912
  const pluginMeta = result.plugin;
1844
- this.plugins.set(pluginMeta.name, pluginMeta);
1845
- this.logger.info(`Plugin registered: ${pluginMeta.name}@${pluginMeta.version}`, {
1846
- plugin: pluginMeta.name,
1847
- version: pluginMeta.version
1848
- });
1913
+ const superseded = registerPluginByName(this.plugins, pluginMeta, this.logger);
1914
+ if (superseded === void 0) {
1915
+ this.logger.info(`Plugin registered: ${pluginMeta.name}@${pluginMeta.version}`, {
1916
+ plugin: pluginMeta.name,
1917
+ version: pluginMeta.version
1918
+ });
1919
+ }
1849
1920
  return this;
1850
1921
  }
1851
1922
  /**
@@ -2007,14 +2078,11 @@ var ObjectKernel = class {
2007
2078
  this.logger.info("Graceful shutdown started");
2008
2079
  const shutdownTimeoutError = new Error("Shutdown timeout exceeded");
2009
2080
  try {
2010
- const shutdownPromise = this.performShutdown();
2011
- const timeoutPromise = new Promise((_, reject) => {
2012
- const t = setTimeout(() => {
2013
- reject(shutdownTimeoutError);
2014
- }, this.config.shutdownTimeout);
2015
- if (t.unref) t.unref();
2016
- });
2017
- await Promise.race([shutdownPromise, timeoutPromise]);
2081
+ await raceWithTimeout(
2082
+ this.performShutdown(),
2083
+ this.config.shutdownTimeout,
2084
+ () => shutdownTimeoutError
2085
+ );
2018
2086
  this.state = "stopped";
2019
2087
  this.logger.info("\u2705 Graceful shutdown complete");
2020
2088
  } catch (error) {
@@ -2132,25 +2200,17 @@ var ObjectKernel = class {
2132
2200
  * as well: if the hook never settles and nothing else keeps the loop alive,
2133
2201
  * Node exits before the timer can fire and the timeout is never reported.
2134
2202
  * The guard has to stay ref'd exactly as long as the race is undecided,
2135
- * which is what `clearTimeout` in a `finally` expresses.
2203
+ * which is what clearing on settle expresses.
2136
2204
  *
2137
- * `operation` is widened to `T | PromiseLike<T>` because the Plugin
2138
- * contract permits a synchronous hook (`init`/`start` return
2139
- * `void | Promise<void>`); such a hook wins the race immediately and the
2140
- * guard is reclaimed on the same turn.
2205
+ * Clearing the timer was only half of it, though (#10604): the promise the
2206
+ * race still holds a reaction on has to SETTLE, or it and that reaction are
2207
+ * retained past the end of the run two leaking promises per boot, which
2208
+ * is what `vitest --detectAsyncLeaks` names here. Both halves now live in
2209
+ * `TimeoutGuard.reclaim()`, shared with `shutdown()`, so the two sites
2210
+ * cannot drift into doing one half each again.
2141
2211
  */
2142
2212
  async raceStartupTimeout(operation, timeout, message) {
2143
- let guard;
2144
- const timeoutPromise = new Promise((_, reject) => {
2145
- guard = setTimeout(() => {
2146
- reject(new Error(message));
2147
- }, timeout);
2148
- });
2149
- try {
2150
- return await Promise.race([operation, timeoutPromise]);
2151
- } finally {
2152
- clearTimeout(guard);
2153
- }
2213
+ return raceWithTimeout(operation, timeout, () => new Error(message));
2154
2214
  }
2155
2215
  /**
2156
2216
  * Whether a service is resolvable on this kernel right now — direct
@@ -2323,14 +2383,21 @@ var LiteKernel = class extends ObjectKernelBase {
2323
2383
  /**
2324
2384
  * Register a plugin
2325
2385
  * @param plugin - Plugin instance
2386
+ *
2387
+ * Duplicate names OVERWRITE, with one `warn` naming both versions — the
2388
+ * declared contract in `plugin-registration.ts`, applied identically by
2389
+ * `ObjectKernel.use()` (#9864, maintainer ruling 2026-08-19).
2390
+ *
2391
+ * This method used to `throw` `[Kernel] Plugin '<name>' already
2392
+ * registered` here while `ObjectKernel` overwrote silently, so one input
2393
+ * had two meanings depending on which kernel was running — and the kernel
2394
+ * that runs in production was the silent one. The ruling converged them on
2395
+ * the behaviour that already works (an app config superseding a plugin the
2396
+ * CLI auto-registered, #9863) and made it audible rather than removing it.
2326
2397
  */
2327
2398
  use(plugin) {
2328
2399
  this.validateIdle();
2329
- const pluginName = plugin.name;
2330
- if (this.plugins.has(pluginName)) {
2331
- throw new Error(`[Kernel] Plugin '${pluginName}' already registered`);
2332
- }
2333
- this.plugins.set(pluginName, plugin);
2400
+ registerPluginByName(this.plugins, plugin, this.logger);
2334
2401
  return this;
2335
2402
  }
2336
2403
  /**
@@ -2580,27 +2647,117 @@ var TestRunner = class {
2580
2647
 
2581
2648
  // src/qa/http-adapter.ts
2582
2649
  import { RestApiConfigSchema, CrudEndpointsConfigSchema } from "@objectstack/spec/api";
2583
- var dataPathCache;
2584
- function defaultDataPath() {
2585
- if (dataPathCache === void 0) {
2650
+ var conventionCache;
2651
+ function conventionMounts() {
2652
+ if (conventionCache === void 0) {
2586
2653
  const api = RestApiConfigSchema.parse({});
2587
2654
  const crud = CrudEndpointsConfigSchema.parse({});
2588
- dataPathCache = `${api.apiPath ?? `${api.basePath}/${api.version}`}${crud.dataPrefix}`;
2655
+ const apiBase = api.apiPath ?? `${api.basePath}/${api.version}`;
2656
+ conventionCache = { apiBase, dataPath: `${apiBase}${crud.dataPrefix}` };
2589
2657
  }
2590
- return dataPathCache;
2658
+ return conventionCache;
2591
2659
  }
2592
2660
  var HttpTestAdapter = class {
2593
2661
  constructor(baseUrl, authToken) {
2594
2662
  this.baseUrl = baseUrl;
2595
2663
  this.authToken = authToken;
2596
2664
  }
2597
- /** `{baseUrl}{apiBasePath}{dataPrefix}/{object}` the collection URL. */
2598
- collectionUrl(objectName) {
2599
- return `${this.baseUrl}${defaultDataPath()}/${encodeURIComponent(objectName)}`;
2665
+ /** The resolved data mount; probes at most once per adapter. */
2666
+ dataMount() {
2667
+ if (this.mountPromise === void 0) {
2668
+ this.mountPromise = this.resolveDataMount();
2669
+ }
2670
+ return this.mountPromise;
2671
+ }
2672
+ /**
2673
+ * Ask the server where it serves the Data Protocol, and fall back to the
2674
+ * convention — loudly — when it cannot say.
2675
+ *
2676
+ * ## [#7983] What the probe recovers, measured rather than assumed
2677
+ *
2678
+ * `@objectstack/client` answers the same question through discovery
2679
+ * (`getRoute`, `packages/client/src/index.ts`), and this follows it: prefer
2680
+ * the server's own `routes.data`, fall back to the convention. Measured on a
2681
+ * booted stack (REST generator + dispatcher bridge, three configs):
2682
+ *
2683
+ * | deployment | `{apiBase}/discovery` | serves |
2684
+ * |--------------------------------|-----------------------|---------------|
2685
+ * | stock | 200 `/api/v1/data` | `/api/v1/data`|
2686
+ * | `crud.dataPrefix: '/objects'` | 200 `/api/v1/objects` | `/api/v1/objects` |
2687
+ * | `api.apiPath: '/api/2026-01'` | **404** | `/api/2026-01/data` |
2688
+ *
2689
+ * So the probe closes the `dataPrefix` row exactly: `RestServer`'s discovery
2690
+ * handler substitutes the configured prefix into `routes.data`, and reading
2691
+ * it is strictly better than recomputing it here. The `apiPath` row it cannot
2692
+ * close, and the reason is structural rather than an oversight — `apiPath`
2693
+ * moves the base that discovery itself is mounted under, so the document that
2694
+ * would name the new mount is behind the very prefix we are missing.
2695
+ *
2696
+ * ⛔ And the one discovery document at a FIXED path does not rescue it:
2697
+ * `/.well-known/objectstack` is mounted at the site root by the dispatcher
2698
+ * bridge, but its `routes.data` is the DISPATCHER's own `${prefix}/data` —
2699
+ * measured as `/api/v1/data` under all three configs above, including the two
2700
+ * where the server serves elsewhere. Falling back to it would turn "we could
2701
+ * not resolve the mount" into "discovery told us `/api/v1/data`": the same
2702
+ * 404, now with a false provenance attached. Not probed, deliberately.
2703
+ *
2704
+ * Hence: one probe, then a diagnostic that NAMES the mount, the evidence and
2705
+ * the remedy. `api_call` takes the path it is given and is unaffected either
2706
+ * way — it stays the escape hatch for a host this cannot reach.
2707
+ */
2708
+ async resolveDataMount() {
2709
+ const { apiBase, dataPath } = conventionMounts();
2710
+ const probeUrl = `${this.baseUrl}${apiBase}/discovery`;
2711
+ let why;
2712
+ try {
2713
+ const headers = {};
2714
+ if (this.authToken) {
2715
+ headers["Authorization"] = `Bearer ${this.authToken}`;
2716
+ }
2717
+ const response = await fetch(probeUrl, { method: "GET", headers });
2718
+ if (response.ok) {
2719
+ const body = await response.json();
2720
+ const doc = body && typeof body === "object" && "routes" in body ? body : body?.data;
2721
+ const advertised = doc?.routes?.data;
2722
+ if (typeof advertised === "string" && advertised.length > 0) {
2723
+ return {
2724
+ path: advertised,
2725
+ source: "discovery",
2726
+ why: `GET ${probeUrl} advertised routes.data`
2727
+ };
2728
+ }
2729
+ why = `GET ${probeUrl} answered ${response.status} but carried no routes.data`;
2730
+ } else {
2731
+ why = `GET ${probeUrl} answered ${response.status}`;
2732
+ }
2733
+ } catch (error) {
2734
+ why = `GET ${probeUrl} could not be reached (${error.message})`;
2735
+ }
2736
+ const mount = { path: dataPath, source: "convention", why };
2737
+ console.warn(
2738
+ `[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.`
2739
+ );
2740
+ return mount;
2741
+ }
2742
+ /** `{baseUrl}{dataMount}/{object}` — the collection URL. */
2743
+ collectionUrl(mount, objectName) {
2744
+ return `${this.baseUrl}${mount.path}/${encodeURIComponent(objectName)}`;
2600
2745
  }
2601
2746
  /** `{collection}/{id}` — the single-record URL. */
2602
- recordUrl(objectName, id) {
2603
- return `${this.collectionUrl(objectName)}/${encodeURIComponent(String(id))}`;
2747
+ recordUrl(mount, objectName, id) {
2748
+ return `${this.collectionUrl(mount, objectName)}/${encodeURIComponent(String(id))}`;
2749
+ }
2750
+ /**
2751
+ * The provenance clause appended to a failed record action's error.
2752
+ *
2753
+ * The card this closes is about a 404 that reads like the author's own URL
2754
+ * mistake; the mount is the one fact that distinguishes the two, so it rides
2755
+ * on the failure itself rather than only on a warning printed earlier in the
2756
+ * transcript.
2757
+ */
2758
+ mountNote(mount) {
2759
+ const how = mount.source === "discovery" ? "resolved from discovery" : "assumed by convention \u2014 the server was not able to state it";
2760
+ return `record actions addressed ${this.baseUrl}${mount.path} (data mount ${how}: ${mount.why})`;
2604
2761
  }
2605
2762
  async execute(action, _context) {
2606
2763
  const headers = {
@@ -2633,48 +2790,53 @@ var HttpTestAdapter = class {
2633
2790
  }
2634
2791
  }
2635
2792
  async createRecord(objectName, data, headers) {
2636
- const response = await fetch(this.collectionUrl(objectName), {
2793
+ const mount = await this.dataMount();
2794
+ const response = await fetch(this.collectionUrl(mount, objectName), {
2637
2795
  method: "POST",
2638
2796
  headers,
2639
2797
  body: JSON.stringify(data)
2640
2798
  });
2641
- return this.handleResponse(response);
2799
+ return this.handleResponse(response, this.mountNote(mount));
2642
2800
  }
2643
2801
  async updateRecord(objectName, data, headers) {
2644
2802
  const { id, ...fields } = data;
2645
2803
  if (!id) throw new Error("Update record requires id in payload");
2646
- const response = await fetch(this.recordUrl(objectName, id), {
2804
+ const mount = await this.dataMount();
2805
+ const response = await fetch(this.recordUrl(mount, objectName, id), {
2647
2806
  method: "PATCH",
2648
2807
  headers,
2649
2808
  body: JSON.stringify(fields)
2650
2809
  });
2651
- return this.handleResponse(response);
2810
+ return this.handleResponse(response, this.mountNote(mount));
2652
2811
  }
2653
2812
  async deleteRecord(objectName, data, headers) {
2654
2813
  const id = data.id;
2655
2814
  if (!id) throw new Error("Delete record requires id in payload");
2656
- const response = await fetch(this.recordUrl(objectName, id), {
2815
+ const mount = await this.dataMount();
2816
+ const response = await fetch(this.recordUrl(mount, objectName, id), {
2657
2817
  method: "DELETE",
2658
2818
  headers
2659
2819
  });
2660
- return this.handleResponse(response);
2820
+ return this.handleResponse(response, this.mountNote(mount));
2661
2821
  }
2662
2822
  async readRecord(objectName, data, headers) {
2663
2823
  const id = data.id;
2664
2824
  if (!id) throw new Error("Read record requires id in payload");
2665
- const response = await fetch(this.recordUrl(objectName, id), {
2825
+ const mount = await this.dataMount();
2826
+ const response = await fetch(this.recordUrl(mount, objectName, id), {
2666
2827
  method: "GET",
2667
2828
  headers
2668
2829
  });
2669
- return this.handleResponse(response);
2830
+ return this.handleResponse(response, this.mountNote(mount));
2670
2831
  }
2671
2832
  async queryRecords(objectName, data, headers) {
2672
- const response = await fetch(`${this.collectionUrl(objectName)}/query`, {
2833
+ const mount = await this.dataMount();
2834
+ const response = await fetch(`${this.collectionUrl(mount, objectName)}/query`, {
2673
2835
  method: "POST",
2674
2836
  headers,
2675
2837
  body: JSON.stringify(data)
2676
2838
  });
2677
- return this.handleResponse(response);
2839
+ return this.handleResponse(response, this.mountNote(mount));
2678
2840
  }
2679
2841
  async rawApiCall(endpoint, data, headers) {
2680
2842
  const method = data.method || "GET";
@@ -2687,10 +2849,13 @@ var HttpTestAdapter = class {
2687
2849
  });
2688
2850
  return this.handleResponse(response);
2689
2851
  }
2690
- async handleResponse(response) {
2852
+ async handleResponse(response, mountNote) {
2691
2853
  if (!response.ok) {
2692
2854
  const text = await response.text();
2693
- throw new Error(`HTTP Error ${response.status}: ${text}`);
2855
+ const wrongUrlShaped = response.status === 404 || response.status === 405;
2856
+ throw new Error(
2857
+ `HTTP Error ${response.status}: ${text}${wrongUrlShaped && mountNote ? ` \u2014 ${mountNote}` : ""}`
2858
+ );
2694
2859
  }
2695
2860
  const contentType = response.headers.get("content-type");
2696
2861
  if (contentType && contentType.includes("application/json")) {
@@ -4032,6 +4197,7 @@ var PluginSecurityScanner = class {
4032
4197
 
4033
4198
  // src/security/api-key.ts
4034
4199
  import { createHash, randomBytes } from "crypto";
4200
+ import { postureEnforcesWall, postureUsesUnionScope, normalizeTenancyPosture } from "@objectstack/spec/security";
4035
4201
  var API_KEY_PREFIX = "osk_";
4036
4202
  var API_KEY_ENTROPY_BYTES = 32;
4037
4203
  var VISIBLE_PREFIX_LEN = 12;
@@ -4085,10 +4251,18 @@ function isExpired(value, nowMs) {
4085
4251
  if (Number.isNaN(ms)) return false;
4086
4252
  return ms <= nowMs;
4087
4253
  }
4088
- async function resolveApiKeyPrincipal(ql, headers, nowMs = Date.now()) {
4254
+ function effectiveTenancyPosture(tenancy) {
4255
+ if (!tenancy) return void 0;
4256
+ return normalizeTenancyPosture(tenancy.posture) ?? (tenancy.isolationActive ? "isolated" : "single");
4257
+ }
4258
+ async function resolveApiKeyPrincipal(ql, headers, nowMs = Date.now(), tenancyPosture) {
4259
+ const admission = await resolveApiKeyAdmission(ql, headers, nowMs, tenancyPosture);
4260
+ return admission.outcome === "admitted" ? admission.principal : void 0;
4261
+ }
4262
+ async function resolveApiKeyAdmission(ql, headers, nowMs = Date.now(), tenancyPosture) {
4089
4263
  const apiKey = extractApiKey(headers);
4090
- if (!apiKey) return void 0;
4091
- if (!ql || typeof ql.find !== "function") return void 0;
4264
+ if (!apiKey) return { outcome: "none" };
4265
+ if (!ql || typeof ql.find !== "function") return { outcome: "none" };
4092
4266
  let rows;
4093
4267
  try {
4094
4268
  rows = await ql.find("sys_api_key", {
@@ -4097,19 +4271,29 @@ async function resolveApiKeyPrincipal(ql, headers, nowMs = Date.now()) {
4097
4271
  context: { isSystem: true }
4098
4272
  });
4099
4273
  } catch {
4100
- return void 0;
4274
+ return { outcome: "none" };
4101
4275
  }
4102
4276
  if (rows && rows.value) rows = rows.value;
4103
4277
  const row = Array.isArray(rows) ? rows[0] : void 0;
4104
- if (!row || row.revoked === true) return void 0;
4278
+ if (!row || row.revoked === true) return { outcome: "none" };
4105
4279
  const expiresAt = row.expires_at ?? row.expiresAt;
4106
- if (isExpired(expiresAt, nowMs)) return void 0;
4280
+ if (isExpired(expiresAt, nowMs)) return { outcome: "none" };
4107
4281
  const userId = row.user_id ?? row.userId;
4108
- if (!userId || typeof userId !== "string") return void 0;
4282
+ if (!userId || typeof userId !== "string") return { outcome: "none" };
4283
+ const tenantId = typeof row.active_organization_id === "string" && row.active_organization_id ? row.active_organization_id : void 0;
4284
+ if (!tenantId && tenancyPosture) {
4285
+ const posture = tenancyPosture;
4286
+ if (postureEnforcesWall(posture) && !postureUsesUnionScope(posture)) {
4287
+ return {
4288
+ outcome: "refused",
4289
+ reason: "organization_required",
4290
+ 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."
4291
+ };
4292
+ }
4293
+ }
4109
4294
  return {
4110
- userId,
4111
- tenantId: row.organization_id ?? row.organizationId ?? void 0,
4112
- scopes: parseScopes(row.scopes)
4295
+ outcome: "admitted",
4296
+ principal: { userId, tenantId, scopes: parseScopes(row.scopes) }
4113
4297
  };
4114
4298
  }
4115
4299
  function readHeader(headers, name) {
@@ -4142,6 +4326,7 @@ import {
4142
4326
  ADMIN_FULL_ACCESS,
4143
4327
  ORGANIZATION_ADMIN_GRANTS
4144
4328
  } from "@objectstack/spec";
4329
+ import { postureEnforcesWall as postureEnforcesWall2 } from "@objectstack/spec/security";
4145
4330
 
4146
4331
  // src/security/grant-validity.ts
4147
4332
  function toEpochMs(value) {
@@ -4224,6 +4409,15 @@ function postureVisibleRows(posture, rows, principal) {
4224
4409
  }
4225
4410
  }
4226
4411
 
4412
+ // src/security/row-active.ts
4413
+ var DEACTIVATED_VALUES = [false, 0, "0", "false"];
4414
+ function isRowActive(row) {
4415
+ if (!row) return false;
4416
+ const value = row.active;
4417
+ if (value === void 0 || value === null) return true;
4418
+ return !DEACTIVATED_VALUES.includes(value);
4419
+ }
4420
+
4227
4421
  // src/security/resolve-authz-context.ts
4228
4422
  function safeJsonParse2(s, fallback) {
4229
4423
  try {
@@ -4232,10 +4426,11 @@ function safeJsonParse2(s, fallback) {
4232
4426
  return fallback;
4233
4427
  }
4234
4428
  }
4235
- async function tryFind(ql, object, where, limit = 100) {
4429
+ async function tryFind(ql, object, where, limit = 100, organizationId) {
4236
4430
  if (!ql || typeof ql.find !== "function") return [];
4237
4431
  try {
4238
- let rows = await ql.find(object, { where, limit, context: { isSystem: true } });
4432
+ const context = organizationId ? { isSystem: true, tenantId: organizationId } : { isSystem: true };
4433
+ let rows = await ql.find(object, { where, limit, context });
4239
4434
  if (rows && rows.value) rows = rows.value;
4240
4435
  return Array.isArray(rows) ? rows : [];
4241
4436
  } catch {
@@ -4253,7 +4448,12 @@ async function resolveAuthzContext(input) {
4253
4448
  };
4254
4449
  let userId;
4255
4450
  let tenantId;
4256
- const keyPrincipal = await resolveApiKeyPrincipal(ql, headers, input.nowMs);
4451
+ const admission = await resolveApiKeyAdmission(ql, headers, input.nowMs, input.tenancyPosture);
4452
+ if (admission.outcome === "refused") {
4453
+ ctx.authRefusal = { reason: admission.reason, message: admission.message };
4454
+ return ctx;
4455
+ }
4456
+ const keyPrincipal = admission.outcome === "admitted" ? admission.principal : void 0;
4257
4457
  if (keyPrincipal) {
4258
4458
  userId = keyPrincipal.userId;
4259
4459
  tenantId = keyPrincipal.tenantId;
@@ -4281,6 +4481,22 @@ async function resolveAuthzContext(input) {
4281
4481
  seedPermissions: ctx.permissions,
4282
4482
  seedEmail: ctx.email
4283
4483
  });
4484
+ if (keyPrincipal?.tenantId && input.tenancyPosture) {
4485
+ const posture = input.tenancyPosture;
4486
+ if (postureEnforcesWall2(posture) && !grants.accessible_org_ids.includes(keyPrincipal.tenantId)) {
4487
+ return {
4488
+ positions: [],
4489
+ permissions: [],
4490
+ systemPermissions: [],
4491
+ org_user_ids: [],
4492
+ accessible_org_ids: [],
4493
+ authRefusal: {
4494
+ reason: "organization_membership_ended",
4495
+ 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."
4496
+ }
4497
+ };
4498
+ }
4499
+ }
4284
4500
  ctx.positions = grants.positions;
4285
4501
  ctx.permissions = grants.permissions;
4286
4502
  ctx.systemPermissions = grants.systemPermissions;
@@ -4312,12 +4528,19 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4312
4528
  }
4313
4529
  return userRow;
4314
4530
  };
4531
+ const needsUserRow = !grants.email || !grants.permissions.includes("ai_seat");
4532
+ const [, members, userPositionRows, orgMembersLeg, upsRowsAll] = await Promise.all([
4533
+ needsUserRow ? getUserRow() : Promise.resolve(void 0),
4534
+ tryFind(ql, "sys_member", { user_id: userId }, 200),
4535
+ tryFind(ql, "sys_user_position", { user_id: userId }, 200),
4536
+ tenantId ? tryFind(ql, "sys_member", { organization_id: tenantId }, 1e3) : Promise.resolve([]),
4537
+ tryFind(ql, "sys_user_permission_set", { user_id: userId }, 100)
4538
+ ]);
4315
4539
  if (!grants.email) {
4316
4540
  const u = await getUserRow();
4317
4541
  if (u?.email) grants.email = String(u.email);
4318
4542
  }
4319
4543
  const nowMs = opts.nowMs ?? Date.now();
4320
- const members = await tryFind(ql, "sys_member", { user_id: userId }, 200);
4321
4544
  const accessibleOrgIds = /* @__PURE__ */ new Set();
4322
4545
  for (const m of members) {
4323
4546
  if (!isGrantActive(m, nowMs)) continue;
@@ -4325,7 +4548,9 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4325
4548
  if (typeof org === "string" && org) accessibleOrgIds.add(org);
4326
4549
  }
4327
4550
  grants.accessible_org_ids = Array.from(accessibleOrgIds);
4328
- const activeMembers = tenantId ? members.filter((m) => (m.organization_id ?? m.organizationId) === tenantId) : members;
4551
+ const activeMembers = members.filter(
4552
+ (m) => isGrantActive(m, nowMs) && (!tenantId || (m.organization_id ?? m.organizationId) === tenantId)
4553
+ );
4329
4554
  for (const m of activeMembers) {
4330
4555
  if (m.role && typeof m.role === "string") {
4331
4556
  for (const raw of m.role.split(",").map((s) => s.trim()).filter(Boolean)) {
@@ -4334,7 +4559,6 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4334
4559
  }
4335
4560
  }
4336
4561
  }
4337
- const userPositionRows = await tryFind(ql, "sys_user_position", { user_id: userId }, 200);
4338
4562
  for (const ur of userPositionRows) {
4339
4563
  const org = ur.organization_id ?? null;
4340
4564
  if (org && tenantId && org !== tenantId) continue;
@@ -4343,14 +4567,13 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4343
4567
  if (typeof r === "string" && r && !grants.positions.includes(r)) grants.positions.push(r);
4344
4568
  }
4345
4569
  if (tenantId) {
4346
- const orgMembers = await tryFind(ql, "sys_member", { organization_id: tenantId }, 1e3);
4570
+ const orgMembers = orgMembersLeg;
4347
4571
  const ids = new Set(
4348
4572
  orgMembers.map((m) => m.user_id ?? m.userId).filter((v) => typeof v === "string" && v.length > 0)
4349
4573
  );
4350
4574
  ids.add(userId);
4351
4575
  grants.org_user_ids = Array.from(ids);
4352
4576
  }
4353
- const upsRowsAll = await tryFind(ql, "sys_user_permission_set", { user_id: userId }, 100);
4354
4577
  const upsRows = upsRowsAll.filter((r) => isGrantActive(r, nowMs));
4355
4578
  const psIds = new Set(
4356
4579
  upsRows.filter((r) => {
@@ -4364,8 +4587,14 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4364
4587
  let hasPlatformAdminGrant = false;
4365
4588
  if (!grants.positions.includes("everyone")) grants.positions.push("everyone");
4366
4589
  if (grants.positions.length > 0) {
4367
- const positionRows = await tryFind(ql, "sys_position", { name: { $in: grants.positions } }, 100);
4368
- const positionIds = positionRows.map((r) => r.id).filter(Boolean);
4590
+ const positionRows = await tryFind(ql, "sys_position", { name: { $in: grants.positions } }, 200, tenantId);
4591
+ const deactivatedNames = new Set(
4592
+ positionRows.filter((r) => !isRowActive(r)).map((r) => r.name).filter(Boolean)
4593
+ );
4594
+ if (deactivatedNames.size > 0) {
4595
+ grants.positions = grants.positions.filter((n) => !deactivatedNames.has(n));
4596
+ }
4597
+ const positionIds = positionRows.filter((r) => isRowActive(r)).map((r) => r.id).filter(Boolean);
4369
4598
  if (positionIds.length > 0) {
4370
4599
  const rpsRows = await tryFind(ql, "sys_position_permission_set", { position_id: { $in: positionIds } }, 500);
4371
4600
  for (const r of rpsRows) {
@@ -4375,7 +4604,8 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4375
4604
  }
4376
4605
  }
4377
4606
  if (psIds.size > 0) {
4378
- const psRows = await tryFind(ql, "sys_permission_set", { id: { $in: Array.from(psIds) } }, 500);
4607
+ const psRowsAll = await tryFind(ql, "sys_permission_set", { id: { $in: Array.from(psIds) } }, 500);
4608
+ const psRows = psRowsAll.filter((r) => isRowActive(r));
4379
4609
  const tabRank = { hidden: 0, default_off: 1, default_on: 2, visible: 3 };
4380
4610
  const mergedTabs = {};
4381
4611
  for (const ps of psRows) {
@@ -4435,34 +4665,89 @@ function coerceCurrency(value) {
4435
4665
  const s = typeof value === "string" ? value.trim().toUpperCase() : "";
4436
4666
  return /^[A-Z]{3}$/.test(s) ? s : void 0;
4437
4667
  }
4668
+ var LOCALIZATION_FAILURE_CACHE_TTL_MS = 3e4;
4669
+ var localizationFailureCache = /* @__PURE__ */ new WeakMap();
4438
4670
  async function resolveLocalizationContext(input) {
4671
+ const { ql, tenantId, userId } = input;
4672
+ const cacheKey = `${tenantId ?? ""}|${userId ?? ""}`;
4673
+ if (ql && typeof ql === "object") {
4674
+ const hit = localizationFailureCache.get(ql)?.get(cacheKey);
4675
+ if (hit && hit.expiresAt > Date.now()) return hit.value;
4676
+ }
4677
+ const { value, failed } = await resolveLocalizationContextUncached(input);
4678
+ if (failed && ql && typeof ql === "object") {
4679
+ const bucket = localizationFailureCache.get(ql) ?? /* @__PURE__ */ new Map();
4680
+ bucket.set(cacheKey, { value, expiresAt: Date.now() + LOCALIZATION_FAILURE_CACHE_TTL_MS });
4681
+ localizationFailureCache.set(ql, bucket);
4682
+ }
4683
+ return value;
4684
+ }
4685
+ async function resolveLocalizationContextUncached(input) {
4439
4686
  const { ql, settings, tenantId, userId } = input;
4687
+ let failed = false;
4440
4688
  try {
4441
4689
  if (settings && typeof settings.get === "function") {
4442
4690
  const sctx = { tenantId, userId };
4443
- const [tzRes, localeRes, currencyRes] = await Promise.all([
4444
- settings.get("localization", "timezone", sctx).catch(() => void 0),
4445
- settings.get("localization", "locale", sctx).catch(() => void 0),
4446
- settings.get("localization", "currency", sctx).catch(() => void 0)
4447
- ]);
4691
+ let tzRes;
4692
+ let localeRes;
4693
+ let currencyRes;
4694
+ if (typeof settings.getMany === "function") {
4695
+ try {
4696
+ const many = await settings.getMany("localization", ["timezone", "locale", "currency"], sctx);
4697
+ tzRes = many.timezone;
4698
+ localeRes = many.locale;
4699
+ currencyRes = many.currency;
4700
+ } catch {
4701
+ failed = true;
4702
+ }
4703
+ } else {
4704
+ [tzRes, localeRes, currencyRes] = await Promise.all([
4705
+ settings.get("localization", "timezone", sctx).catch(() => {
4706
+ failed = true;
4707
+ return void 0;
4708
+ }),
4709
+ settings.get("localization", "locale", sctx).catch(() => {
4710
+ failed = true;
4711
+ return void 0;
4712
+ }),
4713
+ settings.get("localization", "currency", sctx).catch(() => {
4714
+ failed = true;
4715
+ return void 0;
4716
+ })
4717
+ ]);
4718
+ }
4448
4719
  const tz = coerceTimeZone(tzRes?.value);
4449
4720
  const locale = coerceLocale(localeRes?.value);
4450
4721
  const currency = coerceCurrency(currencyRes?.value);
4451
- if (tz || locale || currency) return { timezone: tz ?? "UTC", locale: locale ?? "en-US", currency };
4722
+ if (tz || locale || currency) {
4723
+ return { value: { timezone: tz ?? "UTC", locale: locale ?? "en-US", currency }, failed: false };
4724
+ }
4452
4725
  }
4453
4726
  } catch {
4727
+ failed = true;
4728
+ }
4729
+ let rows = [];
4730
+ if (ql && typeof ql.find === "function") {
4731
+ try {
4732
+ let result = await ql.find("sys_setting", {
4733
+ where: { namespace: "localization", key: { $in: ["timezone", "locale", "currency"] }, scope: "tenant" },
4734
+ limit: 10,
4735
+ context: { isSystem: true }
4736
+ });
4737
+ if (result && result.value) result = result.value;
4738
+ rows = Array.isArray(result) ? result : [];
4739
+ } catch {
4740
+ failed = true;
4741
+ }
4454
4742
  }
4455
- const rows = await tryFind(
4456
- ql,
4457
- "sys_setting",
4458
- { namespace: "localization", key: { $in: ["timezone", "locale", "currency"] }, scope: "tenant" },
4459
- 10
4460
- );
4461
4743
  const valueOf = (k) => rows.find((r) => r.key === k)?.value;
4462
4744
  return {
4463
- timezone: coerceTimeZone(valueOf("timezone")) ?? "UTC",
4464
- locale: coerceLocale(valueOf("locale")) ?? "en-US",
4465
- currency: coerceCurrency(valueOf("currency"))
4745
+ value: {
4746
+ timezone: coerceTimeZone(valueOf("timezone")) ?? "UTC",
4747
+ locale: coerceLocale(valueOf("locale")) ?? "en-US",
4748
+ currency: coerceCurrency(valueOf("currency"))
4749
+ },
4750
+ failed
4466
4751
  };
4467
4752
  }
4468
4753
 
@@ -4606,6 +4891,7 @@ var ANONYMOUS_DENY_CODE = "UNAUTHENTICATED";
4606
4891
  var ANONYMOUS_DENY_MESSAGE = "Authentication is required to access this endpoint.";
4607
4892
  var ANONYMOUS_DENY_BODY = {
4608
4893
  error: ANONYMOUS_DENY_CODE,
4894
+ code: ANONYMOUS_DENY_CODE,
4609
4895
  message: ANONYMOUS_DENY_MESSAGE
4610
4896
  };
4611
4897
  function shouldDenyAnonymous(input) {
@@ -4619,6 +4905,76 @@ function shouldDenyAnonymous(input) {
4619
4905
  return true;
4620
4906
  }
4621
4907
 
4908
+ // src/security/admin-standing-surface.ts
4909
+ var ADMIN_STANDING_SURFACE = {
4910
+ sys_permission_set: {
4911
+ role: "derives",
4912
+ 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.",
4913
+ columns: [
4914
+ "id",
4915
+ "name",
4916
+ "active",
4917
+ "system_permissions",
4918
+ "systemPermissions",
4919
+ "tab_permissions",
4920
+ "tabPermissions"
4921
+ ]
4922
+ },
4923
+ sys_user_permission_set: {
4924
+ role: "derives",
4925
+ 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.",
4926
+ columns: [
4927
+ "user_id",
4928
+ "permission_set_id",
4929
+ "permissionSetId",
4930
+ "organization_id",
4931
+ "organizationId",
4932
+ "valid_from",
4933
+ "validFrom",
4934
+ "valid_until",
4935
+ "validUntil"
4936
+ ]
4937
+ },
4938
+ sys_member: {
4939
+ role: "derives",
4940
+ 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.",
4941
+ columns: [
4942
+ "user_id",
4943
+ "userId",
4944
+ "organization_id",
4945
+ "organizationId",
4946
+ "role",
4947
+ "valid_from",
4948
+ "validFrom",
4949
+ "valid_until",
4950
+ "validUntil"
4951
+ ]
4952
+ },
4953
+ sys_user: {
4954
+ role: "reads-only",
4955
+ 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."
4956
+ },
4957
+ sys_user_position: {
4958
+ role: "reads-only",
4959
+ 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."
4960
+ },
4961
+ sys_position: {
4962
+ role: "reads-only",
4963
+ reason: "Read to drop DEACTIVATED positions (ADR-0049, \xA76a). Same reason as `sys_user_position`: the position path cannot reach `hasPlatformAdminGrant`."
4964
+ },
4965
+ sys_position_permission_set: {
4966
+ role: "reads-only",
4967
+ 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."
4968
+ }
4969
+ };
4970
+ function adminStandingTables() {
4971
+ return Object.entries(ADMIN_STANDING_SURFACE).filter(([, t]) => t.role === "derives").map(([name]) => name).sort();
4972
+ }
4973
+ function adminStandingColumns(table) {
4974
+ const entry = ADMIN_STANDING_SURFACE[table];
4975
+ return entry?.role === "derives" ? entry.columns : void 0;
4976
+ }
4977
+
4622
4978
  // src/security/audience-binding-suggestion-status.ts
4623
4979
  var AUDIENCE_BINDING_SUGGESTION_STATUSES = {
4624
4980
  pending: true,
@@ -4669,10 +5025,26 @@ function calendarPartsInTzOrUtc(d, tz) {
4669
5025
  }
4670
5026
  function zonedDateStartToUtcMs(ymd2, tz) {
4671
5027
  const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ymd2);
4672
- const wallAsUtc = m ? Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])) : NaN;
5028
+ if (!m) return NaN;
5029
+ return zonedWallClockToUtcMs(
5030
+ { year: Number(m[1]), month: Number(m[2]), day: Number(m[3]) },
5031
+ tz
5032
+ );
5033
+ }
5034
+ function zonedWallClockToUtcMs(parts, tz) {
5035
+ const wallAsUtc = Date.UTC(
5036
+ parts.year,
5037
+ parts.month - 1,
5038
+ parts.day,
5039
+ parts.hour ?? 0,
5040
+ parts.minute ?? 0,
5041
+ parts.second ?? 0,
5042
+ parts.millisecond ?? 0
5043
+ );
4673
5044
  if (!tz || tz === "UTC" || Number.isNaN(wallAsUtc)) return wallAsUtc;
4674
5045
  try {
4675
5046
  const offsetAt = (t) => {
5047
+ const whole = Math.floor(t / 1e3) * 1e3;
4676
5048
  const p = new Intl.DateTimeFormat("en-US", {
4677
5049
  timeZone: tz,
4678
5050
  hourCycle: "h23",
@@ -4682,9 +5054,9 @@ function zonedDateStartToUtcMs(ymd2, tz) {
4682
5054
  hour: "2-digit",
4683
5055
  minute: "2-digit",
4684
5056
  second: "2-digit"
4685
- }).formatToParts(new Date(t));
5057
+ }).formatToParts(new Date(whole));
4686
5058
  const g = (k) => Number(p.find((x) => x.type === k)?.value);
4687
- return Date.UTC(g("year"), g("month") - 1, g("day"), g("hour"), g("minute"), g("second")) - t;
5059
+ return Date.UTC(g("year"), g("month") - 1, g("day"), g("hour"), g("minute"), g("second")) - whole;
4688
5060
  };
4689
5061
  const off1 = offsetAt(wallAsUtc - offsetAt(wallAsUtc));
4690
5062
  return wallAsUtc - off1;
@@ -5500,6 +5872,37 @@ function filterTokenContextFrom(execCtx, now) {
5500
5872
  };
5501
5873
  }
5502
5874
 
5875
+ // src/utils/temporal-comparand.ts
5876
+ import { classifyFilterToken as classifyFilterToken2 } from "@objectstack/spec/data";
5877
+ function temporalComparandKind(fieldType) {
5878
+ if (fieldType === "datetime") return "datetime";
5879
+ if (fieldType === "date") return "date";
5880
+ if (fieldType === "time") return "time";
5881
+ return null;
5882
+ }
5883
+ function readsAsInstant(s) {
5884
+ if (/^-?\d+$/.test(s)) return Number.isFinite(new Date(Number(s)).getTime());
5885
+ 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;
5886
+ return Number.isFinite(Date.parse(iso));
5887
+ }
5888
+ function readsAsCalendarDay(s) {
5889
+ return /^\d{4}-\d{2}-\d{2}/.test(s);
5890
+ }
5891
+ function readsAsWallClock(s) {
5892
+ const m = /^(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?$/.exec(s);
5893
+ if (!m) return false;
5894
+ return Number(m[1]) <= 23 && Number(m[2]) <= 59 && Number(m[3] ?? "0") <= 59;
5895
+ }
5896
+ function isUninterpretableTemporalComparand(kind, value) {
5897
+ if (typeof value !== "string") return false;
5898
+ const s = value.trim();
5899
+ if (s === "") return false;
5900
+ if (classifyFilterToken2(value) !== null) return false;
5901
+ if (kind === "datetime") return !readsAsInstant(s);
5902
+ if (kind === "date") return !readsAsCalendarDay(s);
5903
+ return !(readsAsWallClock(s) || readsAsInstant(s));
5904
+ }
5905
+
5503
5906
  // src/utils/record-not-found.ts
5504
5907
  function recordNotFoundError(object, id) {
5505
5908
  const err = new Error(`Record ${id} not found in ${object}`);
@@ -6485,7 +6888,11 @@ var NamespaceResolver = class {
6485
6888
  return `${shortName}_${ns}`;
6486
6889
  }
6487
6890
  };
6891
+
6892
+ // src/index.ts
6893
+ import { UNMATCHED_ROUTE_PATTERN } from "@objectstack/spec/contracts";
6488
6894
  export {
6895
+ ADMIN_STANDING_SURFACE,
6489
6896
  ANONYMOUS_DENY_BODY,
6490
6897
  ANONYMOUS_DENY_CODE,
6491
6898
  ANONYMOUS_DENY_MESSAGE,
@@ -6521,8 +6928,11 @@ export {
6521
6928
  SecurePluginContext,
6522
6929
  SemanticVersionManager,
6523
6930
  ServiceLifecycle,
6931
+ UNMATCHED_ROUTE_PATTERN,
6524
6932
  UnknownFilterTokenError,
6525
6933
  UnresolvedFilterTokenError,
6934
+ adminStandingColumns,
6935
+ adminStandingTables,
6526
6936
  assembleExecutionContext,
6527
6937
  assembleExecutionContextOrGuest,
6528
6938
  assertInitServiceRequirements,
@@ -6547,6 +6957,7 @@ export {
6547
6957
  defaultIsTransientError,
6548
6958
  derivePosture,
6549
6959
  describeInitOrderFault,
6960
+ effectiveTenancyPosture,
6550
6961
  engineCanRollBack,
6551
6962
  evaluateAuthGate,
6552
6963
  extractApiKey,
@@ -6564,6 +6975,8 @@ export {
6564
6975
  isGrantActive,
6565
6976
  isGrantExpired,
6566
6977
  isNode,
6978
+ isRowActive,
6979
+ isUninterpretableTemporalComparand,
6567
6980
  nextUtcCalendarDay,
6568
6981
  normalizeAuthGate,
6569
6982
  omitInternalFieldsFromWriteResponse,
@@ -6574,6 +6987,7 @@ export {
6574
6987
  readAuthoredTranslationLayer,
6575
6988
  readRunJournal,
6576
6989
  recordNotFoundError,
6990
+ resolveApiKeyAdmission,
6577
6991
  resolveApiKeyPrincipal,
6578
6992
  resolveAuthzContext,
6579
6993
  resolveFilterToken,
@@ -6587,6 +7001,7 @@ export {
6587
7001
  safeExit,
6588
7002
  shouldDenyAnonymous,
6589
7003
  signPayload,
7004
+ temporalComparandKind,
6590
7005
  unknownAudienceBindingSuggestionStatusMessage,
6591
7006
  utcInstantMs,
6592
7007
  validateInitServiceContract,
@@ -6597,6 +7012,7 @@ export {
6597
7012
  wireAuthoredTranslationSync,
6598
7013
  withTransientRetry,
6599
7014
  withoutOperationPrivateKeys,
6600
- zonedDateStartToUtcMs
7015
+ zonedDateStartToUtcMs,
7016
+ zonedWallClockToUtcMs
6601
7017
  };
6602
7018
  //# sourceMappingURL=index.js.map