@objectstack/rest 10.3.0 → 11.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/LICENSE +202 -93
- package/dist/index.cjs +76 -150
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +77 -151
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.d.cts
CHANGED
|
@@ -344,6 +344,19 @@ declare class RestServer {
|
|
|
344
344
|
* is never mutated so cached metadata stays clean.
|
|
345
345
|
*/
|
|
346
346
|
private filterAppForUser;
|
|
347
|
+
/**
|
|
348
|
+
* ADR-0057 D10 (dashboards): strip dashboard widgets whose `requiresService`
|
|
349
|
+
* capability gate names a kernel service that isn't registered — the same
|
|
350
|
+
* "server is the authoritative visibility gate" rule already applied to app
|
|
351
|
+
* nav entries (see {@link filterAppForUser}). Without this, a widget bound to
|
|
352
|
+
* an optional service renders a dead tile in deployments where the service is
|
|
353
|
+
* off (e.g. the Organizations KPI under multi-tenant `org-scoping`, which is
|
|
354
|
+
* absent in a single-tenant runtime while its nav entry is correctly hidden).
|
|
355
|
+
*
|
|
356
|
+
* Fail-open when the gate can't be probed (serviceGate undefined). Never
|
|
357
|
+
* mutates the original — returns a shallow copy only when a widget is dropped.
|
|
358
|
+
*/
|
|
359
|
+
private filterDashboardForUser;
|
|
347
360
|
/**
|
|
348
361
|
* Probe which `requiresService` capability gates referenced anywhere in
|
|
349
362
|
* `items` are actually registered in the runtime kernel. Returns `null`
|
package/dist/index.d.ts
CHANGED
|
@@ -344,6 +344,19 @@ declare class RestServer {
|
|
|
344
344
|
* is never mutated so cached metadata stays clean.
|
|
345
345
|
*/
|
|
346
346
|
private filterAppForUser;
|
|
347
|
+
/**
|
|
348
|
+
* ADR-0057 D10 (dashboards): strip dashboard widgets whose `requiresService`
|
|
349
|
+
* capability gate names a kernel service that isn't registered — the same
|
|
350
|
+
* "server is the authoritative visibility gate" rule already applied to app
|
|
351
|
+
* nav entries (see {@link filterAppForUser}). Without this, a widget bound to
|
|
352
|
+
* an optional service renders a dead tile in deployments where the service is
|
|
353
|
+
* off (e.g. the Organizations KPI under multi-tenant `org-scoping`, which is
|
|
354
|
+
* absent in a single-tenant runtime while its nav entry is correctly hidden).
|
|
355
|
+
*
|
|
356
|
+
* Fail-open when the gate can't be probed (serviceGate undefined). Never
|
|
357
|
+
* mutates the original — returns a shallow copy only when a widget is dropped.
|
|
358
|
+
*/
|
|
359
|
+
private filterDashboardForUser;
|
|
347
360
|
/**
|
|
348
361
|
* Probe which `requiresService` capability gates referenced anywhere in
|
|
349
362
|
* `items` are actually registered in the runtime kernel. Returns `null`
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/rest-server.ts
|
|
2
|
-
import {
|
|
2
|
+
import { resolveAuthzContext, resolveLocalizationContext } from "@objectstack/core";
|
|
3
3
|
|
|
4
4
|
// src/route-manager.ts
|
|
5
5
|
var RouteManager = class {
|
|
@@ -762,155 +762,36 @@ var RestServer = class {
|
|
|
762
762
|
} else {
|
|
763
763
|
return void 0;
|
|
764
764
|
}
|
|
765
|
-
const
|
|
766
|
-
const
|
|
767
|
-
const roles = [];
|
|
768
|
-
let identityQl;
|
|
769
|
-
if (kernel) identityQl = await kernel.getServiceAsync("objectql").catch(() => void 0);
|
|
770
|
-
if (!identityQl && this.objectQLProvider) {
|
|
771
|
-
identityQl = await this.objectQLProvider(environmentId).catch(() => void 0);
|
|
772
|
-
}
|
|
773
|
-
let userId;
|
|
774
|
-
let tenantId;
|
|
775
|
-
let email;
|
|
776
|
-
const keyPrincipal = await resolveApiKeyPrincipal(identityQl, headers).catch(() => void 0);
|
|
777
|
-
if (keyPrincipal) {
|
|
778
|
-
userId = keyPrincipal.userId;
|
|
779
|
-
tenantId = keyPrincipal.tenantId;
|
|
780
|
-
for (const s of keyPrincipal.scopes) if (!permissions.includes(s)) permissions.push(s);
|
|
781
|
-
} else {
|
|
782
|
-
const session = await api.getSession({ headers });
|
|
783
|
-
if (!session?.user?.id) return void 0;
|
|
784
|
-
userId = session.user.id;
|
|
785
|
-
tenantId = session.session?.activeOrganizationId ?? void 0;
|
|
786
|
-
if (session.user?.email) email = String(session.user.email);
|
|
787
|
-
}
|
|
788
|
-
if (!email && identityQl && typeof identityQl.find === "function") {
|
|
789
|
-
const urows = await identityQl.find("sys_user", { where: { id: userId }, limit: 1, context: { isSystem: true } }).catch(() => []);
|
|
790
|
-
if (urows?.[0]?.email) email = String(urows[0].email);
|
|
791
|
-
}
|
|
792
|
-
try {
|
|
793
|
-
let ql;
|
|
794
|
-
if (kernel) {
|
|
795
|
-
ql = await kernel.getServiceAsync("objectql").catch(() => void 0);
|
|
796
|
-
}
|
|
797
|
-
if (!ql && this.objectQLProvider) {
|
|
798
|
-
ql = await this.objectQLProvider(environmentId).catch(() => void 0);
|
|
799
|
-
}
|
|
800
|
-
if (ql && typeof ql.find === "function") {
|
|
801
|
-
const sysOpts = { context: { isSystem: true } };
|
|
802
|
-
const memberRows = await ql.find("sys_member", {
|
|
803
|
-
where: tenantId ? { user_id: userId, organization_id: tenantId } : { user_id: userId },
|
|
804
|
-
limit: 50,
|
|
805
|
-
...sysOpts
|
|
806
|
-
}).catch(() => []);
|
|
807
|
-
for (const m of memberRows ?? []) {
|
|
808
|
-
if (typeof m.role === "string") {
|
|
809
|
-
for (const r of m.role.split(",").map((s) => s.trim()).filter(Boolean)) {
|
|
810
|
-
if (!roles.includes(r)) roles.push(r);
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
const upsRows = await ql.find("sys_user_permission_set", {
|
|
815
|
-
where: { user_id: userId },
|
|
816
|
-
limit: 100,
|
|
817
|
-
...sysOpts
|
|
818
|
-
}).catch(() => []);
|
|
819
|
-
const psIds = /* @__PURE__ */ new Set();
|
|
820
|
-
for (const r of upsRows ?? []) {
|
|
821
|
-
const orgScope = r.organization_id ?? null;
|
|
822
|
-
if (!orgScope || tenantId && orgScope === tenantId) {
|
|
823
|
-
const pid = r.permission_set_id ?? r.permissionSetId;
|
|
824
|
-
if (pid) psIds.add(pid);
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
if (psIds.size > 0) {
|
|
828
|
-
const psRows = await ql.find("sys_permission_set", {
|
|
829
|
-
where: { id: { $in: Array.from(psIds) } },
|
|
830
|
-
limit: 500,
|
|
831
|
-
...sysOpts
|
|
832
|
-
}).catch(() => []);
|
|
833
|
-
for (const ps of psRows ?? []) {
|
|
834
|
-
if (ps.name && !permissions.includes(ps.name)) permissions.push(ps.name);
|
|
835
|
-
const rawSys = typeof ps.system_permissions === "string" ? (() => {
|
|
836
|
-
try {
|
|
837
|
-
return JSON.parse(ps.system_permissions);
|
|
838
|
-
} catch {
|
|
839
|
-
return [];
|
|
840
|
-
}
|
|
841
|
-
})() : ps.system_permissions ?? ps.systemPermissions;
|
|
842
|
-
if (Array.isArray(rawSys)) {
|
|
843
|
-
for (const sp of rawSys) {
|
|
844
|
-
if (typeof sp === "string" && !systemPermissions.includes(sp)) {
|
|
845
|
-
systemPermissions.push(sp);
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
} catch {
|
|
853
|
-
}
|
|
854
|
-
let org_user_ids = [userId];
|
|
855
|
-
if (tenantId) {
|
|
765
|
+
const ql = kernel ? await kernel.getServiceAsync("objectql").catch(() => void 0) : this.objectQLProvider ? await this.objectQLProvider(environmentId).catch(() => void 0) : void 0;
|
|
766
|
+
const getSession = async (h) => {
|
|
856
767
|
try {
|
|
857
|
-
|
|
858
|
-
if (kernel) {
|
|
859
|
-
ql = await kernel.getServiceAsync("objectql").catch(() => void 0);
|
|
860
|
-
}
|
|
861
|
-
if (!ql && this.objectQLProvider) {
|
|
862
|
-
ql = await this.objectQLProvider(environmentId).catch(() => void 0);
|
|
863
|
-
}
|
|
864
|
-
if (ql && typeof ql.find === "function") {
|
|
865
|
-
const sysOpts = { context: { isSystem: true } };
|
|
866
|
-
const memberRows = await ql.find("sys_member", {
|
|
867
|
-
where: { organization_id: tenantId },
|
|
868
|
-
limit: 1e3,
|
|
869
|
-
...sysOpts
|
|
870
|
-
}).catch(() => []);
|
|
871
|
-
const ids = /* @__PURE__ */ new Set([userId]);
|
|
872
|
-
for (const m of memberRows ?? []) {
|
|
873
|
-
const uid = m.user_id ?? m.userId;
|
|
874
|
-
if (typeof uid === "string" && uid.length > 0) ids.add(uid);
|
|
875
|
-
}
|
|
876
|
-
org_user_ids = Array.from(ids);
|
|
877
|
-
}
|
|
768
|
+
return await api.getSession({ headers: h });
|
|
878
769
|
} catch {
|
|
770
|
+
return void 0;
|
|
879
771
|
}
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
settings.get("localization", "locale", sctx).catch(() => void 0),
|
|
891
|
-
settings.get("localization", "currency", sctx).catch(() => void 0)
|
|
892
|
-
]);
|
|
893
|
-
const tzVal = tzRes?.value;
|
|
894
|
-
const localeVal = localeRes?.value;
|
|
895
|
-
const currencyVal = currencyRes?.value;
|
|
896
|
-
if (typeof tzVal === "string" && tzVal.trim()) timezone = tzVal.trim();
|
|
897
|
-
if (typeof localeVal === "string" && localeVal.trim()) locale = localeVal.trim();
|
|
898
|
-
if (typeof currencyVal === "string" && currencyVal.trim()) currency = currencyVal.trim().toUpperCase();
|
|
899
|
-
}
|
|
900
|
-
} catch {
|
|
901
|
-
}
|
|
772
|
+
};
|
|
773
|
+
const authz = await resolveAuthzContext({ ql, headers, getSession });
|
|
774
|
+
if (!authz.userId) return void 0;
|
|
775
|
+
const settings = this.settingsServiceProvider ? await this.settingsServiceProvider(environmentId).catch(() => void 0) : void 0;
|
|
776
|
+
const localization = await resolveLocalizationContext({
|
|
777
|
+
ql,
|
|
778
|
+
settings,
|
|
779
|
+
tenantId: authz.tenantId,
|
|
780
|
+
userId: authz.userId
|
|
781
|
+
});
|
|
902
782
|
return {
|
|
903
|
-
userId,
|
|
904
|
-
tenantId,
|
|
905
|
-
email,
|
|
906
|
-
roles,
|
|
907
|
-
permissions,
|
|
908
|
-
systemPermissions,
|
|
783
|
+
userId: authz.userId,
|
|
784
|
+
tenantId: authz.tenantId,
|
|
785
|
+
email: authz.email,
|
|
786
|
+
roles: authz.roles,
|
|
787
|
+
permissions: authz.permissions,
|
|
788
|
+
systemPermissions: authz.systemPermissions,
|
|
789
|
+
...authz.tabPermissions ? { tabPermissions: authz.tabPermissions } : {},
|
|
909
790
|
isSystem: false,
|
|
910
|
-
org_user_ids,
|
|
911
|
-
...timezone ? { timezone } : {},
|
|
912
|
-
...locale ? { locale } : {},
|
|
913
|
-
...currency ? { currency } : {},
|
|
791
|
+
org_user_ids: authz.org_user_ids,
|
|
792
|
+
...localization.timezone ? { timezone: localization.timezone } : {},
|
|
793
|
+
...localization.locale ? { locale: localization.locale } : {},
|
|
794
|
+
...localization.currency ? { currency: localization.currency } : {},
|
|
914
795
|
// Internal: resolved kernel so the nav-serving path can probe
|
|
915
796
|
// requiresService capability gates (ADR-0057 D10). NOT an
|
|
916
797
|
// authorization input — never read by RLS/permission logic.
|
|
@@ -970,6 +851,30 @@ var RestServer = class {
|
|
|
970
851
|
};
|
|
971
852
|
return { ...item, navigation: filterNav(nav) };
|
|
972
853
|
}
|
|
854
|
+
/**
|
|
855
|
+
* ADR-0057 D10 (dashboards): strip dashboard widgets whose `requiresService`
|
|
856
|
+
* capability gate names a kernel service that isn't registered — the same
|
|
857
|
+
* "server is the authoritative visibility gate" rule already applied to app
|
|
858
|
+
* nav entries (see {@link filterAppForUser}). Without this, a widget bound to
|
|
859
|
+
* an optional service renders a dead tile in deployments where the service is
|
|
860
|
+
* off (e.g. the Organizations KPI under multi-tenant `org-scoping`, which is
|
|
861
|
+
* absent in a single-tenant runtime while its nav entry is correctly hidden).
|
|
862
|
+
*
|
|
863
|
+
* Fail-open when the gate can't be probed (serviceGate undefined). Never
|
|
864
|
+
* mutates the original — returns a shallow copy only when a widget is dropped.
|
|
865
|
+
*/
|
|
866
|
+
filterDashboardForUser(item, serviceGate) {
|
|
867
|
+
if (!item || typeof item !== "object" || !serviceGate) return item;
|
|
868
|
+
if (isMetaEnvelope(item)) {
|
|
869
|
+
const body = this.filterDashboardForUser(item.item, serviceGate);
|
|
870
|
+
return body === item.item ? item : { ...item, item: body };
|
|
871
|
+
}
|
|
872
|
+
if (!Array.isArray(item.widgets)) return item;
|
|
873
|
+
const widgets = item.widgets.filter(
|
|
874
|
+
(w) => !(w && typeof w.requiresService === "string" && serviceGate(w.requiresService) === false)
|
|
875
|
+
);
|
|
876
|
+
return widgets.length === item.widgets.length ? item : { ...item, widgets };
|
|
877
|
+
}
|
|
973
878
|
/**
|
|
974
879
|
* Probe which `requiresService` capability gates referenced anywhere in
|
|
975
880
|
* `items` are actually registered in the runtime kernel. Returns `null`
|
|
@@ -1006,7 +911,7 @@ var RestServer = class {
|
|
|
1006
911
|
return;
|
|
1007
912
|
}
|
|
1008
913
|
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;
|
|
914
|
+
const kids = Array.isArray(e.navigation) ? e.navigation : Array.isArray(e.children) ? e.children : Array.isArray(e.widgets) ? e.widgets : null;
|
|
1010
915
|
if (kids) for (const k of kids) walk(k);
|
|
1011
916
|
};
|
|
1012
917
|
for (const it of items) walk(it);
|
|
@@ -1294,10 +1199,10 @@ var RestServer = class {
|
|
|
1294
1199
|
this.registerReportsEndpoints(bp);
|
|
1295
1200
|
this.registerApprovalsEndpoints(bp);
|
|
1296
1201
|
this.registerAnalyticsEndpoints(bp);
|
|
1202
|
+
this.registerDataActionEndpoints(bp);
|
|
1297
1203
|
if (this.config.api.enableCrud) {
|
|
1298
1204
|
this.registerCrudEndpoints(bp);
|
|
1299
1205
|
}
|
|
1300
|
-
this.registerDataActionEndpoints(bp);
|
|
1301
1206
|
if (this.config.api.enableBatch) {
|
|
1302
1207
|
this.registerBatchEndpoints(bp);
|
|
1303
1208
|
}
|
|
@@ -1634,6 +1539,19 @@ var RestServer = class {
|
|
|
1634
1539
|
}
|
|
1635
1540
|
}
|
|
1636
1541
|
}
|
|
1542
|
+
if (req.params.type === "dashboard") {
|
|
1543
|
+
const raw = visible;
|
|
1544
|
+
const list = Array.isArray(raw) ? raw : raw && typeof raw === "object" && Array.isArray(raw.items) ? raw.items : null;
|
|
1545
|
+
if (list) {
|
|
1546
|
+
const ctx = await this.resolveExecCtx(environmentId, req).catch(() => void 0);
|
|
1547
|
+
const registered = await this.resolveRegisteredServices(ctx?.__kernel, list);
|
|
1548
|
+
const serviceGate = registered ? (n) => registered.has(n) : void 0;
|
|
1549
|
+
if (serviceGate) {
|
|
1550
|
+
const filtered = list.map((it) => this.filterDashboardForUser(it, serviceGate));
|
|
1551
|
+
visible = Array.isArray(raw) ? filtered : { ...raw, items: filtered };
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1637
1555
|
if (req.params.type === "view" && req.query?.object) {
|
|
1638
1556
|
const obj = String(req.query.object);
|
|
1639
1557
|
const raw = visible;
|
|
@@ -1806,9 +1724,11 @@ var RestServer = class {
|
|
|
1806
1724
|
res.header("Last-Modified", new Date(result.lastModified).toUTCString());
|
|
1807
1725
|
}
|
|
1808
1726
|
if (result.cacheControl) {
|
|
1809
|
-
const
|
|
1810
|
-
|
|
1811
|
-
|
|
1727
|
+
const parts = result.cacheControl.directives.filter((d) => d !== "max-age");
|
|
1728
|
+
if (result.cacheControl.maxAge != null) {
|
|
1729
|
+
parts.push(`max-age=${result.cacheControl.maxAge}`);
|
|
1730
|
+
}
|
|
1731
|
+
res.header("Cache-Control", parts.join(", "));
|
|
1812
1732
|
}
|
|
1813
1733
|
res.header("Vary", "Accept-Language");
|
|
1814
1734
|
res.json(await this.translateMetaItem(req, req.params.type, environmentId, result.data, cacheI18n));
|
|
@@ -1841,6 +1761,12 @@ var RestServer = class {
|
|
|
1841
1761
|
}
|
|
1842
1762
|
}
|
|
1843
1763
|
}
|
|
1764
|
+
if (req.params.type === "dashboard" && visible) {
|
|
1765
|
+
const ctx = await this.resolveExecCtx(environmentId, req).catch(() => void 0);
|
|
1766
|
+
const registered = await this.resolveRegisteredServices(ctx?.__kernel, [visible]);
|
|
1767
|
+
const serviceGate = registered ? (n) => registered.has(n) : void 0;
|
|
1768
|
+
if (serviceGate) visible = this.filterDashboardForUser(visible, serviceGate);
|
|
1769
|
+
}
|
|
1844
1770
|
if (req.params.type === "doc" && visible) {
|
|
1845
1771
|
const locale = this.extractLocale(req);
|
|
1846
1772
|
const { resolveDocLocale } = await import("@objectstack/spec/system");
|
|
@@ -2969,7 +2895,7 @@ var RestServer = class {
|
|
|
2969
2895
|
const allow = (name, cfg) => {
|
|
2970
2896
|
const def = objectSchema?.fields?.[name];
|
|
2971
2897
|
const t = def?.type;
|
|
2972
|
-
if (t !== "lookup" && t !== "master_detail") return true;
|
|
2898
|
+
if (t !== "lookup" && t !== "master_detail" && t !== "user") return true;
|
|
2973
2899
|
return !!cfg?.publicPicker;
|
|
2974
2900
|
};
|
|
2975
2901
|
const sections = match.form.sections.map((sec) => {
|