@objectstack/core 17.0.0-rc.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);
@@ -1425,24 +1538,12 @@ var ObjectKernel = class {
1425
1538
  this.services.set(name, loaderService);
1426
1539
  return loaderService;
1427
1540
  }
1428
- try {
1429
- const service2 = this.pluginLoader.getService(name);
1430
- if (service2 instanceof Promise) {
1431
- service2.catch(() => {
1432
- });
1433
- throw new Error(`Service '${name}' is async - use await`);
1434
- }
1435
- return service2;
1436
- } catch (error) {
1437
- if (error.message?.includes("is async")) {
1438
- throw error;
1439
- }
1440
- const isNotFoundError = error.message === `Service '${name}' not found`;
1441
- if (!isNotFoundError) {
1442
- throw error;
1443
- }
1444
- 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
+ );
1445
1545
  }
1546
+ throw new Error(`Service '${name}' is async - use await`);
1446
1547
  },
1447
1548
  replaceService: (name, implementation) => {
1448
1549
  const hasService = this.services.has(name) || this.pluginLoader.hasService(name);
@@ -1601,6 +1702,7 @@ var ObjectKernel = class {
1601
1702
  this.logger.warn("Circular service dependencies detected:", { cycles });
1602
1703
  }
1603
1704
  const orderedPlugins = this.resolveDependencies();
1705
+ validateInitServiceContract(orderedPlugins, (name) => this.hasAnyService(name));
1604
1706
  this.logger.info("Phase 1: Init plugins");
1605
1707
  for (const plugin of orderedPlugins) {
1606
1708
  await this.initPluginWithTimeout(plugin);
@@ -1746,13 +1848,36 @@ var ObjectKernel = class {
1746
1848
  async initPluginWithTimeout(plugin) {
1747
1849
  const timeout = plugin.startupTimeout || this.config.defaultStartupTimeout;
1748
1850
  this.logger.debug(`Init: ${plugin.name}`, { plugin: plugin.name });
1749
- const initPromise = plugin.init(this.context);
1750
- const timeoutPromise = new Promise((_, reject) => {
1751
- setTimeout(() => {
1752
- reject(new Error(`Plugin ${plugin.name} init timeout after ${timeout}ms`));
1753
- }, timeout);
1754
- });
1755
- 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);
1756
1881
  }
1757
1882
  async startPluginWithTimeout(plugin) {
1758
1883
  if (!plugin.start) {
@@ -1826,35 +1951,13 @@ var ObjectKernel = class {
1826
1951
  }
1827
1952
  }
1828
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
+ */
1829
1959
  resolveDependencies() {
1830
- const resolved = [];
1831
- const visited = /* @__PURE__ */ new Set();
1832
- const visiting = /* @__PURE__ */ new Set();
1833
- const visit = (pluginName) => {
1834
- if (visited.has(pluginName)) return;
1835
- if (visiting.has(pluginName)) {
1836
- throw new Error(`[Kernel] Circular dependency detected: ${pluginName}`);
1837
- }
1838
- const plugin = this.plugins.get(pluginName);
1839
- if (!plugin) {
1840
- throw new Error(`[Kernel] Plugin '${pluginName}' not found`);
1841
- }
1842
- visiting.add(pluginName);
1843
- const deps = plugin.dependencies || [];
1844
- for (const dep of deps) {
1845
- if (!this.plugins.has(dep)) {
1846
- throw new Error(`[Kernel] Dependency '${dep}' not found for plugin '${pluginName}'`);
1847
- }
1848
- visit(dep);
1849
- }
1850
- visiting.delete(pluginName);
1851
- visited.add(pluginName);
1852
- resolved.push(plugin);
1853
- };
1854
- for (const pluginName of this.plugins.keys()) {
1855
- visit(pluginName);
1856
- }
1857
- return resolved;
1960
+ return resolvePluginOrder(this.plugins);
1858
1961
  }
1859
1962
  registerShutdownSignals() {
1860
1963
  const signals = ["SIGINT", "SIGTERM", "SIGQUIT"];
@@ -1920,6 +2023,7 @@ var LiteKernel = class extends ObjectKernelBase {
1920
2023
  this.state = "initializing";
1921
2024
  this.logger.info("Bootstrap started");
1922
2025
  const orderedPlugins = this.resolveDependencies();
2026
+ this.validateInitServices(orderedPlugins);
1923
2027
  this.logger.info("Phase 1: Init plugins");
1924
2028
  for (const plugin of orderedPlugins) {
1925
2029
  await this.runPluginInit(plugin);
@@ -2520,6 +2624,11 @@ function createApiRegistryPlugin(config = {}) {
2520
2624
  } = config;
2521
2625
  return {
2522
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"],
2523
2632
  type: "standard",
2524
2633
  version: "1.0.0",
2525
2634
  init: async (ctx) => {
@@ -4587,14 +4696,13 @@ function evaluateAuthGate(sessionUser, path) {
4587
4696
 
4588
4697
  // src/security/anonymous-deny.ts
4589
4698
  var ANONYMOUS_DENY_STATUS = 401;
4590
- var ANONYMOUS_DENY_CODE = "unauthenticated";
4699
+ var ANONYMOUS_DENY_CODE = "UNAUTHENTICATED";
4591
4700
  var ANONYMOUS_DENY_MESSAGE = "Authentication is required to access this endpoint.";
4592
4701
  var ANONYMOUS_DENY_BODY = {
4593
4702
  error: ANONYMOUS_DENY_CODE,
4594
4703
  message: ANONYMOUS_DENY_MESSAGE
4595
4704
  };
4596
4705
  function shouldDenyAnonymous(input) {
4597
- if (!input.requireAuth) return false;
4598
4706
  if (typeof input.method === "string" && input.method.toUpperCase() === "OPTIONS") {
4599
4707
  return false;
4600
4708
  }
@@ -4606,6 +4714,7 @@ function shouldDenyAnonymous(input) {
4606
4714
  }
4607
4715
 
4608
4716
  // src/utils/datetime.ts
4717
+ import { nextUtcCalendarDay, utcInstantMs } from "@objectstack/spec/data";
4609
4718
  function calendarPartsInTz(d, tz) {
4610
4719
  const parts = new Intl.DateTimeFormat("en-US", {
4611
4720
  timeZone: tz,
@@ -5998,6 +6107,7 @@ export {
5998
6107
  ServiceLifecycle,
5999
6108
  UnknownFilterTokenError,
6000
6109
  UnresolvedFilterTokenError,
6110
+ assertInitServiceRequirements,
6001
6111
  bucketKeyToCalendarRange,
6002
6112
  buildPermissionsFromGrants,
6003
6113
  bulkWrite,
@@ -6016,6 +6126,7 @@ export {
6016
6126
  deepMerge,
6017
6127
  defaultIsTransientError,
6018
6128
  derivePosture,
6129
+ describeInitOrderFault,
6019
6130
  evaluateAuthGate,
6020
6131
  extractApiKey,
6021
6132
  filterTokenContextFrom,
@@ -6029,6 +6140,7 @@ export {
6029
6140
  isGrantActive,
6030
6141
  isGrantExpired,
6031
6142
  isNode,
6143
+ nextUtcCalendarDay,
6032
6144
  parseScopes,
6033
6145
  parseSignature,
6034
6146
  postureVisibleRows,
@@ -6039,10 +6151,13 @@ export {
6039
6151
  resolveFilterTokens,
6040
6152
  resolveLocale,
6041
6153
  resolveLocalizationContext,
6154
+ resolvePluginOrder,
6042
6155
  resolveUserAuthzGrants,
6043
6156
  safeExit,
6044
6157
  shouldDenyAnonymous,
6045
6158
  signPayload,
6159
+ utcInstantMs,
6160
+ validateInitServiceContract,
6046
6161
  verifyPayload,
6047
6162
  verifyPlatformSignature,
6048
6163
  verifyPluginArtifact,