@objectstack/runtime 11.10.0 → 12.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -33,7 +33,7 @@ interface RuntimeConfig {
33
33
  * - `ClusterCapabilityConfigInput`: forwarded to `defineCluster()`.
34
34
  * - `{ cluster: IClusterService }`: bring your own instance.
35
35
  *
36
- * See `content/docs/concepts/cluster-semantics.mdx` for driver options.
36
+ * See `content/docs/kernel/cluster.mdx` for driver options.
37
37
  */
38
38
  cluster?: false | ClusterCapabilityConfigInput | ClusterServicePluginOptions;
39
39
  }
@@ -242,6 +242,27 @@ declare class AppPlugin implements Plugin {
242
242
  private readonly empty;
243
243
  constructor(bundle: any, projectContext?: AppPluginProjectContext);
244
244
  init: (ctx: PluginContext) => Promise<void>;
245
+ /**
246
+ * Install the engine's DEFAULT hook body runner (`engine.setDefaultBodyRunner`).
247
+ *
248
+ * Hooks authored at runtime (Studio → `protocol.saveMetaItem` → publish)
249
+ * bind through paths that pass no explicit `bodyRunner` — notably
250
+ * ObjectQLPlugin's metadata-service bind — so without this default their
251
+ * L1/L2 `body` is silently dropped by `bindHooksToEngine` and the hook
252
+ * never runs (#2588). The runtime owns the sandbox bridge (objectql stays
253
+ * sandbox-free), so this is the boot point that wires it: same
254
+ * QuickJS-sandboxed, capability-gated runner the `defineStack({ hooks })`
255
+ * bind already uses.
256
+ *
257
+ * `OS_DISABLE_AUTHORED_HOOKS=1` opts out for deployments that want
258
+ * runtime-authored (DB-stored, non-code-reviewed) hook bodies to stay
259
+ * inert; code-shipped hooks are unaffected (AppPlugin passes its own
260
+ * runner explicitly).
261
+ *
262
+ * Idempotent: the first AppPlugin to run installs it; the runner is
263
+ * bundle-agnostic (it only closes over the engine + logger).
264
+ */
265
+ private installDefaultHookBodyRunner;
245
266
  start: (ctx: PluginContext) => Promise<void>;
246
267
  stop: (ctx: PluginContext) => Promise<void>;
247
268
  /**
@@ -965,6 +986,17 @@ declare class HttpDispatcher {
965
986
  private resolveDefaultProject;
966
987
  private success;
967
988
  private error;
989
+ /**
990
+ * Build an error response from a THROWN service/protocol error, preserving
991
+ * the error's own HTTP `status` and — critically — any structured `issues`
992
+ * array (e.g. spec-validation `{ path, message, code }[]` from
993
+ * `protocol.saveMetaItem`). The plain `error(msg, code)` path collapses a
994
+ * validation failure to a single message, so the UI can only show a generic
995
+ * banner; carrying `issues` (and the semantic `code`) in `details` lets it
996
+ * map each error back to the offending field. Falls back to `fallbackStatus`
997
+ * and behaves exactly like `error()` for errors that carry neither.
998
+ */
999
+ private errorFromThrown;
968
1000
  /**
969
1001
  * ADR-0046: `doc` list responses omit `content` by default — manuals
970
1002
  * are the one metadata payload that grows unbounded, and the list
package/dist/index.d.ts CHANGED
@@ -33,7 +33,7 @@ interface RuntimeConfig {
33
33
  * - `ClusterCapabilityConfigInput`: forwarded to `defineCluster()`.
34
34
  * - `{ cluster: IClusterService }`: bring your own instance.
35
35
  *
36
- * See `content/docs/concepts/cluster-semantics.mdx` for driver options.
36
+ * See `content/docs/kernel/cluster.mdx` for driver options.
37
37
  */
38
38
  cluster?: false | ClusterCapabilityConfigInput | ClusterServicePluginOptions;
39
39
  }
@@ -242,6 +242,27 @@ declare class AppPlugin implements Plugin {
242
242
  private readonly empty;
243
243
  constructor(bundle: any, projectContext?: AppPluginProjectContext);
244
244
  init: (ctx: PluginContext) => Promise<void>;
245
+ /**
246
+ * Install the engine's DEFAULT hook body runner (`engine.setDefaultBodyRunner`).
247
+ *
248
+ * Hooks authored at runtime (Studio → `protocol.saveMetaItem` → publish)
249
+ * bind through paths that pass no explicit `bodyRunner` — notably
250
+ * ObjectQLPlugin's metadata-service bind — so without this default their
251
+ * L1/L2 `body` is silently dropped by `bindHooksToEngine` and the hook
252
+ * never runs (#2588). The runtime owns the sandbox bridge (objectql stays
253
+ * sandbox-free), so this is the boot point that wires it: same
254
+ * QuickJS-sandboxed, capability-gated runner the `defineStack({ hooks })`
255
+ * bind already uses.
256
+ *
257
+ * `OS_DISABLE_AUTHORED_HOOKS=1` opts out for deployments that want
258
+ * runtime-authored (DB-stored, non-code-reviewed) hook bodies to stay
259
+ * inert; code-shipped hooks are unaffected (AppPlugin passes its own
260
+ * runner explicitly).
261
+ *
262
+ * Idempotent: the first AppPlugin to run installs it; the runner is
263
+ * bundle-agnostic (it only closes over the engine + logger).
264
+ */
265
+ private installDefaultHookBodyRunner;
245
266
  start: (ctx: PluginContext) => Promise<void>;
246
267
  stop: (ctx: PluginContext) => Promise<void>;
247
268
  /**
@@ -965,6 +986,17 @@ declare class HttpDispatcher {
965
986
  private resolveDefaultProject;
966
987
  private success;
967
988
  private error;
989
+ /**
990
+ * Build an error response from a THROWN service/protocol error, preserving
991
+ * the error's own HTTP `status` and — critically — any structured `issues`
992
+ * array (e.g. spec-validation `{ path, message, code }[]` from
993
+ * `protocol.saveMetaItem`). The plain `error(msg, code)` path collapses a
994
+ * validation failure to a single message, so the UI can only show a generic
995
+ * banner; carrying `issues` (and the semantic `code`) in `details` lets it
996
+ * map each error back to the offending field. Falls back to `fallbackStatus`
997
+ * and behaves exactly like `error()` for errors that carry neither.
998
+ */
999
+ private errorFromThrown;
968
1000
  /**
969
1001
  * ADR-0046: `doc` list responses omit `content` by default — manuals
970
1002
  * are the one metadata payload that grows unbounded, and the list
package/dist/index.js CHANGED
@@ -892,6 +892,7 @@ var init_app_plugin = __esm({
892
892
  /** When true, init/start become no-ops — env has no app payload. */
893
893
  this.empty = false;
894
894
  this.init = async (ctx) => {
895
+ this.installDefaultHookBodyRunner(ctx);
895
896
  if (this.empty) {
896
897
  ctx.logger.debug("[AppPlugin] empty env \u2014 no app payload, skipping init", {
897
898
  pluginName: this.name
@@ -1414,6 +1415,46 @@ var init_app_plugin = __esm({
1414
1415
  this.name = `plugin.app.${appId}`;
1415
1416
  this.version = sys?.version;
1416
1417
  }
1418
+ /**
1419
+ * Install the engine's DEFAULT hook body runner (`engine.setDefaultBodyRunner`).
1420
+ *
1421
+ * Hooks authored at runtime (Studio → `protocol.saveMetaItem` → publish)
1422
+ * bind through paths that pass no explicit `bodyRunner` — notably
1423
+ * ObjectQLPlugin's metadata-service bind — so without this default their
1424
+ * L1/L2 `body` is silently dropped by `bindHooksToEngine` and the hook
1425
+ * never runs (#2588). The runtime owns the sandbox bridge (objectql stays
1426
+ * sandbox-free), so this is the boot point that wires it: same
1427
+ * QuickJS-sandboxed, capability-gated runner the `defineStack({ hooks })`
1428
+ * bind already uses.
1429
+ *
1430
+ * `OS_DISABLE_AUTHORED_HOOKS=1` opts out for deployments that want
1431
+ * runtime-authored (DB-stored, non-code-reviewed) hook bodies to stay
1432
+ * inert; code-shipped hooks are unaffected (AppPlugin passes its own
1433
+ * runner explicitly).
1434
+ *
1435
+ * Idempotent: the first AppPlugin to run installs it; the runner is
1436
+ * bundle-agnostic (it only closes over the engine + logger).
1437
+ */
1438
+ installDefaultHookBodyRunner(ctx) {
1439
+ if (process.env.OS_DISABLE_AUTHORED_HOOKS === "1") {
1440
+ ctx.logger.info("[AppPlugin] OS_DISABLE_AUTHORED_HOOKS=1 \u2014 runtime-authored hook bodies will not execute");
1441
+ return;
1442
+ }
1443
+ let ql;
1444
+ try {
1445
+ ql = ctx.getService("objectql");
1446
+ } catch {
1447
+ return;
1448
+ }
1449
+ if (!ql || typeof ql.setDefaultBodyRunner !== "function") return;
1450
+ if (ql._defaultBodyRunner) return;
1451
+ ql.setDefaultBodyRunner(hookBodyRunnerFactory(new QuickJSScriptRunner(), {
1452
+ ql,
1453
+ logger: ctx.logger,
1454
+ appId: "runtime-authored"
1455
+ }));
1456
+ ctx.logger.info("[AppPlugin] Installed default hook body runner (runtime-authored hooks can execute)");
1457
+ }
1417
1458
  /**
1418
1459
  * Emit a kernel hook so the control-plane `AppCatalogService` can
1419
1460
  * upsert / delete the corresponding `sys_app` row. Silently no-ops
@@ -2167,6 +2208,22 @@ var _HttpDispatcher = class _HttpDispatcher {
2167
2208
  body: { success: false, error: { message, code, details } }
2168
2209
  };
2169
2210
  }
2211
+ /**
2212
+ * Build an error response from a THROWN service/protocol error, preserving
2213
+ * the error's own HTTP `status` and — critically — any structured `issues`
2214
+ * array (e.g. spec-validation `{ path, message, code }[]` from
2215
+ * `protocol.saveMetaItem`). The plain `error(msg, code)` path collapses a
2216
+ * validation failure to a single message, so the UI can only show a generic
2217
+ * banner; carrying `issues` (and the semantic `code`) in `details` lets it
2218
+ * map each error back to the offending field. Falls back to `fallbackStatus`
2219
+ * and behaves exactly like `error()` for errors that carry neither.
2220
+ */
2221
+ errorFromThrown(e, fallbackStatus = 500) {
2222
+ const status = typeof e?.status === "number" ? e.status : typeof e?.statusCode === "number" ? e.statusCode : fallbackStatus;
2223
+ const issues = Array.isArray(e?.issues) ? e.issues : void 0;
2224
+ const details = issues || e?.code ? { ...e?.code ? { code: e.code } : {}, ...issues ? { issues } : {} } : void 0;
2225
+ return this.error(e?.message ?? String(e), status, details);
2226
+ }
2170
2227
  /**
2171
2228
  * ADR-0046: `doc` list responses omit `content` by default — manuals
2172
2229
  * are the one metadata payload that grows unbounded, and the list
@@ -3238,7 +3295,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3238
3295
  const result = await protocol.saveMetaItem({ type, name, item: body, organizationId, ...packageId ? { packageId } : {} });
3239
3296
  return { handled: true, response: this.success(result) };
3240
3297
  } catch (e) {
3241
- return { handled: true, response: this.error(e.message, 400) };
3298
+ return { handled: true, response: this.errorFromThrown(e, 400) };
3242
3299
  }
3243
3300
  }
3244
3301
  const metaSvc = await this.resolveService("metadata", _context.environmentId);
@@ -3721,9 +3778,20 @@ var _HttpDispatcher = class _HttpDispatcher {
3721
3778
  } catch (e) {
3722
3779
  result.unhideError = e?.message ?? "visibility flip failed";
3723
3780
  }
3781
+ try {
3782
+ const changed = [
3783
+ ...(result?.published ?? []).map((p) => `${p.type}/${p.name}`),
3784
+ ...(result?.unhiddenApps ?? []).map((n) => `app/${n}`)
3785
+ ];
3786
+ if (changed.length > 0 && this.kernel?.context?.trigger) {
3787
+ await this.kernel.context.trigger("metadata:reloaded", { changed });
3788
+ }
3789
+ } catch (e) {
3790
+ result.rebindError = e?.message ?? "metadata:reloaded announce failed";
3791
+ }
3724
3792
  return { handled: true, response: this.success(result) };
3725
3793
  } catch (e) {
3726
- return { handled: true, response: this.error(e.message, e.statusCode || 500) };
3794
+ return { handled: true, response: this.errorFromThrown(e, 500) };
3727
3795
  }
3728
3796
  }
3729
3797
  return { handled: true, response: this.error("Draft publishing not supported", 501) };
@@ -5696,6 +5764,60 @@ function createDispatcherPlugin(config = {}) {
5696
5764
  errorResponse(err, res);
5697
5765
  }
5698
5766
  });
5767
+ server.post(`${prefix}/packages/:id/duplicate`, async (req, res) => {
5768
+ try {
5769
+ const result = await dispatcher.handlePackages(`/${req.params.id}/duplicate`, "POST", req.body, {}, { request: req });
5770
+ sendResult(result, res);
5771
+ } catch (err) {
5772
+ errorResponse(err, res);
5773
+ }
5774
+ });
5775
+ server.post(`${prefix}/packages/:id/adopt-orphans`, async (req, res) => {
5776
+ try {
5777
+ const result = await dispatcher.handlePackages(`/${req.params.id}/adopt-orphans`, "POST", req.body, {}, { request: req });
5778
+ sendResult(result, res);
5779
+ } catch (err) {
5780
+ errorResponse(err, res);
5781
+ }
5782
+ });
5783
+ server.post(`${prefix}/packages/:id/discard-drafts`, async (req, res) => {
5784
+ try {
5785
+ const result = await dispatcher.handlePackages(`/${req.params.id}/discard-drafts`, "POST", req.body, {}, { request: req });
5786
+ sendResult(result, res);
5787
+ } catch (err) {
5788
+ errorResponse(err, res);
5789
+ }
5790
+ });
5791
+ server.get(`${prefix}/packages/:id/commits`, async (req, res) => {
5792
+ try {
5793
+ const result = await dispatcher.handlePackages(`/${req.params.id}/commits`, "GET", void 0, req.query ?? {}, { request: req });
5794
+ sendResult(result, res);
5795
+ } catch (err) {
5796
+ errorResponse(err, res);
5797
+ }
5798
+ });
5799
+ server.post(`${prefix}/packages/:id/commits/:commitId/revert`, async (req, res) => {
5800
+ try {
5801
+ const result = await dispatcher.handlePackages(
5802
+ `/${req.params.id}/commits/${req.params.commitId}/revert`,
5803
+ "POST",
5804
+ req.body,
5805
+ {},
5806
+ { request: req }
5807
+ );
5808
+ sendResult(result, res);
5809
+ } catch (err) {
5810
+ errorResponse(err, res);
5811
+ }
5812
+ });
5813
+ server.post(`${prefix}/packages/:id/rollback`, async (req, res) => {
5814
+ try {
5815
+ const result = await dispatcher.handlePackages(`/${req.params.id}/rollback`, "POST", req.body, {}, { request: req });
5816
+ sendResult(result, res);
5817
+ } catch (err) {
5818
+ errorResponse(err, res);
5819
+ }
5820
+ });
5699
5821
  server.post(`${prefix}/storage/upload`, async (req, res) => {
5700
5822
  try {
5701
5823
  const result = await dispatcher.handleStorage("upload", "POST", req.body, { request: req });