@objectstack/core 17.0.0-rc.0 → 17.0.0-rc.2

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,70 @@ 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);
1851
+ assertInitServiceRequirements(plugin, (name) => this.hasAnyService(name));
1852
+ this.currentlyInitializing = plugin.name;
1853
+ try {
1854
+ await this.raceStartupTimeout(
1855
+ plugin.init(this.context),
1856
+ timeout,
1857
+ `Plugin ${plugin.name} init timeout after ${timeout}ms`
1858
+ );
1859
+ } finally {
1860
+ this.currentlyInitializing = void 0;
1861
+ }
1862
+ }
1863
+ /**
1864
+ * Race a plugin lifecycle hook against its startup-timeout guard, and
1865
+ * reclaim the guard the moment the race settles (#4813).
1866
+ *
1867
+ * The guard used to be armed and then abandoned: when the plugin won the
1868
+ * race, its `setTimeout` stayed ref'd in the event loop for the full
1869
+ * `startupTimeout`, so every process idled that long after its work was
1870
+ * done. One `os migrate` finished in 3s and then sat for 120s
1871
+ * (`ObjectQLPlugin.startupTimeout`), held open by 8 orphaned guards — one
1872
+ * per init plus one per start.
1873
+ *
1874
+ * Clearing on settle rather than `unref()`-ing at arm time is deliberate.
1875
+ * An unref'd guard also stops pinning the loop, but it stops being a guard
1876
+ * as well: if the hook never settles and nothing else keeps the loop alive,
1877
+ * Node exits before the timer can fire and the timeout is never reported.
1878
+ * The guard has to stay ref'd exactly as long as the race is undecided,
1879
+ * which is what `clearTimeout` in a `finally` expresses.
1880
+ *
1881
+ * `operation` is widened to `T | PromiseLike<T>` because the Plugin
1882
+ * contract permits a synchronous hook (`init`/`start` return
1883
+ * `void | Promise<void>`); such a hook wins the race immediately and the
1884
+ * guard is reclaimed on the same turn.
1885
+ */
1886
+ async raceStartupTimeout(operation, timeout, message) {
1887
+ let guard;
1750
1888
  const timeoutPromise = new Promise((_, reject) => {
1751
- setTimeout(() => {
1752
- reject(new Error(`Plugin ${plugin.name} init timeout after ${timeout}ms`));
1889
+ guard = setTimeout(() => {
1890
+ reject(new Error(message));
1753
1891
  }, timeout);
1754
1892
  });
1755
- await Promise.race([initPromise, timeoutPromise]);
1893
+ try {
1894
+ return await Promise.race([operation, timeoutPromise]);
1895
+ } finally {
1896
+ clearTimeout(guard);
1897
+ }
1898
+ }
1899
+ /**
1900
+ * Whether a service is resolvable on this kernel right now — direct
1901
+ * registration or a loader-registered factory. Backs the init-service
1902
+ * contract checks (#4131).
1903
+ */
1904
+ hasAnyService(name) {
1905
+ return this.services.has(name) || this.pluginLoader.hasService(name);
1906
+ }
1907
+ /**
1908
+ * When a getService miss happens while a plugin's init() is running,
1909
+ * append the structural diagnosis (#4131): which plugin was initializing,
1910
+ * and — when a composed plugin declares the service — who provides it.
1911
+ * Empty string outside Phase 1, so non-boot messages stay unchanged.
1912
+ */
1913
+ describeInitOrderFault(serviceName) {
1914
+ return describeInitOrderFault(this.currentlyInitializing, this.plugins.values(), serviceName);
1756
1915
  }
1757
1916
  async startPluginWithTimeout(plugin) {
1758
1917
  if (!plugin.start) {
@@ -1762,13 +1921,11 @@ var ObjectKernel = class {
1762
1921
  const startTime = Date.now();
1763
1922
  this.logger.debug(`Start: ${plugin.name}`, { plugin: plugin.name });
1764
1923
  try {
1765
- const startPromise = plugin.start(this.context);
1766
- const timeoutPromise = new Promise((_, reject) => {
1767
- setTimeout(() => {
1768
- reject(new Error(`Plugin ${plugin.name} start timeout after ${timeout}ms`));
1769
- }, timeout);
1770
- });
1771
- await Promise.race([startPromise, timeoutPromise]);
1924
+ await this.raceStartupTimeout(
1925
+ plugin.start(this.context),
1926
+ timeout,
1927
+ `Plugin ${plugin.name} start timeout after ${timeout}ms`
1928
+ );
1772
1929
  const duration = Date.now() - startTime;
1773
1930
  this.startedPlugins.add(plugin.name);
1774
1931
  this.pluginStartTimes.set(plugin.name, duration);
@@ -1826,35 +1983,13 @@ var ObjectKernel = class {
1826
1983
  }
1827
1984
  }
1828
1985
  }
1986
+ /**
1987
+ * Topological order over `dependencies` (hard) + `optionalDependencies`
1988
+ * (order-if-present) — ADR-0116, #4131. One implementation shared with
1989
+ * LiteKernel via `plugin-order.ts`.
1990
+ */
1829
1991
  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;
1992
+ return resolvePluginOrder(this.plugins);
1858
1993
  }
1859
1994
  registerShutdownSignals() {
1860
1995
  const signals = ["SIGINT", "SIGTERM", "SIGQUIT"];
@@ -1920,6 +2055,7 @@ var LiteKernel = class extends ObjectKernelBase {
1920
2055
  this.state = "initializing";
1921
2056
  this.logger.info("Bootstrap started");
1922
2057
  const orderedPlugins = this.resolveDependencies();
2058
+ this.validateInitServices(orderedPlugins);
1923
2059
  this.logger.info("Phase 1: Init plugins");
1924
2060
  for (const plugin of orderedPlugins) {
1925
2061
  await this.runPluginInit(plugin);
@@ -2520,6 +2656,11 @@ function createApiRegistryPlugin(config = {}) {
2520
2656
  } = config;
2521
2657
  return {
2522
2658
  name: "com.objectstack.core.api-registry",
2659
+ /**
2660
+ * Services init() registers on every path (ADR-0116, #4131) — lets the
2661
+ * kernel name this plugin when a consumer requires one before it inits.
2662
+ */
2663
+ providesServices: ["api-registry"],
2523
2664
  type: "standard",
2524
2665
  version: "1.0.0",
2525
2666
  init: async (ctx) => {
@@ -4587,14 +4728,13 @@ function evaluateAuthGate(sessionUser, path) {
4587
4728
 
4588
4729
  // src/security/anonymous-deny.ts
4589
4730
  var ANONYMOUS_DENY_STATUS = 401;
4590
- var ANONYMOUS_DENY_CODE = "unauthenticated";
4731
+ var ANONYMOUS_DENY_CODE = "UNAUTHENTICATED";
4591
4732
  var ANONYMOUS_DENY_MESSAGE = "Authentication is required to access this endpoint.";
4592
4733
  var ANONYMOUS_DENY_BODY = {
4593
4734
  error: ANONYMOUS_DENY_CODE,
4594
4735
  message: ANONYMOUS_DENY_MESSAGE
4595
4736
  };
4596
4737
  function shouldDenyAnonymous(input) {
4597
- if (!input.requireAuth) return false;
4598
4738
  if (typeof input.method === "string" && input.method.toUpperCase() === "OPTIONS") {
4599
4739
  return false;
4600
4740
  }
@@ -4606,6 +4746,7 @@ function shouldDenyAnonymous(input) {
4606
4746
  }
4607
4747
 
4608
4748
  // src/utils/datetime.ts
4749
+ import { nextUtcCalendarDay, utcInstantMs } from "@objectstack/spec/data";
4609
4750
  function calendarPartsInTz(d, tz) {
4610
4751
  const parts = new Intl.DateTimeFormat("en-US", {
4611
4752
  timeZone: tz,
@@ -4846,6 +4987,395 @@ async function bulkWrite(rows, opts) {
4846
4987
  return results;
4847
4988
  }
4848
4989
 
4990
+ // src/utils/migration-journal.ts
4991
+ import { createHash as createHash2, randomUUID } from "crypto";
4992
+ import {
4993
+ MIGRATION_JOURNAL_OBJECT
4994
+ } from "@objectstack/spec/system";
4995
+ var SYSTEM_CTX = { isSystem: true };
4996
+ var DEFAULT_CHUNK_SIZE = 200;
4997
+ function engineCanRollBack(engine) {
4998
+ const e = engine;
4999
+ if (typeof e?.transaction !== "function") return false;
5000
+ const defaultDriverName = e.getDefaultDriverName?.();
5001
+ const defaultDriver = defaultDriverName ? e.getDriverByName?.(defaultDriverName) : void 0;
5002
+ return !defaultDriver || typeof defaultDriver.beginTransaction === "function";
5003
+ }
5004
+ var MigrationPlanRegistry = class {
5005
+ constructor() {
5006
+ this.plans = /* @__PURE__ */ new Map();
5007
+ }
5008
+ register(plan) {
5009
+ this.plans.set(plan.id, plan);
5010
+ }
5011
+ get(planId) {
5012
+ return this.plans.get(planId);
5013
+ }
5014
+ list() {
5015
+ return [...this.plans.values()];
5016
+ }
5017
+ };
5018
+ var MigrationJournalRefusal = class extends Error {
5019
+ constructor(code, message) {
5020
+ super(message);
5021
+ this.name = "MigrationJournalRefusal";
5022
+ this.code = code;
5023
+ }
5024
+ };
5025
+ function planChunks(plan, rowCounts, chunkSize = plan.chunkSize ?? DEFAULT_CHUNK_SIZE) {
5026
+ const size = Math.max(1, chunkSize);
5027
+ const chunks = [];
5028
+ plan.steps.forEach((step, stepIndex) => {
5029
+ const total = rowCounts[stepIndex] ?? 0;
5030
+ for (let offset = 0; offset < total; offset += size) {
5031
+ chunks.push({
5032
+ index: chunks.length,
5033
+ stepIndex,
5034
+ stepName: step.name,
5035
+ offset,
5036
+ length: Math.min(size, total - offset)
5037
+ });
5038
+ }
5039
+ });
5040
+ return chunks;
5041
+ }
5042
+ function hashMigrationPlan(plan, chunks) {
5043
+ const shape = JSON.stringify({
5044
+ id: plan.id,
5045
+ steps: plan.steps.map((s) => s.name),
5046
+ chunks: chunks.map((c) => [c.stepIndex, c.offset, c.length])
5047
+ });
5048
+ return createHash2("sha256").update(shape, "utf8").digest("hex").slice(0, 32);
5049
+ }
5050
+ async function appendEvent(engine, event, execContext) {
5051
+ await engine.insert(
5052
+ MIGRATION_JOURNAL_OBJECT,
5053
+ { ...event, created_at: event.created_at ?? (/* @__PURE__ */ new Date()).toISOString() },
5054
+ { context: execContext ?? { ...SYSTEM_CTX } }
5055
+ );
5056
+ }
5057
+ async function readRunJournal(engine, runId) {
5058
+ const rows = await engine.find(
5059
+ MIGRATION_JOURNAL_OBJECT,
5060
+ { where: { run_id: runId } },
5061
+ { context: { ...SYSTEM_CTX } }
5062
+ );
5063
+ return [...rows ?? []].sort((a, b) => Number(a.seq) - Number(b.seq));
5064
+ }
5065
+ function chunkSetOf(events, kind) {
5066
+ const out = /* @__PURE__ */ new Set();
5067
+ for (const e of events) {
5068
+ if (e.kind === kind && typeof e.chunk_index === "number") out.add(e.chunk_index);
5069
+ }
5070
+ return out;
5071
+ }
5072
+ async function findInterruptedRuns(engine) {
5073
+ const started = await engine.find(
5074
+ MIGRATION_JOURNAL_OBJECT,
5075
+ { where: { kind: "run_started" } },
5076
+ { context: { ...SYSTEM_CTX } }
5077
+ );
5078
+ const out = [];
5079
+ for (const start of started ?? []) {
5080
+ const events = await readRunJournal(engine, start.run_id);
5081
+ if (events.some((e) => e.kind === "run_done")) continue;
5082
+ const committed = chunkSetOf(events, "chunk_done");
5083
+ const compensated = chunkSetOf(events, "compensated");
5084
+ const outstanding = [...committed].filter((i) => !compensated.has(i));
5085
+ if (events.some((e) => e.kind === "run_failed") && outstanding.length === 0) continue;
5086
+ const unknown = [...chunkSetOf(events, "chunk_started")].filter((i) => !committed.has(i));
5087
+ let planId = start.run_id;
5088
+ try {
5089
+ planId = start.detail ? JSON.parse(start.detail).planId ?? start.run_id : start.run_id;
5090
+ } catch {
5091
+ }
5092
+ out.push({
5093
+ runId: start.run_id,
5094
+ planId,
5095
+ planHash: start.plan_hash ?? "",
5096
+ migrationId: start.migration_id,
5097
+ startedAt: start.created_at,
5098
+ committedChunks: [...committed].sort((a, b) => a - b),
5099
+ unknownChunks: unknown.sort((a, b) => a - b),
5100
+ compensatedChunks: [...compensated].sort((a, b) => a - b)
5101
+ });
5102
+ }
5103
+ return out;
5104
+ }
5105
+ async function loadPlan(engine, plan, chunkSize) {
5106
+ const rowsByStep = [];
5107
+ for (const step of plan.steps) rowsByStep.push(await step.load(engine) ?? []);
5108
+ const chunks = planChunks(plan, rowsByStep.map((r) => r.length), chunkSize ?? plan.chunkSize);
5109
+ return { chunks, planHash: hashMigrationPlan(plan, chunks), rowsByStep };
5110
+ }
5111
+ async function runMigrationJournal(engine, plan, options = {}) {
5112
+ const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
5113
+ if (!engineCanRollBack(engine)) {
5114
+ throw new MigrationJournalRefusal(
5115
+ "NOT_IMPLEMENTED",
5116
+ `Migration plan '${plan.id}' requires engine transaction support; this runtime cannot roll back. The journal's chunk_done markers would not mean "committed", so the run is refused rather than started.`
5117
+ );
5118
+ }
5119
+ const { chunks, planHash, rowsByStep } = await loadPlan(engine, plan, options.chunkSize);
5120
+ const resuming = Boolean(options.runId);
5121
+ const runId = options.runId ?? randomUUID();
5122
+ let events = [];
5123
+ let seq = 0;
5124
+ let committed = /* @__PURE__ */ new Set();
5125
+ let compensated = /* @__PURE__ */ new Set();
5126
+ const attemptsByChunk = /* @__PURE__ */ new Map();
5127
+ if (resuming) {
5128
+ events = await readRunJournal(engine, runId);
5129
+ if (events.length === 0) {
5130
+ throw new MigrationJournalRefusal("NO_SUCH_RUN", `No journal rows for run '${runId}'.`);
5131
+ }
5132
+ const start = events.find((e) => e.kind === "run_started");
5133
+ if (start?.plan_hash && start.plan_hash !== planHash) {
5134
+ throw new MigrationJournalRefusal(
5135
+ "PLAN_CHANGED",
5136
+ `Refusing to resume run '${runId}': plan hash ${planHash} does not match the journal's ${start.plan_hash}. The chunk boundaries recorded in the journal describe a different plan.`
5137
+ );
5138
+ }
5139
+ if (events.some((e) => e.kind === "run_done")) {
5140
+ return {
5141
+ runId,
5142
+ status: "completed",
5143
+ chunksTotal: chunks.length,
5144
+ chunksCommitted: chunkSetOf(events, "chunk_done").size,
5145
+ chunksCompensated: chunkSetOf(events, "compensated").size,
5146
+ planHash
5147
+ };
5148
+ }
5149
+ seq = events.reduce((m, e) => Math.max(m, Number(e.seq) + 1), 0);
5150
+ committed = chunkSetOf(events, "chunk_done");
5151
+ compensated = chunkSetOf(events, "compensated");
5152
+ for (const e of events) {
5153
+ if (e.kind === "chunk_started" && typeof e.chunk_index === "number") {
5154
+ attemptsByChunk.set(e.chunk_index, (attemptsByChunk.get(e.chunk_index) ?? 0) + 1);
5155
+ }
5156
+ }
5157
+ }
5158
+ for (const step of plan.steps) {
5159
+ if (!step.preflight) continue;
5160
+ try {
5161
+ await step.preflight(engine);
5162
+ } catch (err) {
5163
+ throw new MigrationJournalRefusal(
5164
+ "PREFLIGHT_FAILED",
5165
+ `Migration plan '${plan.id}' refused: preflight for step '${step.name}' failed: ${errText(err)}`
5166
+ );
5167
+ }
5168
+ }
5169
+ if (plan.onCrash === "compensate") {
5170
+ const missing = plan.steps.filter((s) => !s.compensate).map((s) => s.name);
5171
+ if (missing.length > 0) {
5172
+ throw new MigrationJournalRefusal(
5173
+ "NOT_COMPENSABLE",
5174
+ `Migration plan '${plan.id}' declares onCrash: 'compensate' but step(s) ${missing.join(", ")} declare no compensate().`
5175
+ );
5176
+ }
5177
+ }
5178
+ const rowsOf = (c) => rowsByStep[c.stepIndex].slice(c.offset, c.offset + c.length);
5179
+ const next = () => seq++;
5180
+ if (!resuming) {
5181
+ await appendEvent(engine, {
5182
+ run_id: runId,
5183
+ seq: next(),
5184
+ kind: "run_started",
5185
+ plan_hash: planHash,
5186
+ migration_id: plan.migrationId,
5187
+ created_at: now(),
5188
+ detail: JSON.stringify({
5189
+ planId: plan.id,
5190
+ onCrash: plan.onCrash ?? "resume",
5191
+ chunks: chunks.map((c) => ({ i: c.index, step: c.stepName, offset: c.offset, length: c.length }))
5192
+ })
5193
+ });
5194
+ }
5195
+ if (resuming && plan.onCrash === "compensate") {
5196
+ return await unwind(engine, plan, {
5197
+ runId,
5198
+ planHash,
5199
+ chunks,
5200
+ rowsOf,
5201
+ next,
5202
+ now,
5203
+ committed,
5204
+ compensated,
5205
+ chunksTotal: chunks.length,
5206
+ cause: new Error(`run '${runId}' rediscovered after interruption; plan policy is compensate`)
5207
+ });
5208
+ }
5209
+ for (const chunk of chunks) {
5210
+ if (committed.has(chunk.index)) continue;
5211
+ const attempt = (attemptsByChunk.get(chunk.index) ?? 0) + 1;
5212
+ attemptsByChunk.set(chunk.index, attempt);
5213
+ const step = plan.steps[chunk.stepIndex];
5214
+ const rows = rowsOf(chunk);
5215
+ await appendEvent(engine, {
5216
+ run_id: runId,
5217
+ seq: next(),
5218
+ kind: "chunk_started",
5219
+ chunk_index: chunk.index,
5220
+ attempt,
5221
+ migration_id: plan.migrationId,
5222
+ created_at: now()
5223
+ });
5224
+ try {
5225
+ await engine.transaction(async (trxCtx) => {
5226
+ await step.forward(rows, { runId, chunkIndex: chunk.index, attempt, context: trxCtx }, engine);
5227
+ await appendEvent(
5228
+ engine,
5229
+ {
5230
+ run_id: runId,
5231
+ seq: next(),
5232
+ kind: "chunk_done",
5233
+ chunk_index: chunk.index,
5234
+ attempt,
5235
+ migration_id: plan.migrationId,
5236
+ created_at: now()
5237
+ },
5238
+ trxCtx
5239
+ );
5240
+ }, { ...SYSTEM_CTX });
5241
+ committed.add(chunk.index);
5242
+ } catch (err) {
5243
+ return await unwind(engine, plan, {
5244
+ runId,
5245
+ planHash,
5246
+ chunks,
5247
+ rowsOf,
5248
+ next,
5249
+ now,
5250
+ committed,
5251
+ compensated,
5252
+ chunksTotal: chunks.length,
5253
+ cause: err
5254
+ });
5255
+ }
5256
+ }
5257
+ await appendEvent(engine, {
5258
+ run_id: runId,
5259
+ seq: next(),
5260
+ kind: "run_done",
5261
+ migration_id: plan.migrationId,
5262
+ created_at: now()
5263
+ });
5264
+ return {
5265
+ runId,
5266
+ status: "completed",
5267
+ chunksTotal: chunks.length,
5268
+ chunksCommitted: committed.size,
5269
+ chunksCompensated: compensated.size,
5270
+ planHash
5271
+ };
5272
+ }
5273
+ async function unwind(engine, plan, a) {
5274
+ const order = [...a.committed].sort((x, y) => y - x);
5275
+ for (const index of order) {
5276
+ if (a.compensated.has(index)) continue;
5277
+ const chunk = a.chunks[index];
5278
+ const step = plan.steps[chunk.stepIndex];
5279
+ if (!step.compensate) {
5280
+ await appendEvent(engine, {
5281
+ run_id: a.runId,
5282
+ seq: a.next(),
5283
+ kind: "run_failed",
5284
+ chunk_index: index,
5285
+ migration_id: plan.migrationId,
5286
+ created_at: a.now(),
5287
+ detail: JSON.stringify({
5288
+ phase: "compensate",
5289
+ reason: "step declares no compensate()",
5290
+ step: step.name,
5291
+ cause: errText(a.cause)
5292
+ })
5293
+ });
5294
+ return {
5295
+ runId: a.runId,
5296
+ status: "failed",
5297
+ chunksTotal: a.chunksTotal,
5298
+ chunksCommitted: a.committed.size,
5299
+ chunksCompensated: a.compensated.size,
5300
+ planHash: a.planHash,
5301
+ error: a.cause
5302
+ };
5303
+ }
5304
+ const attempt = 1;
5305
+ try {
5306
+ await engine.transaction(async (trxCtx) => {
5307
+ await step.compensate(a.rowsOf(chunk), { runId: a.runId, chunkIndex: index, attempt, context: trxCtx }, engine);
5308
+ await appendEvent(
5309
+ engine,
5310
+ {
5311
+ run_id: a.runId,
5312
+ seq: a.next(),
5313
+ kind: "compensated",
5314
+ chunk_index: index,
5315
+ attempt,
5316
+ migration_id: plan.migrationId,
5317
+ created_at: a.now()
5318
+ },
5319
+ trxCtx
5320
+ );
5321
+ }, { ...SYSTEM_CTX });
5322
+ a.compensated.add(index);
5323
+ } catch (err) {
5324
+ await appendEvent(engine, {
5325
+ run_id: a.runId,
5326
+ seq: a.next(),
5327
+ kind: "run_failed",
5328
+ chunk_index: index,
5329
+ migration_id: plan.migrationId,
5330
+ created_at: a.now(),
5331
+ detail: JSON.stringify({
5332
+ phase: "compensate",
5333
+ step: step.name,
5334
+ error: errText(err),
5335
+ cause: errText(a.cause)
5336
+ })
5337
+ });
5338
+ return {
5339
+ runId: a.runId,
5340
+ status: "failed",
5341
+ chunksTotal: a.chunksTotal,
5342
+ chunksCommitted: a.committed.size,
5343
+ chunksCompensated: a.compensated.size,
5344
+ planHash: a.planHash,
5345
+ error: err
5346
+ };
5347
+ }
5348
+ }
5349
+ await appendEvent(engine, {
5350
+ run_id: a.runId,
5351
+ seq: a.next(),
5352
+ kind: "run_failed",
5353
+ migration_id: plan.migrationId,
5354
+ created_at: a.now(),
5355
+ detail: JSON.stringify({ phase: "forward", error: errText(a.cause), compensated: [...a.compensated].sort((x, y) => x - y) })
5356
+ });
5357
+ return {
5358
+ runId: a.runId,
5359
+ status: "compensated",
5360
+ chunksTotal: a.chunksTotal,
5361
+ chunksCommitted: a.committed.size,
5362
+ chunksCompensated: a.compensated.size,
5363
+ planHash: a.planHash,
5364
+ error: a.cause
5365
+ };
5366
+ }
5367
+ async function resumeMigrationJournal(engine, plan, runId, options = {}) {
5368
+ return runMigrationJournal(engine, plan, { ...options, runId });
5369
+ }
5370
+ function errText(err) {
5371
+ if (err instanceof Error) return err.message;
5372
+ try {
5373
+ return String(err);
5374
+ } catch {
5375
+ return "<unprintable error>";
5376
+ }
5377
+ }
5378
+
4849
5379
  // src/utils/filter-tokens.ts
4850
5380
  import {
4851
5381
  classifyFilterToken,
@@ -5295,7 +5825,7 @@ var PluginHealthMonitor = class {
5295
5825
  };
5296
5826
 
5297
5827
  // src/hot-reload.ts
5298
- import { createHash as createHash2 } from "crypto";
5828
+ import { createHash as createHash3 } from "crypto";
5299
5829
  var generateUUID = () => {
5300
5830
  if (typeof crypto !== "undefined" && crypto.randomUUID) {
5301
5831
  return crypto.randomUUID();
@@ -5390,7 +5920,7 @@ var PluginStateManager = class {
5390
5920
  */
5391
5921
  calculateChecksum(state) {
5392
5922
  const stateStr = JSON.stringify(state);
5393
- return createHash2("sha256").update(stateStr).digest("hex");
5923
+ return createHash3("sha256").update(stateStr).digest("hex");
5394
5924
  }
5395
5925
  /**
5396
5926
  * Shutdown state manager
@@ -5976,6 +6506,8 @@ export {
5976
6506
  DependencyResolver,
5977
6507
  HotReloadManager,
5978
6508
  LiteKernel,
6509
+ MigrationJournalRefusal,
6510
+ MigrationPlanRegistry,
5979
6511
  NamespaceResolver,
5980
6512
  ObjectKernel,
5981
6513
  ObjectKernelBase,
@@ -5998,6 +6530,7 @@ export {
5998
6530
  ServiceLifecycle,
5999
6531
  UnknownFilterTokenError,
6000
6532
  UnresolvedFilterTokenError,
6533
+ assertInitServiceRequirements,
6001
6534
  bucketKeyToCalendarRange,
6002
6535
  buildPermissionsFromGrants,
6003
6536
  bulkWrite,
@@ -6016,33 +6549,45 @@ export {
6016
6549
  deepMerge,
6017
6550
  defaultIsTransientError,
6018
6551
  derivePosture,
6552
+ describeInitOrderFault,
6553
+ engineCanRollBack,
6019
6554
  evaluateAuthGate,
6020
6555
  extractApiKey,
6021
6556
  filterTokenContextFrom,
6557
+ findInterruptedRuns,
6022
6558
  generateApiKey,
6023
6559
  generateEd25519KeyPair,
6024
6560
  getEnv,
6025
6561
  getMemoryUsage,
6026
6562
  hashApiKey,
6563
+ hashMigrationPlan,
6027
6564
  isAuthGateAllowlisted,
6028
6565
  isExpired,
6029
6566
  isGrantActive,
6030
6567
  isGrantExpired,
6031
6568
  isNode,
6569
+ nextUtcCalendarDay,
6032
6570
  parseScopes,
6033
6571
  parseSignature,
6572
+ planChunks,
6034
6573
  postureVisibleRows,
6035
6574
  readAuthoredTranslationLayer,
6575
+ readRunJournal,
6036
6576
  resolveApiKeyPrincipal,
6037
6577
  resolveAuthzContext,
6038
6578
  resolveFilterToken,
6039
6579
  resolveFilterTokens,
6040
6580
  resolveLocale,
6041
6581
  resolveLocalizationContext,
6582
+ resolvePluginOrder,
6042
6583
  resolveUserAuthzGrants,
6584
+ resumeMigrationJournal,
6585
+ runMigrationJournal,
6043
6586
  safeExit,
6044
6587
  shouldDenyAnonymous,
6045
6588
  signPayload,
6589
+ utcInstantMs,
6590
+ validateInitServiceContract,
6046
6591
  verifyPayload,
6047
6592
  verifyPlatformSignature,
6048
6593
  verifyPluginArtifact,