@objectstack/rest 9.11.0 → 10.2.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
@@ -238,7 +238,11 @@ declare class RestServer {
238
238
  private i18nServiceProvider?;
239
239
  private analyticsServiceProvider?;
240
240
  private settingsServiceProvider?;
241
- constructor(server: IHttpServer, protocol: ObjectStackProtocol, config?: RestServerConfig, kernelManager?: RestKernelManager, envRegistry?: RestEnvRegistry, defaultEnvironmentIdProvider?: () => string | undefined, authServiceProvider?: (environmentId?: string) => Promise<any | undefined>, objectQLProvider?: (environmentId?: string) => Promise<any | undefined>, emailServiceProvider?: (environmentId?: string) => Promise<any | undefined>, sharingServiceProvider?: (environmentId?: string) => Promise<any | undefined>, reportsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, approvalsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, sharingRulesServiceProvider?: (environmentId?: string) => Promise<any | undefined>, i18nServiceProvider?: (environmentId?: string) => Promise<any | undefined>, analyticsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, settingsServiceProvider?: (environmentId?: string) => Promise<any | undefined>);
241
+ /** Sync probe: is a kernel service registered? Single-env path for nav
242
+ * capability gates (ADR-0057 D10) — resolveExecCtx sets no kernel in
243
+ * single-kernel deployments, so this prevents the gate failing open. */
244
+ private serviceExistsProvider?;
245
+ constructor(server: IHttpServer, protocol: ObjectStackProtocol, config?: RestServerConfig, kernelManager?: RestKernelManager, envRegistry?: RestEnvRegistry, defaultEnvironmentIdProvider?: () => string | undefined, authServiceProvider?: (environmentId?: string) => Promise<any | undefined>, objectQLProvider?: (environmentId?: string) => Promise<any | undefined>, emailServiceProvider?: (environmentId?: string) => Promise<any | undefined>, sharingServiceProvider?: (environmentId?: string) => Promise<any | undefined>, reportsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, approvalsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, sharingRulesServiceProvider?: (environmentId?: string) => Promise<any | undefined>, i18nServiceProvider?: (environmentId?: string) => Promise<any | undefined>, analyticsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, settingsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, serviceExistsProvider?: (name: string) => boolean);
242
246
  /**
243
247
  * Resolve the protocol for a given request. When `environmentId` is present
244
248
  * and a KernelManager is wired, fetch the per-project kernel's
@@ -340,6 +344,14 @@ declare class RestServer {
340
344
  * is never mutated so cached metadata stays clean.
341
345
  */
342
346
  private filterAppForUser;
347
+ /**
348
+ * Probe which `requiresService` capability gates referenced anywhere in
349
+ * `items` are actually registered in the runtime kernel. Returns `null`
350
+ * when the kernel can't be probed — callers then SKIP service gating
351
+ * (fail-open, matching the prior "send everything, let the client hide"
352
+ * behaviour). ADR-0057 addendum D10.
353
+ */
354
+ private resolveRegisteredServices;
343
355
  /**
344
356
  * Build a `TranslationBundle` (`Record<locale, TranslationData>`) from an
345
357
  * `II18nService` instance. Returns `undefined` when no locales are
package/dist/index.d.ts CHANGED
@@ -238,7 +238,11 @@ declare class RestServer {
238
238
  private i18nServiceProvider?;
239
239
  private analyticsServiceProvider?;
240
240
  private settingsServiceProvider?;
241
- constructor(server: IHttpServer, protocol: ObjectStackProtocol, config?: RestServerConfig, kernelManager?: RestKernelManager, envRegistry?: RestEnvRegistry, defaultEnvironmentIdProvider?: () => string | undefined, authServiceProvider?: (environmentId?: string) => Promise<any | undefined>, objectQLProvider?: (environmentId?: string) => Promise<any | undefined>, emailServiceProvider?: (environmentId?: string) => Promise<any | undefined>, sharingServiceProvider?: (environmentId?: string) => Promise<any | undefined>, reportsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, approvalsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, sharingRulesServiceProvider?: (environmentId?: string) => Promise<any | undefined>, i18nServiceProvider?: (environmentId?: string) => Promise<any | undefined>, analyticsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, settingsServiceProvider?: (environmentId?: string) => Promise<any | undefined>);
241
+ /** Sync probe: is a kernel service registered? Single-env path for nav
242
+ * capability gates (ADR-0057 D10) — resolveExecCtx sets no kernel in
243
+ * single-kernel deployments, so this prevents the gate failing open. */
244
+ private serviceExistsProvider?;
245
+ constructor(server: IHttpServer, protocol: ObjectStackProtocol, config?: RestServerConfig, kernelManager?: RestKernelManager, envRegistry?: RestEnvRegistry, defaultEnvironmentIdProvider?: () => string | undefined, authServiceProvider?: (environmentId?: string) => Promise<any | undefined>, objectQLProvider?: (environmentId?: string) => Promise<any | undefined>, emailServiceProvider?: (environmentId?: string) => Promise<any | undefined>, sharingServiceProvider?: (environmentId?: string) => Promise<any | undefined>, reportsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, approvalsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, sharingRulesServiceProvider?: (environmentId?: string) => Promise<any | undefined>, i18nServiceProvider?: (environmentId?: string) => Promise<any | undefined>, analyticsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, settingsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, serviceExistsProvider?: (name: string) => boolean);
242
246
  /**
243
247
  * Resolve the protocol for a given request. When `environmentId` is present
244
248
  * and a KernelManager is wired, fetch the per-project kernel's
@@ -340,6 +344,14 @@ declare class RestServer {
340
344
  * is never mutated so cached metadata stays clean.
341
345
  */
342
346
  private filterAppForUser;
347
+ /**
348
+ * Probe which `requiresService` capability gates referenced anywhere in
349
+ * `items` are actually registered in the runtime kernel. Returns `null`
350
+ * when the kernel can't be probed — callers then SKIP service gating
351
+ * (fail-open, matching the prior "send everything, let the client hide"
352
+ * behaviour). ADR-0057 addendum D10.
353
+ */
354
+ private resolveRegisteredServices;
343
355
  /**
344
356
  * Build a `TranslationBundle` (`Record<locale, TranslationData>`) from an
345
357
  * `II18nService` instance. Returns `undefined` when no locales are
package/dist/index.js CHANGED
@@ -308,6 +308,7 @@ function mapDataError(error, object) {
308
308
  error: `${field} is required`,
309
309
  code: "VALIDATION_FAILED",
310
310
  fields: [{ field, code: "required", message: `${field} is required` }],
311
+ hint: `If '${field}' is optional in your object metadata, the database column is still NOT NULL \u2014 the physical schema has drifted from metadata. Run 'os migrate' to reconcile (or reset the dev database).`,
311
312
  ...object ? { object } : {}
312
313
  }
313
314
  };
@@ -449,7 +450,7 @@ function rowsToCsv(fields, rows, includeHeader) {
449
450
  return lines.join("\r\n") + (lines.length > 0 ? "\r\n" : "");
450
451
  }
451
452
  var RestServer = class {
452
- constructor(server, protocol, config = {}, kernelManager, envRegistry, defaultEnvironmentIdProvider, authServiceProvider, objectQLProvider, emailServiceProvider, sharingServiceProvider, reportsServiceProvider, approvalsServiceProvider, sharingRulesServiceProvider, i18nServiceProvider, analyticsServiceProvider, settingsServiceProvider) {
453
+ constructor(server, protocol, config = {}, kernelManager, envRegistry, defaultEnvironmentIdProvider, authServiceProvider, objectQLProvider, emailServiceProvider, sharingServiceProvider, reportsServiceProvider, approvalsServiceProvider, sharingRulesServiceProvider, i18nServiceProvider, analyticsServiceProvider, settingsServiceProvider, serviceExistsProvider) {
453
454
  /**
454
455
  * Short-TTL cache for `hostname → environmentId` (P1-4). `resolveByHostname`
455
456
  * is a control-plane lookup (typically a DB query) that otherwise runs on
@@ -481,6 +482,7 @@ var RestServer = class {
481
482
  this.i18nServiceProvider = i18nServiceProvider;
482
483
  this.analyticsServiceProvider = analyticsServiceProvider;
483
484
  this.settingsServiceProvider = settingsServiceProvider;
485
+ this.serviceExistsProvider = serviceExistsProvider;
484
486
  }
485
487
  /**
486
488
  * Resolve the protocol for a given request. When `environmentId` is present
@@ -878,18 +880,22 @@ var RestServer = class {
878
880
  }
879
881
  let timezone;
880
882
  let locale;
883
+ let currency;
881
884
  try {
882
885
  const settings = this.settingsServiceProvider ? await this.settingsServiceProvider(environmentId).catch(() => void 0) : void 0;
883
886
  if (settings && typeof settings.get === "function") {
884
887
  const sctx = { tenantId, userId };
885
- const [tzRes, localeRes] = await Promise.all([
888
+ const [tzRes, localeRes, currencyRes] = await Promise.all([
886
889
  settings.get("localization", "timezone", sctx).catch(() => void 0),
887
- settings.get("localization", "locale", sctx).catch(() => void 0)
890
+ settings.get("localization", "locale", sctx).catch(() => void 0),
891
+ settings.get("localization", "currency", sctx).catch(() => void 0)
888
892
  ]);
889
893
  const tzVal = tzRes?.value;
890
894
  const localeVal = localeRes?.value;
895
+ const currencyVal = currencyRes?.value;
891
896
  if (typeof tzVal === "string" && tzVal.trim()) timezone = tzVal.trim();
892
897
  if (typeof localeVal === "string" && localeVal.trim()) locale = localeVal.trim();
898
+ if (typeof currencyVal === "string" && currencyVal.trim()) currency = currencyVal.trim().toUpperCase();
893
899
  }
894
900
  } catch {
895
901
  }
@@ -903,7 +909,12 @@ var RestServer = class {
903
909
  isSystem: false,
904
910
  org_user_ids,
905
911
  ...timezone ? { timezone } : {},
906
- ...locale ? { locale } : {}
912
+ ...locale ? { locale } : {},
913
+ ...currency ? { currency } : {},
914
+ // Internal: resolved kernel so the nav-serving path can probe
915
+ // requiresService capability gates (ADR-0057 D10). NOT an
916
+ // authorization input — never read by RLS/permission logic.
917
+ __kernel: kernel
907
918
  };
908
919
  } catch {
909
920
  return void 0;
@@ -922,8 +933,12 @@ var RestServer = class {
922
933
  * shallow copy with a filtered `navigation` tree otherwise — the original
923
934
  * is never mutated so cached metadata stays clean.
924
935
  */
925
- filterAppForUser(item, sysPerms) {
936
+ filterAppForUser(item, sysPerms, serviceGate) {
926
937
  if (!item || typeof item !== "object") return item;
938
+ if (isMetaEnvelope(item)) {
939
+ const body = this.filterAppForUser(item.item, sysPerms, serviceGate);
940
+ return body == null ? null : { ...item, item: body };
941
+ }
927
942
  if (item.hidden === true && !sysPerms.has("studio.access") && !sysPerms.has("setup.access")) {
928
943
  return null;
929
944
  }
@@ -931,6 +946,9 @@ var RestServer = class {
931
946
  if (reqApp.length > 0 && !reqApp.every((p) => sysPerms.has(p))) {
932
947
  return null;
933
948
  }
949
+ if (typeof item.requiresService === "string" && serviceGate && serviceGate(item.requiresService) === false) {
950
+ return null;
951
+ }
934
952
  const nav = Array.isArray(item.navigation) ? item.navigation : null;
935
953
  if (!nav) return item;
936
954
  const filterNav = (entries) => {
@@ -939,6 +957,7 @@ var RestServer = class {
939
957
  if (!e || typeof e !== "object") continue;
940
958
  const req = Array.isArray(e.requiredPermissions) ? e.requiredPermissions : [];
941
959
  if (req.length > 0 && !req.every((p) => sysPerms.has(p))) continue;
960
+ if (typeof e.requiresService === "string" && serviceGate && serviceGate(e.requiresService) === false) continue;
942
961
  if (Array.isArray(e.children) && e.children.length > 0) {
943
962
  const kids = filterNav(e.children);
944
963
  if (e.type === "group" && kids.length === 0) continue;
@@ -951,6 +970,53 @@ var RestServer = class {
951
970
  };
952
971
  return { ...item, navigation: filterNav(nav) };
953
972
  }
973
+ /**
974
+ * Probe which `requiresService` capability gates referenced anywhere in
975
+ * `items` are actually registered in the runtime kernel. Returns `null`
976
+ * when the kernel can't be probed — callers then SKIP service gating
977
+ * (fail-open, matching the prior "send everything, let the client hide"
978
+ * behaviour). ADR-0057 addendum D10.
979
+ */
980
+ async resolveRegisteredServices(kernel, items) {
981
+ let probe = null;
982
+ if (kernel && typeof kernel.getServiceAsync === "function") {
983
+ probe = async (name) => {
984
+ try {
985
+ return await kernel.getServiceAsync(name) != null;
986
+ } catch {
987
+ return false;
988
+ }
989
+ };
990
+ } else if (this.serviceExistsProvider) {
991
+ const exists = this.serviceExistsProvider;
992
+ probe = async (name) => {
993
+ try {
994
+ return exists(name) === true;
995
+ } catch {
996
+ return false;
997
+ }
998
+ };
999
+ }
1000
+ if (!probe) return null;
1001
+ const wanted = /* @__PURE__ */ new Set();
1002
+ const walk = (e) => {
1003
+ if (!e || typeof e !== "object") return;
1004
+ if (isMetaEnvelope(e)) {
1005
+ walk(e.item);
1006
+ return;
1007
+ }
1008
+ if (typeof e.requiresService === "string") wanted.add(e.requiresService);
1009
+ const kids = Array.isArray(e.navigation) ? e.navigation : Array.isArray(e.children) ? e.children : null;
1010
+ if (kids) for (const k of kids) walk(k);
1011
+ };
1012
+ for (const it of items) walk(it);
1013
+ if (wanted.size === 0) return /* @__PURE__ */ new Set();
1014
+ const registered = /* @__PURE__ */ new Set();
1015
+ for (const name of wanted) {
1016
+ if (await probe(name)) registered.add(name);
1017
+ }
1018
+ return registered;
1019
+ }
954
1020
  /**
955
1021
  * Build a `TranslationBundle` (`Record<locale, TranslationData>`) from an
956
1022
  * `II18nService` instance. Returns `undefined` when no locales are
@@ -1561,7 +1627,9 @@ var RestServer = class {
1561
1627
  const sysPerms = new Set(
1562
1628
  Array.isArray(ctx.systemPermissions) ? ctx.systemPermissions : []
1563
1629
  );
1564
- const filtered = list.map((it) => this.filterAppForUser(it, sysPerms)).filter((it) => it != null);
1630
+ const registered = await this.resolveRegisteredServices(ctx.__kernel, list);
1631
+ const serviceGate = registered ? (n) => registered.has(n) : void 0;
1632
+ const filtered = list.map((it) => this.filterAppForUser(it, sysPerms, serviceGate)).filter((it) => it != null);
1565
1633
  visible = Array.isArray(raw) ? filtered : { ...raw, items: filtered };
1566
1634
  }
1567
1635
  }
@@ -1761,7 +1829,9 @@ var RestServer = class {
1761
1829
  const sysPerms = new Set(
1762
1830
  Array.isArray(ctx.systemPermissions) ? ctx.systemPermissions : []
1763
1831
  );
1764
- visible = this.filterAppForUser(item, sysPerms);
1832
+ const registered = await this.resolveRegisteredServices(ctx.__kernel, [item]);
1833
+ const serviceGate = registered ? (n) => registered.has(n) : void 0;
1834
+ visible = this.filterAppForUser(item, sysPerms, serviceGate);
1765
1835
  if (visible == null) {
1766
1836
  res.status(404).json({
1767
1837
  error: "not_found",
@@ -4453,8 +4523,15 @@ function createRestApiPlugin(config = {}) {
4453
4523
  return;
4454
4524
  }
4455
4525
  ctx.logger.info("Hydrating REST API from Protocol...");
4526
+ const serviceExistsProvider = (name) => {
4527
+ try {
4528
+ return ctx.getService(name) != null;
4529
+ } catch {
4530
+ return false;
4531
+ }
4532
+ };
4456
4533
  try {
4457
- const restServer = new RestServer(server, protocol, config.api, kernelManager, envRegistry, defaultEnvironmentIdProvider, authServiceProvider, objectQLProvider, emailServiceProvider, sharingServiceProvider, reportsServiceProvider, approvalsServiceProvider, sharingRulesServiceProvider, i18nServiceProvider, analyticsServiceProvider, settingsServiceProvider);
4534
+ const restServer = new RestServer(server, protocol, config.api, kernelManager, envRegistry, defaultEnvironmentIdProvider, authServiceProvider, objectQLProvider, emailServiceProvider, sharingServiceProvider, reportsServiceProvider, approvalsServiceProvider, sharingRulesServiceProvider, i18nServiceProvider, analyticsServiceProvider, settingsServiceProvider, serviceExistsProvider);
4458
4535
  restServer.registerRoutes();
4459
4536
  ctx.logger.info("REST API successfully registered");
4460
4537
  if (!config.api?.requireAuth) {