@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.cjs +85 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +85 -8
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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) {
|