@objectstack/runtime 11.9.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.cjs CHANGED
@@ -916,6 +916,7 @@ var init_app_plugin = __esm({
916
916
  /** When true, init/start become no-ops — env has no app payload. */
917
917
  this.empty = false;
918
918
  this.init = async (ctx) => {
919
+ this.installDefaultHookBodyRunner(ctx);
919
920
  if (this.empty) {
920
921
  ctx.logger.debug("[AppPlugin] empty env \u2014 no app payload, skipping init", {
921
922
  pluginName: this.name
@@ -1438,6 +1439,46 @@ var init_app_plugin = __esm({
1438
1439
  this.name = `plugin.app.${appId}`;
1439
1440
  this.version = sys?.version;
1440
1441
  }
1442
+ /**
1443
+ * Install the engine's DEFAULT hook body runner (`engine.setDefaultBodyRunner`).
1444
+ *
1445
+ * Hooks authored at runtime (Studio → `protocol.saveMetaItem` → publish)
1446
+ * bind through paths that pass no explicit `bodyRunner` — notably
1447
+ * ObjectQLPlugin's metadata-service bind — so without this default their
1448
+ * L1/L2 `body` is silently dropped by `bindHooksToEngine` and the hook
1449
+ * never runs (#2588). The runtime owns the sandbox bridge (objectql stays
1450
+ * sandbox-free), so this is the boot point that wires it: same
1451
+ * QuickJS-sandboxed, capability-gated runner the `defineStack({ hooks })`
1452
+ * bind already uses.
1453
+ *
1454
+ * `OS_DISABLE_AUTHORED_HOOKS=1` opts out for deployments that want
1455
+ * runtime-authored (DB-stored, non-code-reviewed) hook bodies to stay
1456
+ * inert; code-shipped hooks are unaffected (AppPlugin passes its own
1457
+ * runner explicitly).
1458
+ *
1459
+ * Idempotent: the first AppPlugin to run installs it; the runner is
1460
+ * bundle-agnostic (it only closes over the engine + logger).
1461
+ */
1462
+ installDefaultHookBodyRunner(ctx) {
1463
+ if (process.env.OS_DISABLE_AUTHORED_HOOKS === "1") {
1464
+ ctx.logger.info("[AppPlugin] OS_DISABLE_AUTHORED_HOOKS=1 \u2014 runtime-authored hook bodies will not execute");
1465
+ return;
1466
+ }
1467
+ let ql;
1468
+ try {
1469
+ ql = ctx.getService("objectql");
1470
+ } catch {
1471
+ return;
1472
+ }
1473
+ if (!ql || typeof ql.setDefaultBodyRunner !== "function") return;
1474
+ if (ql._defaultBodyRunner) return;
1475
+ ql.setDefaultBodyRunner(hookBodyRunnerFactory(new QuickJSScriptRunner(), {
1476
+ ql,
1477
+ logger: ctx.logger,
1478
+ appId: "runtime-authored"
1479
+ }));
1480
+ ctx.logger.info("[AppPlugin] Installed default hook body runner (runtime-authored hooks can execute)");
1481
+ }
1441
1482
  /**
1442
1483
  * Emit a kernel hook so the control-plane `AppCatalogService` can
1443
1484
  * upsert / delete the corresponding `sys_app` row. Silently no-ops
@@ -2232,6 +2273,22 @@ var _HttpDispatcher = class _HttpDispatcher {
2232
2273
  body: { success: false, error: { message, code, details } }
2233
2274
  };
2234
2275
  }
2276
+ /**
2277
+ * Build an error response from a THROWN service/protocol error, preserving
2278
+ * the error's own HTTP `status` and — critically — any structured `issues`
2279
+ * array (e.g. spec-validation `{ path, message, code }[]` from
2280
+ * `protocol.saveMetaItem`). The plain `error(msg, code)` path collapses a
2281
+ * validation failure to a single message, so the UI can only show a generic
2282
+ * banner; carrying `issues` (and the semantic `code`) in `details` lets it
2283
+ * map each error back to the offending field. Falls back to `fallbackStatus`
2284
+ * and behaves exactly like `error()` for errors that carry neither.
2285
+ */
2286
+ errorFromThrown(e, fallbackStatus = 500) {
2287
+ const status = typeof e?.status === "number" ? e.status : typeof e?.statusCode === "number" ? e.statusCode : fallbackStatus;
2288
+ const issues = Array.isArray(e?.issues) ? e.issues : void 0;
2289
+ const details = issues || e?.code ? { ...e?.code ? { code: e.code } : {}, ...issues ? { issues } : {} } : void 0;
2290
+ return this.error(e?.message ?? String(e), status, details);
2291
+ }
2235
2292
  /**
2236
2293
  * ADR-0046: `doc` list responses omit `content` by default — manuals
2237
2294
  * are the one metadata payload that grows unbounded, and the list
@@ -3303,7 +3360,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3303
3360
  const result = await protocol.saveMetaItem({ type, name, item: body, organizationId, ...packageId ? { packageId } : {} });
3304
3361
  return { handled: true, response: this.success(result) };
3305
3362
  } catch (e) {
3306
- return { handled: true, response: this.error(e.message, 400) };
3363
+ return { handled: true, response: this.errorFromThrown(e, 400) };
3307
3364
  }
3308
3365
  }
3309
3366
  const metaSvc = await this.resolveService("metadata", _context.environmentId);
@@ -3786,9 +3843,20 @@ var _HttpDispatcher = class _HttpDispatcher {
3786
3843
  } catch (e) {
3787
3844
  result.unhideError = e?.message ?? "visibility flip failed";
3788
3845
  }
3846
+ try {
3847
+ const changed = [
3848
+ ...(result?.published ?? []).map((p) => `${p.type}/${p.name}`),
3849
+ ...(result?.unhiddenApps ?? []).map((n) => `app/${n}`)
3850
+ ];
3851
+ if (changed.length > 0 && this.kernel?.context?.trigger) {
3852
+ await this.kernel.context.trigger("metadata:reloaded", { changed });
3853
+ }
3854
+ } catch (e) {
3855
+ result.rebindError = e?.message ?? "metadata:reloaded announce failed";
3856
+ }
3789
3857
  return { handled: true, response: this.success(result) };
3790
3858
  } catch (e) {
3791
- return { handled: true, response: this.error(e.message, e.statusCode || 500) };
3859
+ return { handled: true, response: this.errorFromThrown(e, 500) };
3792
3860
  }
3793
3861
  }
3794
3862
  return { handled: true, response: this.error("Draft publishing not supported", 501) };
@@ -5751,6 +5819,60 @@ function createDispatcherPlugin(config = {}) {
5751
5819
  errorResponse(err, res);
5752
5820
  }
5753
5821
  });
5822
+ server.post(`${prefix}/packages/:id/duplicate`, async (req, res) => {
5823
+ try {
5824
+ const result = await dispatcher.handlePackages(`/${req.params.id}/duplicate`, "POST", req.body, {}, { request: req });
5825
+ sendResult(result, res);
5826
+ } catch (err) {
5827
+ errorResponse(err, res);
5828
+ }
5829
+ });
5830
+ server.post(`${prefix}/packages/:id/adopt-orphans`, async (req, res) => {
5831
+ try {
5832
+ const result = await dispatcher.handlePackages(`/${req.params.id}/adopt-orphans`, "POST", req.body, {}, { request: req });
5833
+ sendResult(result, res);
5834
+ } catch (err) {
5835
+ errorResponse(err, res);
5836
+ }
5837
+ });
5838
+ server.post(`${prefix}/packages/:id/discard-drafts`, async (req, res) => {
5839
+ try {
5840
+ const result = await dispatcher.handlePackages(`/${req.params.id}/discard-drafts`, "POST", req.body, {}, { request: req });
5841
+ sendResult(result, res);
5842
+ } catch (err) {
5843
+ errorResponse(err, res);
5844
+ }
5845
+ });
5846
+ server.get(`${prefix}/packages/:id/commits`, async (req, res) => {
5847
+ try {
5848
+ const result = await dispatcher.handlePackages(`/${req.params.id}/commits`, "GET", void 0, req.query ?? {}, { request: req });
5849
+ sendResult(result, res);
5850
+ } catch (err) {
5851
+ errorResponse(err, res);
5852
+ }
5853
+ });
5854
+ server.post(`${prefix}/packages/:id/commits/:commitId/revert`, async (req, res) => {
5855
+ try {
5856
+ const result = await dispatcher.handlePackages(
5857
+ `/${req.params.id}/commits/${req.params.commitId}/revert`,
5858
+ "POST",
5859
+ req.body,
5860
+ {},
5861
+ { request: req }
5862
+ );
5863
+ sendResult(result, res);
5864
+ } catch (err) {
5865
+ errorResponse(err, res);
5866
+ }
5867
+ });
5868
+ server.post(`${prefix}/packages/:id/rollback`, async (req, res) => {
5869
+ try {
5870
+ const result = await dispatcher.handlePackages(`/${req.params.id}/rollback`, "POST", req.body, {}, { request: req });
5871
+ sendResult(result, res);
5872
+ } catch (err) {
5873
+ errorResponse(err, res);
5874
+ }
5875
+ });
5754
5876
  server.post(`${prefix}/storage/upload`, async (req, res) => {
5755
5877
  try {
5756
5878
  const result = await dispatcher.handleStorage("upload", "POST", req.body, { request: req });