@objectstack/rest 11.0.0 → 11.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/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  ## Overview
9
9
 
10
- `@objectstack/rest` is registered as a kernel plugin and emits HTTP routes for every object defined in the protocol metadata. Framework adapters (`@objectstack/express`, `hono`, `fastify`, `nextjs`, `nuxt`, `nestjs`, `sveltekit`) invoke these routes through `HttpDispatcher` from `@objectstack/runtime`.
10
+ `@objectstack/rest` is registered as a kernel plugin and emits HTTP routes for every object defined in the protocol metadata. The Hono framework adapter (`@objectstack/hono`) invokes these routes through `HttpDispatcher` from `@objectstack/runtime`.
11
11
 
12
12
  ## Installation
13
13
 
package/dist/index.cjs CHANGED
@@ -500,6 +500,19 @@ var RestServer = class {
500
500
  */
501
501
  this.hostnameCache = /* @__PURE__ */ new Map();
502
502
  this.hostnameCacheTtlMs = 3e4;
503
+ /**
504
+ * Request-scoped memoization for `resolveExecCtx`. A single HTTP request
505
+ * resolves the SAME execution context (identity + RBAC/RLS + localization)
506
+ * many times — the data operation itself, app-nav RBAC filtering, dashboard
507
+ * widget gating, the auth gate, etc. Each resolution is ~16 sequential
508
+ * queries (the `resolveAuthzContext` aggregation plus localization), so a
509
+ * request that resolves twice pays for duplicate authz and repeated
510
+ * localization. Keyed by the per-request `req` object (a `WeakMap`, so the
511
+ * entry is collected with the request — naturally request-scoped, no TTL,
512
+ * no cross-request leak) and the input `environmentId`. We cache the
513
+ * in-flight Promise so concurrent callers share one resolution.
514
+ */
515
+ this.execCtxMemo = /* @__PURE__ */ new WeakMap();
503
516
  /**
504
517
  * Lazily load the OpenAPI spec JSON shipped by @objectstack/spec.
505
518
  * Cached after first read. Resilient to missing files / parse errors
@@ -673,6 +686,11 @@ var RestServer = class {
673
686
  * requests (they're internal-only), so they cannot bypass this gate.
674
687
  */
675
688
  enforceAuth(req, res, context) {
689
+ const gate = context?.authGate;
690
+ if (gate && req?.method !== "OPTIONS" && !(0, import_core.isAuthGateAllowlisted)(req?.path)) {
691
+ res.status(403).json({ error: { code: gate.code, message: gate.message } });
692
+ return true;
693
+ }
676
694
  if (!this.config.api.requireAuth) return false;
677
695
  if (context?.userId) return false;
678
696
  if (req?.method === "OPTIONS") return false;
@@ -743,6 +761,21 @@ var RestServer = class {
743
761
  * to the protocol layer (the SecurityPlugin treats undefined as anon).
744
762
  */
745
763
  async resolveExecCtx(environmentId, req) {
764
+ if (!req || typeof req !== "object") return this.computeExecCtx(environmentId, req);
765
+ const key = environmentId ?? "\0default";
766
+ let perReq = this.execCtxMemo.get(req);
767
+ if (!perReq) {
768
+ perReq = /* @__PURE__ */ new Map();
769
+ this.execCtxMemo.set(req, perReq);
770
+ }
771
+ const cached = perReq.get(key);
772
+ if (cached) return cached;
773
+ const pending = this.computeExecCtx(environmentId, req);
774
+ perReq.set(key, pending);
775
+ return pending;
776
+ }
777
+ /** Heavy path behind `resolveExecCtx` — resolve identity + RBAC/RLS + localization. */
778
+ async computeExecCtx(environmentId, req) {
746
779
  try {
747
780
  if (!environmentId && req && this.envRegistry && this.kernelManager) {
748
781
  const host = this.extractHostname(req);
@@ -819,6 +852,14 @@ var RestServer = class {
819
852
  tenantId: authz.tenantId,
820
853
  userId: authz.userId
821
854
  });
855
+ let authGate;
856
+ try {
857
+ if (typeof authService.isAuthGateActive === "function" && authService.isAuthGateActive()) {
858
+ const gatedSession = await getSession(headers).catch(() => void 0);
859
+ if (gatedSession?.user?.authGate) authGate = gatedSession.user.authGate;
860
+ }
861
+ } catch {
862
+ }
822
863
  return {
823
864
  userId: authz.userId,
824
865
  tenantId: authz.tenantId,
@@ -829,6 +870,7 @@ var RestServer = class {
829
870
  ...authz.tabPermissions ? { tabPermissions: authz.tabPermissions } : {},
830
871
  isSystem: false,
831
872
  org_user_ids: authz.org_user_ids,
873
+ ...authGate ? { authGate } : {},
832
874
  ...localization.timezone ? { timezone: localization.timezone } : {},
833
875
  ...localization.locale ? { locale: localization.locale } : {},
834
876
  ...localization.currency ? { currency: localization.currency } : {},