@objectstack/rest 10.3.0 → 11.1.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/README.md +1 -1
- package/dist/index.cjs +114 -146
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +115 -147
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.d.cts
CHANGED
|
@@ -227,6 +227,19 @@ declare class RestServer {
|
|
|
227
227
|
*/
|
|
228
228
|
private readonly hostnameCache;
|
|
229
229
|
private readonly hostnameCacheTtlMs;
|
|
230
|
+
/**
|
|
231
|
+
* Request-scoped memoization for `resolveExecCtx`. A single HTTP request
|
|
232
|
+
* resolves the SAME execution context (identity + RBAC/RLS + localization)
|
|
233
|
+
* many times — the data operation itself, app-nav RBAC filtering, dashboard
|
|
234
|
+
* widget gating, the auth gate, etc. Each resolution is ~16 sequential
|
|
235
|
+
* queries (the `resolveAuthzContext` aggregation plus localization), so a
|
|
236
|
+
* request that resolves twice pays for duplicate authz and repeated
|
|
237
|
+
* localization. Keyed by the per-request `req` object (a `WeakMap`, so the
|
|
238
|
+
* entry is collected with the request — naturally request-scoped, no TTL,
|
|
239
|
+
* no cross-request leak) and the input `environmentId`. We cache the
|
|
240
|
+
* in-flight Promise so concurrent callers share one resolution.
|
|
241
|
+
*/
|
|
242
|
+
private readonly execCtxMemo;
|
|
230
243
|
private defaultEnvironmentIdProvider?;
|
|
231
244
|
private authServiceProvider?;
|
|
232
245
|
private objectQLProvider?;
|
|
@@ -330,6 +343,8 @@ declare class RestServer {
|
|
|
330
343
|
* to the protocol layer (the SecurityPlugin treats undefined as anon).
|
|
331
344
|
*/
|
|
332
345
|
private resolveExecCtx;
|
|
346
|
+
/** Heavy path behind `resolveExecCtx` — resolve identity + RBAC/RLS + localization. */
|
|
347
|
+
private computeExecCtx;
|
|
333
348
|
/**
|
|
334
349
|
* Filter an `App` metadata item by the current user's `systemPermissions`.
|
|
335
350
|
*
|
|
@@ -344,6 +359,19 @@ declare class RestServer {
|
|
|
344
359
|
* is never mutated so cached metadata stays clean.
|
|
345
360
|
*/
|
|
346
361
|
private filterAppForUser;
|
|
362
|
+
/**
|
|
363
|
+
* ADR-0057 D10 (dashboards): strip dashboard widgets whose `requiresService`
|
|
364
|
+
* capability gate names a kernel service that isn't registered — the same
|
|
365
|
+
* "server is the authoritative visibility gate" rule already applied to app
|
|
366
|
+
* nav entries (see {@link filterAppForUser}). Without this, a widget bound to
|
|
367
|
+
* an optional service renders a dead tile in deployments where the service is
|
|
368
|
+
* off (e.g. the Organizations KPI under multi-tenant `org-scoping`, which is
|
|
369
|
+
* absent in a single-tenant runtime while its nav entry is correctly hidden).
|
|
370
|
+
*
|
|
371
|
+
* Fail-open when the gate can't be probed (serviceGate undefined). Never
|
|
372
|
+
* mutates the original — returns a shallow copy only when a widget is dropped.
|
|
373
|
+
*/
|
|
374
|
+
private filterDashboardForUser;
|
|
347
375
|
/**
|
|
348
376
|
* Probe which `requiresService` capability gates referenced anywhere in
|
|
349
377
|
* `items` are actually registered in the runtime kernel. Returns `null`
|
package/dist/index.d.ts
CHANGED
|
@@ -227,6 +227,19 @@ declare class RestServer {
|
|
|
227
227
|
*/
|
|
228
228
|
private readonly hostnameCache;
|
|
229
229
|
private readonly hostnameCacheTtlMs;
|
|
230
|
+
/**
|
|
231
|
+
* Request-scoped memoization for `resolveExecCtx`. A single HTTP request
|
|
232
|
+
* resolves the SAME execution context (identity + RBAC/RLS + localization)
|
|
233
|
+
* many times — the data operation itself, app-nav RBAC filtering, dashboard
|
|
234
|
+
* widget gating, the auth gate, etc. Each resolution is ~16 sequential
|
|
235
|
+
* queries (the `resolveAuthzContext` aggregation plus localization), so a
|
|
236
|
+
* request that resolves twice pays for duplicate authz and repeated
|
|
237
|
+
* localization. Keyed by the per-request `req` object (a `WeakMap`, so the
|
|
238
|
+
* entry is collected with the request — naturally request-scoped, no TTL,
|
|
239
|
+
* no cross-request leak) and the input `environmentId`. We cache the
|
|
240
|
+
* in-flight Promise so concurrent callers share one resolution.
|
|
241
|
+
*/
|
|
242
|
+
private readonly execCtxMemo;
|
|
230
243
|
private defaultEnvironmentIdProvider?;
|
|
231
244
|
private authServiceProvider?;
|
|
232
245
|
private objectQLProvider?;
|
|
@@ -330,6 +343,8 @@ declare class RestServer {
|
|
|
330
343
|
* to the protocol layer (the SecurityPlugin treats undefined as anon).
|
|
331
344
|
*/
|
|
332
345
|
private resolveExecCtx;
|
|
346
|
+
/** Heavy path behind `resolveExecCtx` — resolve identity + RBAC/RLS + localization. */
|
|
347
|
+
private computeExecCtx;
|
|
333
348
|
/**
|
|
334
349
|
* Filter an `App` metadata item by the current user's `systemPermissions`.
|
|
335
350
|
*
|
|
@@ -344,6 +359,19 @@ declare class RestServer {
|
|
|
344
359
|
* is never mutated so cached metadata stays clean.
|
|
345
360
|
*/
|
|
346
361
|
private filterAppForUser;
|
|
362
|
+
/**
|
|
363
|
+
* ADR-0057 D10 (dashboards): strip dashboard widgets whose `requiresService`
|
|
364
|
+
* capability gate names a kernel service that isn't registered — the same
|
|
365
|
+
* "server is the authoritative visibility gate" rule already applied to app
|
|
366
|
+
* nav entries (see {@link filterAppForUser}). Without this, a widget bound to
|
|
367
|
+
* an optional service renders a dead tile in deployments where the service is
|
|
368
|
+
* off (e.g. the Organizations KPI under multi-tenant `org-scoping`, which is
|
|
369
|
+
* absent in a single-tenant runtime while its nav entry is correctly hidden).
|
|
370
|
+
*
|
|
371
|
+
* Fail-open when the gate can't be probed (serviceGate undefined). Never
|
|
372
|
+
* mutates the original — returns a shallow copy only when a widget is dropped.
|
|
373
|
+
*/
|
|
374
|
+
private filterDashboardForUser;
|
|
347
375
|
/**
|
|
348
376
|
* Probe which `requiresService` capability gates referenced anywhere in
|
|
349
377
|
* `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, isAuthGateAllowlisted } from "@objectstack/core";
|
|
3
3
|
|
|
4
4
|
// src/route-manager.ts
|
|
5
5
|
var RouteManager = class {
|
|
@@ -460,6 +460,19 @@ var RestServer = class {
|
|
|
460
460
|
*/
|
|
461
461
|
this.hostnameCache = /* @__PURE__ */ new Map();
|
|
462
462
|
this.hostnameCacheTtlMs = 3e4;
|
|
463
|
+
/**
|
|
464
|
+
* Request-scoped memoization for `resolveExecCtx`. A single HTTP request
|
|
465
|
+
* resolves the SAME execution context (identity + RBAC/RLS + localization)
|
|
466
|
+
* many times — the data operation itself, app-nav RBAC filtering, dashboard
|
|
467
|
+
* widget gating, the auth gate, etc. Each resolution is ~16 sequential
|
|
468
|
+
* queries (the `resolveAuthzContext` aggregation plus localization), so a
|
|
469
|
+
* request that resolves twice pays for duplicate authz and repeated
|
|
470
|
+
* localization. Keyed by the per-request `req` object (a `WeakMap`, so the
|
|
471
|
+
* entry is collected with the request — naturally request-scoped, no TTL,
|
|
472
|
+
* no cross-request leak) and the input `environmentId`. We cache the
|
|
473
|
+
* in-flight Promise so concurrent callers share one resolution.
|
|
474
|
+
*/
|
|
475
|
+
this.execCtxMemo = /* @__PURE__ */ new WeakMap();
|
|
463
476
|
/**
|
|
464
477
|
* Lazily load the OpenAPI spec JSON shipped by @objectstack/spec.
|
|
465
478
|
* Cached after first read. Resilient to missing files / parse errors
|
|
@@ -633,6 +646,11 @@ var RestServer = class {
|
|
|
633
646
|
* requests (they're internal-only), so they cannot bypass this gate.
|
|
634
647
|
*/
|
|
635
648
|
enforceAuth(req, res, context) {
|
|
649
|
+
const gate = context?.authGate;
|
|
650
|
+
if (gate && req?.method !== "OPTIONS" && !isAuthGateAllowlisted(req?.path)) {
|
|
651
|
+
res.status(403).json({ error: { code: gate.code, message: gate.message } });
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
636
654
|
if (!this.config.api.requireAuth) return false;
|
|
637
655
|
if (context?.userId) return false;
|
|
638
656
|
if (req?.method === "OPTIONS") return false;
|
|
@@ -703,6 +721,21 @@ var RestServer = class {
|
|
|
703
721
|
* to the protocol layer (the SecurityPlugin treats undefined as anon).
|
|
704
722
|
*/
|
|
705
723
|
async resolveExecCtx(environmentId, req) {
|
|
724
|
+
if (!req || typeof req !== "object") return this.computeExecCtx(environmentId, req);
|
|
725
|
+
const key = environmentId ?? "\0default";
|
|
726
|
+
let perReq = this.execCtxMemo.get(req);
|
|
727
|
+
if (!perReq) {
|
|
728
|
+
perReq = /* @__PURE__ */ new Map();
|
|
729
|
+
this.execCtxMemo.set(req, perReq);
|
|
730
|
+
}
|
|
731
|
+
const cached = perReq.get(key);
|
|
732
|
+
if (cached) return cached;
|
|
733
|
+
const pending = this.computeExecCtx(environmentId, req);
|
|
734
|
+
perReq.set(key, pending);
|
|
735
|
+
return pending;
|
|
736
|
+
}
|
|
737
|
+
/** Heavy path behind `resolveExecCtx` — resolve identity + RBAC/RLS + localization. */
|
|
738
|
+
async computeExecCtx(environmentId, req) {
|
|
706
739
|
try {
|
|
707
740
|
if (!environmentId && req && this.envRegistry && this.kernelManager) {
|
|
708
741
|
const host = this.extractHostname(req);
|
|
@@ -762,155 +795,45 @@ var RestServer = class {
|
|
|
762
795
|
} else {
|
|
763
796
|
return void 0;
|
|
764
797
|
}
|
|
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) {
|
|
798
|
+
const ql = kernel ? await kernel.getServiceAsync("objectql").catch(() => void 0) : this.objectQLProvider ? await this.objectQLProvider(environmentId).catch(() => void 0) : void 0;
|
|
799
|
+
const getSession = async (h) => {
|
|
856
800
|
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
|
-
}
|
|
801
|
+
return await api.getSession({ headers: h });
|
|
878
802
|
} catch {
|
|
803
|
+
return void 0;
|
|
879
804
|
}
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
805
|
+
};
|
|
806
|
+
const authz = await resolveAuthzContext({ ql, headers, getSession });
|
|
807
|
+
if (!authz.userId) return void 0;
|
|
808
|
+
const settings = this.settingsServiceProvider ? await this.settingsServiceProvider(environmentId).catch(() => void 0) : void 0;
|
|
809
|
+
const localization = await resolveLocalizationContext({
|
|
810
|
+
ql,
|
|
811
|
+
settings,
|
|
812
|
+
tenantId: authz.tenantId,
|
|
813
|
+
userId: authz.userId
|
|
814
|
+
});
|
|
815
|
+
let authGate;
|
|
884
816
|
try {
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
const [tzRes, localeRes, currencyRes] = await Promise.all([
|
|
889
|
-
settings.get("localization", "timezone", sctx).catch(() => void 0),
|
|
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();
|
|
817
|
+
if (typeof authService.isAuthGateActive === "function" && authService.isAuthGateActive()) {
|
|
818
|
+
const gatedSession = await getSession(headers).catch(() => void 0);
|
|
819
|
+
if (gatedSession?.user?.authGate) authGate = gatedSession.user.authGate;
|
|
899
820
|
}
|
|
900
821
|
} catch {
|
|
901
822
|
}
|
|
902
823
|
return {
|
|
903
|
-
userId,
|
|
904
|
-
tenantId,
|
|
905
|
-
email,
|
|
906
|
-
roles,
|
|
907
|
-
permissions,
|
|
908
|
-
systemPermissions,
|
|
824
|
+
userId: authz.userId,
|
|
825
|
+
tenantId: authz.tenantId,
|
|
826
|
+
email: authz.email,
|
|
827
|
+
roles: authz.roles,
|
|
828
|
+
permissions: authz.permissions,
|
|
829
|
+
systemPermissions: authz.systemPermissions,
|
|
830
|
+
...authz.tabPermissions ? { tabPermissions: authz.tabPermissions } : {},
|
|
909
831
|
isSystem: false,
|
|
910
|
-
org_user_ids,
|
|
911
|
-
...
|
|
912
|
-
...
|
|
913
|
-
...
|
|
832
|
+
org_user_ids: authz.org_user_ids,
|
|
833
|
+
...authGate ? { authGate } : {},
|
|
834
|
+
...localization.timezone ? { timezone: localization.timezone } : {},
|
|
835
|
+
...localization.locale ? { locale: localization.locale } : {},
|
|
836
|
+
...localization.currency ? { currency: localization.currency } : {},
|
|
914
837
|
// Internal: resolved kernel so the nav-serving path can probe
|
|
915
838
|
// requiresService capability gates (ADR-0057 D10). NOT an
|
|
916
839
|
// authorization input — never read by RLS/permission logic.
|
|
@@ -970,6 +893,30 @@ var RestServer = class {
|
|
|
970
893
|
};
|
|
971
894
|
return { ...item, navigation: filterNav(nav) };
|
|
972
895
|
}
|
|
896
|
+
/**
|
|
897
|
+
* ADR-0057 D10 (dashboards): strip dashboard widgets whose `requiresService`
|
|
898
|
+
* capability gate names a kernel service that isn't registered — the same
|
|
899
|
+
* "server is the authoritative visibility gate" rule already applied to app
|
|
900
|
+
* nav entries (see {@link filterAppForUser}). Without this, a widget bound to
|
|
901
|
+
* an optional service renders a dead tile in deployments where the service is
|
|
902
|
+
* off (e.g. the Organizations KPI under multi-tenant `org-scoping`, which is
|
|
903
|
+
* absent in a single-tenant runtime while its nav entry is correctly hidden).
|
|
904
|
+
*
|
|
905
|
+
* Fail-open when the gate can't be probed (serviceGate undefined). Never
|
|
906
|
+
* mutates the original — returns a shallow copy only when a widget is dropped.
|
|
907
|
+
*/
|
|
908
|
+
filterDashboardForUser(item, serviceGate) {
|
|
909
|
+
if (!item || typeof item !== "object" || !serviceGate) return item;
|
|
910
|
+
if (isMetaEnvelope(item)) {
|
|
911
|
+
const body = this.filterDashboardForUser(item.item, serviceGate);
|
|
912
|
+
return body === item.item ? item : { ...item, item: body };
|
|
913
|
+
}
|
|
914
|
+
if (!Array.isArray(item.widgets)) return item;
|
|
915
|
+
const widgets = item.widgets.filter(
|
|
916
|
+
(w) => !(w && typeof w.requiresService === "string" && serviceGate(w.requiresService) === false)
|
|
917
|
+
);
|
|
918
|
+
return widgets.length === item.widgets.length ? item : { ...item, widgets };
|
|
919
|
+
}
|
|
973
920
|
/**
|
|
974
921
|
* Probe which `requiresService` capability gates referenced anywhere in
|
|
975
922
|
* `items` are actually registered in the runtime kernel. Returns `null`
|
|
@@ -1006,7 +953,7 @@ var RestServer = class {
|
|
|
1006
953
|
return;
|
|
1007
954
|
}
|
|
1008
955
|
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;
|
|
956
|
+
const kids = Array.isArray(e.navigation) ? e.navigation : Array.isArray(e.children) ? e.children : Array.isArray(e.widgets) ? e.widgets : null;
|
|
1010
957
|
if (kids) for (const k of kids) walk(k);
|
|
1011
958
|
};
|
|
1012
959
|
for (const it of items) walk(it);
|
|
@@ -1294,10 +1241,10 @@ var RestServer = class {
|
|
|
1294
1241
|
this.registerReportsEndpoints(bp);
|
|
1295
1242
|
this.registerApprovalsEndpoints(bp);
|
|
1296
1243
|
this.registerAnalyticsEndpoints(bp);
|
|
1244
|
+
this.registerDataActionEndpoints(bp);
|
|
1297
1245
|
if (this.config.api.enableCrud) {
|
|
1298
1246
|
this.registerCrudEndpoints(bp);
|
|
1299
1247
|
}
|
|
1300
|
-
this.registerDataActionEndpoints(bp);
|
|
1301
1248
|
if (this.config.api.enableBatch) {
|
|
1302
1249
|
this.registerBatchEndpoints(bp);
|
|
1303
1250
|
}
|
|
@@ -1634,6 +1581,19 @@ var RestServer = class {
|
|
|
1634
1581
|
}
|
|
1635
1582
|
}
|
|
1636
1583
|
}
|
|
1584
|
+
if (req.params.type === "dashboard") {
|
|
1585
|
+
const raw = visible;
|
|
1586
|
+
const list = Array.isArray(raw) ? raw : raw && typeof raw === "object" && Array.isArray(raw.items) ? raw.items : null;
|
|
1587
|
+
if (list) {
|
|
1588
|
+
const ctx = await this.resolveExecCtx(environmentId, req).catch(() => void 0);
|
|
1589
|
+
const registered = await this.resolveRegisteredServices(ctx?.__kernel, list);
|
|
1590
|
+
const serviceGate = registered ? (n) => registered.has(n) : void 0;
|
|
1591
|
+
if (serviceGate) {
|
|
1592
|
+
const filtered = list.map((it) => this.filterDashboardForUser(it, serviceGate));
|
|
1593
|
+
visible = Array.isArray(raw) ? filtered : { ...raw, items: filtered };
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1637
1597
|
if (req.params.type === "view" && req.query?.object) {
|
|
1638
1598
|
const obj = String(req.query.object);
|
|
1639
1599
|
const raw = visible;
|
|
@@ -1806,9 +1766,11 @@ var RestServer = class {
|
|
|
1806
1766
|
res.header("Last-Modified", new Date(result.lastModified).toUTCString());
|
|
1807
1767
|
}
|
|
1808
1768
|
if (result.cacheControl) {
|
|
1809
|
-
const
|
|
1810
|
-
|
|
1811
|
-
|
|
1769
|
+
const parts = result.cacheControl.directives.filter((d) => d !== "max-age");
|
|
1770
|
+
if (result.cacheControl.maxAge != null) {
|
|
1771
|
+
parts.push(`max-age=${result.cacheControl.maxAge}`);
|
|
1772
|
+
}
|
|
1773
|
+
res.header("Cache-Control", parts.join(", "));
|
|
1812
1774
|
}
|
|
1813
1775
|
res.header("Vary", "Accept-Language");
|
|
1814
1776
|
res.json(await this.translateMetaItem(req, req.params.type, environmentId, result.data, cacheI18n));
|
|
@@ -1841,6 +1803,12 @@ var RestServer = class {
|
|
|
1841
1803
|
}
|
|
1842
1804
|
}
|
|
1843
1805
|
}
|
|
1806
|
+
if (req.params.type === "dashboard" && visible) {
|
|
1807
|
+
const ctx = await this.resolveExecCtx(environmentId, req).catch(() => void 0);
|
|
1808
|
+
const registered = await this.resolveRegisteredServices(ctx?.__kernel, [visible]);
|
|
1809
|
+
const serviceGate = registered ? (n) => registered.has(n) : void 0;
|
|
1810
|
+
if (serviceGate) visible = this.filterDashboardForUser(visible, serviceGate);
|
|
1811
|
+
}
|
|
1844
1812
|
if (req.params.type === "doc" && visible) {
|
|
1845
1813
|
const locale = this.extractLocale(req);
|
|
1846
1814
|
const { resolveDocLocale } = await import("@objectstack/spec/system");
|
|
@@ -2969,7 +2937,7 @@ var RestServer = class {
|
|
|
2969
2937
|
const allow = (name, cfg) => {
|
|
2970
2938
|
const def = objectSchema?.fields?.[name];
|
|
2971
2939
|
const t = def?.type;
|
|
2972
|
-
if (t !== "lookup" && t !== "master_detail") return true;
|
|
2940
|
+
if (t !== "lookup" && t !== "master_detail" && t !== "user") return true;
|
|
2973
2941
|
return !!cfg?.publicPicker;
|
|
2974
2942
|
};
|
|
2975
2943
|
const sections = match.form.sections.map((sec) => {
|