@objectstack/core 17.0.0-rc.6 → 17.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -30,11 +30,14 @@ 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,
36
37
  ANONYMOUS_DENY_STATUS: () => ANONYMOUS_DENY_STATUS,
37
38
  API_KEY_PREFIX: () => API_KEY_PREFIX,
39
+ AUDIENCE_BINDING_SUGGESTION_STATUSES: () => AUDIENCE_BINDING_SUGGESTION_STATUSES,
40
+ AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES: () => AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES,
38
41
  CORE_FALLBACK_FACTORIES: () => CORE_FALLBACK_FACTORIES,
39
42
  DependencyResolver: () => DependencyResolver,
40
43
  ENTRY_EXECUTION_CONTEXT_FIELDS: () => ENTRY_EXECUTION_CONTEXT_FIELDS,
@@ -63,16 +66,22 @@ __export(index_exports, {
63
66
  SecurePluginContext: () => SecurePluginContext,
64
67
  SemanticVersionManager: () => SemanticVersionManager,
65
68
  ServiceLifecycle: () => ServiceLifecycle,
69
+ UNMATCHED_ROUTE_PATTERN: () => import_contracts.UNMATCHED_ROUTE_PATTERN,
66
70
  UnknownFilterTokenError: () => UnknownFilterTokenError,
67
71
  UnresolvedFilterTokenError: () => UnresolvedFilterTokenError,
72
+ adminStandingColumns: () => adminStandingColumns,
73
+ adminStandingTables: () => adminStandingTables,
68
74
  assembleExecutionContext: () => assembleExecutionContext,
69
75
  assembleExecutionContextOrGuest: () => assembleExecutionContextOrGuest,
70
76
  assertInitServiceRequirements: () => assertInitServiceRequirements,
77
+ assertMetadataRegisterContract: () => assertMetadataRegisterContract,
71
78
  bucketKeyToCalendarRange: () => bucketKeyToCalendarRange,
72
79
  buildPermissionsFromGrants: () => buildPermissionsFromGrants,
73
80
  bulkWrite: () => bulkWrite,
74
81
  calendarPartsInTz: () => calendarPartsInTz,
75
82
  calendarPartsInTzOrUtc: () => calendarPartsInTzOrUtc,
83
+ canonicalMetadataServiceType: () => canonicalMetadataServiceType,
84
+ collectInternalWriteResponseFields: () => collectInternalWriteResponseFields,
76
85
  counterSignPayload: () => counterSignPayload,
77
86
  createLogger: () => createLogger,
78
87
  createMemoryCache: () => createMemoryCache,
@@ -86,6 +95,7 @@ __export(index_exports, {
86
95
  defaultIsTransientError: () => defaultIsTransientError,
87
96
  derivePosture: () => derivePosture,
88
97
  describeInitOrderFault: () => describeInitOrderFault,
98
+ effectiveTenancyPosture: () => effectiveTenancyPosture,
89
99
  engineCanRollBack: () => engineCanRollBack,
90
100
  evaluateAuthGate: () => evaluateAuthGate,
91
101
  extractApiKey: () => extractApiKey,
@@ -97,19 +107,25 @@ __export(index_exports, {
97
107
  getMemoryUsage: () => getMemoryUsage,
98
108
  hashApiKey: () => hashApiKey,
99
109
  hashMigrationPlan: () => hashMigrationPlan,
110
+ isAudienceBindingSuggestionStatus: () => isAudienceBindingSuggestionStatus,
100
111
  isAuthGateAllowlisted: () => isAuthGateAllowlisted,
101
112
  isExpired: () => isExpired,
102
113
  isGrantActive: () => isGrantActive,
103
114
  isGrantExpired: () => isGrantExpired,
104
115
  isNode: () => isNode,
116
+ isRowActive: () => isRowActive,
117
+ isUninterpretableTemporalComparand: () => isUninterpretableTemporalComparand,
105
118
  nextUtcCalendarDay: () => import_data.nextUtcCalendarDay,
106
119
  normalizeAuthGate: () => normalizeAuthGate,
120
+ omitInternalFieldsFromWriteResponse: () => omitInternalFieldsFromWriteResponse,
107
121
  parseScopes: () => parseScopes,
108
122
  parseSignature: () => parseSignature,
109
123
  planChunks: () => planChunks,
110
124
  postureVisibleRows: () => postureVisibleRows,
111
125
  readAuthoredTranslationLayer: () => readAuthoredTranslationLayer,
112
126
  readRunJournal: () => readRunJournal,
127
+ recordNotFoundError: () => recordNotFoundError,
128
+ resolveApiKeyAdmission: () => resolveApiKeyAdmission,
113
129
  resolveApiKeyPrincipal: () => resolveApiKeyPrincipal,
114
130
  resolveAuthzContext: () => resolveAuthzContext,
115
131
  resolveFilterToken: () => resolveFilterToken,
@@ -123,6 +139,8 @@ __export(index_exports, {
123
139
  safeExit: () => safeExit,
124
140
  shouldDenyAnonymous: () => shouldDenyAnonymous,
125
141
  signPayload: () => signPayload,
142
+ temporalComparandKind: () => temporalComparandKind,
143
+ unknownAudienceBindingSuggestionStatusMessage: () => unknownAudienceBindingSuggestionStatusMessage,
126
144
  utcInstantMs: () => import_data.utcInstantMs,
127
145
  validateInitServiceContract: () => validateInitServiceContract,
128
146
  verifyPayload: () => verifyPayload,
@@ -132,7 +150,8 @@ __export(index_exports, {
132
150
  wireAuthoredTranslationSync: () => wireAuthoredTranslationSync,
133
151
  withTransientRetry: () => withTransientRetry,
134
152
  withoutOperationPrivateKeys: () => withoutOperationPrivateKeys,
135
- zonedDateStartToUtcMs: () => zonedDateStartToUtcMs
153
+ zonedDateStartToUtcMs: () => zonedDateStartToUtcMs,
154
+ zonedWallClockToUtcMs: () => zonedWallClockToUtcMs
136
155
  });
137
156
  module.exports = __toCommonJS(index_exports);
138
157
 
@@ -837,7 +856,7 @@ function createLogger(config) {
837
856
  }
838
857
 
839
858
  // src/kernel.ts
840
- var import_system2 = require("@objectstack/spec/system");
859
+ var import_system3 = require("@objectstack/spec/system");
841
860
 
842
861
  // src/security/plugin-config-validator.ts
843
862
  var import_zod = require("zod");
@@ -1505,6 +1524,7 @@ function createMemoryJob() {
1505
1524
  }
1506
1525
 
1507
1526
  // src/fallbacks/memory-i18n.ts
1527
+ var import_system = require("@objectstack/spec/system");
1508
1528
  function deepMerge(target, source) {
1509
1529
  const result = { ...target };
1510
1530
  for (const key of Object.keys(source)) {
@@ -1538,6 +1558,7 @@ function createMemoryI18n() {
1538
1558
  const translations = /* @__PURE__ */ new Map();
1539
1559
  const authored = /* @__PURE__ */ new Map();
1540
1560
  let defaultLocale = "en";
1561
+ let supportedLocales;
1541
1562
  function resolveKey(data, key) {
1542
1563
  const parts = key.split(".");
1543
1564
  let current = data;
@@ -1603,9 +1624,29 @@ function createMemoryI18n() {
1603
1624
  authored.set(locale, { ...data });
1604
1625
  }
1605
1626
  },
1627
+ /**
1628
+ * Report the locales this stack offers.
1629
+ *
1630
+ * [#7679] When the app declared `i18n.supportedLocales`, that declaration
1631
+ * IS the answer — in declared order, and including a declared locale no
1632
+ * bundle was ever loaded for (declared-but-unserved). Reporting the
1633
+ * declaration rather than an intersection is what gives a client the
1634
+ * signal that the locale it is being offered has nothing behind it yet;
1635
+ * quietly dropping it would leave the gap invisible on both sides. It is
1636
+ * also the only answer that does not depend on how much had loaded by the
1637
+ * time this was called.
1638
+ *
1639
+ * With nothing declared, the loaded set — the behaviour every app that
1640
+ * never opted in already has.
1641
+ */
1606
1642
  getLocales() {
1643
+ if (supportedLocales) return [...supportedLocales];
1607
1644
  return [.../* @__PURE__ */ new Set([...translations.keys(), ...authored.keys()])];
1608
1645
  },
1646
+ /** @see II18nService.setSupportedLocales — [#7679] */
1647
+ setSupportedLocales(locales) {
1648
+ supportedLocales = (0, import_system.normalizeSupportedLocales)(locales);
1649
+ },
1609
1650
  getDefaultLocale() {
1610
1651
  return defaultLocale;
1611
1652
  },
@@ -1615,14 +1656,42 @@ function createMemoryI18n() {
1615
1656
  };
1616
1657
  }
1617
1658
 
1659
+ // src/metadata-service-contract.ts
1660
+ var import_shared = require("@objectstack/spec/shared");
1661
+ var REGISTER_REFUSAL_CODE = "VALIDATION_ERROR";
1662
+ function canonicalMetadataServiceType(type) {
1663
+ return (0, import_shared.pluralToSingular)(type);
1664
+ }
1665
+ function registerRefusal(message) {
1666
+ const err = new Error(message);
1667
+ err.code = REGISTER_REFUSAL_CODE;
1668
+ err.status = 400;
1669
+ return err;
1670
+ }
1671
+ function assertMetadataRegisterContract(type, name, data) {
1672
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
1673
+ const shape = data === null ? "null" : Array.isArray(data) ? "an array" : `a ${typeof data}`;
1674
+ throw registerRefusal(
1675
+ `IMetadataService.register('${type}', '${name}'): data is ${shape}, not a metadata document. register() stores plain-object documents only \u2014 accepting a value the service cannot key was measured as accept-then-drop on document-keyed stores (#7378 row 3: refuse loudly, never coerce into storability). Wrap the value in a document object whose shape the '${type}' type's schema accepts, or store it under a type that declares one.`
1676
+ );
1677
+ }
1678
+ const documentName = data.name;
1679
+ if (documentName !== void 0 && documentName !== name) {
1680
+ throw registerRefusal(
1681
+ `IMetadataService.register('${type}', '${name}'): data.name is '${String(documentName)}', which disagrees with the name argument '${name}'. A disagreement is almost always an authoring bug, and resolving it silently in either direction can file the item under a key the caller never wrote (#7378 row 1: refuse loudly, locate the mismatch). Register under one name: pass the intended key as the argument and make data.name match it, or omit data.name.`
1682
+ );
1683
+ }
1684
+ }
1685
+
1618
1686
  // src/fallbacks/memory-metadata.ts
1619
1687
  function createMemoryMetadata() {
1620
1688
  const store = /* @__PURE__ */ new Map();
1621
1689
  function getTypeMap(type) {
1622
- let map = store.get(type);
1690
+ const canonical = canonicalMetadataServiceType(type);
1691
+ let map = store.get(canonical);
1623
1692
  if (!map) {
1624
1693
  map = /* @__PURE__ */ new Map();
1625
- store.set(type, map);
1694
+ store.set(canonical, map);
1626
1695
  }
1627
1696
  return map;
1628
1697
  }
@@ -1637,6 +1706,7 @@ function createMemoryMetadata() {
1637
1706
  },
1638
1707
  _serviceName: "metadata",
1639
1708
  async register(type, name, data) {
1709
+ assertMetadataRegisterContract(type, name, data);
1640
1710
  getTypeMap(type).set(name, data);
1641
1711
  },
1642
1712
  // Mirror MetadataManager.registerInMemory (synchronous, no persistence).
@@ -1648,7 +1718,11 @@ function createMemoryMetadata() {
1648
1718
  // so `defineStack({ datasources })` entries silently never reached the
1649
1719
  // registry and were absent from GET /api/v1/datasources and
1650
1720
  // GET /api/v1/meta/datasource (ADR-0015 §18). This store is already
1651
- // in-memory only, so registerInMemory and register share an implementation.
1721
+ // in-memory only, so registerInMemory and register share a store — but
1722
+ // NOT the [#7378] refusals: the ruling names `register`, and this member
1723
+ // is a boot-time seeding primitive for source-control-owned artefacts
1724
+ // (see assertMetadataRegisterContract's header for the boundary). It does
1725
+ // share the row-2 canonical type fold, via getTypeMap.
1652
1726
  registerInMemory(type, name, data) {
1653
1727
  getTypeMap(type).set(name, data);
1654
1728
  },
@@ -1677,7 +1751,7 @@ function createMemoryMetadata() {
1677
1751
  }
1678
1752
 
1679
1753
  // src/fallbacks/authored-translation-sync.ts
1680
- var import_system = require("@objectstack/spec/system");
1754
+ var import_system2 = require("@objectstack/spec/system");
1681
1755
  var OWNER_PROP = "__authoredTranslationSyncOwner";
1682
1756
  var LOCALE_LIKE = /^[a-z]{2,3}([_-]([A-Za-z]{4}|[A-Za-z]{2}|[0-9]{3}))?$/;
1683
1757
  async function readAuthoredTranslationLayer(engine, logger) {
@@ -1707,7 +1781,7 @@ async function readAuthoredTranslationLayer(engine, logger) {
1707
1781
  continue;
1708
1782
  }
1709
1783
  if (!data || typeof data !== "object") continue;
1710
- const legacyKeys = import_system.LEGACY_OBJECT_FIRST_KEYS.filter((key) => data[key] !== void 0);
1784
+ const legacyKeys = import_system2.LEGACY_OBJECT_FIRST_KEYS.filter((key) => data[key] !== void 0);
1711
1785
  if (legacyKeys.length > 0) {
1712
1786
  logger?.warn?.(
1713
1787
  `[i18n] authored translation '${row?.name}' uses the retired object-first shape (${legacyKeys.join(", ")}) \u2014 nothing resolves from it; re-author it under 'objects.<object_name>' with a top-level 'locale' \u2014 skipped`
@@ -1812,6 +1886,26 @@ var CORE_FALLBACK_FACTORIES = {
1812
1886
  i18n: createMemoryI18n
1813
1887
  };
1814
1888
 
1889
+ // src/plugin-registration.ts
1890
+ function versionLabel(plugin) {
1891
+ return plugin.version ? `v${plugin.version}` : "unversioned";
1892
+ }
1893
+ function describeSupersededRegistration(previous, next) {
1894
+ 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.`;
1895
+ }
1896
+ function registerPluginByName(registry, plugin, logger) {
1897
+ const previous = registry.get(plugin.name);
1898
+ if (previous !== void 0) {
1899
+ logger.warn(describeSupersededRegistration(previous, plugin), {
1900
+ plugin: plugin.name,
1901
+ supersededVersion: previous.version,
1902
+ supersedingVersion: plugin.version
1903
+ });
1904
+ }
1905
+ registry.set(plugin.name, plugin);
1906
+ return previous;
1907
+ }
1908
+
1815
1909
  // src/kernel.ts
1816
1910
  var ObjectKernel = class {
1817
1911
  constructor(config = {}) {
@@ -1896,6 +1990,14 @@ var ObjectKernel = class {
1896
1990
  }
1897
1991
  /**
1898
1992
  * Register a plugin with enhanced validation
1993
+ *
1994
+ * Duplicate names OVERWRITE, with one `warn` naming both versions — the
1995
+ * declared contract in `plugin-registration.ts`, applied identically by
1996
+ * `LiteKernel.use()` (#9864, maintainer ruling 2026-08-19). The overwrite
1997
+ * itself is unchanged: it is what lets an app config's `plugins` entry
1998
+ * supersede a plugin the CLI auto-registered earlier in the same boot
1999
+ * (#9863). What changes is that it is no longer silent, and no longer
2000
+ * disagrees with the other kernel.
1899
2001
  */
1900
2002
  async use(plugin) {
1901
2003
  if (this.state !== "idle") {
@@ -1906,11 +2008,13 @@ var ObjectKernel = class {
1906
2008
  throw new Error(`Failed to load plugin: ${plugin.name} - ${result.error?.message}`);
1907
2009
  }
1908
2010
  const pluginMeta = result.plugin;
1909
- this.plugins.set(pluginMeta.name, pluginMeta);
1910
- this.logger.info(`Plugin registered: ${pluginMeta.name}@${pluginMeta.version}`, {
1911
- plugin: pluginMeta.name,
1912
- version: pluginMeta.version
1913
- });
2011
+ const superseded = registerPluginByName(this.plugins, pluginMeta, this.logger);
2012
+ if (superseded === void 0) {
2013
+ this.logger.info(`Plugin registered: ${pluginMeta.name}@${pluginMeta.version}`, {
2014
+ plugin: pluginMeta.name,
2015
+ version: pluginMeta.version
2016
+ });
2017
+ }
1914
2018
  return this;
1915
2019
  }
1916
2020
  /**
@@ -1945,7 +2049,7 @@ var ObjectKernel = class {
1945
2049
  */
1946
2050
  preInjectCoreFallbacks() {
1947
2051
  if (this.config.skipSystemValidation) return;
1948
- for (const [serviceName, criticality] of Object.entries(import_system2.ServiceRequirementDef)) {
2052
+ for (const [serviceName, criticality] of Object.entries(import_system3.ServiceRequirementDef)) {
1949
2053
  if (criticality !== "core") continue;
1950
2054
  const hasService = this.services.has(serviceName) || this.pluginLoader.hasService(serviceName);
1951
2055
  if (!hasService) {
@@ -1969,7 +2073,7 @@ var ObjectKernel = class {
1969
2073
  this.logger.debug("Validating system service requirements...");
1970
2074
  const missingServices = [];
1971
2075
  const missingCoreServices = [];
1972
- for (const [serviceName, criticality] of Object.entries(import_system2.ServiceRequirementDef)) {
2076
+ for (const [serviceName, criticality] of Object.entries(import_system3.ServiceRequirementDef)) {
1973
2077
  const hasService = this.services.has(serviceName) || this.pluginLoader.hasService(serviceName);
1974
2078
  if (!hasService) {
1975
2079
  if (criticality === "required") {
@@ -2388,14 +2492,21 @@ var LiteKernel = class extends ObjectKernelBase {
2388
2492
  /**
2389
2493
  * Register a plugin
2390
2494
  * @param plugin - Plugin instance
2495
+ *
2496
+ * Duplicate names OVERWRITE, with one `warn` naming both versions — the
2497
+ * declared contract in `plugin-registration.ts`, applied identically by
2498
+ * `ObjectKernel.use()` (#9864, maintainer ruling 2026-08-19).
2499
+ *
2500
+ * This method used to `throw` `[Kernel] Plugin '<name>' already
2501
+ * registered` here while `ObjectKernel` overwrote silently, so one input
2502
+ * had two meanings depending on which kernel was running — and the kernel
2503
+ * that runs in production was the silent one. The ruling converged them on
2504
+ * the behaviour that already works (an app config superseding a plugin the
2505
+ * CLI auto-registered, #9863) and made it audible rather than removing it.
2391
2506
  */
2392
2507
  use(plugin) {
2393
2508
  this.validateIdle();
2394
- const pluginName = plugin.name;
2395
- if (this.plugins.has(pluginName)) {
2396
- throw new Error(`[Kernel] Plugin '${pluginName}' already registered`);
2397
- }
2398
- this.plugins.set(pluginName, plugin);
2509
+ registerPluginByName(this.plugins, plugin, this.logger);
2399
2510
  return this;
2400
2511
  }
2401
2512
  /**
@@ -2644,11 +2755,119 @@ var TestRunner = class {
2644
2755
  };
2645
2756
 
2646
2757
  // src/qa/http-adapter.ts
2758
+ var import_api = require("@objectstack/spec/api");
2759
+ var conventionCache;
2760
+ function conventionMounts() {
2761
+ if (conventionCache === void 0) {
2762
+ const api = import_api.RestApiConfigSchema.parse({});
2763
+ const crud = import_api.CrudEndpointsConfigSchema.parse({});
2764
+ const apiBase = api.apiPath ?? `${api.basePath}/${api.version}`;
2765
+ conventionCache = { apiBase, dataPath: `${apiBase}${crud.dataPrefix}` };
2766
+ }
2767
+ return conventionCache;
2768
+ }
2647
2769
  var HttpTestAdapter = class {
2648
2770
  constructor(baseUrl, authToken) {
2649
2771
  this.baseUrl = baseUrl;
2650
2772
  this.authToken = authToken;
2651
2773
  }
2774
+ /** The resolved data mount; probes at most once per adapter. */
2775
+ dataMount() {
2776
+ if (this.mountPromise === void 0) {
2777
+ this.mountPromise = this.resolveDataMount();
2778
+ }
2779
+ return this.mountPromise;
2780
+ }
2781
+ /**
2782
+ * Ask the server where it serves the Data Protocol, and fall back to the
2783
+ * convention — loudly — when it cannot say.
2784
+ *
2785
+ * ## [#7983] What the probe recovers, measured rather than assumed
2786
+ *
2787
+ * `@objectstack/client` answers the same question through discovery
2788
+ * (`getRoute`, `packages/client/src/index.ts`), and this follows it: prefer
2789
+ * the server's own `routes.data`, fall back to the convention. Measured on a
2790
+ * booted stack (REST generator + dispatcher bridge, three configs):
2791
+ *
2792
+ * | deployment | `{apiBase}/discovery` | serves |
2793
+ * |--------------------------------|-----------------------|---------------|
2794
+ * | stock | 200 `/api/v1/data` | `/api/v1/data`|
2795
+ * | `crud.dataPrefix: '/objects'` | 200 `/api/v1/objects` | `/api/v1/objects` |
2796
+ * | `api.apiPath: '/api/2026-01'` | **404** | `/api/2026-01/data` |
2797
+ *
2798
+ * So the probe closes the `dataPrefix` row exactly: `RestServer`'s discovery
2799
+ * handler substitutes the configured prefix into `routes.data`, and reading
2800
+ * it is strictly better than recomputing it here. The `apiPath` row it cannot
2801
+ * close, and the reason is structural rather than an oversight — `apiPath`
2802
+ * moves the base that discovery itself is mounted under, so the document that
2803
+ * would name the new mount is behind the very prefix we are missing.
2804
+ *
2805
+ * ⛔ And the one discovery document at a FIXED path does not rescue it:
2806
+ * `/.well-known/objectstack` is mounted at the site root by the dispatcher
2807
+ * bridge, but its `routes.data` is the DISPATCHER's own `${prefix}/data` —
2808
+ * measured as `/api/v1/data` under all three configs above, including the two
2809
+ * where the server serves elsewhere. Falling back to it would turn "we could
2810
+ * not resolve the mount" into "discovery told us `/api/v1/data`": the same
2811
+ * 404, now with a false provenance attached. Not probed, deliberately.
2812
+ *
2813
+ * Hence: one probe, then a diagnostic that NAMES the mount, the evidence and
2814
+ * the remedy. `api_call` takes the path it is given and is unaffected either
2815
+ * way — it stays the escape hatch for a host this cannot reach.
2816
+ */
2817
+ async resolveDataMount() {
2818
+ const { apiBase, dataPath } = conventionMounts();
2819
+ const probeUrl = `${this.baseUrl}${apiBase}/discovery`;
2820
+ let why;
2821
+ try {
2822
+ const headers = {};
2823
+ if (this.authToken) {
2824
+ headers["Authorization"] = `Bearer ${this.authToken}`;
2825
+ }
2826
+ const response = await fetch(probeUrl, { method: "GET", headers });
2827
+ if (response.ok) {
2828
+ const body = await response.json();
2829
+ const doc = body && typeof body === "object" && "routes" in body ? body : body?.data;
2830
+ const advertised = doc?.routes?.data;
2831
+ if (typeof advertised === "string" && advertised.length > 0) {
2832
+ return {
2833
+ path: advertised,
2834
+ source: "discovery",
2835
+ why: `GET ${probeUrl} advertised routes.data`
2836
+ };
2837
+ }
2838
+ why = `GET ${probeUrl} answered ${response.status} but carried no routes.data`;
2839
+ } else {
2840
+ why = `GET ${probeUrl} answered ${response.status}`;
2841
+ }
2842
+ } catch (error) {
2843
+ why = `GET ${probeUrl} could not be reached (${error.message})`;
2844
+ }
2845
+ const mount = { path: dataPath, source: "convention", why };
2846
+ console.warn(
2847
+ `[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.`
2848
+ );
2849
+ return mount;
2850
+ }
2851
+ /** `{baseUrl}{dataMount}/{object}` — the collection URL. */
2852
+ collectionUrl(mount, objectName) {
2853
+ return `${this.baseUrl}${mount.path}/${encodeURIComponent(objectName)}`;
2854
+ }
2855
+ /** `{collection}/{id}` — the single-record URL. */
2856
+ recordUrl(mount, objectName, id) {
2857
+ return `${this.collectionUrl(mount, objectName)}/${encodeURIComponent(String(id))}`;
2858
+ }
2859
+ /**
2860
+ * The provenance clause appended to a failed record action's error.
2861
+ *
2862
+ * The card this closes is about a 404 that reads like the author's own URL
2863
+ * mistake; the mount is the one fact that distinguishes the two, so it rides
2864
+ * on the failure itself rather than only on a warning printed earlier in the
2865
+ * transcript.
2866
+ */
2867
+ mountNote(mount) {
2868
+ const how = mount.source === "discovery" ? "resolved from discovery" : "assumed by convention \u2014 the server was not able to state it";
2869
+ return `record actions addressed ${this.baseUrl}${mount.path} (data mount ${how}: ${mount.why})`;
2870
+ }
2652
2871
  async execute(action, _context) {
2653
2872
  const headers = {
2654
2873
  "Content-Type": "application/json"
@@ -2680,48 +2899,53 @@ var HttpTestAdapter = class {
2680
2899
  }
2681
2900
  }
2682
2901
  async createRecord(objectName, data, headers) {
2683
- const response = await fetch(`${this.baseUrl}/api/data/${objectName}`, {
2902
+ const mount = await this.dataMount();
2903
+ const response = await fetch(this.collectionUrl(mount, objectName), {
2684
2904
  method: "POST",
2685
2905
  headers,
2686
2906
  body: JSON.stringify(data)
2687
2907
  });
2688
- return this.handleResponse(response);
2908
+ return this.handleResponse(response, this.mountNote(mount));
2689
2909
  }
2690
2910
  async updateRecord(objectName, data, headers) {
2691
- const id = data.id;
2911
+ const { id, ...fields } = data;
2692
2912
  if (!id) throw new Error("Update record requires id in payload");
2693
- const response = await fetch(`${this.baseUrl}/api/data/${objectName}/${id}`, {
2694
- method: "PUT",
2913
+ const mount = await this.dataMount();
2914
+ const response = await fetch(this.recordUrl(mount, objectName, id), {
2915
+ method: "PATCH",
2695
2916
  headers,
2696
- body: JSON.stringify(data)
2917
+ body: JSON.stringify(fields)
2697
2918
  });
2698
- return this.handleResponse(response);
2919
+ return this.handleResponse(response, this.mountNote(mount));
2699
2920
  }
2700
2921
  async deleteRecord(objectName, data, headers) {
2701
2922
  const id = data.id;
2702
2923
  if (!id) throw new Error("Delete record requires id in payload");
2703
- const response = await fetch(`${this.baseUrl}/api/data/${objectName}/${id}`, {
2924
+ const mount = await this.dataMount();
2925
+ const response = await fetch(this.recordUrl(mount, objectName, id), {
2704
2926
  method: "DELETE",
2705
2927
  headers
2706
2928
  });
2707
- return this.handleResponse(response);
2929
+ return this.handleResponse(response, this.mountNote(mount));
2708
2930
  }
2709
2931
  async readRecord(objectName, data, headers) {
2710
2932
  const id = data.id;
2711
2933
  if (!id) throw new Error("Read record requires id in payload");
2712
- const response = await fetch(`${this.baseUrl}/api/data/${objectName}/${id}`, {
2934
+ const mount = await this.dataMount();
2935
+ const response = await fetch(this.recordUrl(mount, objectName, id), {
2713
2936
  method: "GET",
2714
2937
  headers
2715
2938
  });
2716
- return this.handleResponse(response);
2939
+ return this.handleResponse(response, this.mountNote(mount));
2717
2940
  }
2718
2941
  async queryRecords(objectName, data, headers) {
2719
- const response = await fetch(`${this.baseUrl}/api/data/${objectName}/query`, {
2942
+ const mount = await this.dataMount();
2943
+ const response = await fetch(`${this.collectionUrl(mount, objectName)}/query`, {
2720
2944
  method: "POST",
2721
2945
  headers,
2722
2946
  body: JSON.stringify(data)
2723
2947
  });
2724
- return this.handleResponse(response);
2948
+ return this.handleResponse(response, this.mountNote(mount));
2725
2949
  }
2726
2950
  async rawApiCall(endpoint, data, headers) {
2727
2951
  const method = data.method || "GET";
@@ -2734,10 +2958,13 @@ var HttpTestAdapter = class {
2734
2958
  });
2735
2959
  return this.handleResponse(response);
2736
2960
  }
2737
- async handleResponse(response) {
2961
+ async handleResponse(response, mountNote) {
2738
2962
  if (!response.ok) {
2739
2963
  const text = await response.text();
2740
- throw new Error(`HTTP Error ${response.status}: ${text}`);
2964
+ const wrongUrlShaped = response.status === 404 || response.status === 405;
2965
+ throw new Error(
2966
+ `HTTP Error ${response.status}: ${text}${wrongUrlShaped && mountNote ? ` \u2014 ${mountNote}` : ""}`
2967
+ );
2741
2968
  }
2742
2969
  const contentType = response.headers.get("content-type");
2743
2970
  if (contentType && contentType.includes("application/json")) {
@@ -4079,6 +4306,7 @@ var PluginSecurityScanner = class {
4079
4306
 
4080
4307
  // src/security/api-key.ts
4081
4308
  var import_node_crypto2 = require("crypto");
4309
+ var import_security = require("@objectstack/spec/security");
4082
4310
  var API_KEY_PREFIX = "osk_";
4083
4311
  var API_KEY_ENTROPY_BYTES = 32;
4084
4312
  var VISIBLE_PREFIX_LEN = 12;
@@ -4132,10 +4360,18 @@ function isExpired(value, nowMs) {
4132
4360
  if (Number.isNaN(ms)) return false;
4133
4361
  return ms <= nowMs;
4134
4362
  }
4135
- async function resolveApiKeyPrincipal(ql, headers, nowMs = Date.now()) {
4363
+ function effectiveTenancyPosture(tenancy) {
4364
+ if (!tenancy) return void 0;
4365
+ return (0, import_security.normalizeTenancyPosture)(tenancy.posture) ?? (tenancy.isolationActive ? "isolated" : "single");
4366
+ }
4367
+ async function resolveApiKeyPrincipal(ql, headers, nowMs = Date.now(), tenancyPosture) {
4368
+ const admission = await resolveApiKeyAdmission(ql, headers, nowMs, tenancyPosture);
4369
+ return admission.outcome === "admitted" ? admission.principal : void 0;
4370
+ }
4371
+ async function resolveApiKeyAdmission(ql, headers, nowMs = Date.now(), tenancyPosture) {
4136
4372
  const apiKey = extractApiKey(headers);
4137
- if (!apiKey) return void 0;
4138
- if (!ql || typeof ql.find !== "function") return void 0;
4373
+ if (!apiKey) return { outcome: "none" };
4374
+ if (!ql || typeof ql.find !== "function") return { outcome: "none" };
4139
4375
  let rows;
4140
4376
  try {
4141
4377
  rows = await ql.find("sys_api_key", {
@@ -4144,19 +4380,29 @@ async function resolveApiKeyPrincipal(ql, headers, nowMs = Date.now()) {
4144
4380
  context: { isSystem: true }
4145
4381
  });
4146
4382
  } catch {
4147
- return void 0;
4383
+ return { outcome: "none" };
4148
4384
  }
4149
4385
  if (rows && rows.value) rows = rows.value;
4150
4386
  const row = Array.isArray(rows) ? rows[0] : void 0;
4151
- if (!row || row.revoked === true) return void 0;
4387
+ if (!row || row.revoked === true) return { outcome: "none" };
4152
4388
  const expiresAt = row.expires_at ?? row.expiresAt;
4153
- if (isExpired(expiresAt, nowMs)) return void 0;
4389
+ if (isExpired(expiresAt, nowMs)) return { outcome: "none" };
4154
4390
  const userId = row.user_id ?? row.userId;
4155
- if (!userId || typeof userId !== "string") return void 0;
4391
+ if (!userId || typeof userId !== "string") return { outcome: "none" };
4392
+ const tenantId = typeof row.active_organization_id === "string" && row.active_organization_id ? row.active_organization_id : void 0;
4393
+ if (!tenantId && tenancyPosture) {
4394
+ const posture = tenancyPosture;
4395
+ if ((0, import_security.postureEnforcesWall)(posture) && !(0, import_security.postureUsesUnionScope)(posture)) {
4396
+ return {
4397
+ outcome: "refused",
4398
+ reason: "organization_required",
4399
+ 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."
4400
+ };
4401
+ }
4402
+ }
4156
4403
  return {
4157
- userId,
4158
- tenantId: row.organization_id ?? row.organizationId ?? void 0,
4159
- scopes: parseScopes(row.scopes)
4404
+ outcome: "admitted",
4405
+ principal: { userId, tenantId, scopes: parseScopes(row.scopes) }
4160
4406
  };
4161
4407
  }
4162
4408
  function readHeader(headers, name) {
@@ -4184,6 +4430,7 @@ function safeJsonParse(s, fallback) {
4184
4430
 
4185
4431
  // src/security/resolve-authz-context.ts
4186
4432
  var import_spec = require("@objectstack/spec");
4433
+ var import_security2 = require("@objectstack/spec/security");
4187
4434
 
4188
4435
  // src/security/grant-validity.ts
4189
4436
  function toEpochMs(value) {
@@ -4266,6 +4513,15 @@ function postureVisibleRows(posture, rows, principal) {
4266
4513
  }
4267
4514
  }
4268
4515
 
4516
+ // src/security/row-active.ts
4517
+ var DEACTIVATED_VALUES = [false, 0, "0", "false"];
4518
+ function isRowActive(row) {
4519
+ if (!row) return false;
4520
+ const value = row.active;
4521
+ if (value === void 0 || value === null) return true;
4522
+ return !DEACTIVATED_VALUES.includes(value);
4523
+ }
4524
+
4269
4525
  // src/security/resolve-authz-context.ts
4270
4526
  function safeJsonParse2(s, fallback) {
4271
4527
  try {
@@ -4295,7 +4551,12 @@ async function resolveAuthzContext(input) {
4295
4551
  };
4296
4552
  let userId;
4297
4553
  let tenantId;
4298
- const keyPrincipal = await resolveApiKeyPrincipal(ql, headers, input.nowMs);
4554
+ const admission = await resolveApiKeyAdmission(ql, headers, input.nowMs, input.tenancyPosture);
4555
+ if (admission.outcome === "refused") {
4556
+ ctx.authRefusal = { reason: admission.reason, message: admission.message };
4557
+ return ctx;
4558
+ }
4559
+ const keyPrincipal = admission.outcome === "admitted" ? admission.principal : void 0;
4299
4560
  if (keyPrincipal) {
4300
4561
  userId = keyPrincipal.userId;
4301
4562
  tenantId = keyPrincipal.tenantId;
@@ -4323,6 +4584,22 @@ async function resolveAuthzContext(input) {
4323
4584
  seedPermissions: ctx.permissions,
4324
4585
  seedEmail: ctx.email
4325
4586
  });
4587
+ if (keyPrincipal?.tenantId && input.tenancyPosture) {
4588
+ const posture = input.tenancyPosture;
4589
+ if ((0, import_security2.postureEnforcesWall)(posture) && !grants.accessible_org_ids.includes(keyPrincipal.tenantId)) {
4590
+ return {
4591
+ positions: [],
4592
+ permissions: [],
4593
+ systemPermissions: [],
4594
+ org_user_ids: [],
4595
+ accessible_org_ids: [],
4596
+ authRefusal: {
4597
+ reason: "organization_membership_ended",
4598
+ 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."
4599
+ }
4600
+ };
4601
+ }
4602
+ }
4326
4603
  ctx.positions = grants.positions;
4327
4604
  ctx.permissions = grants.permissions;
4328
4605
  ctx.systemPermissions = grants.systemPermissions;
@@ -4407,7 +4684,13 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4407
4684
  if (!grants.positions.includes("everyone")) grants.positions.push("everyone");
4408
4685
  if (grants.positions.length > 0) {
4409
4686
  const positionRows = await tryFind(ql, "sys_position", { name: { $in: grants.positions } }, 100);
4410
- const positionIds = positionRows.map((r) => r.id).filter(Boolean);
4687
+ const deactivatedNames = new Set(
4688
+ positionRows.filter((r) => !isRowActive(r)).map((r) => r.name).filter(Boolean)
4689
+ );
4690
+ if (deactivatedNames.size > 0) {
4691
+ grants.positions = grants.positions.filter((n) => !deactivatedNames.has(n));
4692
+ }
4693
+ const positionIds = positionRows.filter((r) => isRowActive(r)).map((r) => r.id).filter(Boolean);
4411
4694
  if (positionIds.length > 0) {
4412
4695
  const rpsRows = await tryFind(ql, "sys_position_permission_set", { position_id: { $in: positionIds } }, 500);
4413
4696
  for (const r of rpsRows) {
@@ -4417,7 +4700,8 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4417
4700
  }
4418
4701
  }
4419
4702
  if (psIds.size > 0) {
4420
- const psRows = await tryFind(ql, "sys_permission_set", { id: { $in: Array.from(psIds) } }, 500);
4703
+ const psRowsAll = await tryFind(ql, "sys_permission_set", { id: { $in: Array.from(psIds) } }, 500);
4704
+ const psRows = psRowsAll.filter((r) => isRowActive(r));
4421
4705
  const tabRank = { hidden: 0, default_off: 1, default_on: 2, visible: 3 };
4422
4706
  const mergedTabs = {};
4423
4707
  for (const ps of psRows) {
@@ -4648,6 +4932,7 @@ var ANONYMOUS_DENY_CODE = "UNAUTHENTICATED";
4648
4932
  var ANONYMOUS_DENY_MESSAGE = "Authentication is required to access this endpoint.";
4649
4933
  var ANONYMOUS_DENY_BODY = {
4650
4934
  error: ANONYMOUS_DENY_CODE,
4935
+ code: ANONYMOUS_DENY_CODE,
4651
4936
  message: ANONYMOUS_DENY_MESSAGE
4652
4937
  };
4653
4938
  function shouldDenyAnonymous(input) {
@@ -4661,6 +4946,88 @@ function shouldDenyAnonymous(input) {
4661
4946
  return true;
4662
4947
  }
4663
4948
 
4949
+ // src/security/admin-standing-surface.ts
4950
+ var ADMIN_STANDING_SURFACE = {
4951
+ sys_permission_set: {
4952
+ role: "derives",
4953
+ 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.",
4954
+ columns: [
4955
+ "id",
4956
+ "name",
4957
+ "active",
4958
+ "system_permissions",
4959
+ "systemPermissions",
4960
+ "tab_permissions",
4961
+ "tabPermissions"
4962
+ ]
4963
+ },
4964
+ sys_user_permission_set: {
4965
+ role: "derives",
4966
+ 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.",
4967
+ columns: [
4968
+ "user_id",
4969
+ "permission_set_id",
4970
+ "permissionSetId",
4971
+ "organization_id",
4972
+ "organizationId",
4973
+ "valid_from",
4974
+ "validFrom",
4975
+ "valid_until",
4976
+ "validUntil"
4977
+ ]
4978
+ },
4979
+ sys_member: {
4980
+ role: "derives",
4981
+ 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.",
4982
+ columns: [
4983
+ "user_id",
4984
+ "userId",
4985
+ "organization_id",
4986
+ "organizationId",
4987
+ "role",
4988
+ "valid_from",
4989
+ "validFrom",
4990
+ "valid_until",
4991
+ "validUntil"
4992
+ ]
4993
+ },
4994
+ sys_user: {
4995
+ role: "reads-only",
4996
+ 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."
4997
+ },
4998
+ sys_user_position: {
4999
+ role: "reads-only",
5000
+ 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."
5001
+ },
5002
+ sys_position: {
5003
+ role: "reads-only",
5004
+ reason: "Read to drop DEACTIVATED positions (ADR-0049, \xA76a). Same reason as `sys_user_position`: the position path cannot reach `hasPlatformAdminGrant`."
5005
+ },
5006
+ sys_position_permission_set: {
5007
+ role: "reads-only",
5008
+ 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."
5009
+ }
5010
+ };
5011
+ function adminStandingTables() {
5012
+ return Object.entries(ADMIN_STANDING_SURFACE).filter(([, t]) => t.role === "derives").map(([name]) => name).sort();
5013
+ }
5014
+ function adminStandingColumns(table) {
5015
+ const entry = ADMIN_STANDING_SURFACE[table];
5016
+ return entry?.role === "derives" ? entry.columns : void 0;
5017
+ }
5018
+
5019
+ // src/security/audience-binding-suggestion-status.ts
5020
+ var AUDIENCE_BINDING_SUGGESTION_STATUSES = {
5021
+ pending: true,
5022
+ confirmed: true,
5023
+ dismissed: true
5024
+ };
5025
+ var AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES = Object.keys(
5026
+ AUDIENCE_BINDING_SUGGESTION_STATUSES
5027
+ );
5028
+ var isAudienceBindingSuggestionStatus = (value) => Object.prototype.hasOwnProperty.call(AUDIENCE_BINDING_SUGGESTION_STATUSES, value);
5029
+ var unknownAudienceBindingSuggestionStatusMessage = (value) => `Unknown status filter '${value}' \u2014 expected one of: ${AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES.join(", ")}`;
5030
+
4664
5031
  // src/security/operation-private-keys.ts
4665
5032
  var OPERATION_PRIVATE_KEY_PREFIX = "__";
4666
5033
  function withoutOperationPrivateKeys(exec) {
@@ -4699,10 +5066,26 @@ function calendarPartsInTzOrUtc(d, tz) {
4699
5066
  }
4700
5067
  function zonedDateStartToUtcMs(ymd2, tz) {
4701
5068
  const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ymd2);
4702
- const wallAsUtc = m ? Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])) : NaN;
5069
+ if (!m) return NaN;
5070
+ return zonedWallClockToUtcMs(
5071
+ { year: Number(m[1]), month: Number(m[2]), day: Number(m[3]) },
5072
+ tz
5073
+ );
5074
+ }
5075
+ function zonedWallClockToUtcMs(parts, tz) {
5076
+ const wallAsUtc = Date.UTC(
5077
+ parts.year,
5078
+ parts.month - 1,
5079
+ parts.day,
5080
+ parts.hour ?? 0,
5081
+ parts.minute ?? 0,
5082
+ parts.second ?? 0,
5083
+ parts.millisecond ?? 0
5084
+ );
4703
5085
  if (!tz || tz === "UTC" || Number.isNaN(wallAsUtc)) return wallAsUtc;
4704
5086
  try {
4705
5087
  const offsetAt = (t) => {
5088
+ const whole = Math.floor(t / 1e3) * 1e3;
4706
5089
  const p = new Intl.DateTimeFormat("en-US", {
4707
5090
  timeZone: tz,
4708
5091
  hourCycle: "h23",
@@ -4712,9 +5095,9 @@ function zonedDateStartToUtcMs(ymd2, tz) {
4712
5095
  hour: "2-digit",
4713
5096
  minute: "2-digit",
4714
5097
  second: "2-digit"
4715
- }).formatToParts(new Date(t));
5098
+ }).formatToParts(new Date(whole));
4716
5099
  const g = (k) => Number(p.find((x) => x.type === k)?.value);
4717
- return Date.UTC(g("year"), g("month") - 1, g("day"), g("hour"), g("minute"), g("second")) - t;
5100
+ return Date.UTC(g("year"), g("month") - 1, g("day"), g("hour"), g("minute"), g("second")) - whole;
4718
5101
  };
4719
5102
  const off1 = offsetAt(wallAsUtc - offsetAt(wallAsUtc));
4720
5103
  return wallAsUtc - off1;
@@ -4914,9 +5297,30 @@ async function bulkWrite(rows, opts) {
4914
5297
  return results;
4915
5298
  }
4916
5299
 
5300
+ // src/utils/internal-write-response.ts
5301
+ function collectInternalWriteResponseFields(schema) {
5302
+ const fields = schema?.fields;
5303
+ if (!fields || typeof fields !== "object") return [];
5304
+ const out = [];
5305
+ for (const [name, def] of Object.entries(fields)) {
5306
+ if (def && def.internal === true) out.push(name);
5307
+ }
5308
+ return out;
5309
+ }
5310
+ function omitInternalFieldsFromWriteResponse(schema, records) {
5311
+ if (!records) return;
5312
+ const internalFields = collectInternalWriteResponseFields(schema);
5313
+ if (internalFields.length === 0) return;
5314
+ const list = Array.isArray(records) ? records : [records];
5315
+ for (const row of list) {
5316
+ if (!row || typeof row !== "object") continue;
5317
+ for (const field of internalFields) delete row[field];
5318
+ }
5319
+ }
5320
+
4917
5321
  // src/utils/migration-journal.ts
4918
5322
  var import_node_crypto3 = require("crypto");
4919
- var import_system3 = require("@objectstack/spec/system");
5323
+ var import_system4 = require("@objectstack/spec/system");
4920
5324
  var SYSTEM_CTX = { isSystem: true };
4921
5325
  var DEFAULT_CHUNK_SIZE = 200;
4922
5326
  function engineCanRollBack(engine) {
@@ -4974,14 +5378,14 @@ function hashMigrationPlan(plan, chunks) {
4974
5378
  }
4975
5379
  async function appendEvent(engine, event, execContext) {
4976
5380
  await engine.insert(
4977
- import_system3.MIGRATION_JOURNAL_OBJECT,
5381
+ import_system4.MIGRATION_JOURNAL_OBJECT,
4978
5382
  { ...event, created_at: event.created_at ?? (/* @__PURE__ */ new Date()).toISOString() },
4979
5383
  { context: execContext ?? { ...SYSTEM_CTX } }
4980
5384
  );
4981
5385
  }
4982
5386
  async function readRunJournal(engine, runId) {
4983
5387
  const rows = await engine.find(
4984
- import_system3.MIGRATION_JOURNAL_OBJECT,
5388
+ import_system4.MIGRATION_JOURNAL_OBJECT,
4985
5389
  { where: { run_id: runId } },
4986
5390
  { context: { ...SYSTEM_CTX } }
4987
5391
  );
@@ -4996,7 +5400,7 @@ function chunkSetOf(events, kind) {
4996
5400
  }
4997
5401
  async function findInterruptedRuns(engine) {
4998
5402
  const started = await engine.find(
4999
- import_system3.MIGRATION_JOURNAL_OBJECT,
5403
+ import_system4.MIGRATION_JOURNAL_OBJECT,
5000
5404
  { where: { kind: "run_started" } },
5001
5405
  { context: { ...SYSTEM_CTX } }
5002
5406
  );
@@ -5504,6 +5908,46 @@ function filterTokenContextFrom(execCtx, now) {
5504
5908
  };
5505
5909
  }
5506
5910
 
5911
+ // src/utils/temporal-comparand.ts
5912
+ var import_data3 = require("@objectstack/spec/data");
5913
+ function temporalComparandKind(fieldType) {
5914
+ if (fieldType === "datetime") return "datetime";
5915
+ if (fieldType === "date") return "date";
5916
+ if (fieldType === "time") return "time";
5917
+ return null;
5918
+ }
5919
+ function readsAsInstant(s) {
5920
+ if (/^-?\d+$/.test(s)) return Number.isFinite(new Date(Number(s)).getTime());
5921
+ 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;
5922
+ return Number.isFinite(Date.parse(iso));
5923
+ }
5924
+ function readsAsCalendarDay(s) {
5925
+ return /^\d{4}-\d{2}-\d{2}/.test(s);
5926
+ }
5927
+ function readsAsWallClock(s) {
5928
+ const m = /^(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?$/.exec(s);
5929
+ if (!m) return false;
5930
+ return Number(m[1]) <= 23 && Number(m[2]) <= 59 && Number(m[3] ?? "0") <= 59;
5931
+ }
5932
+ function isUninterpretableTemporalComparand(kind, value) {
5933
+ if (typeof value !== "string") return false;
5934
+ const s = value.trim();
5935
+ if (s === "") return false;
5936
+ if ((0, import_data3.classifyFilterToken)(value) !== null) return false;
5937
+ if (kind === "datetime") return !readsAsInstant(s);
5938
+ if (kind === "date") return !readsAsCalendarDay(s);
5939
+ return !(readsAsWallClock(s) || readsAsInstant(s));
5940
+ }
5941
+
5942
+ // src/utils/record-not-found.ts
5943
+ function recordNotFoundError(object, id) {
5944
+ const err = new Error(`Record ${id} not found in ${object}`);
5945
+ err.code = "RECORD_NOT_FOUND";
5946
+ err.status = 404;
5947
+ err.object = object;
5948
+ return err;
5949
+ }
5950
+
5507
5951
  // src/health-monitor.ts
5508
5952
  var PluginHealthMonitor = class {
5509
5953
  constructor(logger) {
@@ -6480,13 +6924,19 @@ var NamespaceResolver = class {
6480
6924
  return `${shortName}_${ns}`;
6481
6925
  }
6482
6926
  };
6927
+
6928
+ // src/index.ts
6929
+ var import_contracts = require("@objectstack/spec/contracts");
6483
6930
  // Annotate the CommonJS export names for ESM import in node:
6484
6931
  0 && (module.exports = {
6932
+ ADMIN_STANDING_SURFACE,
6485
6933
  ANONYMOUS_DENY_BODY,
6486
6934
  ANONYMOUS_DENY_CODE,
6487
6935
  ANONYMOUS_DENY_MESSAGE,
6488
6936
  ANONYMOUS_DENY_STATUS,
6489
6937
  API_KEY_PREFIX,
6938
+ AUDIENCE_BINDING_SUGGESTION_STATUSES,
6939
+ AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES,
6490
6940
  CORE_FALLBACK_FACTORIES,
6491
6941
  DependencyResolver,
6492
6942
  ENTRY_EXECUTION_CONTEXT_FIELDS,
@@ -6515,16 +6965,22 @@ var NamespaceResolver = class {
6515
6965
  SecurePluginContext,
6516
6966
  SemanticVersionManager,
6517
6967
  ServiceLifecycle,
6968
+ UNMATCHED_ROUTE_PATTERN,
6518
6969
  UnknownFilterTokenError,
6519
6970
  UnresolvedFilterTokenError,
6971
+ adminStandingColumns,
6972
+ adminStandingTables,
6520
6973
  assembleExecutionContext,
6521
6974
  assembleExecutionContextOrGuest,
6522
6975
  assertInitServiceRequirements,
6976
+ assertMetadataRegisterContract,
6523
6977
  bucketKeyToCalendarRange,
6524
6978
  buildPermissionsFromGrants,
6525
6979
  bulkWrite,
6526
6980
  calendarPartsInTz,
6527
6981
  calendarPartsInTzOrUtc,
6982
+ canonicalMetadataServiceType,
6983
+ collectInternalWriteResponseFields,
6528
6984
  counterSignPayload,
6529
6985
  createLogger,
6530
6986
  createMemoryCache,
@@ -6538,6 +6994,7 @@ var NamespaceResolver = class {
6538
6994
  defaultIsTransientError,
6539
6995
  derivePosture,
6540
6996
  describeInitOrderFault,
6997
+ effectiveTenancyPosture,
6541
6998
  engineCanRollBack,
6542
6999
  evaluateAuthGate,
6543
7000
  extractApiKey,
@@ -6549,19 +7006,25 @@ var NamespaceResolver = class {
6549
7006
  getMemoryUsage,
6550
7007
  hashApiKey,
6551
7008
  hashMigrationPlan,
7009
+ isAudienceBindingSuggestionStatus,
6552
7010
  isAuthGateAllowlisted,
6553
7011
  isExpired,
6554
7012
  isGrantActive,
6555
7013
  isGrantExpired,
6556
7014
  isNode,
7015
+ isRowActive,
7016
+ isUninterpretableTemporalComparand,
6557
7017
  nextUtcCalendarDay,
6558
7018
  normalizeAuthGate,
7019
+ omitInternalFieldsFromWriteResponse,
6559
7020
  parseScopes,
6560
7021
  parseSignature,
6561
7022
  planChunks,
6562
7023
  postureVisibleRows,
6563
7024
  readAuthoredTranslationLayer,
6564
7025
  readRunJournal,
7026
+ recordNotFoundError,
7027
+ resolveApiKeyAdmission,
6565
7028
  resolveApiKeyPrincipal,
6566
7029
  resolveAuthzContext,
6567
7030
  resolveFilterToken,
@@ -6575,6 +7038,8 @@ var NamespaceResolver = class {
6575
7038
  safeExit,
6576
7039
  shouldDenyAnonymous,
6577
7040
  signPayload,
7041
+ temporalComparandKind,
7042
+ unknownAudienceBindingSuggestionStatusMessage,
6578
7043
  utcInstantMs,
6579
7044
  validateInitServiceContract,
6580
7045
  verifyPayload,
@@ -6584,6 +7049,7 @@ var NamespaceResolver = class {
6584
7049
  wireAuthoredTranslationSync,
6585
7050
  withTransientRetry,
6586
7051
  withoutOperationPrivateKeys,
6587
- zonedDateStartToUtcMs
7052
+ zonedDateStartToUtcMs,
7053
+ zonedWallClockToUtcMs
6588
7054
  });
6589
7055
  //# sourceMappingURL=index.cjs.map