@aooth/arbac-moost 0.1.72 → 0.1.74

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.
@@ -1,4 +1,4 @@
1
- import { r as AoothArbacClaims, t as ArbacUserProvider } from "../user.provider-NMSzynGK.mjs";
1
+ import { r as AoothArbacClaims, t as ArbacUserProvider } from "../user.provider-CKjmUhkx.mjs";
2
2
  import { TAtscriptAnnotatedType, TAtscriptTypeObject, TMetadataMap, TValidatorOptions, Validator } from "@atscript/typescript/utils";
3
3
  import { AoothUserCredentials } from "@aooth/user/atscript-db/model.as";
4
4
  //#region src/atscript/auto-provider.d.ts
@@ -36,10 +36,8 @@ interface ArbacUserTable<T extends object> {
36
36
  * `@Injectable()` (SINGLETON) — moost@0.6.x's `Injectable` metadata is NOT
37
37
  * inherited from the base `ArbacUserProvider` by infact, so the decorator
38
38
  * must be re-applied here. SINGLETON is the right scope: there is no
39
- * per-instance state worth scoping per event, and FOR_EVENT trips a
40
- * scope-id mismatch when the interceptor resolves the provider in a
41
- * multi-adapter app (HTTP + WF). Per-event memoization is provided via
42
- * a wooks-slot cache keyed by `this` — see `useFetchCache` above.
39
+ * per-instance state worth scoping per event. Per-event memoization is
40
+ * provided via a wooks-slot cache keyed by `this` — see `useFetchCache` above.
43
41
  *
44
42
  * Consumers extend this class, implement `getUserId()` (typically reading
45
43
  * the JWT subject from the auth composable), inject their atscript-db
package/dist/index.d.mts CHANGED
@@ -1,10 +1,10 @@
1
- import { _ as ProjectionOf, a as conjoinArbacDbScopes, b as TArbacMeta, c as ArbacDbScope, d as extractUsedControlValues, f as applyAllowedFieldsAndSet, g as OwnFieldKey, h as NavTarget, i as ConjoinArbacDbScopesOptions, l as AsArbacDbController, m as NavRelationKey, n as ArbacUserProviderToken, o as RefForeignKey, p as ControlsOf, r as AoothArbacClaims, s as RefTableSource, t as ArbacUserProvider, u as enforceControlsPolicy, v as ARBAC_DELEGATED_AUTH, x as getArbacMate, y as ArbacDelegatedAuth } from "./user.provider-NMSzynGK.mjs";
1
+ import { _ as ProjectionOf, a as conjoinArbacDbScopes, b as TArbacMeta, c as ArbacDbScope, d as extractUsedControlValues, f as applyAllowedFieldsAndSet, g as OwnFieldKey, h as NavTarget, i as ConjoinArbacDbScopesOptions, l as AsArbacDbController, m as NavRelationKey, n as ArbacUserProviderToken, o as RefForeignKey, p as ControlsOf, r as AoothArbacClaims, s as RefTableSource, t as ArbacUserProvider, u as enforceControlsPolicy, v as ARBAC_DELEGATED_AUTH, x as getArbacMate, y as ArbacDelegatedAuth } from "./user.provider-CKjmUhkx.mjs";
2
2
  import { Arbac, Arbac as Arbac$1, TArbacCompiledRule, TArbacEvalResult, TArbacEvalResult as TArbacEvalResult$1, TArbacRole, TArbacRoleForResource, TArbacRule, arbacPatternToRegex } from "@aooth/arbac-core";
3
+ import { ControlGate, TProjection, TScopeFieldRules, TScopeFilter, conjoinScopeFilters } from "@aooth/arbac";
3
4
  import { TAuthGuardDef } from "@moostjs/event-http";
4
5
  import { EventContext } from "@wooksjs/event-core";
5
- import { ControlGate, TProjection, TScopeFieldRules, TScopeFilter } from "@aooth/arbac";
6
- import { FilterExpr, TDbRemoveGuardContext, TDbWriteCheckContext, TDbWriteGuardContext, TDeleteOptions, TMetaResponse, TRowResolveOptions, TWriteOptions } from "@atscript/db";
7
- import { AsDbReadableController, AsJsonValueHelpController, AsValueHelpController, TDbControlsType, TDbIndexFieldPaths, TDbRequestContext, ValueHelpSelect } from "@atscript/moost-db";
6
+ import { FilterExpr, TDbRemoveGuardContext, TDbWriteCheckContext, TDbWriteGuardContext, TDeleteOptions, TMetaResponse, TRowResolveOptions, TWriteOptions, UniqueryControls } from "@atscript/db";
7
+ import { AsDbReadableController, AsJsonValueHelpController, AsValueHelpController, TDbActionScopeContext, TDbControlsType, TDbIndexFieldPaths, TDbRequestContext, ValueHelpSelect } from "@atscript/moost-db";
8
8
  import { TAtscriptAnnotatedType, TAtscriptDataType } from "@atscript/typescript/utils";
9
9
  //#region src/db/visibility.d.ts
10
10
  /**
@@ -54,8 +54,6 @@ interface VisibilityTableSource {
54
54
  * @since 0.1.72
55
55
  */
56
56
  jsonParents?: ReadonlySet<string>;
57
- /** Whether the adapter searches natively (`/meta` search-surface pruning). @since 0.1.72 */
58
- isSearchable?(): boolean;
59
57
  }
60
58
  /** Relation name → the visibility its joined rows obey, or `null` when hidden. */
61
59
  type ArbacRelationResolution = ReadonlyMap<string, MetaVisibility | null>;
@@ -291,6 +289,7 @@ declare const ArbacResource: (name: string) => MethodDecorator & ClassDecorator
291
289
  declare const ArbacAction: (name: string) => MethodDecorator & ClassDecorator & ParameterDecorator & PropertyDecorator;
292
290
  //#endregion
293
291
  //#region src/db/as-arbac-db-readable-controller.d.ts
292
+ type TSelect = UniqueryControls["$select"];
294
293
  /**
295
294
  * Read-only mirror of {@link AsArbacDbController} for view-style controllers
296
295
  * built on top of `@atscript/moost-db`'s {@link AsDbReadableController}.
@@ -303,9 +302,11 @@ declare class AsArbacDbReadableController<T extends TAtscriptAnnotatedType = TAt
303
302
  /** Same contract as {@link AsArbacDbController.prepareRequest}. */
304
303
  protected prepareRequest(ctx: TDbRequestContext): Promise<void>;
305
304
  /** Same contract as {@link AsArbacDbController.transformFilter}. */
306
- protected transformFilter(filter: Record<string, unknown> | undefined): Record<string, unknown>;
305
+ protected transformFilter(filter: Record<string, unknown> | undefined): FilterExpr | Promise<FilterExpr>;
306
+ /** Same contract as {@link AsArbacDbController.transformRelationFilter}. Since 0.1.74. */
307
+ protected transformRelationFilter(path: string, filter: FilterExpr): FilterExpr | Promise<FilterExpr>;
307
308
  /** Same contract as {@link AsArbacDbController.transformProjection}. */
308
- protected transformProjection(projection?: TProjection): TProjection | undefined;
309
+ protected transformProjection(projection?: TSelect): TSelect | undefined | Promise<TSelect | undefined>;
309
310
  /** Same contract as {@link AsArbacDbController.validateControls}. */
310
311
  protected validateControls(controls: Record<string, unknown>, type: TDbControlsType): string | undefined;
311
312
  /** Same contract as {@link AsArbacDbController.applyMetaOverlay}. */
@@ -313,7 +314,7 @@ declare class AsArbacDbReadableController<T extends TAtscriptAnnotatedType = TAt
313
314
  /** Same contract as {@link AsArbacDbController.authorizeForm}. */
314
315
  protected authorizeForm(_name: string, actionNames: readonly string[]): Promise<boolean>;
315
316
  /** Same contract as {@link AsArbacDbController.actionRowScope}. */
316
- protected actionRowScope(name: string): Promise<Record<string, unknown> | undefined>;
317
+ protected actionRowScope(name: string, _ctx?: TDbActionScopeContext): Promise<Record<string, unknown> | undefined>;
317
318
  /** Same contract as {@link AsArbacDbController.allowedActions}. */
318
319
  protected allowedActions(names: readonly string[]): Promise<string[]>;
319
320
  /** Same contract as {@link AsArbacDbController.[ARBAC_DELEGATED_AUTH]}. */
@@ -360,21 +361,16 @@ declare abstract class AsArbacValueHelpController<T extends TAtscriptAnnotatedTy
360
361
  private _arbacHost?;
361
362
  private get arbacHost();
362
363
  protected prepareRequest(_ctx: TDbRequestContext): Promise<void>;
363
- protected transformFilter(filter: FilterExpr): FilterExpr;
364
- protected transformProjection(select: ValueHelpSelect<DataType> | undefined): ValueHelpSelect<DataType> | undefined;
364
+ /** Same contract as {@link AsArbacDbController.transformFilter}. */
365
+ protected transformFilter(filter: FilterExpr): FilterExpr | Promise<FilterExpr>;
366
+ protected transformProjection(select: ValueHelpSelect<DataType> | undefined): ValueHelpSelect<DataType> | undefined | Promise<ValueHelpSelect<DataType> | undefined>;
365
367
  protected hasField(path: string): boolean;
366
368
  protected validateControls(controls: Record<string, unknown>, type: TDbControlsType): string | undefined;
367
369
  protected applyMetaOverlay(meta: TMetaResponse): Promise<TMetaResponse>;
368
- runQuery(url: string): Promise<import("@wooksjs/event-http").HttpError<import("@wooksjs/event-http").TWooksErrorBody> | DataType[]>;
369
- runPages(url: string): Promise<import("@wooksjs/event-http").HttpError<import("@wooksjs/event-http").TWooksErrorBody> | {
370
- data: DataType[];
371
- page: number;
372
- itemsPerPage: number;
373
- pages: number;
374
- count: number;
375
- }>;
376
- runGetOne(id: string): Promise<DataType | import("@wooksjs/event-http").HttpError<import("@wooksjs/event-http").TWooksErrorBody>>;
377
- runGetOneComposite(query: Record<string, string>): Promise<DataType | import("@wooksjs/event-http").HttpError<import("@wooksjs/event-http").TWooksErrorBody>>;
370
+ runQuery(url: string): ReturnType<AsValueHelpController<T, DataType>["runQuery"]>;
371
+ runPages(url: string): ReturnType<AsValueHelpController<T, DataType>["runPages"]>;
372
+ runGetOne(id: string): ReturnType<AsValueHelpController<T, DataType>["runGetOne"]>;
373
+ runGetOneComposite(query: Record<string, string>): ReturnType<AsValueHelpController<T, DataType>["runGetOneComposite"]>;
378
374
  }
379
375
  /**
380
376
  * ARBAC-scoped mirror of `@atscript/moost-db`'s
@@ -398,21 +394,16 @@ declare class AsArbacJsonValueHelpController<T extends TAtscriptAnnotatedType =
398
394
  private _arbacHost?;
399
395
  private get arbacHost();
400
396
  protected prepareRequest(_ctx: TDbRequestContext): Promise<void>;
401
- protected transformFilter(filter: FilterExpr): FilterExpr;
402
- protected transformProjection(select: ValueHelpSelect<DataType> | undefined): ValueHelpSelect<DataType> | undefined;
397
+ /** Same contract as {@link AsArbacDbController.transformFilter}. */
398
+ protected transformFilter(filter: FilterExpr): FilterExpr | Promise<FilterExpr>;
399
+ protected transformProjection(select: ValueHelpSelect<DataType> | undefined): ValueHelpSelect<DataType> | undefined | Promise<ValueHelpSelect<DataType> | undefined>;
403
400
  protected hasField(path: string): boolean;
404
401
  protected validateControls(controls: Record<string, unknown>, type: TDbControlsType): string | undefined;
405
402
  protected applyMetaOverlay(meta: TMetaResponse): Promise<TMetaResponse>;
406
- runQuery(url: string): Promise<import("@wooksjs/event-http").HttpError<import("@wooksjs/event-http").TWooksErrorBody> | DataType[]>;
407
- runPages(url: string): Promise<import("@wooksjs/event-http").HttpError<import("@wooksjs/event-http").TWooksErrorBody> | {
408
- data: DataType[];
409
- page: number;
410
- itemsPerPage: number;
411
- pages: number;
412
- count: number;
413
- }>;
414
- runGetOne(id: string): Promise<import("@wooksjs/event-http").HttpError<import("@wooksjs/event-http").TWooksErrorBody> | DataType>;
415
- runGetOneComposite(query: Record<string, string>): Promise<import("@wooksjs/event-http").HttpError<import("@wooksjs/event-http").TWooksErrorBody> | DataType>;
403
+ runQuery(url: string): ReturnType<AsJsonValueHelpController<T, DataType>["runQuery"]>;
404
+ runPages(url: string): ReturnType<AsJsonValueHelpController<T, DataType>["runPages"]>;
405
+ runGetOne(id: string): ReturnType<AsJsonValueHelpController<T, DataType>["runGetOne"]>;
406
+ runGetOneComposite(query: Record<string, string>): ReturnType<AsJsonValueHelpController<T, DataType>["runGetOneComposite"]>;
416
407
  }
417
408
  //#endregion
418
409
  //#region src/db/relation-policy.d.ts
@@ -488,18 +479,17 @@ declare function pruneMetaByVisibility(meta: TMetaResponse, vis: MetaVisibility,
488
479
  * `crud` already advertises no read surface, and write-only principals
489
480
  * still need `type` for their insert/update forms. PK + `preferredId` are
490
481
  * never pruned.
491
- * 3. The search surface: `searchIndexes` reading a hidden field are dropped,
492
- * `searchable` / `vectorSearchable` / `geoSearchable` recomputed, and the
493
- * `query` / `pages` control lists lose the controls that became unusable
494
- * (`crud.geo` goes when no geo index is left).
482
+ *
483
+ * The search surface (`searchIndexes`, `searchable`, `vectorSearchable`,
484
+ * `geoSearchable`) is narrowed by moost-db itself (≥ 0.1.147, under the
485
+ * overridden `hasField`), after this overlay.
495
486
  *
496
487
  * @param source - the controller's `this.readable` (identifiers, relations
497
488
  * and schema rules are derived from it); an identifier set (the 0.1.67
498
489
  * signature) still works, without them
499
- * @param indexes - the controller's `indexFieldPaths()`; defaults to calling
500
- * it on the current controller when it has one
490
+ * @param _indexes - @deprecated ignored since 0.1.74 (moost-db prunes the search surface)
501
491
  */
502
- declare function applyArbacMetaOverlay(meta: TMetaResponse, source: VisibilityTableSource | ReadonlySet<string>, indexes?: readonly TDbIndexFieldPaths[]): Promise<TMetaResponse>;
492
+ declare function applyArbacMetaOverlay(meta: TMetaResponse, source: VisibilityTableSource | ReadonlySet<string>, _indexes?: readonly TDbIndexFieldPaths[]): Promise<TMetaResponse>;
503
493
  /**
504
494
  * Test-friendly internal helper — exported for unit tests and helper
505
495
  * composition; regular consumers should not call this directly.
@@ -566,7 +556,18 @@ declare function isScopedFieldVisible(scopes: ArbacDbScope[], path: string, sour
566
556
  * action id / row is read): resolve the request's scopes
567
557
  * ({@link resolveRequestScopes} — 403 on denial, `arbacPublic` does not
568
558
  * bypass) and, on read endpoints, the `$with` relation policy for the
569
- * requested relations.
559
+ * requested relations — the `$with` entries and, since 0.1.74, the
560
+ * relations the client filter's relational predicates name (`ctx.filter`):
561
+ * a predicate on a relation is allowed exactly when `$with` of it is (same
562
+ * visibility, same `controls.$with` gates).
563
+ *
564
+ * Delegated endpoints (no grant of their own): `"availableActions"`
565
+ * (`GET /meta/actions…`) — some row-level action grant (or, on a
566
+ * `@DbActionsFrom` controller, none: its own part lists nothing);
567
+ * `"delegatedAction"` (`POST /delegated-actions/:name`, since 0.1.74) — the
568
+ * controller's READ grant, since the route only resolves which of the
569
+ * caller's readable rows match the query; the action itself is authorized
570
+ * by its source controller on every batch.
570
571
  *
571
572
  * @since 0.1.72
572
573
  */
@@ -593,12 +594,17 @@ declare function arbacAllowedActions(names: readonly string[]): Promise<string[]
593
594
  * `@DbAction` `name` may run on — the filter of the caller's grant on the
594
595
  * action ({@link actionArbacIds}: every handler method of the action, or a
595
596
  * class-level entry's fallback ids), including custom-field row filters and
596
- * credential attenuation. No grant → a match-nothing filter (fail closed);
597
- * an unrestricted grant → `undefined`. When the current request IS that
598
- * action (its resolved scopes are the same evaluation), the row overlay
597
+ * credential attenuation. Several grants union (`$or`); attenuation is
598
+ * already intersected into each. No grant → a match-nothing filter (fail
599
+ * closed); an unrestricted grant → `undefined`. When the current request IS
600
+ * that action (its resolved scopes are the same evaluation), the row overlay
599
601
  * already applies it → `undefined`. Equal filters are returned as ONE
600
602
  * object per request.
601
603
  *
604
+ * Candidate-free on purpose (evaluated once per action per event) — see
605
+ * `AsArbacDbController.actionRowScope` to bound an action by its candidate
606
+ * rows.
607
+ *
602
608
  * @since 0.1.72
603
609
  */
604
610
  declare function arbacActionRowScope(name: string): Promise<Record<string, unknown> | undefined>;
@@ -610,6 +616,20 @@ declare function arbacActionRowScope(name: string): Promise<Record<string, unkno
610
616
  * @since 0.1.72
611
617
  */
612
618
  declare function arbacRowFilter(filter: Record<string, unknown> | undefined): Record<string, unknown>;
619
+ /**
620
+ * Body of the ARBAC DB controllers' `transformRelationFilter` (since 0.1.74):
621
+ * the operand of a CLIENT relational predicate at `path` (`ticket`,
622
+ * `ticket.team`, `tickets.issues` inside `$with=tickets(…)`) CONJOINED
623
+ * with the row filter of the related rows the caller may see there — the
624
+ * same policy `$with` of that relation applies: a declared `with.<rel>`
625
+ * sub-scope, else the caller's own read grant on the related table. So
626
+ * `$some` matches, and `$none` excludes, only on rows the caller could
627
+ * load. A relation the request did not resolve answers like an unknown
628
+ * field (400 — the request gate rejects it first).
629
+ *
630
+ * @since 0.1.74
631
+ */
632
+ declare function arbacRelationFilter(path: string, filter: FilterExpr, readable: VisibilityTableSource): FilterExpr;
613
633
  //#endregion
614
634
  //#region src/db/shared-read-helpers.d.ts
615
635
  /**
@@ -848,4 +868,4 @@ declare class MoostArbac<TUserAttrs extends object, TScope extends object> exten
848
868
  getScopeFields(): TScopeFieldRules<TScope>;
849
869
  }
850
870
  //#endregion
851
- export { ARBAC_DELEGATED_AUTH, type AoothArbacClaims, Arbac, ArbacAction, ArbacAuthorize, ArbacDbScope, type ArbacDbScopeHelpers, ArbacDelegatedAuth, ArbacEvaluateOptions, type ArbacGuardedTable, type ArbacRelationResolution, ArbacResource, type ArbacScopedTable, ArbacUserProvider, ArbacUserProviderToken, type ArbacWriteTable, AsArbacDbController, AsArbacDbReadableController, AsArbacJsonValueHelpController, AsArbacValueHelpController, type ConjoinArbacDbScopesOptions, type ControlGate, type ControlsOf, type MetaVisibility, MoostArbac, type NavRelationKey, type NavTarget, type OwnFieldKey, type ProjectionOf, type RefForeignKey, type RefTableSource, type ScopeVisibilityOptions, type TArbacCompiledRule, type TArbacEvalResult, TArbacMeta, type TArbacRole, type TArbacRoleForResource, type TArbacRule, type VisibilityTableSource, applyAllowedFieldsAndSet, applyArbacControls, applyArbacMetaOverlay, applyArbacProjection, applyArbacRelationScopes, arbacActionRowScope, arbacAllowedActions, arbacAuthorizeInterceptor, arbacPatternToRegex, arbacRowFilter, authorizeArbacForm, buildScopeVisibility, cachedRequestScopes, checkArbacWrite, collectMethodNames, collectWithGrantNames, collectWritableFields, conjoinArbacDbScopes, enforceControlsPolicy, extractUsedControlValues, getArbacMate, getArbacScopes, guardArbacRemove, guardArbacWrite, isMetaFieldVisible, isScopedFieldVisible, metaAlwaysVisibleFields, prepareArbacRequest, pruneMetaByVisibility, registerArbacDbTarget, requestFieldVisible, requireRequestScopes, resolveHandlerArbacIds, resolveRequestScopes, unionScopeProjection, useArbac, useArbacDbScope };
871
+ export { ARBAC_DELEGATED_AUTH, type AoothArbacClaims, Arbac, ArbacAction, ArbacAuthorize, ArbacDbScope, type ArbacDbScopeHelpers, ArbacDelegatedAuth, ArbacEvaluateOptions, type ArbacGuardedTable, type ArbacRelationResolution, ArbacResource, type ArbacScopedTable, ArbacUserProvider, ArbacUserProviderToken, type ArbacWriteTable, AsArbacDbController, AsArbacDbReadableController, AsArbacJsonValueHelpController, AsArbacValueHelpController, type ConjoinArbacDbScopesOptions, type ControlGate, type ControlsOf, type MetaVisibility, MoostArbac, type NavRelationKey, type NavTarget, type OwnFieldKey, type ProjectionOf, type RefForeignKey, type RefTableSource, type ScopeVisibilityOptions, type TArbacCompiledRule, type TArbacEvalResult, TArbacMeta, type TArbacRole, type TArbacRoleForResource, type TArbacRule, type VisibilityTableSource, applyAllowedFieldsAndSet, applyArbacControls, applyArbacMetaOverlay, applyArbacProjection, applyArbacRelationScopes, arbacActionRowScope, arbacAllowedActions, arbacAuthorizeInterceptor, arbacPatternToRegex, arbacRelationFilter, arbacRowFilter, authorizeArbacForm, buildScopeVisibility, cachedRequestScopes, checkArbacWrite, collectMethodNames, collectWithGrantNames, collectWritableFields, conjoinArbacDbScopes, conjoinScopeFilters, enforceControlsPolicy, extractUsedControlValues, getArbacMate, getArbacScopes, guardArbacRemove, guardArbacWrite, isMetaFieldVisible, isScopedFieldVisible, metaAlwaysVisibleFields, prepareArbacRequest, pruneMetaByVisibility, registerArbacDbTarget, requestFieldVisible, requireRequestScopes, resolveHandlerArbacIds, resolveRequestScopes, unionScopeProjection, useArbac, useArbacDbScope };
package/dist/index.mjs CHANGED
@@ -1,11 +1,11 @@
1
1
  import { i as __decorate, n as ArbacUserProvider, r as ArbacUserProviderToken, t as __decorateMetadata } from "./decorateMetadata-CU_mqYGT.mjs";
2
2
  import { Arbac, Arbac as Arbac$1, arbacPatternToRegex } from "@aooth/arbac-core";
3
+ import { DB_SCOPE_KEYS, DENY_FILTER, ScopeFieldConfigError, applyScopeFieldFilters, conjoinScopeFilters, conjoinScopeFilters as conjoinScopeFilters$1, conjoinScopes, effectiveScope, expandExcludeToLeaves, getProjectionMode, intersectEnabled, isFieldAllowed, needsInheritedConjunction, normalizeScopes, restrictProjection, stableKey, unionOutcomes } from "@aooth/arbac";
3
4
  import { Authenticate, HttpError } from "@moostjs/event-http";
4
5
  import { cached, cachedBy, current, eventTypeKey, key, tryGetCurrent } from "@wooksjs/event-core";
5
6
  import { Inherit, Injectable, TInterceptorPriority, defineBeforeInterceptor, getConstructor, getMoostMate, useControllerContext, useLogger } from "moost";
6
- import { DB_SCOPE_KEYS, DENY_FILTER, ScopeFieldConfigError, applyScopeFieldFilters, conjoinScopeFilters, conjoinScopes, effectiveScope, expandExcludeToLeaves, getProjectionMode, intersectEnabled, isFieldAllowed, needsInheritedConjunction, normalizeScopes, restrictProjection, stableKey, unionOutcomes } from "@aooth/arbac";
7
- import { computeInsights, findAncestorInSet, getPath, isPlainObject } from "@atscript/db";
8
- import { AsDbController, AsDbReadableController, AsJsonValueHelpController, AsValueHelpController, DB_CRUD_HANDLERS, VALUE_HELP_CRUD_HANDLERS, discoverRowLevelActions, getDbEndpoint, resolveTerminalRef } from "@atscript/moost-db";
7
+ import { computeInsights, containsRelationFilter, findAncestorInSet, getPath, hasRelationOp, isPlainObject, isRelationOp } from "@atscript/db";
8
+ import { AsDbController, AsDbReadableController, AsJsonValueHelpController, AsValueHelpController, DB_CRUD_HANDLERS, VALUE_HELP_CRUD_HANDLERS, badRequest, discoverRowLevelActions, getDbEndpoint, hasActionDelegations, resolveTerminalRef } from "@atscript/moost-db";
9
9
  import { groupByFields, walkFilter } from "@uniqu/core";
10
10
  //#region src/db/helpers.ts
11
11
  /** `map.get(key)`, created with `create()` and stored on a miss. */
@@ -110,6 +110,10 @@ function isMetaFieldVisible(path, vis) {
110
110
  }
111
111
  return allowed(path);
112
112
  }
113
+ /** The level of relation `name` when it is visible at `vis` (see {@link isMetaFieldVisible}). */
114
+ function visibleRelation(name, vis) {
115
+ return isMetaFieldVisible(name, vis) ? vis.relation?.(name) : void 0;
116
+ }
113
117
  const NO_NAMES$1 = /* @__PURE__ */ new Set();
114
118
  const identifierSetCache = /* @__PURE__ */ new WeakMap();
115
119
  /** PK + `preferredId` of a table — the identifiers reads always return. */
@@ -358,7 +362,9 @@ async function evaluateTypeRead(type) {
358
362
  const MAX_WITH_DEPTH = 16;
359
363
  /**
360
364
  * Relation names a parsed `$with` control reaches, per level. A dotted
361
- * name (`a.b`) nests like `a($with=b)`.
365
+ * name (`a.b`) nests like `a($with=b)`. The relational predicates of each
366
+ * entry's sub-filter (`$with=a(b=$some(…))`) join the entry's level
367
+ * ({@link relationFilterNameTree}).
362
368
  */
363
369
  function withNameTree(withArr, into = /* @__PURE__ */ new Map(), depth = 0) {
364
370
  if (!Array.isArray(withArr) || depth >= MAX_WITH_DEPTH) return into;
@@ -370,7 +376,29 @@ function withNameTree(withArr, into = /* @__PURE__ */ new Map(), depth = 0) {
370
376
  if (depth + segments.length > MAX_WITH_DEPTH) continue;
371
377
  let level = into;
372
378
  for (const segment of segments) level = getOrCreate(level, segment, () => /* @__PURE__ */ new Map());
373
- if (entry && typeof entry === "object") withNameTree(entry.controls?.$with ?? entry.$with, level, depth + segments.length);
379
+ if (entry && typeof entry === "object") {
380
+ withNameTree(entry.controls?.$with ?? entry.$with, level, depth + segments.length);
381
+ relationFilterNameTree(entry.filter, level, depth + segments.length);
382
+ }
383
+ }
384
+ return into;
385
+ }
386
+ /**
387
+ * Relation names the relational predicates of a filter reach, per level
388
+ * (`ticket=$some(team=$some(…))` → `ticket → { team }`), through
389
+ * `$and` / `$or` / `$not` and nested predicate operands. Only reads
390
+ * `filter` (moost-db hands `prepareRequest` a deep-frozen copy).
391
+ *
392
+ * @since 0.1.74
393
+ */
394
+ function relationFilterNameTree(filter, into = /* @__PURE__ */ new Map(), depth = 0) {
395
+ if (!filter || typeof filter !== "object" || depth >= MAX_WITH_DEPTH) return into;
396
+ for (const [k, value] of Object.entries(filter)) if (k === "$and" || k === "$or") {
397
+ if (Array.isArray(value)) for (const c of value) relationFilterNameTree(c, into, depth);
398
+ } else if (k === "$not") relationFilterNameTree(value, into, depth);
399
+ else if (!k.startsWith("$") && hasRelationOp(value)) {
400
+ const level = getOrCreate(into, k, () => /* @__PURE__ */ new Map());
401
+ for (const [op, operand] of Object.entries(value)) if (isRelationOp(op)) relationFilterNameTree(operand, level, depth + 1);
374
402
  }
375
403
  return into;
376
404
  }
@@ -412,19 +440,35 @@ async function resolveRelationTree(scopes, table, names, opts = {}) {
412
440
  const requestRelationsKey = key("arbac.db.relations");
413
441
  /**
414
442
  * Per-request `$with` policy for the ARBAC DB controllers' `prepareRequest`:
415
- * resolves every relation `controls.$with` names (recursively) and stores
416
- * the resolution the request's visibility (`hasField`, `validateControls`,
417
- * `transformProjection`) reads.
443
+ * resolves every relation `controls.$with` names (recursively) and — since
444
+ * 0.1.74 — every relation the client `filter`'s relational predicates name
445
+ * (`ticket=$some(…)`, also inside `$with` sub-filters), and stores the
446
+ * resolution the request's visibility reads.
447
+ *
448
+ * @returns the relation names resolved, per level
418
449
  */
419
- async function resolveRequestRelations(scopes, controls, readable) {
450
+ async function resolveRequestRelations(scopes, controls, readable, filter) {
420
451
  const names = withNameTree(controls?.$with);
421
- if (names.size === 0) return;
422
- current().set(requestRelationsKey, await resolveRelationTree(scopes, readable, names));
452
+ const listSiblings = names.size > 0;
453
+ relationFilterNameTree(filter, names);
454
+ if (names.size === 0) return names;
455
+ const relations = await resolveRelationTree(scopes, readable, names, { listSiblings });
456
+ current().set(requestRelationsKey, {
457
+ readable,
458
+ relations
459
+ });
460
+ return names;
423
461
  }
424
- /** The resolution {@link resolveRequestRelations} stored for the current event, if any. */
425
- function requestRelations() {
462
+ /**
463
+ * The resolution {@link resolveRequestRelations} stored for the current
464
+ * event, if any — only for the readable it was resolved for: another
465
+ * controller evaluated in the same event (a `@DbActionsFrom` source) never
466
+ * sees the delegating request's `$with` resolution.
467
+ */
468
+ function requestRelations(readable) {
426
469
  const ctx = current();
427
- return ctx.has(requestRelationsKey) ? ctx.get(requestRelationsKey) : void 0;
470
+ const stored = ctx.has(requestRelationsKey) ? ctx.get(requestRelationsKey) : void 0;
471
+ return stored && stored.readable === readable ? stored.relations : void 0;
428
472
  }
429
473
  //#endregion
430
474
  //#region src/db/write-refs.ts
@@ -741,6 +785,20 @@ function arbacIdsFromMeta(cMeta, mMeta, controller, methodName) {
741
785
  action: mMeta?.arbacActionId || mMeta?.atscript_db_action?.name || cMeta?.arbacActionId || mMeta?.id || methodName
742
786
  };
743
787
  }
788
+ const scopeOwners = /* @__PURE__ */ new WeakMap();
789
+ /** Record that `scopes` were evaluated for `resource` / `action` (see {@link scopesOwnedBy}). */
790
+ function tagScopesOwner(scopes, resource, action) {
791
+ scopeOwners.set(scopes, `${resource}\u0000${action}`);
792
+ }
793
+ /**
794
+ * `true` when the cached `scopes` were set for `resource` / `action`. An
795
+ * event slot read can fall through to a parent context (moost-db evaluates a
796
+ * `@DbActionsFrom` source in a child of the delegating event), so scopes
797
+ * cached for another handler are never taken as this handler's grant.
798
+ */
799
+ function scopesOwnedBy(scopes, resource, action) {
800
+ return scopeOwners.get(scopes) === `${resource}\u0000${action}`;
801
+ }
744
802
  /** The 403 every ARBAC deny answers. */
745
803
  function insufficientPrivileges(resource, action) {
746
804
  return new HttpError(403, `Insufficient privileges for action "${action}" on resource "${resource}"`);
@@ -899,6 +957,7 @@ const useArbac = (_ctx) => {
899
957
  const cc = useControllerContext(ctx);
900
958
  const getScopes = () => getArbacScopes(ctx);
901
959
  const setScopes = (scope) => {
960
+ if (scope) tagScopesOwner(scope, resource, action);
902
961
  ctx.set(arbacScopesKey, scope);
903
962
  };
904
963
  const cMeta = cc.getControllerMeta();
@@ -1222,22 +1281,21 @@ function crudHandlers(op, methods) {
1222
1281
  * `crud` already advertises no read surface, and write-only principals
1223
1282
  * still need `type` for their insert/update forms. PK + `preferredId` are
1224
1283
  * never pruned.
1225
- * 3. The search surface: `searchIndexes` reading a hidden field are dropped,
1226
- * `searchable` / `vectorSearchable` / `geoSearchable` recomputed, and the
1227
- * `query` / `pages` control lists lose the controls that became unusable
1228
- * (`crud.geo` goes when no geo index is left).
1284
+ *
1285
+ * The search surface (`searchIndexes`, `searchable`, `vectorSearchable`,
1286
+ * `geoSearchable`) is narrowed by moost-db itself (≥ 0.1.147, under the
1287
+ * overridden `hasField`), after this overlay.
1229
1288
  *
1230
1289
  * @param source - the controller's `this.readable` (identifiers, relations
1231
1290
  * and schema rules are derived from it); an identifier set (the 0.1.67
1232
1291
  * signature) still works, without them
1233
- * @param indexes - the controller's `indexFieldPaths()`; defaults to calling
1234
- * it on the current controller when it has one
1292
+ * @param _indexes - @deprecated ignored since 0.1.74 (moost-db prunes the search surface)
1235
1293
  */
1236
- async function applyArbacMetaOverlay(meta, source, indexes) {
1294
+ async function applyArbacMetaOverlay(meta, source, _indexes) {
1237
1295
  const instance = useControllerContext().getController();
1238
1296
  const { methods } = controllerMethodIndex(instance);
1239
1297
  const crudKeys = Object.keys(meta.crud);
1240
- const [allowedActions, crudScopes] = await Promise.all([grantedActions(instance, meta.actions.map((entry) => entry.name)), Promise.all(crudKeys.map((op) => evaluateHandlers(crudHandlers(op, methods).map((m) => resolveHandlerArbacIds(instance, m)))))]);
1298
+ const [allowedActions, crudScopes] = await Promise.all([grantedActions(instance, meta.actions.map((entry) => entry.name)), Promise.all(crudKeys.map((op) => evaluateHandlers(crudHandlerIds(instance, op, methods))))]);
1241
1299
  const allowed = new Set(allowedActions);
1242
1300
  const actions = meta.actions.filter((entry) => allowed.has(entry.name));
1243
1301
  const crud = {};
@@ -1267,11 +1325,11 @@ async function applyArbacMetaOverlay(meta, source, indexes) {
1267
1325
  }
1268
1326
  const vis = buildScopeVisibility(readScopes, source, { relations: await resolveRelationTree(readScopes, source, metaRelationNames(meta), { listSiblings: false }) });
1269
1327
  const refTargets = await resolveRefTargets(vis, meta);
1270
- return pruneSearchSurface(pruneMetaByVisibility(overlaid, {
1328
+ return pruneMetaByVisibility(overlaid, {
1271
1329
  ...vis,
1272
1330
  writable,
1273
1331
  nestedWrites
1274
- }, refTargets), meta, vis, indexes ?? controllerIndexes(instance));
1332
+ }, refTargets);
1275
1333
  }
1276
1334
  const metaNamesCache = /* @__PURE__ */ new WeakMap();
1277
1335
  /** The relation names `/meta`'s serialized type reaches: root relations, then nav props of each nav body. */
@@ -1319,8 +1377,8 @@ async function resolveRefTargets(root, meta) {
1319
1377
  if (!props || depth >= MAX_NAV_DEPTH) return;
1320
1378
  for (const name of /* @__PURE__ */ new Set([...vis.relationNames ?? NO_NAMES, ...vis.withGrants])) {
1321
1379
  const prop = props[name];
1322
- if (!prop || !isMetaFieldVisible(name, vis)) continue;
1323
- const sub = vis.relation?.(name);
1380
+ if (!prop) continue;
1381
+ const sub = visibleRelation(name, vis);
1324
1382
  if (sub) visit(sub, prop, depth + 1);
1325
1383
  }
1326
1384
  };
@@ -1348,59 +1406,6 @@ function foreignRefTypes(table) {
1348
1406
  });
1349
1407
  }
1350
1408
  const NO_TYPES = /* @__PURE__ */ new Set();
1351
- /** Controls a text / vector search needs, dropped from `crud.query` / `crud.pages` when unusable. */
1352
- const TEXT_SEARCH_CONTROLS = ["search", "fuzzy"];
1353
- const VECTOR_CONTROLS = ["vector", "threshold"];
1354
- /**
1355
- * Drop `searchIndexes` reading a hidden field, recompute the search /
1356
- * vector / geo flags over what is left (native search, or the visible
1357
- * `@db.column.searchable` fallback), and remove the controls that became
1358
- * unusable from the `query` / `pages` control lists (`crud.geo` goes with
1359
- * the last visible geo index).
1360
- */
1361
- function pruneSearchSurface(meta, original, vis, indexes) {
1362
- if (!indexes) return meta;
1363
- const shown = indexes.filter((e) => e.fields.every((f) => isMetaFieldVisible(f, vis)));
1364
- const shownOf = (type) => shown.some((e) => e.type === type);
1365
- const hasText = indexes.some((e) => e.type === "text");
1366
- const native = vis.table?.isSearchable?.() ?? hasText;
1367
- const fallback = searchFallbackVisible(meta, vis.table);
1368
- const searchable = original.searchable && (native && (!hasText || shownOf("text")) || fallback);
1369
- const vectorSearchable = original.vectorSearchable && shownOf("vector");
1370
- const geoSearchable = original.geoSearchable === true ? shownOf("geo") : original.geoSearchable;
1371
- const out = {
1372
- ...meta,
1373
- searchIndexes: original.searchIndexes.filter((info) => shown.some((e) => e.name === info.name && e.type === (info.type ?? "text"))),
1374
- searchable,
1375
- vectorSearchable
1376
- };
1377
- if (geoSearchable !== void 0) out.geoSearchable = geoSearchable;
1378
- const drop = /* @__PURE__ */ new Set();
1379
- if (original.searchable && !searchable && !vectorSearchable) for (const c of TEXT_SEARCH_CONTROLS) drop.add(c);
1380
- if (original.vectorSearchable && !vectorSearchable) for (const c of VECTOR_CONTROLS) drop.add(c);
1381
- if (hasText && !shownOf("text")) drop.add("index");
1382
- if (drop.size > 0 || original.geoSearchable && !geoSearchable) {
1383
- const crud = { ...meta.crud };
1384
- for (const op of ["query", "pages"]) {
1385
- const list = crud[op];
1386
- if (list && drop.size > 0) crud[op] = list.filter((c) => !drop.has(c));
1387
- }
1388
- if (original.geoSearchable && !geoSearchable) delete crud.geo;
1389
- out.crud = crud;
1390
- }
1391
- return out;
1392
- }
1393
- /** A `@db.column.searchable` field is still listed (visible) in the pruned `fields`. */
1394
- function searchFallbackVisible(meta, table) {
1395
- if (!table?.flatMap) return false;
1396
- for (const path of Object.keys(meta.fields)) if (table.flatMap.get(path)?.metadata?.has?.("db.column.searchable") && !meta.fields[path].writeOnly) return true;
1397
- return false;
1398
- }
1399
- /** The current controller's `indexFieldPaths()` (moost-db 0.1.143 DB readables), if it has one. */
1400
- function controllerIndexes(instance) {
1401
- const c = instance;
1402
- return typeof c.indexFieldPaths === "function" ? c.indexFieldPaths() : void 0;
1403
- }
1404
1409
  const methodIndexCache = /* @__PURE__ */ new WeakMap();
1405
1410
  /**
1406
1411
  * THE ARBAC ids an action is authorized as — used by the `/meta` overlay,
@@ -1432,14 +1437,20 @@ async function grantedActions(instance, names) {
1432
1437
  function controllerArbacResource(instance) {
1433
1438
  return arbacIdsFromMeta(getMoostMate().read(instance), void 0, instance, "").resource;
1434
1439
  }
1435
- /** A controller class's methods and `@DbAction` name → methods map (memoized per class). */
1440
+ /**
1441
+ * A controller class's methods and `@DbAction` name → methods map (memoized
1442
+ * per class). The method metadata is read from `instance` itself — never from
1443
+ * the event's current controller, which differs whenever another
1444
+ * controller's actions are evaluated in this event (a view's
1445
+ * `@DbActionsFrom` delegation).
1446
+ */
1436
1447
  function controllerMethodIndex(instance) {
1437
1448
  return getOrCreate(methodIndexCache, getConstructor(instance), () => {
1438
- const cc = useControllerContext();
1449
+ const mate = getMoostMate();
1439
1450
  const methods = new Set(collectMethodNames(instance));
1440
1451
  const actionMethods = /* @__PURE__ */ new Map();
1441
1452
  for (const methodName of methods) {
1442
- const name = cc.getMethodMeta(methodName)?.atscript_db_action?.name;
1453
+ const name = mate.read(instance, methodName)?.atscript_db_action?.name;
1443
1454
  if (name) getOrCreate(actionMethods, name, () => []).push(methodName);
1444
1455
  }
1445
1456
  return {
@@ -1448,6 +1459,18 @@ function controllerMethodIndex(instance) {
1448
1459
  };
1449
1460
  });
1450
1461
  }
1462
+ /** The ARBAC ids of the handler method(s) serving crud op `op` on `instance` (`useArbac()` precedence). */
1463
+ function crudHandlerIds(instance, op, methods = controllerMethodIndex(instance).methods) {
1464
+ return crudHandlers(op, methods).map((m) => resolveHandlerArbacIds(instance, m));
1465
+ }
1466
+ /**
1467
+ * The ARBAC ids of `instance`'s READ handler — the `query` crud op's
1468
+ * handler method(s), exactly what the `/meta` overlay evaluates for
1469
+ * `crud.query`.
1470
+ */
1471
+ function readHandlerIds(instance) {
1472
+ return crudHandlerIds(instance, "query");
1473
+ }
1451
1474
  /**
1452
1475
  * Test-friendly internal helper — exported for unit tests and helper
1453
1476
  * composition; regular consumers should not call this directly.
@@ -1474,6 +1497,124 @@ function collectMethodNames(instance) {
1474
1497
  return [...names];
1475
1498
  }
1476
1499
  //#endregion
1500
+ //#region src/db/shared-read-helpers.ts
1501
+ /**
1502
+ * Restrict the user-supplied `$select` to the scopes' projection union.
1503
+ * Returns the original projection untouched when no scope declares a
1504
+ * projection (so caller sees the unrestricted shape). Reads the same cached
1505
+ * visibility (`vis.allowed`) `hasField` / `/meta` pruning use, so value
1506
+ * stripping and name hiding cannot drift.
1507
+ *
1508
+ * `projection` is the `$select` as moost-db parsed it: an inclusion is an
1509
+ * ARRAY of names, an exclusion an object — both are normalized first. A
1510
+ * requested parent (`$select=a`) narrows to what the scope shows: to the
1511
+ * whitelisted descendants (`a.b: 1`), or — for a hidden descendant
1512
+ * (`a.c: 0`) — to its visible children via `childrenOf` (without a schema
1513
+ * the parent is dropped). Never the whole object, and never the empty
1514
+ * (universe) projection: when nothing survives, the scope's own projection
1515
+ * applies (`restrictProjection`'s ceiling fallback).
1516
+ *
1517
+ * An exclusion result names nested-object parents by their leaves
1518
+ * (`{ a: 0 }` → `{ "a.b": 0, "a.c": 0 }`): flattening adapters invert an
1519
+ * exclusion against their leaf columns, so a parent key alone would strip
1520
+ * nothing (a scope `{ a: 0 }` would otherwise return all of `a`).
1521
+ *
1522
+ * @since 0.1.72
1523
+ */
1524
+ function applyArbacProjection(projection, scopes, readable) {
1525
+ return restrictSelect(projection, visibilityFor(scopes, readable));
1526
+ }
1527
+ /** {@link applyArbacProjection} over an already-built visibility level. */
1528
+ function restrictSelect(projection, vis) {
1529
+ if (Object.keys(vis.allowed).length === 0) return projection;
1530
+ const childrenOf = fieldChildrenOf(vis.table);
1531
+ return expandExcludeToLeaves(restrictProjection(normalizeSelect(projection) ?? {}, vis.allowed, childrenOf), childrenOf);
1532
+ }
1533
+ /**
1534
+ * Enforce the union of per-scope `controls` gates against the parsed
1535
+ * Uniquery controls. Throws `HttpError(403)` on the first violation.
1536
+ *
1537
+ * @since 0.1.72
1538
+ */
1539
+ function applyArbacControls(controls, scopes) {
1540
+ enforceControlsPolicy(effectiveScope(scopes).controls, controls);
1541
+ }
1542
+ /**
1543
+ * The per-role `controls.$with` gates over every relation a read reaches —
1544
+ * its `$with` entries and its client relational predicates alike (see
1545
+ * `prepareArbacRequest`) — each level against that level's scopes.
1546
+ * Overlaps `validateControls`' gate for the `$with` entries on purpose: one
1547
+ * rule for both. Throws `HttpError(403)` on the first violation; a hidden
1548
+ * relation's nested names are left to moost-db (`Unknown field`).
1549
+ *
1550
+ * @since 0.1.74
1551
+ */
1552
+ function enforceRelationGates(names, vis) {
1553
+ if (!vis?.scopes || names.size === 0) return;
1554
+ enforceControlsPolicy(effectiveScope(vis.scopes).controls, { $with: [...names.keys()] });
1555
+ for (const [name, nested] of names) if (nested.size > 0) enforceRelationGates(nested, visibleRelation(name, vis));
1556
+ }
1557
+ /**
1558
+ * Walk the user-supplied `$with` items in `controls` and overlay each joined
1559
+ * relation with the policy of its rows (the `$with` inherit-target policy):
1560
+ * a relation some scope declares `with.<name>` for gets the union of those
1561
+ * sub-scopes (parent authority — silent roles contribute nothing); any other
1562
+ * gets the caller's OWN read scopes on the related table, as resolved in
1563
+ * `prepareRequest` (for the request's own scopes). Filter conjoined, `$select`
1564
+ * restricted, per-relation `controls` gates enforced, recursively.
1565
+ *
1566
+ * Mutates `controls` in place (matches the `validateControls` contract).
1567
+ * Throws `HttpError(403)` if a per-relation control policy is violated. A
1568
+ * hidden relation is skipped: moost-db (≥ 0.1.143) rejects it at every
1569
+ * level through `hasField`, byte-for-byte like a nonexistent one.
1570
+ *
1571
+ * @since 0.1.72
1572
+ */
1573
+ function applyArbacRelationScopes(controls, scopes, readable) {
1574
+ const withArr = controls.$with;
1575
+ if (!Array.isArray(withArr) || withArr.length === 0) return;
1576
+ applyRelationLevel(controls, visibilityFor(scopes, readable));
1577
+ }
1578
+ /** One `$with` level of {@link applyArbacRelationScopes}. */
1579
+ function applyRelationLevel(controls, vis) {
1580
+ const withArr = controls.$with;
1581
+ if (!Array.isArray(withArr) || withArr.length === 0) return;
1582
+ for (const raw of withArr) {
1583
+ if (!raw || typeof raw !== "object") continue;
1584
+ const entry = raw;
1585
+ if (typeof entry.name !== "string" || entry.name.includes(".")) continue;
1586
+ const level = (!vis.relationNames || vis.relationNames.has(entry.name)) && isMetaFieldVisible(entry.name, vis) ? vis.relation?.(entry.name) : void 0;
1587
+ if (!level) continue;
1588
+ const sub = effectiveScope(level.scopes ?? []);
1589
+ const conjoined = conjoinScopeFilters$1(sub.filter, entry.filter);
1590
+ if (conjoined) entry.filter = conjoined;
1591
+ const entryControls = entry.controls ?? {};
1592
+ const restricted = restrictSelect(entryControls.$select, level);
1593
+ if (restricted !== void 0) {
1594
+ entryControls.$select = restricted;
1595
+ entry.controls = entryControls;
1596
+ }
1597
+ enforceControlsPolicy(sub.controls, entryControls);
1598
+ applyRelationLevel(entryControls, level);
1599
+ }
1600
+ }
1601
+ /**
1602
+ * `$select` may be an array of field names (uniquery inclusion list) or a
1603
+ * `TProjection` object. `restrictProjection` operates on the object form
1604
+ * (it would read `["a"]` as an exclusion keyed `"0"`), so normalize the array
1605
+ * to `{ field: 1 }` first. Returns undefined if the input is empty (caller
1606
+ * treats as "no projection set").
1607
+ */
1608
+ function normalizeSelect(value) {
1609
+ if (value === void 0 || value === null) return void 0;
1610
+ if (Array.isArray(value)) {
1611
+ const out = {};
1612
+ for (const item of value) if (typeof item === "string") out[item] = 1;
1613
+ return Object.keys(out).length > 0 ? out : void 0;
1614
+ }
1615
+ if (typeof value === "object") return value;
1616
+ }
1617
+ //#endregion
1477
1618
  //#region src/db/request-scopes.ts
1478
1619
  /** Endpoints whose `prepareRequest` carries parsed Uniquery controls. */
1479
1620
  const READ_ENDPOINTS = /* @__PURE__ */ new Set([
@@ -1519,7 +1660,8 @@ function cachedRequestScopes() {
1519
1660
  async function resolveRequestScopes() {
1520
1661
  const ctx = current();
1521
1662
  const arbac = useArbac(ctx);
1522
- const fromInterceptor = arbac.getScopes();
1663
+ const cachedScopes = arbac.getScopes();
1664
+ const fromInterceptor = cachedScopes && scopesOwnedBy(cachedScopes, arbac.resource, arbac.action) ? cachedScopes : void 0;
1523
1665
  const prior = resolved();
1524
1666
  if (prior && prior.resource === arbac.resource && prior.action === arbac.action && prior.source === fromInterceptor) return prior.scopes;
1525
1667
  const scopes = normalizeScopes(fromInterceptor !== void 0 ? {
@@ -1551,7 +1693,7 @@ function requireRequestScopes() {
1551
1693
  */
1552
1694
  function visibilityFor(scopes, source) {
1553
1695
  const request = resolved();
1554
- if (source && !isIdentifierSet(source) && request?.scopes === scopes) return getOrCreate(request.visibility, source, () => buildScopeVisibility(scopes, source, { relations: requestRelations() }));
1696
+ if (source && !isIdentifierSet(source) && request?.scopes === scopes) return getOrCreate(request.visibility, source, () => buildScopeVisibility(scopes, source, { relations: requestRelations(source) }));
1555
1697
  return scopeVisibility(scopes, source);
1556
1698
  }
1557
1699
  /**
@@ -1585,7 +1727,18 @@ function isScopedFieldVisible(scopes, path, source) {
1585
1727
  * action id / row is read): resolve the request's scopes
1586
1728
  * ({@link resolveRequestScopes} — 403 on denial, `arbacPublic` does not
1587
1729
  * bypass) and, on read endpoints, the `$with` relation policy for the
1588
- * requested relations.
1730
+ * requested relations — the `$with` entries and, since 0.1.74, the
1731
+ * relations the client filter's relational predicates name (`ctx.filter`):
1732
+ * a predicate on a relation is allowed exactly when `$with` of it is (same
1733
+ * visibility, same `controls.$with` gates).
1734
+ *
1735
+ * Delegated endpoints (no grant of their own): `"availableActions"`
1736
+ * (`GET /meta/actions…`) — some row-level action grant (or, on a
1737
+ * `@DbActionsFrom` controller, none: its own part lists nothing);
1738
+ * `"delegatedAction"` (`POST /delegated-actions/:name`, since 0.1.74) — the
1739
+ * controller's READ grant, since the route only resolves which of the
1740
+ * caller's readable rows match the query; the action itself is authorized
1741
+ * by its source controller on every batch.
1589
1742
  *
1590
1743
  * @since 0.1.72
1591
1744
  */
@@ -1594,8 +1747,12 @@ async function prepareArbacRequest(ctx, readable) {
1594
1747
  await authorizeAvailableActions();
1595
1748
  return;
1596
1749
  }
1750
+ if (ctx.endpoint === "delegatedAction") {
1751
+ await authorizeDelegatedQueryTarget();
1752
+ return;
1753
+ }
1597
1754
  const scopes = await resolveRequestScopes();
1598
- if (ctx.controls && READ_ENDPOINTS.has(ctx.endpoint)) await resolveRequestRelations(scopes, ctx.controls, readable);
1755
+ if (READ_ENDPOINTS.has(ctx.endpoint)) enforceRelationGates(await resolveRequestRelations(scopes, ctx.controls, readable, ctx.filter), visibilityFor(scopes, readable));
1599
1756
  }
1600
1757
  /**
1601
1758
  * `authorizeForm` for the ARBAC DB controllers: a form is served iff the
@@ -1621,14 +1778,42 @@ function actionsIds(names) {
1621
1778
  * overlay stays unrestricted — each action's rows come from
1622
1779
  * `actionRowScope` ({@link arbacActionRowScope}) — while field visibility
1623
1780
  * is never wider than the widest granted action.
1781
+ *
1782
+ * A `@DbActionsFrom` controller without an own row-level grant is served
1783
+ * too (since 0.1.74): its own part lists nothing (a match-nothing row
1784
+ * overlay), and each delegated action is answered by its source controller,
1785
+ * under the caller's grants THERE.
1624
1786
  */
1625
1787
  async function authorizeAvailableActions() {
1626
1788
  const ctx = current();
1627
1789
  const instance = useControllerContext(ctx).getController();
1628
- const granted = await evaluateHandlers(actionsIds(discoverRowLevelActions(getConstructor(instance), instance.app, instance.logger).map((e) => e.info.name)));
1790
+ const ctor = getConstructor(instance);
1791
+ const granted = await evaluateHandlers(actionsIds(discoverRowLevelActions(ctor, instance.app, instance.logger).map((e) => e.info.name)));
1629
1792
  const arbac = useArbac(ctx);
1630
- if (!granted) throw insufficientPrivileges(arbac.resource, arbac.action);
1631
- storeResolved(ctx, arbac, void 0, granted.map(withoutRowPolicy));
1793
+ if (granted) storeResolved(ctx, arbac, void 0, granted.map(withoutRowPolicy));
1794
+ else if (hasActionDelegations(ctor)) storeResolved(ctx, arbac, void 0, NO_OWN_ACTIONS);
1795
+ else throw insufficientPrivileges(arbac.resource, arbac.action);
1796
+ }
1797
+ /** The own scopes of a delegating controller's `GET /meta/actions` without an own grant. */
1798
+ const NO_OWN_ACTIONS = [{
1799
+ filter: DENY_FILTER,
1800
+ check: DENY_FILTER
1801
+ }];
1802
+ /**
1803
+ * `POST /delegated-actions/:name` (endpoint `"delegatedAction"`): the route
1804
+ * resolves which rows of THIS controller match the query — a read, so it
1805
+ * is served under the caller's read grant (its `query` handler; none →
1806
+ * 403), stored as the request's scopes (`transformFilter`, `hasField` and
1807
+ * moost-db's default `queryTargetScope` read them). It grants nothing on
1808
+ * the action: the source controller's route re-authorizes every batch.
1809
+ */
1810
+ async function authorizeDelegatedQueryTarget() {
1811
+ const ctx = current();
1812
+ const arbac = useArbac(ctx);
1813
+ const ids = readHandlerIds(useControllerContext(ctx).getController());
1814
+ const scopes = ids.length > 0 ? await evaluateHandlers(ids) : void 0;
1815
+ if (!scopes) throw insufficientPrivileges(ids[0]?.resource ?? arbac.resource, ids[0]?.action ?? "query");
1816
+ storeResolved(ctx, arbac, void 0, scopes);
1632
1817
  }
1633
1818
  /** A scope without its row policy (`filter` / `check`). */
1634
1819
  function withoutRowPolicy(scope) {
@@ -1653,12 +1838,17 @@ const actionScopes = cached(() => /* @__PURE__ */ new Map());
1653
1838
  * `@DbAction` `name` may run on — the filter of the caller's grant on the
1654
1839
  * action ({@link actionArbacIds}: every handler method of the action, or a
1655
1840
  * class-level entry's fallback ids), including custom-field row filters and
1656
- * credential attenuation. No grant → a match-nothing filter (fail closed);
1657
- * an unrestricted grant → `undefined`. When the current request IS that
1658
- * action (its resolved scopes are the same evaluation), the row overlay
1841
+ * credential attenuation. Several grants union (`$or`); attenuation is
1842
+ * already intersected into each. No grant → a match-nothing filter (fail
1843
+ * closed); an unrestricted grant → `undefined`. When the current request IS
1844
+ * that action (its resolved scopes are the same evaluation), the row overlay
1659
1845
  * already applies it → `undefined`. Equal filters are returned as ONE
1660
1846
  * object per request.
1661
1847
  *
1848
+ * Candidate-free on purpose (evaluated once per action per event) — see
1849
+ * `AsArbacDbController.actionRowScope` to bound an action by its candidate
1850
+ * rows.
1851
+ *
1662
1852
  * @since 0.1.72
1663
1853
  */
1664
1854
  async function arbacActionRowScope(name) {
@@ -1681,110 +1871,26 @@ async function arbacActionRowScope(name) {
1681
1871
  * @since 0.1.72
1682
1872
  */
1683
1873
  function arbacRowFilter(filter) {
1684
- return conjoinScopeFilters(effectiveScope(requireRequestScopes()).filter, filter) ?? {};
1685
- }
1686
- //#endregion
1687
- //#region src/db/shared-read-helpers.ts
1688
- /**
1689
- * Restrict the user-supplied `$select` to the scopes' projection union.
1690
- * Returns the original projection untouched when no scope declares a
1691
- * projection (so caller sees the unrestricted shape). Reads the same cached
1692
- * visibility (`vis.allowed`) `hasField` / `/meta` pruning use, so value
1693
- * stripping and name hiding cannot drift.
1694
- *
1695
- * `projection` is the `$select` as moost-db parsed it: an inclusion is an
1696
- * ARRAY of names, an exclusion an object — both are normalized first. A
1697
- * requested parent (`$select=a`) narrows to what the scope shows: to the
1698
- * whitelisted descendants (`a.b: 1`), or — for a hidden descendant
1699
- * (`a.c: 0`) — to its visible children via `childrenOf` (without a schema
1700
- * the parent is dropped). Never the whole object, and never the empty
1701
- * (universe) projection: when nothing survives, the scope's own projection
1702
- * applies (`restrictProjection`'s ceiling fallback).
1703
- *
1704
- * An exclusion result names nested-object parents by their leaves
1705
- * (`{ a: 0 }` → `{ "a.b": 0, "a.c": 0 }`): flattening adapters invert an
1706
- * exclusion against their leaf columns, so a parent key alone would strip
1707
- * nothing (a scope `{ a: 0 }` would otherwise return all of `a`).
1708
- *
1709
- * @since 0.1.72
1710
- */
1711
- function applyArbacProjection(projection, scopes, readable) {
1712
- return restrictSelect(projection, visibilityFor(scopes, readable));
1713
- }
1714
- /** {@link applyArbacProjection} over an already-built visibility level. */
1715
- function restrictSelect(projection, vis) {
1716
- if (Object.keys(vis.allowed).length === 0) return projection;
1717
- const childrenOf = fieldChildrenOf(vis.table);
1718
- return expandExcludeToLeaves(restrictProjection(normalizeSelect(projection) ?? {}, vis.allowed, childrenOf), childrenOf);
1719
- }
1720
- /**
1721
- * Enforce the union of per-scope `controls` gates against the parsed
1722
- * Uniquery controls. Throws `HttpError(403)` on the first violation.
1723
- *
1724
- * @since 0.1.72
1725
- */
1726
- function applyArbacControls(controls, scopes) {
1727
- enforceControlsPolicy(effectiveScope(scopes).controls, controls);
1728
- }
1729
- /**
1730
- * Walk the user-supplied `$with` items in `controls` and overlay each joined
1731
- * relation with the policy of its rows (the `$with` inherit-target policy):
1732
- * a relation some scope declares `with.<name>` for gets the union of those
1733
- * sub-scopes (parent authority — silent roles contribute nothing); any other
1734
- * gets the caller's OWN read scopes on the related table, as resolved in
1735
- * `prepareRequest` (for the request's own scopes). Filter conjoined, `$select`
1736
- * restricted, per-relation `controls` gates enforced, recursively.
1874
+ return conjoinScopeFilters$1(effectiveScope(requireRequestScopes()).filter, filter) ?? {};
1875
+ }
1876
+ /**
1877
+ * Body of the ARBAC DB controllers' `transformRelationFilter` (since 0.1.74):
1878
+ * the operand of a CLIENT relational predicate at `path` (`ticket`,
1879
+ * `ticket.team`, `tickets.issues` inside `$with=tickets(…)`) CONJOINED
1880
+ * with the row filter of the related rows the caller may see there — the
1881
+ * same policy `$with` of that relation applies: a declared `with.<rel>`
1882
+ * sub-scope, else the caller's own read grant on the related table. So
1883
+ * `$some` matches, and `$none` excludes, only on rows the caller could
1884
+ * load. A relation the request did not resolve answers like an unknown
1885
+ * field (400 — the request gate rejects it first).
1737
1886
  *
1738
- * Mutates `controls` in place (matches the `validateControls` contract).
1739
- * Throws `HttpError(403)` if a per-relation control policy is violated. A
1740
- * hidden relation is skipped: moost-db (≥ 0.1.143) rejects it at every
1741
- * level through `hasField`, byte-for-byte like a nonexistent one.
1742
- *
1743
- * @since 0.1.72
1887
+ * @since 0.1.74
1744
1888
  */
1745
- function applyArbacRelationScopes(controls, scopes, readable) {
1746
- const withArr = controls.$with;
1747
- if (!Array.isArray(withArr) || withArr.length === 0) return;
1748
- applyRelationLevel(controls, visibilityFor(scopes, readable));
1749
- }
1750
- /** One `$with` level of {@link applyArbacRelationScopes}. */
1751
- function applyRelationLevel(controls, vis) {
1752
- const withArr = controls.$with;
1753
- if (!Array.isArray(withArr) || withArr.length === 0) return;
1754
- for (const raw of withArr) {
1755
- if (!raw || typeof raw !== "object") continue;
1756
- const entry = raw;
1757
- if (typeof entry.name !== "string" || entry.name.includes(".")) continue;
1758
- const level = (!vis.relationNames || vis.relationNames.has(entry.name)) && isMetaFieldVisible(entry.name, vis) ? vis.relation?.(entry.name) : void 0;
1759
- if (!level) continue;
1760
- const sub = effectiveScope(level.scopes ?? []);
1761
- const conjoined = conjoinScopeFilters(sub.filter, entry.filter);
1762
- if (conjoined) entry.filter = conjoined;
1763
- const entryControls = entry.controls ?? {};
1764
- const restricted = restrictSelect(entryControls.$select, level);
1765
- if (restricted !== void 0) {
1766
- entryControls.$select = restricted;
1767
- entry.controls = entryControls;
1768
- }
1769
- enforceControlsPolicy(sub.controls, entryControls);
1770
- applyRelationLevel(entryControls, level);
1771
- }
1772
- }
1773
- /**
1774
- * `$select` may be an array of field names (uniquery inclusion list) or a
1775
- * `TProjection` object. `restrictProjection` operates on the object form
1776
- * (it would read `["a"]` as an exclusion keyed `"0"`), so normalize the array
1777
- * to `{ field: 1 }` first. Returns undefined if the input is empty (caller
1778
- * treats as "no projection set").
1779
- */
1780
- function normalizeSelect(value) {
1781
- if (value === void 0 || value === null) return void 0;
1782
- if (Array.isArray(value)) {
1783
- const out = {};
1784
- for (const item of value) if (typeof item === "string") out[item] = 1;
1785
- return Object.keys(out).length > 0 ? out : void 0;
1786
- }
1787
- if (typeof value === "object") return value;
1889
+ function arbacRelationFilter(path, filter, readable) {
1890
+ let level = visibilityFor(requireRequestScopes(), readable);
1891
+ for (const name of path.split(".")) level = level && visibleRelation(name, level);
1892
+ if (!level?.scopes) throw badRequest(path, `Unknown field "${path}"`);
1893
+ return conjoinScopeFilters$1(effectiveScope(level.scopes).filter, filter) ?? filter;
1788
1894
  }
1789
1895
  //#endregion
1790
1896
  //#region src/db/write-fields.ts
@@ -1914,14 +2020,20 @@ var UnsupportedCheckError = class extends Error {};
1914
2020
  * BEFORE the write. Deliberately a small subset of the filter language
1915
2021
  * (`$eq`/`$ne`/`$in`/`$nin`/ordering/`$exists` + `$and`/`$or`/`$not`) with the
1916
2022
  * database's null model (`$eq: null` matches null or missing, `$exists`
1917
- * means "holds a non-null value"); any other operator (e.g. `$regex`) throws
1918
- * {@link UnsupportedCheckError} — callers fail closed.
2023
+ * means "holds a non-null value"); any other operator (e.g. `$regex`) and
2024
+ * a relational predicate (`{ ticket: { $some: … } }` — it needs the related
2025
+ * rows) throw {@link UnsupportedCheckError} — callers fail closed.
1919
2026
  */
1920
2027
  function compileScopeCheck(filter) {
1921
2028
  return walkFilter(filter, checkVisitor) ?? (() => true);
1922
2029
  }
1923
- /** The field paths a filter references (dotted, as written). */
2030
+ /**
2031
+ * The field paths a filter references (dotted, as written). A relational
2032
+ * predicate throws {@link UnsupportedCheckError} (its fields live on the
2033
+ * related table).
2034
+ */
1924
2035
  function checkFields(filter) {
2036
+ if (containsRelationFilter(filter)) throw new UnsupportedCheckError("relational predicate");
1925
2037
  return new Set(computeInsights(filter).keys());
1926
2038
  }
1927
2039
  function same(a, b) {
@@ -1938,6 +2050,9 @@ const checkVisitor = {
1938
2050
  and: (children) => (row) => children.every((c) => c(row)),
1939
2051
  or: (children) => (row) => children.some((c) => c(row)),
1940
2052
  not: (child) => (row) => !child(row),
2053
+ relation(field, op) {
2054
+ throw new UnsupportedCheckError(`${field}: ${op}`);
2055
+ },
1941
2056
  comparison(field, op, value) {
1942
2057
  const read = (row) => getPath(row, field);
1943
2058
  const compare = (test) => (row) => {
@@ -2115,9 +2230,23 @@ let AsArbacDbController = class AsArbacDbController extends AsDbController {
2115
2230
  prepareRequest(ctx) {
2116
2231
  return prepareArbacRequest(ctx, this.readable);
2117
2232
  }
2233
+ /**
2234
+ * The request filter ∧ the union of the scopes' `filter`s. Computed
2235
+ * synchronously, but typed with moost-db's async-capable contract
2236
+ * (`FilterExpr | Promise<FilterExpr>`) so a subclass may override it
2237
+ * `async` — `await super.transformFilter(filter)` — and moost-db awaits it.
2238
+ */
2118
2239
  transformFilter(filter) {
2119
2240
  return arbacRowFilter(filter);
2120
2241
  }
2242
+ /** A client relational predicate's operand ∧ the related rows the caller may see ({@link arbacRelationFilter}). Since 0.1.74. */
2243
+ transformRelationFilter(path, filter) {
2244
+ return arbacRelationFilter(path, filter, this.readable);
2245
+ }
2246
+ /**
2247
+ * `$select` restricted to the scopes' projection union. Synchronous, typed
2248
+ * async-capable like {@link transformFilter}.
2249
+ */
2121
2250
  transformProjection(projection) {
2122
2251
  return applyArbacProjection(projection, requireRequestScopes(), this.readable);
2123
2252
  }
@@ -2180,10 +2309,22 @@ let AsArbacDbController = class AsArbacDbController extends AsDbController {
2180
2309
  }
2181
2310
  /**
2182
2311
  * The rows each `@DbAction` may run on: the caller's grant on that action
2183
- * (see `arbacActionRowScope`). Enforced by the action gate and reflected
2184
- * in `$actions` and `GET /meta/actions/:id`. Since 0.1.72.
2312
+ * ({@link arbacActionRowScope} — several grants union, attenuation
2313
+ * intersects). Enforced by the action gate and reflected in `$actions` and
2314
+ * `GET /meta/actions/:id`. Since 0.1.72.
2315
+ *
2316
+ * The grant does not depend on `ctx` (the candidate rows, since 0.1.74).
2317
+ * To bound an action further by the candidates, override and AND your
2318
+ * filter onto `super`'s — it must never replace it:
2319
+ *
2320
+ * ```ts
2321
+ * protected async actionRowScope(name: string, ctx: TDbActionScopeContext) {
2322
+ * const grant = await super.actionRowScope(name, ctx);
2323
+ * return conjoinScopeFilters(grant, await myCandidateScope(name, ctx));
2324
+ * }
2325
+ * ```
2185
2326
  */
2186
- actionRowScope(name) {
2327
+ actionRowScope(name, _ctx) {
2187
2328
  return arbacActionRowScope(name);
2188
2329
  }
2189
2330
  /**
@@ -2366,6 +2507,10 @@ let AsArbacDbReadableController = class AsArbacDbReadableController extends AsDb
2366
2507
  transformFilter(filter) {
2367
2508
  return arbacRowFilter(filter);
2368
2509
  }
2510
+ /** Same contract as {@link AsArbacDbController.transformRelationFilter}. Since 0.1.74. */
2511
+ transformRelationFilter(path, filter) {
2512
+ return arbacRelationFilter(path, filter, this.readable);
2513
+ }
2369
2514
  /** Same contract as {@link AsArbacDbController.transformProjection}. */
2370
2515
  transformProjection(projection) {
2371
2516
  return applyArbacProjection(projection, requireRequestScopes(), this.readable);
@@ -2387,7 +2532,7 @@ let AsArbacDbReadableController = class AsArbacDbReadableController extends AsDb
2387
2532
  return authorizeArbacForm(actionNames);
2388
2533
  }
2389
2534
  /** Same contract as {@link AsArbacDbController.actionRowScope}. */
2390
- actionRowScope(name) {
2535
+ actionRowScope(name, _ctx) {
2391
2536
  return arbacActionRowScope(name);
2392
2537
  }
2393
2538
  /** Same contract as {@link AsArbacDbController.allowedActions}. */
@@ -2467,6 +2612,7 @@ let AsArbacValueHelpController = class AsArbacValueHelpController extends AsValu
2467
2612
  async prepareRequest(_ctx) {
2468
2613
  await resolveRequestScopes();
2469
2614
  }
2615
+ /** Same contract as {@link AsArbacDbController.transformFilter}. */
2470
2616
  transformFilter(filter) {
2471
2617
  return arbacRowFilter(filter);
2472
2618
  }
@@ -2501,25 +2647,25 @@ __decorate([
2501
2647
  ArbacAction("query"),
2502
2648
  __decorateMetadata("design:type", Function),
2503
2649
  __decorateMetadata("design:paramtypes", [String]),
2504
- __decorateMetadata("design:returntype", void 0)
2650
+ __decorateMetadata("design:returntype", typeof ReturnType === "undefined" ? Object : ReturnType)
2505
2651
  ], AsArbacValueHelpController.prototype, "runQuery", null);
2506
2652
  __decorate([
2507
2653
  ArbacAction("pages"),
2508
2654
  __decorateMetadata("design:type", Function),
2509
2655
  __decorateMetadata("design:paramtypes", [String]),
2510
- __decorateMetadata("design:returntype", void 0)
2656
+ __decorateMetadata("design:returntype", typeof ReturnType === "undefined" ? Object : ReturnType)
2511
2657
  ], AsArbacValueHelpController.prototype, "runPages", null);
2512
2658
  __decorate([
2513
2659
  ArbacAction("getOne"),
2514
2660
  __decorateMetadata("design:type", Function),
2515
2661
  __decorateMetadata("design:paramtypes", [String]),
2516
- __decorateMetadata("design:returntype", void 0)
2662
+ __decorateMetadata("design:returntype", typeof ReturnType === "undefined" ? Object : ReturnType)
2517
2663
  ], AsArbacValueHelpController.prototype, "runGetOne", null);
2518
2664
  __decorate([
2519
2665
  ArbacAction("getOneComposite"),
2520
2666
  __decorateMetadata("design:type", Function),
2521
2667
  __decorateMetadata("design:paramtypes", [typeof Record === "undefined" ? Object : Record]),
2522
- __decorateMetadata("design:returntype", void 0)
2668
+ __decorateMetadata("design:returntype", typeof ReturnType === "undefined" ? Object : ReturnType)
2523
2669
  ], AsArbacValueHelpController.prototype, "runGetOneComposite", null);
2524
2670
  AsArbacValueHelpController = __decorate([Inherit()], AsArbacValueHelpController);
2525
2671
  let AsArbacJsonValueHelpController = class AsArbacJsonValueHelpController extends AsJsonValueHelpController {
@@ -2530,6 +2676,7 @@ let AsArbacJsonValueHelpController = class AsArbacJsonValueHelpController extend
2530
2676
  async prepareRequest(_ctx) {
2531
2677
  await resolveRequestScopes();
2532
2678
  }
2679
+ /** Same contract as {@link AsArbacDbController.transformFilter}. */
2533
2680
  transformFilter(filter) {
2534
2681
  return arbacRowFilter(filter);
2535
2682
  }
@@ -2564,25 +2711,25 @@ __decorate([
2564
2711
  ArbacAction("query"),
2565
2712
  __decorateMetadata("design:type", Function),
2566
2713
  __decorateMetadata("design:paramtypes", [String]),
2567
- __decorateMetadata("design:returntype", void 0)
2714
+ __decorateMetadata("design:returntype", typeof ReturnType === "undefined" ? Object : ReturnType)
2568
2715
  ], AsArbacJsonValueHelpController.prototype, "runQuery", null);
2569
2716
  __decorate([
2570
2717
  ArbacAction("pages"),
2571
2718
  __decorateMetadata("design:type", Function),
2572
2719
  __decorateMetadata("design:paramtypes", [String]),
2573
- __decorateMetadata("design:returntype", void 0)
2720
+ __decorateMetadata("design:returntype", typeof ReturnType === "undefined" ? Object : ReturnType)
2574
2721
  ], AsArbacJsonValueHelpController.prototype, "runPages", null);
2575
2722
  __decorate([
2576
2723
  ArbacAction("getOne"),
2577
2724
  __decorateMetadata("design:type", Function),
2578
2725
  __decorateMetadata("design:paramtypes", [String]),
2579
- __decorateMetadata("design:returntype", void 0)
2726
+ __decorateMetadata("design:returntype", typeof ReturnType === "undefined" ? Object : ReturnType)
2580
2727
  ], AsArbacJsonValueHelpController.prototype, "runGetOne", null);
2581
2728
  __decorate([
2582
2729
  ArbacAction("getOneComposite"),
2583
2730
  __decorateMetadata("design:type", Function),
2584
2731
  __decorateMetadata("design:paramtypes", [typeof Record === "undefined" ? Object : Record]),
2585
- __decorateMetadata("design:returntype", void 0)
2732
+ __decorateMetadata("design:returntype", typeof ReturnType === "undefined" ? Object : ReturnType)
2586
2733
  ], AsArbacJsonValueHelpController.prototype, "runGetOneComposite", null);
2587
2734
  AsArbacJsonValueHelpController = __decorate([Inherit()], AsArbacJsonValueHelpController);
2588
2735
  //#endregion
@@ -2609,7 +2756,7 @@ async function useArbacDbScope() {
2609
2756
  const visibleOn = (table) => (path) => isScopedFieldVisible(scopes, path, table);
2610
2757
  return {
2611
2758
  scopes,
2612
- filter: (extra) => conjoinScopeFilters(eff.filter, extra) ?? {},
2759
+ filter: (extra) => conjoinScopeFilters$1(eff.filter, extra) ?? {},
2613
2760
  set: () => ({ ...eff.set }),
2614
2761
  check: () => eff.check ?? {},
2615
2762
  async assertRowsInScope(table, ids) {
@@ -2649,4 +2796,4 @@ async function useArbacDbScope() {
2649
2796
  };
2650
2797
  }
2651
2798
  //#endregion
2652
- export { ARBAC_DELEGATED_AUTH, Arbac, ArbacAction, ArbacAuthorize, ArbacResource, ArbacUserProvider, ArbacUserProviderToken, AsArbacDbController, AsArbacDbReadableController, AsArbacJsonValueHelpController, AsArbacValueHelpController, MoostArbac, applyAllowedFieldsAndSet, applyArbacControls, applyArbacMetaOverlay, applyArbacProjection, applyArbacRelationScopes, arbacActionRowScope, arbacAllowedActions, arbacAuthorizeInterceptor, arbacPatternToRegex, arbacRowFilter, authorizeArbacForm, buildScopeVisibility, cachedRequestScopes, checkArbacWrite, collectMethodNames, collectWithGrantNames, collectWritableFields, conjoinArbacDbScopes, enforceControlsPolicy, extractUsedControlValues, getArbacMate, getArbacScopes, guardArbacRemove, guardArbacWrite, isMetaFieldVisible, isScopedFieldVisible, metaAlwaysVisibleFields, prepareArbacRequest, pruneMetaByVisibility, registerArbacDbTarget, requestFieldVisible, requireRequestScopes, resolveHandlerArbacIds, resolveRequestScopes, unionScopeProjection, useArbac, useArbacDbScope };
2799
+ export { ARBAC_DELEGATED_AUTH, Arbac, ArbacAction, ArbacAuthorize, ArbacResource, ArbacUserProvider, ArbacUserProviderToken, AsArbacDbController, AsArbacDbReadableController, AsArbacJsonValueHelpController, AsArbacValueHelpController, MoostArbac, applyAllowedFieldsAndSet, applyArbacControls, applyArbacMetaOverlay, applyArbacProjection, applyArbacRelationScopes, arbacActionRowScope, arbacAllowedActions, arbacAuthorizeInterceptor, arbacPatternToRegex, arbacRelationFilter, arbacRowFilter, authorizeArbacForm, buildScopeVisibility, cachedRequestScopes, checkArbacWrite, collectMethodNames, collectWithGrantNames, collectWritableFields, conjoinArbacDbScopes, conjoinScopeFilters, enforceControlsPolicy, extractUsedControlValues, getArbacMate, getArbacScopes, guardArbacRemove, guardArbacWrite, isMetaFieldVisible, isScopedFieldVisible, metaAlwaysVisibleFields, prepareArbacRequest, pruneMetaByVisibility, registerArbacDbTarget, requestFieldVisible, requireRequestScopes, resolveHandlerArbacIds, resolveRequestScopes, unionScopeProjection, useArbac, useArbacDbScope };
@@ -1,7 +1,7 @@
1
- import { Mate, TClassConstructor, TMateParamMeta, TMoostMetadata } from "moost";
2
1
  import { ControlGate, TProjection, TProjectionChildren, TScopeFieldRules, TScopeFilter } from "@aooth/arbac";
3
- import { TDbRemoveGuardContext, TDbWriteAction, TDbWriteCheckContext, TDbWriteGuardContext, TMetaResponse } from "@atscript/db";
4
- import { AsDbController, TDbControlsType, TDbRequestContext } from "@atscript/moost-db";
2
+ import { Mate, TClassConstructor, TMateParamMeta, TMoostMetadata } from "moost";
3
+ import { FilterExpr, TDbRemoveGuardContext, TDbWriteAction, TDbWriteCheckContext, TDbWriteGuardContext, TMetaResponse, UniqueryControls } from "@atscript/db";
4
+ import { AsDbController, TDbActionScopeContext, TDbControlsType, TDbRequestContext } from "@atscript/moost-db";
5
5
  import { NavPropsOf, OwnPropsOf, TAtscriptAnnotatedType, TAtscriptDataType } from "@atscript/typescript/utils";
6
6
  //#region src/arbac.mate.d.ts
7
7
  /**
@@ -124,6 +124,7 @@ type ControlsOf<T> = IsUnknown<T> extends true ? Record<string, ControlGate> : {
124
124
  declare function applyAllowedFieldsAndSet(data: unknown, scopes: ArbacDbScope[], preserveFields?: readonly string[], isMergePatch?: (objectPath: string) => boolean): unknown;
125
125
  //#endregion
126
126
  //#region src/db/as-arbac-db-controller.d.ts
127
+ type TSelect = UniqueryControls["$select"];
127
128
  /**
128
129
  * Contract returned by an ARBAC role's scope predicate on a DB-backed resource.
129
130
  *
@@ -269,8 +270,20 @@ declare class AsArbacDbController<T extends TAtscriptAnnotatedType = TAtscriptAn
269
270
  * is resolved for the requested relations.
270
271
  */
271
272
  protected prepareRequest(ctx: TDbRequestContext): Promise<void>;
272
- protected transformFilter(filter: Record<string, unknown> | undefined): Record<string, unknown>;
273
- protected transformProjection(projection?: TProjection): TProjection | undefined;
273
+ /**
274
+ * The request filter ∧ the union of the scopes' `filter`s. Computed
275
+ * synchronously, but typed with moost-db's async-capable contract
276
+ * (`FilterExpr | Promise<FilterExpr>`) so a subclass may override it
277
+ * `async` — `await super.transformFilter(filter)` — and moost-db awaits it.
278
+ */
279
+ protected transformFilter(filter: Record<string, unknown> | undefined): FilterExpr | Promise<FilterExpr>;
280
+ /** A client relational predicate's operand ∧ the related rows the caller may see ({@link arbacRelationFilter}). Since 0.1.74. */
281
+ protected transformRelationFilter(path: string, filter: FilterExpr): FilterExpr | Promise<FilterExpr>;
282
+ /**
283
+ * `$select` restricted to the scopes' projection union. Synchronous, typed
284
+ * async-capable like {@link transformFilter}.
285
+ */
286
+ protected transformProjection(projection?: TSelect): TSelect | undefined | Promise<TSelect | undefined>;
274
287
  /**
275
288
  * Translate `@uniqu/url` parser failures into HTTP 400 instead of letting them
276
289
  * bubble as 500. The base `parseQueryString` calls `parseUrl()` directly with
@@ -308,10 +321,22 @@ declare class AsArbacDbController<T extends TAtscriptAnnotatedType = TAtscriptAn
308
321
  protected authorizeForm(_name: string, actionNames: readonly string[]): Promise<boolean>;
309
322
  /**
310
323
  * The rows each `@DbAction` may run on: the caller's grant on that action
311
- * (see `arbacActionRowScope`). Enforced by the action gate and reflected
312
- * in `$actions` and `GET /meta/actions/:id`. Since 0.1.72.
324
+ * ({@link arbacActionRowScope} — several grants union, attenuation
325
+ * intersects). Enforced by the action gate and reflected in `$actions` and
326
+ * `GET /meta/actions/:id`. Since 0.1.72.
327
+ *
328
+ * The grant does not depend on `ctx` (the candidate rows, since 0.1.74).
329
+ * To bound an action further by the candidates, override and AND your
330
+ * filter onto `super`'s — it must never replace it:
331
+ *
332
+ * ```ts
333
+ * protected async actionRowScope(name: string, ctx: TDbActionScopeContext) {
334
+ * const grant = await super.actionRowScope(name, ctx);
335
+ * return conjoinScopeFilters(grant, await myCandidateScope(name, ctx));
336
+ * }
337
+ * ```
313
338
  */
314
- protected actionRowScope(name: string): Promise<Record<string, unknown> | undefined>;
339
+ protected actionRowScope(name: string, _ctx?: TDbActionScopeContext): Promise<Record<string, unknown> | undefined>;
315
340
  /**
316
341
  * The row-level actions `$actions` and `GET /meta/actions/:id` may list:
317
342
  * those the caller holds a grant on (the `/meta` overlay's rule, without
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aooth/arbac-moost",
3
- "version": "0.1.72",
3
+ "version": "0.1.74",
4
4
  "description": "Moost RBAC integration for aoothjs (migrated from @moostjs/arbac)",
5
5
  "keywords": [
6
6
  "abac",
@@ -60,31 +60,31 @@
60
60
  "access": "public"
61
61
  },
62
62
  "dependencies": {
63
- "@uniqu/core": "^0.1.11",
64
- "@aooth/arbac": "0.1.72",
65
- "@aooth/user": "0.1.72",
66
- "@aooth/arbac-core": "0.1.72"
63
+ "@uniqu/core": "^0.1.12",
64
+ "@aooth/arbac-core": "0.1.74",
65
+ "@aooth/user": "0.1.74",
66
+ "@aooth/arbac": "0.1.74"
67
67
  },
68
68
  "devDependencies": {
69
- "@atscript/core": "^0.1.97",
70
- "@atscript/db": "^0.1.145",
71
- "@atscript/db-sql-tools": "^0.1.145",
72
- "@atscript/db-sqlite": "^0.1.145",
73
- "@atscript/moost-db": "^0.1.145",
74
- "@atscript/typescript": "^0.1.97",
75
- "@moostjs/event-http": "^0.6.41",
69
+ "@atscript/core": "^0.1.99",
70
+ "@atscript/db": "^0.1.147",
71
+ "@atscript/db-sql-tools": "^0.1.147",
72
+ "@atscript/db-sqlite": "^0.1.147",
73
+ "@atscript/moost-db": "^0.1.147",
74
+ "@atscript/typescript": "^0.1.99",
75
+ "@moostjs/event-http": "^0.6.43",
76
76
  "@types/better-sqlite3": "^7.6.13",
77
77
  "better-sqlite3": "^12.6.2",
78
- "unplugin-atscript": "^0.1.97"
78
+ "unplugin-atscript": "^0.1.99"
79
79
  },
80
80
  "peerDependencies": {
81
- "@atscript/db": "^0.1.145",
82
- "@atscript/moost-db": "^0.1.145",
83
- "@atscript/typescript": "^0.1.97",
84
- "@moostjs/event-http": "^0.6.41",
85
- "@wooksjs/event-core": "^0.7.24",
86
- "@wooksjs/event-http": "^0.7.24",
87
- "moost": "^0.6.41"
81
+ "@atscript/db": "^0.1.147",
82
+ "@atscript/moost-db": "^0.1.147",
83
+ "@atscript/typescript": "^0.1.99",
84
+ "@moostjs/event-http": "^0.6.43",
85
+ "@wooksjs/event-core": "^0.7.26",
86
+ "@wooksjs/event-http": "^0.7.26",
87
+ "moost": "^0.6.43"
88
88
  },
89
89
  "peerDependenciesMeta": {
90
90
  "@atscript/db": {