@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.cjs CHANGED
@@ -60,6 +60,9 @@ __export(index_exports, {
60
60
  SecurePluginContext: () => SecurePluginContext,
61
61
  SemanticVersionManager: () => SemanticVersionManager,
62
62
  ServiceLifecycle: () => ServiceLifecycle,
63
+ UnknownFilterTokenError: () => UnknownFilterTokenError,
64
+ UnresolvedFilterTokenError: () => UnresolvedFilterTokenError,
65
+ assertInitServiceRequirements: () => assertInitServiceRequirements,
63
66
  bucketKeyToCalendarRange: () => bucketKeyToCalendarRange,
64
67
  buildPermissionsFromGrants: () => buildPermissionsFromGrants,
65
68
  bulkWrite: () => bulkWrite,
@@ -78,8 +81,10 @@ __export(index_exports, {
78
81
  deepMerge: () => deepMerge,
79
82
  defaultIsTransientError: () => defaultIsTransientError,
80
83
  derivePosture: () => derivePosture,
84
+ describeInitOrderFault: () => describeInitOrderFault,
81
85
  evaluateAuthGate: () => evaluateAuthGate,
82
86
  extractApiKey: () => extractApiKey,
87
+ filterTokenContextFrom: () => filterTokenContextFrom,
83
88
  generateApiKey: () => generateApiKey,
84
89
  generateEd25519KeyPair: () => generateEd25519KeyPair,
85
90
  getEnv: () => getEnv,
@@ -90,18 +95,24 @@ __export(index_exports, {
90
95
  isGrantActive: () => isGrantActive,
91
96
  isGrantExpired: () => isGrantExpired,
92
97
  isNode: () => isNode,
98
+ nextUtcCalendarDay: () => import_data.nextUtcCalendarDay,
93
99
  parseScopes: () => parseScopes,
94
100
  parseSignature: () => parseSignature,
95
101
  postureVisibleRows: () => postureVisibleRows,
96
102
  readAuthoredTranslationLayer: () => readAuthoredTranslationLayer,
97
103
  resolveApiKeyPrincipal: () => resolveApiKeyPrincipal,
98
104
  resolveAuthzContext: () => resolveAuthzContext,
105
+ resolveFilterToken: () => resolveFilterToken,
106
+ resolveFilterTokens: () => resolveFilterTokens,
99
107
  resolveLocale: () => resolveLocale,
100
108
  resolveLocalizationContext: () => resolveLocalizationContext,
109
+ resolvePluginOrder: () => resolvePluginOrder,
101
110
  resolveUserAuthzGrants: () => resolveUserAuthzGrants,
102
111
  safeExit: () => safeExit,
103
112
  shouldDenyAnonymous: () => shouldDenyAnonymous,
104
113
  signPayload: () => signPayload,
114
+ utcInstantMs: () => import_data.utcInstantMs,
115
+ validateInitServiceContract: () => validateInitServiceContract,
105
116
  verifyPayload: () => verifyPayload,
106
117
  verifyPlatformSignature: () => verifyPlatformSignature,
107
118
  verifyPluginArtifact: () => verifyPluginArtifact,
@@ -112,6 +123,89 @@ __export(index_exports, {
112
123
  });
113
124
  module.exports = __toCommonJS(index_exports);
114
125
 
126
+ // src/plugin-order.ts
127
+ function resolvePluginOrder(plugins) {
128
+ const resolved = [];
129
+ const visited = /* @__PURE__ */ new Set();
130
+ const visiting = /* @__PURE__ */ new Set();
131
+ const visit = (pluginName) => {
132
+ if (visited.has(pluginName)) return;
133
+ if (visiting.has(pluginName)) {
134
+ throw new Error(`[Kernel] Circular dependency detected: ${pluginName}`);
135
+ }
136
+ const plugin = plugins.get(pluginName);
137
+ if (!plugin) {
138
+ throw new Error(`[Kernel] Plugin '${pluginName}' not found`);
139
+ }
140
+ visiting.add(pluginName);
141
+ for (const dep of plugin.dependencies ?? []) {
142
+ if (!plugins.has(dep)) {
143
+ throw new Error(
144
+ `[Kernel] Dependency '${dep}' not found for plugin '${pluginName}'`
145
+ );
146
+ }
147
+ visit(dep);
148
+ }
149
+ for (const dep of plugin.optionalDependencies ?? []) {
150
+ if (plugins.has(dep)) visit(dep);
151
+ }
152
+ visiting.delete(pluginName);
153
+ visited.add(pluginName);
154
+ resolved.push(plugin);
155
+ };
156
+ for (const pluginName of plugins.keys()) {
157
+ visit(pluginName);
158
+ }
159
+ return resolved;
160
+ }
161
+ function validateInitServiceContract(ordered, isServiceRegistered) {
162
+ const providerSlot = /* @__PURE__ */ new Map();
163
+ ordered.forEach((plugin, slot) => {
164
+ for (const service of plugin.providesServices ?? []) {
165
+ if (!providerSlot.has(service)) {
166
+ providerSlot.set(service, { plugin: plugin.name, slot });
167
+ }
168
+ }
169
+ });
170
+ const violations = [];
171
+ ordered.forEach((plugin, slot) => {
172
+ for (const service of plugin.requiresServices ?? []) {
173
+ if (isServiceRegistered(service)) continue;
174
+ const provider = providerSlot.get(service);
175
+ if (provider && provider.slot > slot) {
176
+ violations.push(
177
+ `'${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.`
178
+ );
179
+ }
180
+ }
181
+ });
182
+ if (violations.length > 0) {
183
+ throw new Error(
184
+ `[Kernel] Plugin ordering contract violated (#4131):
185
+ - ${violations.join("\n - ")}`
186
+ );
187
+ }
188
+ }
189
+ function describeInitOrderFault(currentlyInitializing, plugins, serviceName) {
190
+ if (!currentlyInitializing) return "";
191
+ let providerHint = "";
192
+ for (const plugin of plugins) {
193
+ if (plugin.providesServices?.includes(serviceName)) {
194
+ providerHint = ` '${serviceName}' is provided by composed plugin '${plugin.name}', which has not initialized yet \u2014 declare it in the requiring plugin's dependencies/optionalDependencies.`;
195
+ break;
196
+ }
197
+ }
198
+ return ` (while plugin '${currentlyInitializing}' was initializing \u2014 a composition/ordering fault, #4131.${providerHint})`;
199
+ }
200
+ function assertInitServiceRequirements(plugin, isServiceRegistered) {
201
+ for (const service of plugin.requiresServices ?? []) {
202
+ if (isServiceRegistered(service)) continue;
203
+ throw new Error(
204
+ `[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).`
205
+ );
206
+ }
207
+ }
208
+
115
209
  // src/kernel-base.ts
116
210
  var ObjectKernelBase = class {
117
211
  constructor(logger) {
@@ -162,7 +256,9 @@ var ObjectKernelBase = class {
162
256
  if (this.services instanceof Map) {
163
257
  const service = this.services.get(name);
164
258
  if (!service) {
165
- throw new Error(`[Kernel] Service '${name}' not found`);
259
+ throw new Error(
260
+ `[Kernel] Service '${name}' not found${this.describeInitOrderFault(name)}`
261
+ );
166
262
  }
167
263
  return service;
168
264
  } else {
@@ -213,40 +309,37 @@ var ObjectKernelBase = class {
213
309
  };
214
310
  }
215
311
  /**
216
- * Resolve plugin dependencies using topological sort
312
+ * Resolve plugin dependencies using topological sort — `dependencies`
313
+ * hard, `optionalDependencies` order-if-present (ADR-0116, #4131). One
314
+ * implementation shared with ObjectKernel via `plugin-order.ts`.
217
315
  * @returns Ordered list of plugins (dependencies first)
218
316
  */
219
317
  resolveDependencies() {
220
- const resolved = [];
221
- const visited = /* @__PURE__ */ new Set();
222
- const visiting = /* @__PURE__ */ new Set();
223
- const visit = (pluginName) => {
224
- if (visited.has(pluginName)) return;
225
- if (visiting.has(pluginName)) {
226
- throw new Error(`[Kernel] Circular dependency detected: ${pluginName}`);
227
- }
228
- const plugin = this.plugins.get(pluginName);
229
- if (!plugin) {
230
- throw new Error(`[Kernel] Plugin '${pluginName}' not found`);
231
- }
232
- visiting.add(pluginName);
233
- const deps = plugin.dependencies || [];
234
- for (const dep of deps) {
235
- if (!this.plugins.has(dep)) {
236
- throw new Error(
237
- `[Kernel] Dependency '${dep}' not found for plugin '${pluginName}'`
238
- );
239
- }
240
- visit(dep);
241
- }
242
- visiting.delete(pluginName);
243
- visited.add(pluginName);
244
- resolved.push(plugin);
245
- };
246
- for (const pluginName of this.plugins.keys()) {
247
- visit(pluginName);
248
- }
249
- return resolved;
318
+ return resolvePluginOrder(this.plugins);
319
+ }
320
+ /**
321
+ * Whether a service is registered on this kernel right now. Backs the
322
+ * init-service contract checks (#4131).
323
+ */
324
+ hasRegisteredService(name) {
325
+ return this.services.has(name);
326
+ }
327
+ /**
328
+ * Pre-Phase-1 ordering validation (ADR-0116, #4131): a plugin whose
329
+ * `requiresServices` names a service provided only by a LATER plugin is
330
+ * a named boot error before any init side effects.
331
+ */
332
+ validateInitServices(ordered) {
333
+ validateInitServiceContract(ordered, (name) => this.hasRegisteredService(name));
334
+ }
335
+ /**
336
+ * When a getService miss happens while a plugin's init() is running,
337
+ * append the structural diagnosis (#4131): which plugin was initializing,
338
+ * and — when a composed plugin declares the service — who provides it.
339
+ * Empty string outside Phase 1, so non-boot messages stay unchanged.
340
+ */
341
+ describeInitOrderFault(serviceName) {
342
+ return describeInitOrderFault(this.currentlyInitializing, this.plugins.values(), serviceName);
250
343
  }
251
344
  /**
252
345
  * Run plugin init phase
@@ -255,12 +348,16 @@ var ObjectKernelBase = class {
255
348
  async runPluginInit(plugin) {
256
349
  const pluginName = plugin.name;
257
350
  this.logger.info(`Initializing plugin: ${pluginName}`);
351
+ assertInitServiceRequirements(plugin, (name) => this.hasRegisteredService(name));
352
+ this.currentlyInitializing = pluginName;
258
353
  try {
259
354
  await plugin.init(this.context);
260
355
  this.logger.info(`Plugin initialized: ${pluginName}`);
261
356
  } catch (error) {
262
357
  this.logger.error(`Plugin init failed: ${pluginName}`, error);
263
358
  throw error;
359
+ } finally {
360
+ this.currentlyInitializing = void 0;
264
361
  }
265
362
  }
266
363
  /**
@@ -535,7 +632,7 @@ function createLogger(config) {
535
632
  }
536
633
 
537
634
  // src/kernel.ts
538
- var import_system = require("@objectstack/spec/system");
635
+ var import_system2 = require("@objectstack/spec/system");
539
636
 
540
637
  // src/security/plugin-config-validator.ts
541
638
  var import_zod = require("zod");
@@ -1107,7 +1204,11 @@ function createMemoryCache() {
1107
1204
  let hits = 0;
1108
1205
  let misses = 0;
1109
1206
  return {
1110
- _fallback: true,
1207
+ __serviceInfo: {
1208
+ status: "degraded",
1209
+ handlerReady: false,
1210
+ message: "In-process Map cache \u2014 not shared across instances, lost on restart. Register a cache plugin (e.g. Redis) for a real one."
1211
+ },
1111
1212
  _serviceName: "cache",
1112
1213
  async get(key) {
1113
1214
  const entry = store.get(key);
@@ -1142,7 +1243,11 @@ function createMemoryQueue() {
1142
1243
  const handlers = /* @__PURE__ */ new Map();
1143
1244
  let msgId = 0;
1144
1245
  return {
1145
- _fallback: true,
1246
+ __serviceInfo: {
1247
+ status: "degraded",
1248
+ handlerReady: false,
1249
+ 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."
1250
+ },
1146
1251
  _serviceName: "queue",
1147
1252
  async publish(queue, data) {
1148
1253
  const id = `fallback-msg-${++msgId}`;
@@ -1169,7 +1274,11 @@ function createMemoryQueue() {
1169
1274
  function createMemoryJob() {
1170
1275
  const jobs = /* @__PURE__ */ new Map();
1171
1276
  return {
1172
- _fallback: true,
1277
+ __serviceInfo: {
1278
+ status: "degraded",
1279
+ handlerReady: false,
1280
+ 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."
1281
+ },
1173
1282
  _serviceName: "job",
1174
1283
  async schedule(name, schedule, handler) {
1175
1284
  jobs.set(name, { schedule, handler });
@@ -1248,7 +1357,15 @@ function createMemoryI18n() {
1248
1357
  return void 0;
1249
1358
  }
1250
1359
  return {
1251
- _fallback: true,
1360
+ // [#4058] `degraded` (ADR-0076 D12): translations, locale fallback and
1361
+ // interpolation are all real — what is missing is persistence and the
1362
+ // authoring surface service-i18n adds. `handlerReady` left at the
1363
+ // `degraded` default (true): the dispatcher's `/i18n` domain does serve
1364
+ // this implementation.
1365
+ __serviceInfo: {
1366
+ status: "degraded",
1367
+ message: "In-memory translations \u2014 real lookup and locale fallback, but nothing is persisted. Register I18nServicePlugin from @objectstack/service-i18n for the full implementation."
1368
+ },
1252
1369
  _serviceName: "i18n",
1253
1370
  t(key, locale, params) {
1254
1371
  const data = resolveTranslations(locale) ?? mergedLocale(defaultLocale);
@@ -1305,7 +1422,14 @@ function createMemoryMetadata() {
1305
1422
  return map;
1306
1423
  }
1307
1424
  return {
1308
- _fallback: true,
1425
+ // [#4058] `degraded` (ADR-0076 D12): the registry is real — everything
1426
+ // registered is listable and readable back — it simply never reaches disk
1427
+ // or a database. `handlerReady` keeps the `degraded` default (true): the
1428
+ // dispatcher's `/meta` domain serves this implementation.
1429
+ __serviceInfo: {
1430
+ status: "degraded",
1431
+ message: "In-memory metadata registry \u2014 real reads and writes, no persistence (lost on restart). Register MetadataPlugin for a persisted registry."
1432
+ },
1309
1433
  _serviceName: "metadata",
1310
1434
  async register(type, name, data) {
1311
1435
  getTypeMap(type).set(name, data);
@@ -1348,6 +1472,7 @@ function createMemoryMetadata() {
1348
1472
  }
1349
1473
 
1350
1474
  // src/fallbacks/authored-translation-sync.ts
1475
+ var import_system = require("@objectstack/spec/system");
1351
1476
  var OWNER_PROP = "__authoredTranslationSyncOwner";
1352
1477
  var LOCALE_LIKE = /^[a-z]{2,3}([_-]([A-Za-z]{4}|[A-Za-z]{2}|[0-9]{3}))?$/;
1353
1478
  async function readAuthoredTranslationLayer(engine, logger) {
@@ -1377,14 +1502,32 @@ async function readAuthoredTranslationLayer(engine, logger) {
1377
1502
  continue;
1378
1503
  }
1379
1504
  if (!data || typeof data !== "object") continue;
1380
- 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;
1505
+ const legacyKeys = import_system.LEGACY_OBJECT_FIRST_KEYS.filter((key) => data[key] !== void 0);
1506
+ if (legacyKeys.length > 0) {
1507
+ logger?.warn?.(
1508
+ `[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`
1509
+ );
1510
+ continue;
1511
+ }
1512
+ const locale = typeof data?.locale === "string" && data.locale || (typeof row?.name === "string" && LOCALE_LIKE.test(row.name) ? row.name : void 0) || void 0;
1381
1513
  if (!locale) {
1382
1514
  logger?.warn?.(
1383
- `[i18n] authored translation '${row?.name}' has no resolvable locale (set _meta.locale, or name the item after its BCP-47 locale) \u2014 skipped`
1515
+ `[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`
1384
1516
  );
1385
1517
  continue;
1386
1518
  }
1387
- const { name: _n, locale: _l, _packageId: _p, _provenance: _pr, _lock: _lk, ...payload } = data;
1519
+ const {
1520
+ name: _n,
1521
+ locale: _l,
1522
+ _packageId: _p,
1523
+ _packageVersion: _pv,
1524
+ _provenance: _pr,
1525
+ _lock: _lk,
1526
+ _lockReason: _lr,
1527
+ _lockDocsUrl: _ld,
1528
+ _lockSource: _ls,
1529
+ ...payload
1530
+ } = data;
1388
1531
  byLocale[locale] = deepMerge(byLocale[locale] ?? {}, payload);
1389
1532
  }
1390
1533
  return byLocale;
@@ -1502,24 +1645,12 @@ var ObjectKernel = class {
1502
1645
  this.services.set(name, loaderService);
1503
1646
  return loaderService;
1504
1647
  }
1505
- try {
1506
- const service2 = this.pluginLoader.getService(name);
1507
- if (service2 instanceof Promise) {
1508
- service2.catch(() => {
1509
- });
1510
- throw new Error(`Service '${name}' is async - use await`);
1511
- }
1512
- return service2;
1513
- } catch (error) {
1514
- if (error.message?.includes("is async")) {
1515
- throw error;
1516
- }
1517
- const isNotFoundError = error.message === `Service '${name}' not found`;
1518
- if (!isNotFoundError) {
1519
- throw error;
1520
- }
1521
- throw new Error(`[Kernel] Service '${name}' not found`);
1648
+ if (!this.pluginLoader.hasService(name)) {
1649
+ throw new Error(
1650
+ `[Kernel] Service '${name}' not found${this.describeInitOrderFault(name)}`
1651
+ );
1522
1652
  }
1653
+ throw new Error(`Service '${name}' is async - use await`);
1523
1654
  },
1524
1655
  replaceService: (name, implementation) => {
1525
1656
  const hasService = this.services.has(name) || this.pluginLoader.hasService(name);
@@ -1608,7 +1739,7 @@ var ObjectKernel = class {
1608
1739
  */
1609
1740
  preInjectCoreFallbacks() {
1610
1741
  if (this.config.skipSystemValidation) return;
1611
- for (const [serviceName, criticality] of Object.entries(import_system.ServiceRequirementDef)) {
1742
+ for (const [serviceName, criticality] of Object.entries(import_system2.ServiceRequirementDef)) {
1612
1743
  if (criticality !== "core") continue;
1613
1744
  const hasService = this.services.has(serviceName) || this.pluginLoader.hasService(serviceName);
1614
1745
  if (!hasService) {
@@ -1632,7 +1763,7 @@ var ObjectKernel = class {
1632
1763
  this.logger.debug("Validating system service requirements...");
1633
1764
  const missingServices = [];
1634
1765
  const missingCoreServices = [];
1635
- for (const [serviceName, criticality] of Object.entries(import_system.ServiceRequirementDef)) {
1766
+ for (const [serviceName, criticality] of Object.entries(import_system2.ServiceRequirementDef)) {
1636
1767
  const hasService = this.services.has(serviceName) || this.pluginLoader.hasService(serviceName);
1637
1768
  if (!hasService) {
1638
1769
  if (criticality === "required") {
@@ -1678,6 +1809,7 @@ var ObjectKernel = class {
1678
1809
  this.logger.warn("Circular service dependencies detected:", { cycles });
1679
1810
  }
1680
1811
  const orderedPlugins = this.resolveDependencies();
1812
+ validateInitServiceContract(orderedPlugins, (name) => this.hasAnyService(name));
1681
1813
  this.logger.info("Phase 1: Init plugins");
1682
1814
  for (const plugin of orderedPlugins) {
1683
1815
  await this.initPluginWithTimeout(plugin);
@@ -1823,13 +1955,36 @@ var ObjectKernel = class {
1823
1955
  async initPluginWithTimeout(plugin) {
1824
1956
  const timeout = plugin.startupTimeout || this.config.defaultStartupTimeout;
1825
1957
  this.logger.debug(`Init: ${plugin.name}`, { plugin: plugin.name });
1826
- const initPromise = plugin.init(this.context);
1827
- const timeoutPromise = new Promise((_, reject) => {
1828
- setTimeout(() => {
1829
- reject(new Error(`Plugin ${plugin.name} init timeout after ${timeout}ms`));
1830
- }, timeout);
1831
- });
1832
- await Promise.race([initPromise, timeoutPromise]);
1958
+ assertInitServiceRequirements(plugin, (name) => this.hasAnyService(name));
1959
+ this.currentlyInitializing = plugin.name;
1960
+ try {
1961
+ const initPromise = plugin.init(this.context);
1962
+ const timeoutPromise = new Promise((_, reject) => {
1963
+ setTimeout(() => {
1964
+ reject(new Error(`Plugin ${plugin.name} init timeout after ${timeout}ms`));
1965
+ }, timeout);
1966
+ });
1967
+ await Promise.race([initPromise, timeoutPromise]);
1968
+ } finally {
1969
+ this.currentlyInitializing = void 0;
1970
+ }
1971
+ }
1972
+ /**
1973
+ * Whether a service is resolvable on this kernel right now — direct
1974
+ * registration or a loader-registered factory. Backs the init-service
1975
+ * contract checks (#4131).
1976
+ */
1977
+ hasAnyService(name) {
1978
+ return this.services.has(name) || this.pluginLoader.hasService(name);
1979
+ }
1980
+ /**
1981
+ * When a getService miss happens while a plugin's init() is running,
1982
+ * append the structural diagnosis (#4131): which plugin was initializing,
1983
+ * and — when a composed plugin declares the service — who provides it.
1984
+ * Empty string outside Phase 1, so non-boot messages stay unchanged.
1985
+ */
1986
+ describeInitOrderFault(serviceName) {
1987
+ return describeInitOrderFault(this.currentlyInitializing, this.plugins.values(), serviceName);
1833
1988
  }
1834
1989
  async startPluginWithTimeout(plugin) {
1835
1990
  if (!plugin.start) {
@@ -1903,35 +2058,13 @@ var ObjectKernel = class {
1903
2058
  }
1904
2059
  }
1905
2060
  }
2061
+ /**
2062
+ * Topological order over `dependencies` (hard) + `optionalDependencies`
2063
+ * (order-if-present) — ADR-0116, #4131. One implementation shared with
2064
+ * LiteKernel via `plugin-order.ts`.
2065
+ */
1906
2066
  resolveDependencies() {
1907
- const resolved = [];
1908
- const visited = /* @__PURE__ */ new Set();
1909
- const visiting = /* @__PURE__ */ new Set();
1910
- const visit = (pluginName) => {
1911
- if (visited.has(pluginName)) return;
1912
- if (visiting.has(pluginName)) {
1913
- throw new Error(`[Kernel] Circular dependency detected: ${pluginName}`);
1914
- }
1915
- const plugin = this.plugins.get(pluginName);
1916
- if (!plugin) {
1917
- throw new Error(`[Kernel] Plugin '${pluginName}' not found`);
1918
- }
1919
- visiting.add(pluginName);
1920
- const deps = plugin.dependencies || [];
1921
- for (const dep of deps) {
1922
- if (!this.plugins.has(dep)) {
1923
- throw new Error(`[Kernel] Dependency '${dep}' not found for plugin '${pluginName}'`);
1924
- }
1925
- visit(dep);
1926
- }
1927
- visiting.delete(pluginName);
1928
- visited.add(pluginName);
1929
- resolved.push(plugin);
1930
- };
1931
- for (const pluginName of this.plugins.keys()) {
1932
- visit(pluginName);
1933
- }
1934
- return resolved;
2067
+ return resolvePluginOrder(this.plugins);
1935
2068
  }
1936
2069
  registerShutdownSignals() {
1937
2070
  const signals = ["SIGINT", "SIGTERM", "SIGQUIT"];
@@ -1997,6 +2130,7 @@ var LiteKernel = class extends ObjectKernelBase {
1997
2130
  this.state = "initializing";
1998
2131
  this.logger.info("Bootstrap started");
1999
2132
  const orderedPlugins = this.resolveDependencies();
2133
+ this.validateInitServices(orderedPlugins);
2000
2134
  this.logger.info("Phase 1: Init plugins");
2001
2135
  for (const plugin of orderedPlugins) {
2002
2136
  await this.runPluginInit(plugin);
@@ -2597,6 +2731,11 @@ function createApiRegistryPlugin(config = {}) {
2597
2731
  } = config;
2598
2732
  return {
2599
2733
  name: "com.objectstack.core.api-registry",
2734
+ /**
2735
+ * Services init() registers on every path (ADR-0116, #4131) — lets the
2736
+ * kernel name this plugin when a consumer requires one before it inits.
2737
+ */
2738
+ providesServices: ["api-registry"],
2600
2739
  type: "standard",
2601
2740
  version: "1.0.0",
2602
2741
  init: async (ctx) => {
@@ -4411,7 +4550,8 @@ async function resolveAuthzContext(input) {
4411
4550
  positions: [],
4412
4551
  permissions: [],
4413
4552
  systemPermissions: [],
4414
- org_user_ids: []
4553
+ org_user_ids: [],
4554
+ accessible_org_ids: []
4415
4555
  };
4416
4556
  let userId;
4417
4557
  let tenantId;
@@ -4447,6 +4587,7 @@ async function resolveAuthzContext(input) {
4447
4587
  ctx.permissions = grants.permissions;
4448
4588
  ctx.systemPermissions = grants.systemPermissions;
4449
4589
  ctx.org_user_ids = grants.org_user_ids;
4590
+ ctx.accessible_org_ids = grants.accessible_org_ids;
4450
4591
  if (grants.tabPermissions) ctx.tabPermissions = grants.tabPermissions;
4451
4592
  if (grants.posture) ctx.posture = grants.posture;
4452
4593
  if (grants.email && !ctx.email) ctx.email = grants.email;
@@ -4458,7 +4599,8 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4458
4599
  positions: [],
4459
4600
  permissions: Array.isArray(opts.seedPermissions) ? [...opts.seedPermissions] : [],
4460
4601
  systemPermissions: [],
4461
- org_user_ids: [userId]
4602
+ org_user_ids: [userId],
4603
+ accessible_org_ids: []
4462
4604
  };
4463
4605
  if (opts.seedEmail) grants.email = opts.seedEmail;
4464
4606
  if (!ql || typeof ql.find !== "function") return grants;
@@ -4476,9 +4618,17 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4476
4618
  const u = await getUserRow();
4477
4619
  if (u?.email) grants.email = String(u.email);
4478
4620
  }
4479
- const memberWhere = tenantId ? { user_id: userId, organization_id: tenantId } : { user_id: userId };
4480
- const members = await tryFind(ql, "sys_member", memberWhere, 50);
4621
+ const nowMs = opts.nowMs ?? Date.now();
4622
+ const members = await tryFind(ql, "sys_member", { user_id: userId }, 200);
4623
+ const accessibleOrgIds = /* @__PURE__ */ new Set();
4481
4624
  for (const m of members) {
4625
+ if (!isGrantActive(m, nowMs)) continue;
4626
+ const org = m.organization_id ?? m.organizationId;
4627
+ if (typeof org === "string" && org) accessibleOrgIds.add(org);
4628
+ }
4629
+ grants.accessible_org_ids = Array.from(accessibleOrgIds);
4630
+ const activeMembers = tenantId ? members.filter((m) => (m.organization_id ?? m.organizationId) === tenantId) : members;
4631
+ for (const m of activeMembers) {
4482
4632
  if (m.role && typeof m.role === "string") {
4483
4633
  for (const raw of m.role.split(",").map((s) => s.trim()).filter(Boolean)) {
4484
4634
  const r = (0, import_spec.mapMembershipRole)(raw);
@@ -4486,7 +4636,6 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4486
4636
  }
4487
4637
  }
4488
4638
  }
4489
- const nowMs = opts.nowMs ?? Date.now();
4490
4639
  const userPositionRows = await tryFind(ql, "sys_user_position", { user_id: userId }, 200);
4491
4640
  for (const ur of userPositionRows) {
4492
4641
  const org = ur.organization_id ?? null;
@@ -4558,7 +4707,9 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4558
4707
  }
4559
4708
  grants.posture = derivePosture({
4560
4709
  isPlatformAdmin: hasPlatformAdminGrant,
4561
- isTenantAdmin: grants.permissions.includes(import_spec.ORGANIZATION_ADMIN)
4710
+ // [ADR-0105 D4] Either org-admin capability set resolves the rung — the
4711
+ // wall-less variant differs only by withholding the superuser bits.
4712
+ isTenantAdmin: import_spec.ORGANIZATION_ADMIN_GRANTS.some((n) => grants.permissions.includes(n))
4562
4713
  });
4563
4714
  if (!grants.permissions.includes("ai_seat")) {
4564
4715
  const aiAccess = (await getUserRow())?.ai_access;
@@ -4647,14 +4798,13 @@ function evaluateAuthGate(sessionUser, path) {
4647
4798
 
4648
4799
  // src/security/anonymous-deny.ts
4649
4800
  var ANONYMOUS_DENY_STATUS = 401;
4650
- var ANONYMOUS_DENY_CODE = "unauthenticated";
4801
+ var ANONYMOUS_DENY_CODE = "UNAUTHENTICATED";
4651
4802
  var ANONYMOUS_DENY_MESSAGE = "Authentication is required to access this endpoint.";
4652
4803
  var ANONYMOUS_DENY_BODY = {
4653
4804
  error: ANONYMOUS_DENY_CODE,
4654
4805
  message: ANONYMOUS_DENY_MESSAGE
4655
4806
  };
4656
4807
  function shouldDenyAnonymous(input) {
4657
- if (!input.requireAuth) return false;
4658
4808
  if (typeof input.method === "string" && input.method.toUpperCase() === "OPTIONS") {
4659
4809
  return false;
4660
4810
  }
@@ -4666,6 +4816,7 @@ function shouldDenyAnonymous(input) {
4666
4816
  }
4667
4817
 
4668
4818
  // src/utils/datetime.ts
4819
+ var import_data = require("@objectstack/spec/data");
4669
4820
  function calendarPartsInTz(d, tz) {
4670
4821
  const parts = new Intl.DateTimeFormat("en-US", {
4671
4822
  timeZone: tz,
@@ -4689,8 +4840,8 @@ function calendarPartsInTzOrUtc(d, tz) {
4689
4840
  day: d.getUTCDate()
4690
4841
  };
4691
4842
  }
4692
- function zonedDateStartToUtcMs(ymd, tz) {
4693
- const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ymd);
4843
+ function zonedDateStartToUtcMs(ymd2, tz) {
4844
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ymd2);
4694
4845
  const wallAsUtc = m ? Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])) : NaN;
4695
4846
  if (!tz || tz === "UTC" || Number.isNaN(wallAsUtc)) return wallAsUtc;
4696
4847
  try {
@@ -4906,6 +5057,209 @@ async function bulkWrite(rows, opts) {
4906
5057
  return results;
4907
5058
  }
4908
5059
 
5060
+ // src/utils/filter-tokens.ts
5061
+ var import_data2 = require("@objectstack/spec/data");
5062
+ var UnknownFilterTokenError = class extends Error {
5063
+ constructor(token, suggestion) {
5064
+ super(
5065
+ `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."
5066
+ );
5067
+ this.status = 400;
5068
+ this.code = "FILTER_TOKEN_UNKNOWN";
5069
+ this.name = "UnknownFilterTokenError";
5070
+ this.token = token;
5071
+ this.suggestion = suggestion;
5072
+ }
5073
+ };
5074
+ var UnresolvedFilterTokenError = class extends Error {
5075
+ constructor(token, detail) {
5076
+ super(`Filter placeholder "{${token}}" cannot be resolved: ${detail}`);
5077
+ /** 400, not 500 — see {@link UnknownFilterTokenError}. */
5078
+ this.status = 400;
5079
+ this.code = "FILTER_TOKEN_UNRESOLVED";
5080
+ this.name = "UnresolvedFilterTokenError";
5081
+ this.token = token;
5082
+ }
5083
+ };
5084
+ function ymd(year, month, day) {
5085
+ const p = (n) => String(n).padStart(2, "0");
5086
+ return `${year}-${p(month)}-${p(day)}`;
5087
+ }
5088
+ function proxyDay(now, timezone) {
5089
+ const { year, month, day } = calendarPartsInTzOrUtc(now, timezone);
5090
+ return new Date(Date.UTC(year, month - 1, day));
5091
+ }
5092
+ var asYmd = (d) => ymd(d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate());
5093
+ function startOfPeriod(kind, d) {
5094
+ const r = new Date(d.getTime());
5095
+ switch (kind) {
5096
+ case "week": {
5097
+ const dow = (r.getUTCDay() + 6) % 7;
5098
+ r.setUTCDate(r.getUTCDate() - dow);
5099
+ return r;
5100
+ }
5101
+ case "month":
5102
+ return new Date(Date.UTC(r.getUTCFullYear(), r.getUTCMonth(), 1));
5103
+ case "quarter":
5104
+ return new Date(Date.UTC(r.getUTCFullYear(), Math.floor(r.getUTCMonth() / 3) * 3, 1));
5105
+ case "year":
5106
+ return new Date(Date.UTC(r.getUTCFullYear(), 0, 1));
5107
+ }
5108
+ }
5109
+ function daysInMonth(year, month) {
5110
+ return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
5111
+ }
5112
+ function addMonthsClamped(d, n) {
5113
+ const year = d.getUTCFullYear();
5114
+ const month = d.getUTCMonth() + n;
5115
+ const targetYear = year + Math.floor(month / 12);
5116
+ const targetMonth = (month % 12 + 12) % 12;
5117
+ const day = Math.min(d.getUTCDate(), daysInMonth(targetYear, targetMonth));
5118
+ return new Date(Date.UTC(
5119
+ targetYear,
5120
+ targetMonth,
5121
+ day,
5122
+ d.getUTCHours(),
5123
+ d.getUTCMinutes(),
5124
+ d.getUTCSeconds(),
5125
+ d.getUTCMilliseconds()
5126
+ ));
5127
+ }
5128
+ function addPeriods(kind, d, n) {
5129
+ switch (kind) {
5130
+ case "week": {
5131
+ const r = new Date(d.getTime());
5132
+ r.setUTCDate(r.getUTCDate() + n * 7);
5133
+ return r;
5134
+ }
5135
+ case "month":
5136
+ return addMonthsClamped(d, n);
5137
+ case "quarter":
5138
+ return addMonthsClamped(d, n * 3);
5139
+ case "year":
5140
+ return addMonthsClamped(d, n * 12);
5141
+ }
5142
+ }
5143
+ function addUnits(unit, d, n) {
5144
+ const r = new Date(d.getTime());
5145
+ switch (unit) {
5146
+ case "minute":
5147
+ r.setUTCMinutes(r.getUTCMinutes() + n);
5148
+ return r;
5149
+ case "hour":
5150
+ r.setUTCHours(r.getUTCHours() + n);
5151
+ return r;
5152
+ case "day":
5153
+ r.setUTCDate(r.getUTCDate() + n);
5154
+ return r;
5155
+ case "week":
5156
+ r.setUTCDate(r.getUTCDate() + n * 7);
5157
+ return r;
5158
+ // Month/year steps clamp rather than overflow — see addMonthsClamped.
5159
+ case "month":
5160
+ return addMonthsClamped(d, n);
5161
+ case "year":
5162
+ return addMonthsClamped(d, n * 12);
5163
+ }
5164
+ }
5165
+ var PERIOD_RE = /^(?:(current|last|next)_)?(week|month|quarter|year)_(start|end)$/;
5166
+ function resolvePeriodToken(token, today) {
5167
+ const m = PERIOD_RE.exec(token);
5168
+ if (!m) return void 0;
5169
+ const rel = m[1] ?? "current";
5170
+ const kind = m[2];
5171
+ const bound = m[3];
5172
+ const offset = rel === "last" ? -1 : rel === "next" ? 1 : 0;
5173
+ const periodStart = startOfPeriod(kind, addPeriods(kind, startOfPeriod(kind, today), offset));
5174
+ if (bound === "start") return asYmd(periodStart);
5175
+ const next = addPeriods(kind, periodStart, 1);
5176
+ next.setUTCDate(next.getUTCDate() - 1);
5177
+ return asYmd(next);
5178
+ }
5179
+ function resolveFilterToken(token, ctx = {}) {
5180
+ const now = ctx.now ?? /* @__PURE__ */ new Date();
5181
+ if (token === "current_user_id") {
5182
+ if (!ctx.userId) {
5183
+ throw new UnresolvedFilterTokenError(
5184
+ token,
5185
+ "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."
5186
+ );
5187
+ }
5188
+ return ctx.userId;
5189
+ }
5190
+ if (token === "current_org_id") {
5191
+ if (!ctx.orgId) {
5192
+ throw new UnresolvedFilterTokenError(
5193
+ token,
5194
+ "the request carries no active organization (ExecutionContext.tenantId is unset). Set the active org on the request, or drop the token from the filter."
5195
+ );
5196
+ }
5197
+ return ctx.orgId;
5198
+ }
5199
+ const today = proxyDay(now, ctx.timezone);
5200
+ switch (token) {
5201
+ case "now":
5202
+ return now.toISOString();
5203
+ case "today":
5204
+ return asYmd(today);
5205
+ case "yesterday":
5206
+ return asYmd(addUnits("day", today, -1));
5207
+ case "tomorrow":
5208
+ return asYmd(addUnits("day", today, 1));
5209
+ }
5210
+ const period = resolvePeriodToken(token, today);
5211
+ if (period !== void 0) return period;
5212
+ const param = (0, import_data2.parseDateMacroParam)(token);
5213
+ if (param) {
5214
+ const sign = param.direction === "ago" ? -1 : 1;
5215
+ if (param.unit === "minute" || param.unit === "hour") {
5216
+ return addUnits(param.unit, now, sign * param.n).toISOString();
5217
+ }
5218
+ return asYmd(addUnits(param.unit, today, sign * param.n));
5219
+ }
5220
+ return void 0;
5221
+ }
5222
+ function hasFilterToken(node) {
5223
+ if (typeof node === "string") return (0, import_data2.classifyFilterToken)(node) !== null;
5224
+ if (Array.isArray(node)) return node.some(hasFilterToken);
5225
+ if (node && typeof node === "object" && !(node instanceof Date)) {
5226
+ return Object.values(node).some(hasFilterToken);
5227
+ }
5228
+ return false;
5229
+ }
5230
+ function resolveFilterTokens(filter, ctx = {}) {
5231
+ if (filter == null) return filter;
5232
+ if (!hasFilterToken(filter)) return filter;
5233
+ const pinned = { ...ctx, now: ctx.now ?? /* @__PURE__ */ new Date() };
5234
+ const walk = (node) => {
5235
+ if (typeof node === "string") {
5236
+ const cls = (0, import_data2.classifyFilterToken)(node);
5237
+ if (!cls) return node;
5238
+ if (cls.kind === "unknown") throw new UnknownFilterTokenError(cls.token, cls.suggestion);
5239
+ const resolved = resolveFilterToken(cls.token, pinned);
5240
+ if (resolved === void 0) throw new UnknownFilterTokenError(cls.token);
5241
+ return resolved;
5242
+ }
5243
+ if (Array.isArray(node)) return node.map(walk);
5244
+ if (node && typeof node === "object") {
5245
+ if (node instanceof Date) return node;
5246
+ const out = {};
5247
+ for (const [k, v] of Object.entries(node)) out[k] = walk(v);
5248
+ return out;
5249
+ }
5250
+ return node;
5251
+ };
5252
+ return walk(filter);
5253
+ }
5254
+ function filterTokenContextFrom(execCtx, now) {
5255
+ return {
5256
+ now,
5257
+ timezone: execCtx?.timezone,
5258
+ userId: execCtx?.userId,
5259
+ orgId: execCtx?.tenantId
5260
+ };
5261
+ }
5262
+
4909
5263
  // src/health-monitor.ts
4910
5264
  var PluginHealthMonitor = class {
4911
5265
  constructor(logger) {
@@ -5851,6 +6205,9 @@ var NamespaceResolver = class {
5851
6205
  SecurePluginContext,
5852
6206
  SemanticVersionManager,
5853
6207
  ServiceLifecycle,
6208
+ UnknownFilterTokenError,
6209
+ UnresolvedFilterTokenError,
6210
+ assertInitServiceRequirements,
5854
6211
  bucketKeyToCalendarRange,
5855
6212
  buildPermissionsFromGrants,
5856
6213
  bulkWrite,
@@ -5869,8 +6226,10 @@ var NamespaceResolver = class {
5869
6226
  deepMerge,
5870
6227
  defaultIsTransientError,
5871
6228
  derivePosture,
6229
+ describeInitOrderFault,
5872
6230
  evaluateAuthGate,
5873
6231
  extractApiKey,
6232
+ filterTokenContextFrom,
5874
6233
  generateApiKey,
5875
6234
  generateEd25519KeyPair,
5876
6235
  getEnv,
@@ -5881,18 +6240,24 @@ var NamespaceResolver = class {
5881
6240
  isGrantActive,
5882
6241
  isGrantExpired,
5883
6242
  isNode,
6243
+ nextUtcCalendarDay,
5884
6244
  parseScopes,
5885
6245
  parseSignature,
5886
6246
  postureVisibleRows,
5887
6247
  readAuthoredTranslationLayer,
5888
6248
  resolveApiKeyPrincipal,
5889
6249
  resolveAuthzContext,
6250
+ resolveFilterToken,
6251
+ resolveFilterTokens,
5890
6252
  resolveLocale,
5891
6253
  resolveLocalizationContext,
6254
+ resolvePluginOrder,
5892
6255
  resolveUserAuthzGrants,
5893
6256
  safeExit,
5894
6257
  shouldDenyAnonymous,
5895
6258
  signPayload,
6259
+ utcInstantMs,
6260
+ validateInitServiceContract,
5896
6261
  verifyPayload,
5897
6262
  verifyPlatformSignature,
5898
6263
  verifyPluginArtifact,