@objectstack/core 16.1.0 → 17.0.0-rc.1

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
@@ -10,6 +10,89 @@ var __export = (target, all) => {
10
10
  __defProp(target, name, { get: all[name], enumerable: true });
11
11
  };
12
12
 
13
+ // src/plugin-order.ts
14
+ function resolvePluginOrder(plugins) {
15
+ const resolved = [];
16
+ const visited = /* @__PURE__ */ new Set();
17
+ const visiting = /* @__PURE__ */ new Set();
18
+ const visit = (pluginName) => {
19
+ if (visited.has(pluginName)) return;
20
+ if (visiting.has(pluginName)) {
21
+ throw new Error(`[Kernel] Circular dependency detected: ${pluginName}`);
22
+ }
23
+ const plugin = plugins.get(pluginName);
24
+ if (!plugin) {
25
+ throw new Error(`[Kernel] Plugin '${pluginName}' not found`);
26
+ }
27
+ visiting.add(pluginName);
28
+ for (const dep of plugin.dependencies ?? []) {
29
+ if (!plugins.has(dep)) {
30
+ throw new Error(
31
+ `[Kernel] Dependency '${dep}' not found for plugin '${pluginName}'`
32
+ );
33
+ }
34
+ visit(dep);
35
+ }
36
+ for (const dep of plugin.optionalDependencies ?? []) {
37
+ if (plugins.has(dep)) visit(dep);
38
+ }
39
+ visiting.delete(pluginName);
40
+ visited.add(pluginName);
41
+ resolved.push(plugin);
42
+ };
43
+ for (const pluginName of plugins.keys()) {
44
+ visit(pluginName);
45
+ }
46
+ return resolved;
47
+ }
48
+ function validateInitServiceContract(ordered, isServiceRegistered) {
49
+ const providerSlot = /* @__PURE__ */ new Map();
50
+ ordered.forEach((plugin, slot) => {
51
+ for (const service of plugin.providesServices ?? []) {
52
+ if (!providerSlot.has(service)) {
53
+ providerSlot.set(service, { plugin: plugin.name, slot });
54
+ }
55
+ }
56
+ });
57
+ const violations = [];
58
+ ordered.forEach((plugin, slot) => {
59
+ for (const service of plugin.requiresServices ?? []) {
60
+ if (isServiceRegistered(service)) continue;
61
+ const provider = providerSlot.get(service);
62
+ if (provider && provider.slot > slot) {
63
+ violations.push(
64
+ `'${plugin.name}' requires service '${service}' during init, but '${service}' is provided by '${provider.plugin}', which initializes later (slot ${provider.slot} vs ${slot}). Registration order is not a contract \u2014 declare '${provider.plugin}' in '${plugin.name}'.dependencies (hard) or .optionalDependencies (order-if-present) so the kernel hoists it.`
65
+ );
66
+ }
67
+ }
68
+ });
69
+ if (violations.length > 0) {
70
+ throw new Error(
71
+ `[Kernel] Plugin ordering contract violated (#4131):
72
+ - ${violations.join("\n - ")}`
73
+ );
74
+ }
75
+ }
76
+ function describeInitOrderFault(currentlyInitializing, plugins, serviceName) {
77
+ if (!currentlyInitializing) return "";
78
+ let providerHint = "";
79
+ for (const plugin of plugins) {
80
+ if (plugin.providesServices?.includes(serviceName)) {
81
+ providerHint = ` '${serviceName}' is provided by composed plugin '${plugin.name}', which has not initialized yet \u2014 declare it in the requiring plugin's dependencies/optionalDependencies.`;
82
+ break;
83
+ }
84
+ }
85
+ return ` (while plugin '${currentlyInitializing}' was initializing \u2014 a composition/ordering fault, #4131.${providerHint})`;
86
+ }
87
+ function assertInitServiceRequirements(plugin, isServiceRegistered) {
88
+ for (const service of plugin.requiresServices ?? []) {
89
+ if (isServiceRegistered(service)) continue;
90
+ throw new Error(
91
+ `[Kernel] Plugin '${plugin.name}' requires service '${service}' at init, but no such service is registered at this point of the boot. No composed plugin that initializes earlier provides it \u2014 compose a provider (and, if it initializes later without declaring '${service}' in providesServices, order it ahead via this plugin's dependencies/optionalDependencies) (#4131).`
92
+ );
93
+ }
94
+ }
95
+
13
96
  // src/kernel-base.ts
14
97
  var ObjectKernelBase = class {
15
98
  constructor(logger) {
@@ -60,7 +143,9 @@ var ObjectKernelBase = class {
60
143
  if (this.services instanceof Map) {
61
144
  const service = this.services.get(name);
62
145
  if (!service) {
63
- throw new Error(`[Kernel] Service '${name}' not found`);
146
+ throw new Error(
147
+ `[Kernel] Service '${name}' not found${this.describeInitOrderFault(name)}`
148
+ );
64
149
  }
65
150
  return service;
66
151
  } else {
@@ -111,40 +196,37 @@ var ObjectKernelBase = class {
111
196
  };
112
197
  }
113
198
  /**
114
- * Resolve plugin dependencies using topological sort
199
+ * Resolve plugin dependencies using topological sort — `dependencies`
200
+ * hard, `optionalDependencies` order-if-present (ADR-0116, #4131). One
201
+ * implementation shared with ObjectKernel via `plugin-order.ts`.
115
202
  * @returns Ordered list of plugins (dependencies first)
116
203
  */
117
204
  resolveDependencies() {
118
- const resolved = [];
119
- const visited = /* @__PURE__ */ new Set();
120
- const visiting = /* @__PURE__ */ new Set();
121
- const visit = (pluginName) => {
122
- if (visited.has(pluginName)) return;
123
- if (visiting.has(pluginName)) {
124
- throw new Error(`[Kernel] Circular dependency detected: ${pluginName}`);
125
- }
126
- const plugin = this.plugins.get(pluginName);
127
- if (!plugin) {
128
- throw new Error(`[Kernel] Plugin '${pluginName}' not found`);
129
- }
130
- visiting.add(pluginName);
131
- const deps = plugin.dependencies || [];
132
- for (const dep of deps) {
133
- if (!this.plugins.has(dep)) {
134
- throw new Error(
135
- `[Kernel] Dependency '${dep}' not found for plugin '${pluginName}'`
136
- );
137
- }
138
- visit(dep);
139
- }
140
- visiting.delete(pluginName);
141
- visited.add(pluginName);
142
- resolved.push(plugin);
143
- };
144
- for (const pluginName of this.plugins.keys()) {
145
- visit(pluginName);
146
- }
147
- return resolved;
205
+ return resolvePluginOrder(this.plugins);
206
+ }
207
+ /**
208
+ * Whether a service is registered on this kernel right now. Backs the
209
+ * init-service contract checks (#4131).
210
+ */
211
+ hasRegisteredService(name) {
212
+ return this.services.has(name);
213
+ }
214
+ /**
215
+ * Pre-Phase-1 ordering validation (ADR-0116, #4131): a plugin whose
216
+ * `requiresServices` names a service provided only by a LATER plugin is
217
+ * a named boot error before any init side effects.
218
+ */
219
+ validateInitServices(ordered) {
220
+ validateInitServiceContract(ordered, (name) => this.hasRegisteredService(name));
221
+ }
222
+ /**
223
+ * When a getService miss happens while a plugin's init() is running,
224
+ * append the structural diagnosis (#4131): which plugin was initializing,
225
+ * and — when a composed plugin declares the service — who provides it.
226
+ * Empty string outside Phase 1, so non-boot messages stay unchanged.
227
+ */
228
+ describeInitOrderFault(serviceName) {
229
+ return describeInitOrderFault(this.currentlyInitializing, this.plugins.values(), serviceName);
148
230
  }
149
231
  /**
150
232
  * Run plugin init phase
@@ -153,12 +235,16 @@ var ObjectKernelBase = class {
153
235
  async runPluginInit(plugin) {
154
236
  const pluginName = plugin.name;
155
237
  this.logger.info(`Initializing plugin: ${pluginName}`);
238
+ assertInitServiceRequirements(plugin, (name) => this.hasRegisteredService(name));
239
+ this.currentlyInitializing = pluginName;
156
240
  try {
157
241
  await plugin.init(this.context);
158
242
  this.logger.info(`Plugin initialized: ${pluginName}`);
159
243
  } catch (error) {
160
244
  this.logger.error(`Plugin init failed: ${pluginName}`, error);
161
245
  throw error;
246
+ } finally {
247
+ this.currentlyInitializing = void 0;
162
248
  }
163
249
  }
164
250
  /**
@@ -1011,7 +1097,11 @@ function createMemoryCache() {
1011
1097
  let hits = 0;
1012
1098
  let misses = 0;
1013
1099
  return {
1014
- _fallback: true,
1100
+ __serviceInfo: {
1101
+ status: "degraded",
1102
+ handlerReady: false,
1103
+ message: "In-process Map cache \u2014 not shared across instances, lost on restart. Register a cache plugin (e.g. Redis) for a real one."
1104
+ },
1015
1105
  _serviceName: "cache",
1016
1106
  async get(key) {
1017
1107
  const entry = store.get(key);
@@ -1046,7 +1136,11 @@ function createMemoryQueue() {
1046
1136
  const handlers = /* @__PURE__ */ new Map();
1047
1137
  let msgId = 0;
1048
1138
  return {
1049
- _fallback: true,
1139
+ __serviceInfo: {
1140
+ status: "degraded",
1141
+ handlerReady: false,
1142
+ message: "Synchronous in-process delivery \u2014 no durability, retry, or cross-instance fan-out. Register a queue plugin (e.g. BullMQ) for a real one."
1143
+ },
1050
1144
  _serviceName: "queue",
1051
1145
  async publish(queue, data) {
1052
1146
  const id = `fallback-msg-${++msgId}`;
@@ -1073,7 +1167,11 @@ function createMemoryQueue() {
1073
1167
  function createMemoryJob() {
1074
1168
  const jobs = /* @__PURE__ */ new Map();
1075
1169
  return {
1076
- _fallback: true,
1170
+ __serviceInfo: {
1171
+ status: "degraded",
1172
+ handlerReady: false,
1173
+ 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."
1174
+ },
1077
1175
  _serviceName: "job",
1078
1176
  async schedule(name, schedule, handler) {
1079
1177
  jobs.set(name, { schedule, handler });
@@ -1152,7 +1250,15 @@ function createMemoryI18n() {
1152
1250
  return void 0;
1153
1251
  }
1154
1252
  return {
1155
- _fallback: true,
1253
+ // [#4058] `degraded` (ADR-0076 D12): translations, locale fallback and
1254
+ // interpolation are all real — what is missing is persistence and the
1255
+ // authoring surface service-i18n adds. `handlerReady` left at the
1256
+ // `degraded` default (true): the dispatcher's `/i18n` domain does serve
1257
+ // this implementation.
1258
+ __serviceInfo: {
1259
+ status: "degraded",
1260
+ message: "In-memory translations \u2014 real lookup and locale fallback, but nothing is persisted. Register I18nServicePlugin from @objectstack/service-i18n for the full implementation."
1261
+ },
1156
1262
  _serviceName: "i18n",
1157
1263
  t(key, locale, params) {
1158
1264
  const data = resolveTranslations(locale) ?? mergedLocale(defaultLocale);
@@ -1209,7 +1315,14 @@ function createMemoryMetadata() {
1209
1315
  return map;
1210
1316
  }
1211
1317
  return {
1212
- _fallback: true,
1318
+ // [#4058] `degraded` (ADR-0076 D12): the registry is real — everything
1319
+ // registered is listable and readable back — it simply never reaches disk
1320
+ // or a database. `handlerReady` keeps the `degraded` default (true): the
1321
+ // dispatcher's `/meta` domain serves this implementation.
1322
+ __serviceInfo: {
1323
+ status: "degraded",
1324
+ message: "In-memory metadata registry \u2014 real reads and writes, no persistence (lost on restart). Register MetadataPlugin for a persisted registry."
1325
+ },
1213
1326
  _serviceName: "metadata",
1214
1327
  async register(type, name, data) {
1215
1328
  getTypeMap(type).set(name, data);
@@ -1252,6 +1365,7 @@ function createMemoryMetadata() {
1252
1365
  }
1253
1366
 
1254
1367
  // src/fallbacks/authored-translation-sync.ts
1368
+ import { LEGACY_OBJECT_FIRST_KEYS } from "@objectstack/spec/system";
1255
1369
  var OWNER_PROP = "__authoredTranslationSyncOwner";
1256
1370
  var LOCALE_LIKE = /^[a-z]{2,3}([_-]([A-Za-z]{4}|[A-Za-z]{2}|[0-9]{3}))?$/;
1257
1371
  async function readAuthoredTranslationLayer(engine, logger) {
@@ -1281,14 +1395,32 @@ async function readAuthoredTranslationLayer(engine, logger) {
1281
1395
  continue;
1282
1396
  }
1283
1397
  if (!data || typeof data !== "object") continue;
1284
- const locale = typeof data?._meta?.locale === "string" && data._meta.locale || typeof data?.locale === "string" && data.locale || (typeof row?.name === "string" && LOCALE_LIKE.test(row.name) ? row.name : void 0) || void 0;
1398
+ const legacyKeys = LEGACY_OBJECT_FIRST_KEYS.filter((key) => data[key] !== void 0);
1399
+ if (legacyKeys.length > 0) {
1400
+ logger?.warn?.(
1401
+ `[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`
1402
+ );
1403
+ continue;
1404
+ }
1405
+ const locale = typeof data?.locale === "string" && data.locale || (typeof row?.name === "string" && LOCALE_LIKE.test(row.name) ? row.name : void 0) || void 0;
1285
1406
  if (!locale) {
1286
1407
  logger?.warn?.(
1287
- `[i18n] authored translation '${row?.name}' has no resolvable locale (set _meta.locale, or name the item after its BCP-47 locale) \u2014 skipped`
1408
+ `[i18n] authored translation '${row?.name}' has no resolvable locale (set the top-level 'locale', or name the item after its BCP-47 locale) \u2014 skipped`
1288
1409
  );
1289
1410
  continue;
1290
1411
  }
1291
- const { name: _n, locale: _l, _packageId: _p, _provenance: _pr, _lock: _lk, ...payload } = data;
1412
+ const {
1413
+ name: _n,
1414
+ locale: _l,
1415
+ _packageId: _p,
1416
+ _packageVersion: _pv,
1417
+ _provenance: _pr,
1418
+ _lock: _lk,
1419
+ _lockReason: _lr,
1420
+ _lockDocsUrl: _ld,
1421
+ _lockSource: _ls,
1422
+ ...payload
1423
+ } = data;
1292
1424
  byLocale[locale] = deepMerge(byLocale[locale] ?? {}, payload);
1293
1425
  }
1294
1426
  return byLocale;
@@ -1406,24 +1538,12 @@ var ObjectKernel = class {
1406
1538
  this.services.set(name, loaderService);
1407
1539
  return loaderService;
1408
1540
  }
1409
- try {
1410
- const service2 = this.pluginLoader.getService(name);
1411
- if (service2 instanceof Promise) {
1412
- service2.catch(() => {
1413
- });
1414
- throw new Error(`Service '${name}' is async - use await`);
1415
- }
1416
- return service2;
1417
- } catch (error) {
1418
- if (error.message?.includes("is async")) {
1419
- throw error;
1420
- }
1421
- const isNotFoundError = error.message === `Service '${name}' not found`;
1422
- if (!isNotFoundError) {
1423
- throw error;
1424
- }
1425
- throw new Error(`[Kernel] Service '${name}' not found`);
1541
+ if (!this.pluginLoader.hasService(name)) {
1542
+ throw new Error(
1543
+ `[Kernel] Service '${name}' not found${this.describeInitOrderFault(name)}`
1544
+ );
1426
1545
  }
1546
+ throw new Error(`Service '${name}' is async - use await`);
1427
1547
  },
1428
1548
  replaceService: (name, implementation) => {
1429
1549
  const hasService = this.services.has(name) || this.pluginLoader.hasService(name);
@@ -1582,6 +1702,7 @@ var ObjectKernel = class {
1582
1702
  this.logger.warn("Circular service dependencies detected:", { cycles });
1583
1703
  }
1584
1704
  const orderedPlugins = this.resolveDependencies();
1705
+ validateInitServiceContract(orderedPlugins, (name) => this.hasAnyService(name));
1585
1706
  this.logger.info("Phase 1: Init plugins");
1586
1707
  for (const plugin of orderedPlugins) {
1587
1708
  await this.initPluginWithTimeout(plugin);
@@ -1727,13 +1848,36 @@ var ObjectKernel = class {
1727
1848
  async initPluginWithTimeout(plugin) {
1728
1849
  const timeout = plugin.startupTimeout || this.config.defaultStartupTimeout;
1729
1850
  this.logger.debug(`Init: ${plugin.name}`, { plugin: plugin.name });
1730
- const initPromise = plugin.init(this.context);
1731
- const timeoutPromise = new Promise((_, reject) => {
1732
- setTimeout(() => {
1733
- reject(new Error(`Plugin ${plugin.name} init timeout after ${timeout}ms`));
1734
- }, timeout);
1735
- });
1736
- await Promise.race([initPromise, timeoutPromise]);
1851
+ assertInitServiceRequirements(plugin, (name) => this.hasAnyService(name));
1852
+ this.currentlyInitializing = plugin.name;
1853
+ try {
1854
+ const initPromise = plugin.init(this.context);
1855
+ const timeoutPromise = new Promise((_, reject) => {
1856
+ setTimeout(() => {
1857
+ reject(new Error(`Plugin ${plugin.name} init timeout after ${timeout}ms`));
1858
+ }, timeout);
1859
+ });
1860
+ await Promise.race([initPromise, timeoutPromise]);
1861
+ } finally {
1862
+ this.currentlyInitializing = void 0;
1863
+ }
1864
+ }
1865
+ /**
1866
+ * Whether a service is resolvable on this kernel right now — direct
1867
+ * registration or a loader-registered factory. Backs the init-service
1868
+ * contract checks (#4131).
1869
+ */
1870
+ hasAnyService(name) {
1871
+ return this.services.has(name) || this.pluginLoader.hasService(name);
1872
+ }
1873
+ /**
1874
+ * When a getService miss happens while a plugin's init() is running,
1875
+ * append the structural diagnosis (#4131): which plugin was initializing,
1876
+ * and — when a composed plugin declares the service — who provides it.
1877
+ * Empty string outside Phase 1, so non-boot messages stay unchanged.
1878
+ */
1879
+ describeInitOrderFault(serviceName) {
1880
+ return describeInitOrderFault(this.currentlyInitializing, this.plugins.values(), serviceName);
1737
1881
  }
1738
1882
  async startPluginWithTimeout(plugin) {
1739
1883
  if (!plugin.start) {
@@ -1807,35 +1951,13 @@ var ObjectKernel = class {
1807
1951
  }
1808
1952
  }
1809
1953
  }
1954
+ /**
1955
+ * Topological order over `dependencies` (hard) + `optionalDependencies`
1956
+ * (order-if-present) — ADR-0116, #4131. One implementation shared with
1957
+ * LiteKernel via `plugin-order.ts`.
1958
+ */
1810
1959
  resolveDependencies() {
1811
- const resolved = [];
1812
- const visited = /* @__PURE__ */ new Set();
1813
- const visiting = /* @__PURE__ */ new Set();
1814
- const visit = (pluginName) => {
1815
- if (visited.has(pluginName)) return;
1816
- if (visiting.has(pluginName)) {
1817
- throw new Error(`[Kernel] Circular dependency detected: ${pluginName}`);
1818
- }
1819
- const plugin = this.plugins.get(pluginName);
1820
- if (!plugin) {
1821
- throw new Error(`[Kernel] Plugin '${pluginName}' not found`);
1822
- }
1823
- visiting.add(pluginName);
1824
- const deps = plugin.dependencies || [];
1825
- for (const dep of deps) {
1826
- if (!this.plugins.has(dep)) {
1827
- throw new Error(`[Kernel] Dependency '${dep}' not found for plugin '${pluginName}'`);
1828
- }
1829
- visit(dep);
1830
- }
1831
- visiting.delete(pluginName);
1832
- visited.add(pluginName);
1833
- resolved.push(plugin);
1834
- };
1835
- for (const pluginName of this.plugins.keys()) {
1836
- visit(pluginName);
1837
- }
1838
- return resolved;
1960
+ return resolvePluginOrder(this.plugins);
1839
1961
  }
1840
1962
  registerShutdownSignals() {
1841
1963
  const signals = ["SIGINT", "SIGTERM", "SIGQUIT"];
@@ -1901,6 +2023,7 @@ var LiteKernel = class extends ObjectKernelBase {
1901
2023
  this.state = "initializing";
1902
2024
  this.logger.info("Bootstrap started");
1903
2025
  const orderedPlugins = this.resolveDependencies();
2026
+ this.validateInitServices(orderedPlugins);
1904
2027
  this.logger.info("Phase 1: Init plugins");
1905
2028
  for (const plugin of orderedPlugins) {
1906
2029
  await this.runPluginInit(plugin);
@@ -2501,6 +2624,11 @@ function createApiRegistryPlugin(config = {}) {
2501
2624
  } = config;
2502
2625
  return {
2503
2626
  name: "com.objectstack.core.api-registry",
2627
+ /**
2628
+ * Services init() registers on every path (ADR-0116, #4131) — lets the
2629
+ * kernel name this plugin when a consumer requires one before it inits.
2630
+ */
2631
+ providesServices: ["api-registry"],
2504
2632
  type: "standard",
2505
2633
  version: "1.0.0",
2506
2634
  init: async (ctx) => {
@@ -4212,7 +4340,7 @@ import {
4212
4340
  mapMembershipRole,
4213
4341
  BUILTIN_IDENTITY_PLATFORM_ADMIN,
4214
4342
  ADMIN_FULL_ACCESS,
4215
- ORGANIZATION_ADMIN
4343
+ ORGANIZATION_ADMIN_GRANTS
4216
4344
  } from "@objectstack/spec";
4217
4345
 
4218
4346
  // src/security/grant-validity.ts
@@ -4320,7 +4448,8 @@ async function resolveAuthzContext(input) {
4320
4448
  positions: [],
4321
4449
  permissions: [],
4322
4450
  systemPermissions: [],
4323
- org_user_ids: []
4451
+ org_user_ids: [],
4452
+ accessible_org_ids: []
4324
4453
  };
4325
4454
  let userId;
4326
4455
  let tenantId;
@@ -4356,6 +4485,7 @@ async function resolveAuthzContext(input) {
4356
4485
  ctx.permissions = grants.permissions;
4357
4486
  ctx.systemPermissions = grants.systemPermissions;
4358
4487
  ctx.org_user_ids = grants.org_user_ids;
4488
+ ctx.accessible_org_ids = grants.accessible_org_ids;
4359
4489
  if (grants.tabPermissions) ctx.tabPermissions = grants.tabPermissions;
4360
4490
  if (grants.posture) ctx.posture = grants.posture;
4361
4491
  if (grants.email && !ctx.email) ctx.email = grants.email;
@@ -4367,7 +4497,8 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4367
4497
  positions: [],
4368
4498
  permissions: Array.isArray(opts.seedPermissions) ? [...opts.seedPermissions] : [],
4369
4499
  systemPermissions: [],
4370
- org_user_ids: [userId]
4500
+ org_user_ids: [userId],
4501
+ accessible_org_ids: []
4371
4502
  };
4372
4503
  if (opts.seedEmail) grants.email = opts.seedEmail;
4373
4504
  if (!ql || typeof ql.find !== "function") return grants;
@@ -4385,9 +4516,17 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4385
4516
  const u = await getUserRow();
4386
4517
  if (u?.email) grants.email = String(u.email);
4387
4518
  }
4388
- const memberWhere = tenantId ? { user_id: userId, organization_id: tenantId } : { user_id: userId };
4389
- const members = await tryFind(ql, "sys_member", memberWhere, 50);
4519
+ const nowMs = opts.nowMs ?? Date.now();
4520
+ const members = await tryFind(ql, "sys_member", { user_id: userId }, 200);
4521
+ const accessibleOrgIds = /* @__PURE__ */ new Set();
4390
4522
  for (const m of members) {
4523
+ if (!isGrantActive(m, nowMs)) continue;
4524
+ const org = m.organization_id ?? m.organizationId;
4525
+ if (typeof org === "string" && org) accessibleOrgIds.add(org);
4526
+ }
4527
+ grants.accessible_org_ids = Array.from(accessibleOrgIds);
4528
+ const activeMembers = tenantId ? members.filter((m) => (m.organization_id ?? m.organizationId) === tenantId) : members;
4529
+ for (const m of activeMembers) {
4391
4530
  if (m.role && typeof m.role === "string") {
4392
4531
  for (const raw of m.role.split(",").map((s) => s.trim()).filter(Boolean)) {
4393
4532
  const r = mapMembershipRole(raw);
@@ -4395,7 +4534,6 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4395
4534
  }
4396
4535
  }
4397
4536
  }
4398
- const nowMs = opts.nowMs ?? Date.now();
4399
4537
  const userPositionRows = await tryFind(ql, "sys_user_position", { user_id: userId }, 200);
4400
4538
  for (const ur of userPositionRows) {
4401
4539
  const org = ur.organization_id ?? null;
@@ -4467,7 +4605,9 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4467
4605
  }
4468
4606
  grants.posture = derivePosture({
4469
4607
  isPlatformAdmin: hasPlatformAdminGrant,
4470
- isTenantAdmin: grants.permissions.includes(ORGANIZATION_ADMIN)
4608
+ // [ADR-0105 D4] Either org-admin capability set resolves the rung — the
4609
+ // wall-less variant differs only by withholding the superuser bits.
4610
+ isTenantAdmin: ORGANIZATION_ADMIN_GRANTS.some((n) => grants.permissions.includes(n))
4471
4611
  });
4472
4612
  if (!grants.permissions.includes("ai_seat")) {
4473
4613
  const aiAccess = (await getUserRow())?.ai_access;
@@ -4556,14 +4696,13 @@ function evaluateAuthGate(sessionUser, path) {
4556
4696
 
4557
4697
  // src/security/anonymous-deny.ts
4558
4698
  var ANONYMOUS_DENY_STATUS = 401;
4559
- var ANONYMOUS_DENY_CODE = "unauthenticated";
4699
+ var ANONYMOUS_DENY_CODE = "UNAUTHENTICATED";
4560
4700
  var ANONYMOUS_DENY_MESSAGE = "Authentication is required to access this endpoint.";
4561
4701
  var ANONYMOUS_DENY_BODY = {
4562
4702
  error: ANONYMOUS_DENY_CODE,
4563
4703
  message: ANONYMOUS_DENY_MESSAGE
4564
4704
  };
4565
4705
  function shouldDenyAnonymous(input) {
4566
- if (!input.requireAuth) return false;
4567
4706
  if (typeof input.method === "string" && input.method.toUpperCase() === "OPTIONS") {
4568
4707
  return false;
4569
4708
  }
@@ -4575,6 +4714,7 @@ function shouldDenyAnonymous(input) {
4575
4714
  }
4576
4715
 
4577
4716
  // src/utils/datetime.ts
4717
+ import { nextUtcCalendarDay, utcInstantMs } from "@objectstack/spec/data";
4578
4718
  function calendarPartsInTz(d, tz) {
4579
4719
  const parts = new Intl.DateTimeFormat("en-US", {
4580
4720
  timeZone: tz,
@@ -4598,8 +4738,8 @@ function calendarPartsInTzOrUtc(d, tz) {
4598
4738
  day: d.getUTCDate()
4599
4739
  };
4600
4740
  }
4601
- function zonedDateStartToUtcMs(ymd, tz) {
4602
- const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ymd);
4741
+ function zonedDateStartToUtcMs(ymd2, tz) {
4742
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ymd2);
4603
4743
  const wallAsUtc = m ? Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])) : NaN;
4604
4744
  if (!tz || tz === "UTC" || Number.isNaN(wallAsUtc)) return wallAsUtc;
4605
4745
  try {
@@ -4815,6 +4955,212 @@ async function bulkWrite(rows, opts) {
4815
4955
  return results;
4816
4956
  }
4817
4957
 
4958
+ // src/utils/filter-tokens.ts
4959
+ import {
4960
+ classifyFilterToken,
4961
+ parseDateMacroParam
4962
+ } from "@objectstack/spec/data";
4963
+ var UnknownFilterTokenError = class extends Error {
4964
+ constructor(token, suggestion) {
4965
+ super(
4966
+ `Unresolvable filter placeholder "{${token}}". ` + (suggestion ? `Did you mean "{${suggestion}}"? ` : "Resolvable placeholders are the context tokens ({current_user_id}, {current_org_id}) and the date macros ({today}, {current_quarter_start}, {30_days_ago}, \u2026). ") + "Sending it to the data engine verbatim would compare it as a literal string and match nothing, which is indistinguishable from an empty result."
4967
+ );
4968
+ this.status = 400;
4969
+ this.code = "FILTER_TOKEN_UNKNOWN";
4970
+ this.name = "UnknownFilterTokenError";
4971
+ this.token = token;
4972
+ this.suggestion = suggestion;
4973
+ }
4974
+ };
4975
+ var UnresolvedFilterTokenError = class extends Error {
4976
+ constructor(token, detail) {
4977
+ super(`Filter placeholder "{${token}}" cannot be resolved: ${detail}`);
4978
+ /** 400, not 500 — see {@link UnknownFilterTokenError}. */
4979
+ this.status = 400;
4980
+ this.code = "FILTER_TOKEN_UNRESOLVED";
4981
+ this.name = "UnresolvedFilterTokenError";
4982
+ this.token = token;
4983
+ }
4984
+ };
4985
+ function ymd(year, month, day) {
4986
+ const p = (n) => String(n).padStart(2, "0");
4987
+ return `${year}-${p(month)}-${p(day)}`;
4988
+ }
4989
+ function proxyDay(now, timezone) {
4990
+ const { year, month, day } = calendarPartsInTzOrUtc(now, timezone);
4991
+ return new Date(Date.UTC(year, month - 1, day));
4992
+ }
4993
+ var asYmd = (d) => ymd(d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate());
4994
+ function startOfPeriod(kind, d) {
4995
+ const r = new Date(d.getTime());
4996
+ switch (kind) {
4997
+ case "week": {
4998
+ const dow = (r.getUTCDay() + 6) % 7;
4999
+ r.setUTCDate(r.getUTCDate() - dow);
5000
+ return r;
5001
+ }
5002
+ case "month":
5003
+ return new Date(Date.UTC(r.getUTCFullYear(), r.getUTCMonth(), 1));
5004
+ case "quarter":
5005
+ return new Date(Date.UTC(r.getUTCFullYear(), Math.floor(r.getUTCMonth() / 3) * 3, 1));
5006
+ case "year":
5007
+ return new Date(Date.UTC(r.getUTCFullYear(), 0, 1));
5008
+ }
5009
+ }
5010
+ function daysInMonth(year, month) {
5011
+ return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
5012
+ }
5013
+ function addMonthsClamped(d, n) {
5014
+ const year = d.getUTCFullYear();
5015
+ const month = d.getUTCMonth() + n;
5016
+ const targetYear = year + Math.floor(month / 12);
5017
+ const targetMonth = (month % 12 + 12) % 12;
5018
+ const day = Math.min(d.getUTCDate(), daysInMonth(targetYear, targetMonth));
5019
+ return new Date(Date.UTC(
5020
+ targetYear,
5021
+ targetMonth,
5022
+ day,
5023
+ d.getUTCHours(),
5024
+ d.getUTCMinutes(),
5025
+ d.getUTCSeconds(),
5026
+ d.getUTCMilliseconds()
5027
+ ));
5028
+ }
5029
+ function addPeriods(kind, d, n) {
5030
+ switch (kind) {
5031
+ case "week": {
5032
+ const r = new Date(d.getTime());
5033
+ r.setUTCDate(r.getUTCDate() + n * 7);
5034
+ return r;
5035
+ }
5036
+ case "month":
5037
+ return addMonthsClamped(d, n);
5038
+ case "quarter":
5039
+ return addMonthsClamped(d, n * 3);
5040
+ case "year":
5041
+ return addMonthsClamped(d, n * 12);
5042
+ }
5043
+ }
5044
+ function addUnits(unit, d, n) {
5045
+ const r = new Date(d.getTime());
5046
+ switch (unit) {
5047
+ case "minute":
5048
+ r.setUTCMinutes(r.getUTCMinutes() + n);
5049
+ return r;
5050
+ case "hour":
5051
+ r.setUTCHours(r.getUTCHours() + n);
5052
+ return r;
5053
+ case "day":
5054
+ r.setUTCDate(r.getUTCDate() + n);
5055
+ return r;
5056
+ case "week":
5057
+ r.setUTCDate(r.getUTCDate() + n * 7);
5058
+ return r;
5059
+ // Month/year steps clamp rather than overflow — see addMonthsClamped.
5060
+ case "month":
5061
+ return addMonthsClamped(d, n);
5062
+ case "year":
5063
+ return addMonthsClamped(d, n * 12);
5064
+ }
5065
+ }
5066
+ var PERIOD_RE = /^(?:(current|last|next)_)?(week|month|quarter|year)_(start|end)$/;
5067
+ function resolvePeriodToken(token, today) {
5068
+ const m = PERIOD_RE.exec(token);
5069
+ if (!m) return void 0;
5070
+ const rel = m[1] ?? "current";
5071
+ const kind = m[2];
5072
+ const bound = m[3];
5073
+ const offset = rel === "last" ? -1 : rel === "next" ? 1 : 0;
5074
+ const periodStart = startOfPeriod(kind, addPeriods(kind, startOfPeriod(kind, today), offset));
5075
+ if (bound === "start") return asYmd(periodStart);
5076
+ const next = addPeriods(kind, periodStart, 1);
5077
+ next.setUTCDate(next.getUTCDate() - 1);
5078
+ return asYmd(next);
5079
+ }
5080
+ function resolveFilterToken(token, ctx = {}) {
5081
+ const now = ctx.now ?? /* @__PURE__ */ new Date();
5082
+ if (token === "current_user_id") {
5083
+ if (!ctx.userId) {
5084
+ throw new UnresolvedFilterTokenError(
5085
+ token,
5086
+ "the request has no authenticated user. A filter scoped to the signed-in user cannot run for an anonymous or system caller \u2014 gate the surface on authentication, or drop the token from the filter."
5087
+ );
5088
+ }
5089
+ return ctx.userId;
5090
+ }
5091
+ if (token === "current_org_id") {
5092
+ if (!ctx.orgId) {
5093
+ throw new UnresolvedFilterTokenError(
5094
+ token,
5095
+ "the request carries no active organization (ExecutionContext.tenantId is unset). Set the active org on the request, or drop the token from the filter."
5096
+ );
5097
+ }
5098
+ return ctx.orgId;
5099
+ }
5100
+ const today = proxyDay(now, ctx.timezone);
5101
+ switch (token) {
5102
+ case "now":
5103
+ return now.toISOString();
5104
+ case "today":
5105
+ return asYmd(today);
5106
+ case "yesterday":
5107
+ return asYmd(addUnits("day", today, -1));
5108
+ case "tomorrow":
5109
+ return asYmd(addUnits("day", today, 1));
5110
+ }
5111
+ const period = resolvePeriodToken(token, today);
5112
+ if (period !== void 0) return period;
5113
+ const param = parseDateMacroParam(token);
5114
+ if (param) {
5115
+ const sign = param.direction === "ago" ? -1 : 1;
5116
+ if (param.unit === "minute" || param.unit === "hour") {
5117
+ return addUnits(param.unit, now, sign * param.n).toISOString();
5118
+ }
5119
+ return asYmd(addUnits(param.unit, today, sign * param.n));
5120
+ }
5121
+ return void 0;
5122
+ }
5123
+ function hasFilterToken(node) {
5124
+ if (typeof node === "string") return classifyFilterToken(node) !== null;
5125
+ if (Array.isArray(node)) return node.some(hasFilterToken);
5126
+ if (node && typeof node === "object" && !(node instanceof Date)) {
5127
+ return Object.values(node).some(hasFilterToken);
5128
+ }
5129
+ return false;
5130
+ }
5131
+ function resolveFilterTokens(filter, ctx = {}) {
5132
+ if (filter == null) return filter;
5133
+ if (!hasFilterToken(filter)) return filter;
5134
+ const pinned = { ...ctx, now: ctx.now ?? /* @__PURE__ */ new Date() };
5135
+ const walk = (node) => {
5136
+ if (typeof node === "string") {
5137
+ const cls = classifyFilterToken(node);
5138
+ if (!cls) return node;
5139
+ if (cls.kind === "unknown") throw new UnknownFilterTokenError(cls.token, cls.suggestion);
5140
+ const resolved = resolveFilterToken(cls.token, pinned);
5141
+ if (resolved === void 0) throw new UnknownFilterTokenError(cls.token);
5142
+ return resolved;
5143
+ }
5144
+ if (Array.isArray(node)) return node.map(walk);
5145
+ if (node && typeof node === "object") {
5146
+ if (node instanceof Date) return node;
5147
+ const out = {};
5148
+ for (const [k, v] of Object.entries(node)) out[k] = walk(v);
5149
+ return out;
5150
+ }
5151
+ return node;
5152
+ };
5153
+ return walk(filter);
5154
+ }
5155
+ function filterTokenContextFrom(execCtx, now) {
5156
+ return {
5157
+ now,
5158
+ timezone: execCtx?.timezone,
5159
+ userId: execCtx?.userId,
5160
+ orgId: execCtx?.tenantId
5161
+ };
5162
+ }
5163
+
4818
5164
  // src/health-monitor.ts
4819
5165
  var PluginHealthMonitor = class {
4820
5166
  constructor(logger) {
@@ -5759,6 +6105,9 @@ export {
5759
6105
  SecurePluginContext,
5760
6106
  SemanticVersionManager,
5761
6107
  ServiceLifecycle,
6108
+ UnknownFilterTokenError,
6109
+ UnresolvedFilterTokenError,
6110
+ assertInitServiceRequirements,
5762
6111
  bucketKeyToCalendarRange,
5763
6112
  buildPermissionsFromGrants,
5764
6113
  bulkWrite,
@@ -5777,8 +6126,10 @@ export {
5777
6126
  deepMerge,
5778
6127
  defaultIsTransientError,
5779
6128
  derivePosture,
6129
+ describeInitOrderFault,
5780
6130
  evaluateAuthGate,
5781
6131
  extractApiKey,
6132
+ filterTokenContextFrom,
5782
6133
  generateApiKey,
5783
6134
  generateEd25519KeyPair,
5784
6135
  getEnv,
@@ -5789,18 +6140,24 @@ export {
5789
6140
  isGrantActive,
5790
6141
  isGrantExpired,
5791
6142
  isNode,
6143
+ nextUtcCalendarDay,
5792
6144
  parseScopes,
5793
6145
  parseSignature,
5794
6146
  postureVisibleRows,
5795
6147
  readAuthoredTranslationLayer,
5796
6148
  resolveApiKeyPrincipal,
5797
6149
  resolveAuthzContext,
6150
+ resolveFilterToken,
6151
+ resolveFilterTokens,
5798
6152
  resolveLocale,
5799
6153
  resolveLocalizationContext,
6154
+ resolvePluginOrder,
5800
6155
  resolveUserAuthzGrants,
5801
6156
  safeExit,
5802
6157
  shouldDenyAnonymous,
5803
6158
  signPayload,
6159
+ utcInstantMs,
6160
+ validateInitServiceContract,
5804
6161
  verifyPayload,
5805
6162
  verifyPlatformSignature,
5806
6163
  verifyPluginArtifact,