@aooth/arbac-moost 0.1.71 → 0.1.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1,12 +1,201 @@
1
- import { a as ArbacDbScope, c as enforceControlsPolicy, d as NavRelationKey, f as NavTarget, i as conjoinArbacDbScopes, l as extractUsedControlValues, m as ProjectionOf, n as ArbacUserProviderToken, o as AsArbacDbController, p as OwnFieldKey, r as AoothArbacClaims, s as applyAllowedFieldsAndSet, t as ArbacUserProvider, u as ControlsOf } from "./user.provider-C5BrFNxg.mjs";
2
- import { Arbac, Arbac as Arbac$1, TArbacCompiledRule, TArbacEvalResult, TArbacRole, TArbacRoleForResource, TArbacRule, arbacPatternToRegex } from "@aooth/arbac-core";
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";
2
+ import { Arbac, Arbac as Arbac$1, TArbacCompiledRule, TArbacEvalResult, TArbacEvalResult as TArbacEvalResult$1, TArbacRole, TArbacRoleForResource, TArbacRule, arbacPatternToRegex } from "@aooth/arbac-core";
3
3
  import { TAuthGuardDef } from "@moostjs/event-http";
4
4
  import { EventContext } from "@wooksjs/event-core";
5
- import { Mate, TMateParamMeta, TMoostMetadata } from "moost";
6
- import { ControlGate, TProjection } from "@aooth/arbac";
7
- import { TMetaResponse } from "@atscript/db";
8
- import { AsDbReadableController } from "@atscript/moost-db";
9
- import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
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";
8
+ import { TAtscriptAnnotatedType, TAtscriptDataType } from "@atscript/typescript/utils";
9
+ //#region src/db/visibility.d.ts
10
+ /**
11
+ * The slice of an atscript-db readable (table / view) the visibility walk
12
+ * reads — a moost-db controller's `this.readable` satisfies it: identifiers
13
+ * (always visible), nav relations (to reach the joined table), the
14
+ * flattened schema (to split `$select` parents by scope) and the field
15
+ * descriptors (atomic JSON columns, derived-column sources).
16
+ */
17
+ interface VisibilityTableSource {
18
+ primaryKeys: readonly string[];
19
+ preferredId: readonly string[];
20
+ relations?: ReadonlyMap<string, unknown>;
21
+ /**
22
+ * The target table of nav relation `navField` (atscript-db's
23
+ * `readable.relatedTable`); absent / `undefined` → the joined table's
24
+ * identifiers are not exempted.
25
+ */
26
+ relatedTable?(navField: string): VisibilityTableSource | undefined;
27
+ /** Flattened schema (dot-paths) — used to split `$select` parents by scope. */
28
+ flatMap?: ReadonlyMap<string, unknown>;
29
+ /** Navigation field paths — their joined descendants are not own columns. */
30
+ navFields?: ReadonlySet<string>;
31
+ /**
32
+ * The readable's annotated type — the `$with` target registry key and the
33
+ * owner a self-`ref` points at.
34
+ *
35
+ * @since 0.1.72
36
+ */
37
+ type?: object;
38
+ /**
39
+ * Field descriptors: `derived` marks a `@db.column.derived` field
40
+ * (visible only while its source is).
41
+ *
42
+ * @since 0.1.72
43
+ */
44
+ fieldDescriptors?: ReadonlyArray<{
45
+ path: string;
46
+ derived?: {
47
+ sourcePath: string;
48
+ };
49
+ }>;
50
+ /**
51
+ * Paths stored as ONE JSON column (atscript-db's `readable.jsonParents`,
52
+ * relational adapters): visible whole or not at all.
53
+ *
54
+ * @since 0.1.72
55
+ */
56
+ jsonParents?: ReadonlySet<string>;
57
+ /** Whether the adapter searches natively (`/meta` search-surface pruning). @since 0.1.72 */
58
+ isSearchable?(): boolean;
59
+ }
60
+ /** Relation name → the visibility its joined rows obey, or `null` when hidden. */
61
+ type ArbacRelationResolution = ReadonlyMap<string, MetaVisibility | null>;
62
+ /**
63
+ * The principal's field-visibility verdict set, assembled once per request
64
+ * (or per `/meta` overlay) and threaded through the pruning walk.
65
+ */
66
+ interface MetaVisibility {
67
+ /**
68
+ * Projection union for this level's own fields. `{}` (the universe) means
69
+ * unrestricted — possible when only a `with` sub-scope restricts anything.
70
+ */
71
+ allowed: TProjection;
72
+ /**
73
+ * Identifier fields reads ALWAYS return regardless of projection (the read
74
+ * path widens `preferredId` back in, and PK addressing requires the key) —
75
+ * hiding them would advertise less than reads deliver and break `/one/:id`.
76
+ */
77
+ alwaysVisible: ReadonlySet<string>;
78
+ /** Relation names declared via `with.<name>` (see {@link collectWithGrantNames}). */
79
+ withGrants: ReadonlySet<string>;
80
+ /**
81
+ * Visibility of a relation's JOINED fields: for a declared relation, the
82
+ * union of its `with.<name>` sub-scopes; for an undeclared one, the
83
+ * caller's own read scopes on the related table (resolved per request —
84
+ * `undefined` when the relation is hidden). The related table's own
85
+ * identifiers are always visible. A path `rel.x.y` is checked as `x.y`
86
+ * against it, recursively. A hand-built visibility without it lets
87
+ * declared paths through unchecked (legacy behavior).
88
+ */
89
+ relation?: (name: string) => MetaVisibility | undefined;
90
+ /**
91
+ * Relations of this level's table. An undeclared one is visible only when
92
+ * the projection union allows its name AND {@link relation} resolves it
93
+ * (the `$with` inherit-target policy); absent → relation names are
94
+ * ordinary projection paths (legacy).
95
+ */
96
+ relationNames?: ReadonlySet<string>;
97
+ /**
98
+ * Precompiled `isFieldAllowed(path, allowed)` — plus the derived-column
99
+ * rule (a `@db.column.derived` field is visible only while its source is).
100
+ * Supplied by {@link buildScopeVisibility}; a hand-built visibility falls
101
+ * back to `isFieldAllowed`.
102
+ */
103
+ isAllowed?: (path: string) => boolean;
104
+ /** The scopes this level was built from (set by {@link buildScopeVisibility}). */
105
+ scopes?: ArbacDbScope[];
106
+ /** The table this level describes, when known (set by {@link buildScopeVisibility}). */
107
+ table?: VisibilityTableSource;
108
+ /**
109
+ * Fields the principal's WRITE scopes allow (union of `allowedFields`, or
110
+ * `"all"` for an unrestricted write grant). A field that is writable but not
111
+ * read-visible is NOT pruned from `/meta` — it survives with a `db.writeOnly`
112
+ * stamp (type only; rows/projections never carry it), so generic forms and
113
+ * client preflight validators can still SET sealed fields (e.g. credentials
114
+ * behind a read projection). Absent → nothing extra survives.
115
+ */
116
+ writable?: ReadonlySet<string> | "all";
117
+ /**
118
+ * Nav relations the WRITE scopes opt into nested writes (union of
119
+ * `nestedWrites`). A relation path is writable only when listed here (and
120
+ * covered by {@link writable}) — mirroring the 403 the write endpoints
121
+ * answer for any other nav key. Absent → no relation is writable.
122
+ *
123
+ * @since 0.1.72
124
+ */
125
+ nestedWrites?: ReadonlySet<string>;
126
+ }
127
+ /**
128
+ * Union the per-scope `projection` whitelists into the single access-control
129
+ * projection used for FIELD-EXISTENCE decisions (`hasField` parity + `/meta`
130
+ * pruning). Returns `undefined` when the union imposes no restriction — no
131
+ * scopes at all, or any scope without a `projection` (a universal grant makes
132
+ * `unionProjections` collapse to the universe `{}`).
133
+ */
134
+ declare function unionScopeProjection(scopes: ArbacDbScope[]): TProjection | undefined;
135
+ /**
136
+ * Relation names any scope explicitly declares via `with.<name>` — those
137
+ * relations are governed by the declared sub-scopes alone (parent authority),
138
+ * and the declaration implies the relation EXISTS for this principal even
139
+ * when the projection union does not name it (see {@link ArbacDbScope.with}).
140
+ */
141
+ declare function collectWithGrantNames(scopes: ArbacDbScope[]): ReadonlySet<string>;
142
+ /**
143
+ * Union of write-scope `allowedFields`; `"all"` for field-unrestricted write
144
+ * access. The whitelist exists only when at least one scope carries an
145
+ * `allowedFields` array — scoped-but-unlisted writes are field-unrestricted.
146
+ */
147
+ declare function collectWritableFields(scopes: ArbacDbScope[], unrestricted: boolean): ReadonlySet<string> | "all" | undefined;
148
+ /**
149
+ * Whether a flattened dot-path field exists for this principal.
150
+ *
151
+ * - A relation declared via `with.<name>` is visible; a path under it
152
+ * (`rel.x.y`) is checked as `x.y` against its sub-scope visibility.
153
+ * - An undeclared relation of the table (see {@link MetaVisibility.relationNames})
154
+ * is visible only when the projection allows its name and the caller's own
155
+ * grant on the related table resolved ({@link MetaVisibility.relation});
156
+ * paths under it are checked against that grant.
157
+ * - Anything else follows the projection union (and the derived-column rule).
158
+ *
159
+ * So a field a joined table hides is as unknown in a `$with` sub-query as a
160
+ * top-level hidden column is in the main query (no filter/sort value oracle).
161
+ */
162
+ declare function isMetaFieldVisible(path: string, vis: MetaVisibility): boolean;
163
+ /**
164
+ * PK + `preferredId` for a controller's table/readable — the
165
+ * {@link MetaVisibility.alwaysVisible} set. Kept for compatibility: the
166
+ * controllers now pass `this.readable` and it is derived there.
167
+ */
168
+ declare function metaAlwaysVisibleFields(_controller: object, source: {
169
+ primaryKeys: readonly string[];
170
+ preferredId: readonly string[];
171
+ }): ReadonlySet<string>;
172
+ /** Options of {@link buildScopeVisibility}. @since 0.1.72 */
173
+ interface ScopeVisibilityOptions {
174
+ /** Identifiers exempt from the projection; default: the table's PK + `preferredId`. */
175
+ alwaysVisible?: ReadonlySet<string>;
176
+ /**
177
+ * The undeclared relations resolved for this level (the caller's own grant
178
+ * on each related table — see `resolveRelationTree`); an undeclared
179
+ * relation missing here is hidden.
180
+ */
181
+ relations?: ArbacRelationResolution;
182
+ }
183
+ /**
184
+ * Build the {@link MetaVisibility} a set of read scopes implies — the single
185
+ * structure behind `hasField`, `/meta` pruning, `$select` value stripping
186
+ * and the `$with` overlay, so they cannot drift. Own fields: the projection
187
+ * union (`{}` when unrestricted), normalized for atomic JSON columns and
188
+ * derived-column sources, precompiled into `isAllowed`. Relations: an
189
+ * undeclared one → its node in `relations` (none → hidden); a declared
190
+ * `with.<name>` → the union of its sub-scopes (lazily built when `relations`
191
+ * has no node for it). The related table's own PK / `preferredId` stay
192
+ * visible.
193
+ *
194
+ * @param opts - {@link ScopeVisibilityOptions}; a bare identifier set is the
195
+ * 0.1.71 `alwaysVisible` argument
196
+ */
197
+ declare function buildScopeVisibility(scopes: ArbacDbScope[], table: VisibilityTableSource | undefined, opts?: ScopeVisibilityOptions | ReadonlySet<string>): MetaVisibility;
198
+ //#endregion
10
199
  //#region src/arbac.composables.d.ts
11
200
  /**
12
201
  * The scopes cached for the current event (`undefined` before the authorize
@@ -17,13 +206,26 @@ import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
17
206
  * `TScope` is a caller-side type witness (see `evaluate` below).
18
207
  */
19
208
  declare function getArbacScopes<TScope extends object>(ctx?: EventContext): TScope[] | undefined;
209
+ /** Options of `useArbac().evaluate()` / `evaluateOrThrow()`. */
210
+ interface ArbacEvaluateOptions {
211
+ /** Resource to evaluate; defaults to the current handler's. */
212
+ resource?: string;
213
+ /** Action to evaluate; defaults to the current handler's. */
214
+ action?: string;
215
+ /**
216
+ * The atscript-db readable whose schema a credential-attenuation
217
+ * conjunction uses to subtract nested exclusions exactly — pass the
218
+ * related table when evaluating a grant on it (e.g. a `$with` target).
219
+ * Defaults to the current controller's `readable`.
220
+ *
221
+ * @since 0.1.72
222
+ */
223
+ table?: VisibilityTableSource;
224
+ }
20
225
  interface ArbacBindings {
21
226
  getScopes: <TScope extends object>() => TScope[] | undefined;
22
227
  setScopes: <TScope extends object>(scope: TScope[] | undefined) => void;
23
- evaluate: <TScope extends object>(opts?: {
24
- resource?: string;
25
- action?: string;
26
- }) => Promise<{
228
+ evaluate: <TScope extends object>(opts?: ArbacEvaluateOptions) => Promise<{
27
229
  allowed: boolean;
28
230
  scopes?: TScope[];
29
231
  userId: string;
@@ -37,10 +239,7 @@ interface ArbacBindings {
37
239
  * with another policy, to filter UI metadata, or to fall through to a
38
240
  * different authorization path).
39
241
  */
40
- evaluateOrThrow: <TScope extends object>(opts?: {
41
- resource?: string;
42
- action?: string;
43
- }) => Promise<{
242
+ evaluateOrThrow: <TScope extends object>(opts?: ArbacEvaluateOptions) => Promise<{
44
243
  allowed: true;
45
244
  scopes?: TScope[];
46
245
  userId: string;
@@ -91,138 +290,164 @@ declare const ArbacResource: (name: string) => MethodDecorator & ClassDecorator
91
290
  */
92
291
  declare const ArbacAction: (name: string) => MethodDecorator & ClassDecorator & ParameterDecorator & PropertyDecorator;
93
292
  //#endregion
94
- //#region src/arbac.mate.d.ts
95
- /**
96
- * ARBAC metadata fields attached to classes and methods by ARBAC decorators.
97
- *
98
- * Augments the shared `TMoostMetadata` workspace via TypeScript declaration
99
- * merging so other moost-aware tooling (and `getArbacMate()` readers) sees
100
- * these fields with full type safety.
101
- */
102
- interface TArbacMeta {
103
- arbacResourceId?: string;
104
- arbacActionId?: string;
105
- arbacPublic?: boolean;
106
- }
107
- declare module "moost" {
108
- interface TMoostMetadata extends TArbacMeta {}
109
- }
110
- type ArbacMate = Mate<TMoostMetadata & {
111
- params: TMateParamMeta[];
112
- }, TMoostMetadata & {
113
- params: TMateParamMeta[];
114
- }>;
115
- /**
116
- * Returns the shared `Mate` instance typed with ARBAC metadata fields.
117
- *
118
- * All ARBAC decorators write through this typed wrapper so reads and writes
119
- * stay type-checked against `TArbacMeta`.
120
- */
121
- declare function getArbacMate(): ArbacMate;
122
- //#endregion
123
293
  //#region src/db/as-arbac-db-readable-controller.d.ts
124
294
  /**
125
295
  * Read-only mirror of {@link AsArbacDbController} for view-style controllers
126
296
  * built on top of `@atscript/moost-db`'s {@link AsDbReadableController}.
127
- * Applies the same filter / projection / controls overlays — no write-side
128
- * hooks because the parent exposes none.
297
+ * Applies the same fail-closed `prepareRequest` and filter / projection /
298
+ * controls overlays — no write-side hooks because the parent exposes none.
129
299
  */
130
300
  declare class AsArbacDbReadableController<T extends TAtscriptAnnotatedType = TAtscriptAnnotatedType> extends AsDbReadableController<T> {
131
- protected transformFilter(filter: Record<string, unknown> | undefined): Promise<Record<string, unknown>>;
132
- protected transformProjection(projection?: TProjection): TProjection | undefined | Promise<TProjection | undefined>;
133
- protected validateControls(controls: Record<string, unknown>, type: "query" | "pages" | "getOne"): string | undefined;
134
- /**
135
- * Same ARBAC `/meta` overlay as the writable controller (actions/crud
136
- * filtering + BUG-3 field-surface pruning by the read scopes' projection
137
- * union) — view-style metas leak hidden field names identically.
138
- */
301
+ /** Makes this controller the `$with` policy source for its table's ARBAC resource. */
302
+ protected readonly _arbacRelationTarget: true;
303
+ /** Same contract as {@link AsArbacDbController.prepareRequest}. */
304
+ protected prepareRequest(ctx: TDbRequestContext): Promise<void>;
305
+ /** Same contract as {@link AsArbacDbController.transformFilter}. */
306
+ protected transformFilter(filter: Record<string, unknown> | undefined): Record<string, unknown>;
307
+ /** Same contract as {@link AsArbacDbController.transformProjection}. */
308
+ protected transformProjection(projection?: TProjection): TProjection | undefined;
309
+ /** Same contract as {@link AsArbacDbController.validateControls}. */
310
+ protected validateControls(controls: Record<string, unknown>, type: TDbControlsType): string | undefined;
311
+ /** Same contract as {@link AsArbacDbController.applyMetaOverlay}. */
139
312
  protected applyMetaOverlay(meta: TMetaResponse): Promise<TMetaResponse>;
140
- /**
141
- * Scope-aware field visibility — same contract as
142
- * {@link AsArbacDbController.hasField}.
143
- */
313
+ /** Same contract as {@link AsArbacDbController.authorizeForm}. */
314
+ protected authorizeForm(_name: string, actionNames: readonly string[]): Promise<boolean>;
315
+ /** Same contract as {@link AsArbacDbController.actionRowScope}. */
316
+ protected actionRowScope(name: string): Promise<Record<string, unknown> | undefined>;
317
+ /** Same contract as {@link AsArbacDbController.allowedActions}. */
318
+ protected allowedActions(names: readonly string[]): Promise<string[]>;
319
+ /** Same contract as {@link AsArbacDbController.[ARBAC_DELEGATED_AUTH]}. */
320
+ [ARBAC_DELEGATED_AUTH](method: string): boolean;
321
+ /** Same contract as {@link AsArbacDbController.hasField}. */
144
322
  protected hasField(path: string): boolean;
145
323
  }
146
324
  //#endregion
147
- //#region src/db/meta-projection.d.ts
325
+ //#region src/db/as-arbac-value-help-controller.d.ts
148
326
  /**
149
- * Union the per-scope `projection` whitelists into the single access-control
150
- * projection used for FIELD-EXISTENCE decisions (`hasField` parity + `/meta`
151
- * pruning). Returns `undefined` when the union imposes no restriction — no
152
- * scopes at all, or any scope without a `projection` (a universal grant makes
153
- * `unionProjections` collapse to the universe `{}`).
327
+ * ARBAC-scoped mirror of `@atscript/moost-db`'s {@link AsValueHelpController}
328
+ * (bring your own backing source): the same scope contract as
329
+ * {@link AsArbacDbController} applied through the value-help seams.
330
+ *
331
+ * - `prepareRequest` — every route (`/query`, `/pages`, `/one`, `/meta`)
332
+ * resolves the scopes first: the authorize interceptor's cached ones, else
333
+ * a fresh evaluation. Denied → 403. `@Public()` / `arbacPublic` does NOT
334
+ * bypass it, and a missing interceptor fails closed rather than open.
335
+ * - `transformFilter` — the request filter ∧ the union of scope `filter`s
336
+ * (denied → match-nothing). `/one` outside the scope answers 404.
337
+ * - `transformProjection` — `$select` restricted to the scopes' projection
338
+ * union; the PK is always kept.
339
+ * - `hasField` — a scope-hidden field is `Unknown field "x"` (400) in
340
+ * filter / `$sort` / `$select`, and never matches `$search`.
341
+ * - `validateControls` — per-scope `controls` gates (e.g. `$search: false`).
342
+ * - `/meta` — `crud` filtered per action, `fields` / `type` pruned by the
343
+ * read scopes' projection union, `searchable` dropped when no searchable
344
+ * field survives.
345
+ *
346
+ * **Action names**: the data routes are re-tagged with the standard table
347
+ * READ action ids — `runQuery` → `query`, `runPages` → `pages`, `runGetOne`
348
+ * → `getOne`, `runGetOneComposite` → `getOneComposite` (`meta` / `metaForm`
349
+ * already match) — so `allowTableRead(resource, { scope })` grants a
350
+ * value-help source exactly like a table. The resource is the controller's
351
+ * `@ArbacResource(...)` (else the ARBAC fallback: controller id, then class
352
+ * name).
353
+ *
354
+ * Implementations of `query` must honor {@link hasField} for `$search` (skip
355
+ * hidden fields) — the ARBAC layer cannot see inside a custom search.
356
+ *
357
+ * @since 0.1.72
154
358
  */
155
- declare function unionScopeProjection(scopes: ArbacDbScope[]): TProjection | undefined;
359
+ declare abstract class AsArbacValueHelpController<T extends TAtscriptAnnotatedType = TAtscriptAnnotatedType, DataType = TAtscriptDataType<T>> extends AsValueHelpController<T, DataType> {
360
+ private _arbacHost?;
361
+ private get arbacHost();
362
+ protected prepareRequest(_ctx: TDbRequestContext): Promise<void>;
363
+ protected transformFilter(filter: FilterExpr): FilterExpr;
364
+ protected transformProjection(select: ValueHelpSelect<DataType> | undefined): ValueHelpSelect<DataType> | undefined;
365
+ protected hasField(path: string): boolean;
366
+ protected validateControls(controls: Record<string, unknown>, type: TDbControlsType): string | undefined;
367
+ 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>>;
378
+ }
156
379
  /**
157
- * Relation names any scope explicitly grants via `with.<name>` — an explicit
158
- * content grant implies the relation EXISTS for this principal even when the
159
- * projection union does not name it (the sub-scope, not the projection, owns
160
- * the joined rows' policy — see {@link ArbacDbScope.with}).
380
+ * ARBAC-scoped mirror of `@atscript/moost-db`'s
381
+ * {@link AsJsonValueHelpController} (a static in-memory row set) — the same
382
+ * contract, action names and resource naming as
383
+ * {@link AsArbacValueHelpController}; `$search` already skips scope-hidden
384
+ * fields.
385
+ *
386
+ * ```ts
387
+ * @Controller('dicts/status')
388
+ * @ArbacResource('dict-status')
389
+ * class StatusDictController extends AsArbacJsonValueHelpController<typeof StatusDict> {
390
+ * constructor(app: Moost) { super(StatusDict, STATUS_ROWS, app) }
391
+ * }
392
+ * // grant: allowTableRead('dict-status', { scope: () => ({ filter: { active: true } }) })
393
+ * ```
394
+ *
395
+ * @since 0.1.72
161
396
  */
162
- declare function collectWithGrantNames(scopes: ArbacDbScope[]): ReadonlySet<string>;
397
+ declare class AsArbacJsonValueHelpController<T extends TAtscriptAnnotatedType = TAtscriptAnnotatedType, DataType = TAtscriptDataType<T>> extends AsJsonValueHelpController<T, DataType> {
398
+ private _arbacHost?;
399
+ private get arbacHost();
400
+ protected prepareRequest(_ctx: TDbRequestContext): Promise<void>;
401
+ protected transformFilter(filter: FilterExpr): FilterExpr;
402
+ protected transformProjection(select: ValueHelpSelect<DataType> | undefined): ValueHelpSelect<DataType> | undefined;
403
+ protected hasField(path: string): boolean;
404
+ protected validateControls(controls: Record<string, unknown>, type: TDbControlsType): string | undefined;
405
+ 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>;
416
+ }
417
+ //#endregion
418
+ //#region src/db/relation-policy.d.ts
163
419
  /**
164
- * The principal's field-visibility verdict set, assembled once per request
165
- * (or per `/meta` overlay) and threaded through the pruning walk.
420
+ * Register an ARBAC DB controller as an owner of its readable's ARBAC
421
+ * resource, so a `$with` from another controller can evaluate the caller's
422
+ * own grant on the joined table. Called once per controller instance (a
423
+ * field initializer of the ARBAC DB controllers). Scoped per moost app and
424
+ * keyed by the readable's annotated type; a newer instance of the same class
425
+ * replaces the older one.
426
+ *
427
+ * @returns `true` (so it can initialize a field)
428
+ * @since 0.1.72
166
429
  */
167
- interface MetaVisibility {
168
- /**
169
- * Projection union for this level's own fields. `{}` (the universe) means
170
- * unrestricted — possible when only a `with` sub-scope restricts anything.
171
- */
172
- allowed: TProjection;
173
- /**
174
- * Identifier fields reads ALWAYS return regardless of projection (the read
175
- * path widens `preferredId` back in, and PK addressing requires the key) —
176
- * hiding them would advertise less than reads deliver and break `/one/:id`.
177
- */
178
- alwaysVisible: ReadonlySet<string>;
179
- /** Relation names granted via `with.<name>` (see {@link collectWithGrantNames}). */
180
- withGrants: ReadonlySet<string>;
181
- /**
182
- * Visibility of a `with`-granted relation's JOINED fields, built from the
183
- * union of its `with.<name>` sub-scopes (with the related table's own
184
- * identifiers always visible). A path `rel.x.y` is checked as `x.y`
185
- * against it, recursively for nested relations. The scope-driven builders
186
- * ({@link buildScopeVisibility}) always supply it; a hand-built visibility
187
- * without it lets granted paths through unchecked (legacy behavior).
188
- */
189
- relation?: (name: string) => MetaVisibility | undefined;
190
- /**
191
- * Precompiled `isFieldAllowed(path, allowed)`. Supplied by
192
- * {@link buildScopeVisibility}; a hand-built visibility falls back to
193
- * `isFieldAllowed`.
194
- */
195
- isAllowed?: (path: string) => boolean;
196
- /** The scopes this level was built from (set by {@link buildScopeVisibility}). */
197
- scopes?: ArbacDbScope[];
198
- /** The table this level describes, when known (set by {@link buildScopeVisibility}). */
199
- table?: VisibilityTableSource;
200
- /**
201
- * Fields the principal's WRITE scopes allow (union of `allowedFields`, or
202
- * `"all"` for an unrestricted write grant). A field that is writable but not
203
- * read-visible is NOT pruned from `/meta` — it survives with a `db.writeOnly`
204
- * stamp (type only; rows/projections never carry it), so generic forms and
205
- * client preflight validators can still SET sealed fields (e.g. credentials
206
- * behind a read projection). Absent → nothing extra survives.
207
- */
208
- writable?: ReadonlySet<string> | "all";
209
- }
430
+ declare function registerArbacDbTarget(controller: object): true;
210
431
  /**
211
- * Union of write-scope `allowedFields`; `"all"` for field-unrestricted write
212
- * access. Mirrors `prepareScopeOverlay`: the whitelist exists only when at
213
- * least one scope carries an `allowedFields` array — scoped-but-unlisted
214
- * writes are field-unrestricted.
432
+ * The ARBAC resource + action a controller's handler is authorized as — the
433
+ * precedence `useArbac()` applies while that handler serves a request.
434
+ * Resource: method `@ArbacResource` → class `@ArbacResource` → controller id
435
+ * → class name. Action: method `@ArbacAction` → `@DbAction` name → class
436
+ * `@ArbacAction` → method `@Id` → method name.
437
+ *
438
+ * @since 0.1.72
215
439
  */
216
- declare function collectWritableFields(scopes: ArbacDbScope[], unrestricted: boolean): ReadonlySet<string> | "all" | undefined;
440
+ declare function resolveHandlerArbacIds(instance: object, methodName: string): {
441
+ resource: string;
442
+ action: string;
443
+ };
444
+ //#endregion
445
+ //#region src/db/meta-projection.d.ts
217
446
  /**
218
- * Whether a flattened dot-path field exists for this principal. A
219
- * `with`-granted relation itself is always visible (the grant implies it);
220
- * a path UNDER it (`rel.x.y`) is checked as `x.y` against the relation's
221
- * sub-scope visibility ({@link MetaVisibility.relation}) — so a field the
222
- * sub-scope hides is as unknown in a `$with` sub-query as a top-level hidden
223
- * column is in the main query (no filter/sort value oracle).
447
+ * Foreign `ref` targets for `/meta` pruning: annotated type → the caller's
448
+ * visibility on it, or `null` when the caller cannot read it.
224
449
  */
225
- declare function isMetaFieldVisible(path: string, vis: MetaVisibility): boolean;
450
+ type MetaRefTargets = ReadonlyMap<object, MetaVisibility | null>;
226
451
  /**
227
452
  * Prune a `/meta` envelope down to the fields the principal's read scopes can
228
453
  * ever surface, so a scoped UI cannot even OFFER an out-of-scope column (the
@@ -231,96 +456,50 @@ declare function isMetaFieldVisible(path: string, vis: MetaVisibility): boolean;
231
456
  *
232
457
  * - `fields` — the flat capability map (sortable/filterable flags).
233
458
  * - `type` — the serialized annotated type (what dynamic clients build
234
- * tables/forms from); nested object props prune by dot-path. A relation
235
- * prop visible through the projection survives whole; a `with`-granted
236
- * one is pruned by its sub-scope (recursively), matching `hasField`.
237
- * - `relations` — entries neither projected nor `with`-granted.
459
+ * tables/forms from); nested object props prune by dot-path. A visible
460
+ * relation's nav prop is pruned by that relation's visibility (declared
461
+ * sub-scope or the caller's own grant on the target), recursively. A
462
+ * `ref` whose target field is hidden (own table) or whose target table the
463
+ * caller cannot read (`refTargets`) is stripped.
464
+ * - `relations` — hidden relations dropped.
238
465
  * - `versionColumn` — dropped when the OCC column itself is hidden.
239
466
  *
240
467
  * NEVER mutates the input — the base controller caches the static envelope
241
468
  * (`applyMetaOverlay` contract); every pruned branch is a fresh object.
242
469
  */
243
- declare function pruneMetaByVisibility(meta: TMetaResponse, vis: MetaVisibility): TMetaResponse;
470
+ declare function pruneMetaByVisibility(meta: TMetaResponse, vis: MetaVisibility, refTargets?: MetaRefTargets): TMetaResponse;
244
471
  /**
245
- * The slice of an atscript-db readable (table / view) the visibility walk
246
- * reads — a moost-db controller's `this.readable` satisfies it: identifiers
247
- * (always visible), nav relations (to reach the joined table) and the
248
- * flattened schema (to split `$select` parents by scope).
249
- */
250
- interface VisibilityTableSource {
251
- primaryKeys: readonly string[];
252
- preferredId: readonly string[];
253
- relations?: ReadonlyMap<string, unknown>;
254
- /**
255
- * The target table of nav relation `navField` (atscript-db's
256
- * `readable.relatedTable`); absent / `undefined` → the joined table's
257
- * identifiers are not exempted.
258
- */
259
- relatedTable?(navField: string): VisibilityTableSource | undefined;
260
- /** Flattened schema (dot-paths) — used to split `$select` parents by scope. */
261
- flatMap?: ReadonlyMap<string, unknown>;
262
- /** Navigation field paths — their joined descendants are not own columns. */
263
- navFields?: ReadonlySet<string>;
264
- }
265
- /**
266
- * Build the {@link MetaVisibility} a set of read scopes implies — the single
267
- * structure behind `hasField`, `/meta` pruning, `$select` value stripping
268
- * and the `$with` overlay, so they cannot drift. Own fields: the projection
269
- * union (`{}` when unrestricted), precompiled into `isAllowed`. A
270
- * `with`-granted relation: a lazily built (memoized) child visibility over
271
- * the union of its `with.<name>` sub-scopes — the same sub-scopes joined rows
272
- * are stripped with — whose `alwaysVisible` is the related table's own PK /
273
- * `preferredId` (reached through `table.relatedTable`; without it, no
274
- * related identifier is exempt).
472
+ * The full ARBAC `/meta` overlay, shared by the ARBAC DB and value-help
473
+ * controllers.
275
474
  *
276
- * @param alwaysVisible - identifiers exempt from the projection; defaults to
277
- * the table's PK + `preferredId`
278
- */
279
- declare function buildScopeVisibility(scopes: ArbacDbScope[], table: VisibilityTableSource | undefined, alwaysVisible?: ReadonlySet<string>): MetaVisibility;
280
- /**
281
- * Shared body of both ARBAC controllers' `hasField` overrides (BUG-3 twin of
282
- * the `/meta` pruning): a field outside the read-scope projection union —
283
- * or, for a `rel.x` path under a `with`-granted relation, outside that
284
- * relation's sub-scope union — must be indistinguishable from a field that
285
- * does not exist. Returns `true` when there are no scopes (unscoped grant).
475
+ * 1. `actions` and `crud` are filtered by evaluating each one exactly as its
476
+ * handler is authorized (`useArbac` precedence): a crud op through its
477
+ * handler method(s) — `one` via `getOne` / `getOneComposite`, `remove`
478
+ * via `remove` / `removeComposite` (allowed when any is) — and an action
479
+ * through its method (`@ArbacAction` → `@DbAction` name → …).
480
+ * 2. The FIELD surface (`fields`, serialized `type`, `relations`,
481
+ * `versionColumn`) is pruned by the read scopes' visibility: the
482
+ * projection union, derived-column sources, atomic JSON columns, and the
483
+ * `$with` relation policy (declared `with` sub-scopes, else the caller's
484
+ * own grant on the related table — no grant → relation and nav type
485
+ * dropped). `ref`s to hidden targets are stripped. An allowed read op
486
+ * without scopes is an unscoped grant (no field restriction; relations
487
+ * still follow the relation policy). No read op allowed → no pruning:
488
+ * `crud` already advertises no read surface, and write-only principals
489
+ * still need `type` for their insert/update forms. PK + `preferredId` are
490
+ * 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).
286
495
  *
287
- * @param source - the controller's `this.readable`; an identifier set (the
288
- * 0.1.67 signature) still works, without related-table identifiers
496
+ * @param source - the controller's `this.readable` (identifiers, relations
497
+ * and schema rules are derived from it); an identifier set (the 0.1.67
498
+ * 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
289
501
  */
290
- declare function isScopedFieldVisible(scopes: ArbacDbScope[], path: string, source: VisibilityTableSource | ReadonlySet<string>): boolean;
291
- /**
292
- * PK + `preferredId` for a controller's table/readable — the
293
- * {@link MetaVisibility.alwaysVisible} set. Kept for compatibility: the
294
- * controllers now pass `this.readable` and it is derived there.
295
- */
296
- declare function metaAlwaysVisibleFields(_controller: object, source: {
297
- primaryKeys: readonly string[];
298
- preferredId: readonly string[];
299
- }): ReadonlySet<string>;
300
- /**
301
- * The full ARBAC `/meta` overlay, shared by `AsArbacDbController` and
302
- * `AsArbacDbReadableController`: filter `actions` + `crud` by per-action
303
- * evaluation, then prune the FIELD surface (`fields`, serialized `type`,
304
- * `relations`, `versionColumn`) by the read scopes' projection union.
305
- *
306
- * Field pruning (BUG-3): a scope projection must remove fields from the META
307
- * entirely — `transformProjection` already strips their VALUES from every
308
- * read, but the envelope still advertised the full field map, so a scoped UI
309
- * offered columns that could never populate, and secret-bearing column NAMES
310
- * leaked. The pruning union comes from the read-op evaluations already
311
- * computed for the `crud` overlay (same `unionProjections` the read path
312
- * applies). An allowed read op WITHOUT scopes is an unscoped grant —
313
- * universe, no pruning (unchanged behavior for unscoped roles). No read op
314
- * allowed → the projection union is empty → no pruning: `crud` already
315
- * advertises no read surface, and write-only principals still need `type`
316
- * for their insert/update forms. PK + `preferredId` are never pruned —
317
- * reads always return those fields (projection widening / id addressing).
318
- *
319
- * @param source - the controller's `this.readable` (identifiers and related
320
- * tables are derived from it); an identifier set (the 0.1.67 signature)
321
- * still works, without related-table identifiers
322
- */
323
- declare function applyArbacMetaOverlay(meta: TMetaResponse, source: VisibilityTableSource | ReadonlySet<string>): Promise<TMetaResponse>;
502
+ declare function applyArbacMetaOverlay(meta: TMetaResponse, source: VisibilityTableSource | ReadonlySet<string>, indexes?: readonly TDbIndexFieldPaths[]): Promise<TMetaResponse>;
324
503
  /**
325
504
  * Test-friendly internal helper — exported for unit tests and helper
326
505
  * composition; regular consumers should not call this directly.
@@ -335,6 +514,277 @@ declare function applyArbacMetaOverlay(meta: TMetaResponse, source: VisibilityTa
335
514
  */
336
515
  declare function collectMethodNames(instance: object): string[];
337
516
  //#endregion
517
+ //#region src/db/request-scopes.d.ts
518
+ /**
519
+ * The scopes resolved for the current event, or `undefined` before
520
+ * {@link resolveRequestScopes} ran.
521
+ *
522
+ * @since 0.1.72
523
+ */
524
+ declare function cachedRequestScopes(): ArbacDbScope[] | undefined;
525
+ /**
526
+ * Resolve the current event's scopes once: the ones the authorize
527
+ * interceptor cached, else an ARBAC evaluation of the handler's
528
+ * resource/action. The empty-scope rule (`normalizeScopes` from
529
+ * `@aooth/arbac`): an allowed grant without scopes is unrestricted (`[{}]`),
530
+ * a scope predicate returning nothing contributes nothing, and an allowed
531
+ * evaluation left with NO scope is a deny. A deny throws 403.
532
+ *
533
+ * @since 0.1.72
534
+ */
535
+ declare function resolveRequestScopes(): Promise<ArbacDbScope[]>;
536
+ /**
537
+ * The resolved scopes, synchronously — for the hooks that run after the
538
+ * entry point (`validateControls`, `transformFilter`, the write guards …).
539
+ * Unresolved scopes are a 403, never "unrestricted".
540
+ *
541
+ * @since 0.1.72
542
+ */
543
+ declare function requireRequestScopes(): ArbacDbScope[];
544
+ /**
545
+ * Shared body of the ARBAC controllers' `hasField` overrides: a field
546
+ * outside the read-scope visibility — hidden by the projection union, a
547
+ * derived field whose source is hidden, a path inside a hidden atomic JSON
548
+ * column, a hidden relation, or a `rel.x` path the relation's policy hides —
549
+ * is indistinguishable from a field that does not exist. Unresolved scopes
550
+ * hide every field.
551
+ *
552
+ * @since 0.1.72
553
+ */
554
+ declare function requestFieldVisible(path: string, source: VisibilityTableSource | ReadonlySet<string>): boolean;
555
+ /**
556
+ * Whether `path` is visible to `scopes` over `source` (see
557
+ * {@link requestFieldVisible}, which reads the request's scopes).
558
+ *
559
+ * @param source - the controller's `this.readable`; an identifier set (the
560
+ * 0.1.67 signature) still works, without relation / schema rules
561
+ */
562
+ declare function isScopedFieldVisible(scopes: ArbacDbScope[], path: string, source: VisibilityTableSource | ReadonlySet<string>): boolean;
563
+ /**
564
+ * Body of the ARBAC DB / value-help controllers' `prepareRequest` — every
565
+ * endpoint, `@DbAction` handlers included (`endpoint: "action"`, before any
566
+ * action id / row is read): resolve the request's scopes
567
+ * ({@link resolveRequestScopes} — 403 on denial, `arbacPublic` does not
568
+ * bypass) and, on read endpoints, the `$with` relation policy for the
569
+ * requested relations.
570
+ *
571
+ * @since 0.1.72
572
+ */
573
+ declare function prepareArbacRequest(ctx: TDbRequestContext, readable?: VisibilityTableSource): Promise<void>;
574
+ /**
575
+ * `authorizeForm` for the ARBAC DB controllers: a form is served iff the
576
+ * caller may run at least one of the actions that take it as input — each
577
+ * evaluated as the resource/action its handler is authorized as.
578
+ *
579
+ * @since 0.1.72
580
+ */
581
+ declare function authorizeArbacForm(actionNames: readonly string[]): Promise<boolean>;
582
+ /**
583
+ * Body of the ARBAC DB controllers' `allowedActions`: the action `names` the
584
+ * caller holds a grant on — each evaluated as {@link actionArbacIds} names
585
+ * it, exactly the `/meta` overlay's action rule (memoized per event, so the
586
+ * evaluations `prepareRequest` / the overlay already ran are reused).
587
+ *
588
+ * @since 0.1.72
589
+ */
590
+ declare function arbacAllowedActions(names: readonly string[]): Promise<string[]>;
591
+ /**
592
+ * Body of the ARBAC DB controllers' `actionRowScope`: the rows the
593
+ * `@DbAction` `name` may run on — the filter of the caller's grant on the
594
+ * action ({@link actionArbacIds}: every handler method of the action, or a
595
+ * 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
599
+ * already applies it → `undefined`. Equal filters are returned as ONE
600
+ * object per request.
601
+ *
602
+ * @since 0.1.72
603
+ */
604
+ declare function arbacActionRowScope(name: string): Promise<Record<string, unknown> | undefined>;
605
+ /**
606
+ * Body of the ARBAC controllers' `transformFilter` (and so of the row
607
+ * overlay `/one`, `DELETE` and `@DbAction` ids use): the user filter
608
+ * CONJOINED with the union of the request's scope filters.
609
+ *
610
+ * @since 0.1.72
611
+ */
612
+ declare function arbacRowFilter(filter: Record<string, unknown> | undefined): Record<string, unknown>;
613
+ //#endregion
614
+ //#region src/db/shared-read-helpers.d.ts
615
+ /**
616
+ * Restrict the user-supplied `$select` to the scopes' projection union.
617
+ * Returns the original projection untouched when no scope declares a
618
+ * projection (so caller sees the unrestricted shape). Reads the same cached
619
+ * visibility (`vis.allowed`) `hasField` / `/meta` pruning use, so value
620
+ * stripping and name hiding cannot drift.
621
+ *
622
+ * `projection` is the `$select` as moost-db parsed it: an inclusion is an
623
+ * ARRAY of names, an exclusion an object — both are normalized first. A
624
+ * requested parent (`$select=a`) narrows to what the scope shows: to the
625
+ * whitelisted descendants (`a.b: 1`), or — for a hidden descendant
626
+ * (`a.c: 0`) — to its visible children via `childrenOf` (without a schema
627
+ * the parent is dropped). Never the whole object, and never the empty
628
+ * (universe) projection: when nothing survives, the scope's own projection
629
+ * applies (`restrictProjection`'s ceiling fallback).
630
+ *
631
+ * An exclusion result names nested-object parents by their leaves
632
+ * (`{ a: 0 }` → `{ "a.b": 0, "a.c": 0 }`): flattening adapters invert an
633
+ * exclusion against their leaf columns, so a parent key alone would strip
634
+ * nothing (a scope `{ a: 0 }` would otherwise return all of `a`).
635
+ *
636
+ * @since 0.1.72
637
+ */
638
+ declare function applyArbacProjection(projection: unknown, scopes: ArbacDbScope[], readable?: VisibilityTableSource): TProjection | undefined;
639
+ /**
640
+ * Enforce the union of per-scope `controls` gates against the parsed
641
+ * Uniquery controls. Throws `HttpError(403)` on the first violation.
642
+ *
643
+ * @since 0.1.72
644
+ */
645
+ declare function applyArbacControls(controls: Record<string, unknown>, scopes: ArbacDbScope[]): void;
646
+ /**
647
+ * Walk the user-supplied `$with` items in `controls` and overlay each joined
648
+ * relation with the policy of its rows (the `$with` inherit-target policy):
649
+ * a relation some scope declares `with.<name>` for gets the union of those
650
+ * sub-scopes (parent authority — silent roles contribute nothing); any other
651
+ * gets the caller's OWN read scopes on the related table, as resolved in
652
+ * `prepareRequest` (for the request's own scopes). Filter conjoined, `$select`
653
+ * restricted, per-relation `controls` gates enforced, recursively.
654
+ *
655
+ * Mutates `controls` in place (matches the `validateControls` contract).
656
+ * Throws `HttpError(403)` if a per-relation control policy is violated. A
657
+ * hidden relation is skipped: moost-db (≥ 0.1.143) rejects it at every
658
+ * level through `hasField`, byte-for-byte like a nonexistent one.
659
+ *
660
+ * @since 0.1.72
661
+ */
662
+ declare function applyArbacRelationScopes(controls: Record<string, unknown>, scopes: ArbacDbScope[], readable?: VisibilityTableSource): void;
663
+ //#endregion
664
+ //#region src/db/write-policy.d.ts
665
+ /** The table surface the write guards need (`AtscriptDbTable` satisfies it). */
666
+ interface ArbacWriteTable {
667
+ count(query: {
668
+ filter: TScopeFilter;
669
+ controls?: Record<string, never>;
670
+ }): Promise<number>;
671
+ getAdapter(): {
672
+ isInTransaction(): boolean;
673
+ };
674
+ }
675
+ /**
676
+ * The in-transaction write guard of the ARBAC DB layer — the body of
677
+ * `AsArbacDbController.guardWrite` and of `useArbacDbScope().writeOptions()`'s
678
+ * `guard`:
679
+ *
680
+ * - USING — every update / replace target (the exact record filter the
681
+ * write uses) must match the scope filter — one count, no pre-image read
682
+ * (missing or out of scope → 404);
683
+ * - `checkRefs` — every enforced FK the write sets must reference rows the
684
+ * caller can read on the target table (→ 403);
685
+ * - WITH CHECK when the adapter cannot roll back — validated here, before
686
+ * the write (see {@link checkArbacWrite} for the transactional path).
687
+ */
688
+ declare function guardArbacWrite(ctx: TDbWriteGuardContext, scopes: readonly ArbacDbScope[], table: ArbacWriteTable, refs: RefTableSource): Promise<void>;
689
+ /**
690
+ * WITH CHECK after the write, inside its transaction: every written row must
691
+ * match the scopes' `check` (default `filter`) — else 403 and the write rolls
692
+ * back. Non-transactional adapters were checked in {@link guardArbacWrite}.
693
+ */
694
+ declare function checkArbacWrite(ctx: TDbWriteCheckContext, scopes: readonly ArbacDbScope[]): Promise<void>;
695
+ /** USING for deletes: the exact row the delete targets must match the scope filter (else 404). */
696
+ declare function guardArbacRemove(ctx: TDbRemoveGuardContext, scopes: readonly ArbacDbScope[], table: Pick<ArbacWriteTable, "count">): Promise<void>;
697
+ //#endregion
698
+ //#region src/db/use-arbac-db-scope.d.ts
699
+ /**
700
+ * The slice of an atscript-db table {@link ArbacDbScopeHelpers.assertRowsInScope}
701
+ * needs — satisfied by `AtscriptDbTable` / a controller's `this.table`.
702
+ */
703
+ interface ArbacScopedTable extends VisibilityTableSource {
704
+ resolveRowFilter(id: unknown, opts?: TRowResolveOptions): Promise<object | null | undefined>;
705
+ count(query: {
706
+ filter: TScopeFilter;
707
+ controls?: Record<string, never>;
708
+ }): Promise<number>;
709
+ }
710
+ /**
711
+ * The table surface {@link ArbacDbScopeHelpers.writeOptions} /
712
+ * {@link ArbacDbScopeHelpers.removeOptions} guard — satisfied by
713
+ * `AtscriptDbTable` / a controller's `this.table`.
714
+ *
715
+ * @since 0.1.72
716
+ */
717
+ type ArbacGuardedTable = ArbacScopedTable & ArbacWriteTable & RefTableSource;
718
+ /** The current event's merged ARBAC DB scope — returned by {@link useArbacDbScope}. */
719
+ interface ArbacDbScopeHelpers<T = unknown> {
720
+ /** The evaluated scopes (one per matching rule; one composite when attenuated). */
721
+ scopes: ArbacDbScope<T>[];
722
+ /**
723
+ * Union of the scopes' row filters CONJOINED (`$and`) with `extra` — never
724
+ * spread, so `extra` can only narrow. `{}` when unrestricted.
725
+ */
726
+ filter(extra?: TScopeFilter): TScopeFilter;
727
+ /** Merged `set` overrides (later scopes win) — overlay them on inserted / updated data. */
728
+ set(): Record<string, unknown>;
729
+ /**
730
+ * Union of the scopes' effective WITH CHECK filters (`check`, defaulting to
731
+ * `filter`; `{}` = none) — what a written row must match. `{}` when unrestricted.
732
+ */
733
+ check(): TScopeFilter;
734
+ /**
735
+ * 404 `Not found` unless every id resolves (PK first, else a visible unique
736
+ * key — exactly one row per id) to a row inside the scope filter. Duplicate
737
+ * ids count once.
738
+ */
739
+ assertRowsInScope(table: ArbacScopedTable, ids: readonly unknown[]): Promise<void>;
740
+ /**
741
+ * `checkRefs` for rows a handler is about to insert: 403 `Referenced row
742
+ * "<field>" is outside your scope` unless every foreign key the scopes
743
+ * enforce (see {@link ArbacDbScope.checkRefs}, union rule included)
744
+ * references a row the caller can read on the target table. Null / absent
745
+ * FKs pass. The same check the ARBAC DB controllers run on their CRUD writes.
746
+ *
747
+ * @since 0.1.72
748
+ */
749
+ assertRefsInScope(table: RefTableSource, rows: ReadonlyArray<Record<string, unknown>>): Promise<void>;
750
+ /**
751
+ * `TWriteOptions` for `table.insertOne/Many`, `replaceOne` / `bulkReplace`,
752
+ * `updateOne` / `bulkUpdate` in a handler: the same in-transaction
753
+ * enforcement the ARBAC DB controllers' CRUD writes get — nested writes
754
+ * (`nestedWrites`, 403), USING (pre-image in scope, 404), `checkRefs` (403)
755
+ * and WITH CHECK (403, rolled back). `allowedFields` / `set` are not applied
756
+ * — overlay {@link set} yourself.
757
+ *
758
+ * @since 0.1.72
759
+ */
760
+ writeOptions<Row extends object = Record<string, unknown>>(table: ArbacGuardedTable): TWriteOptions<Row>;
761
+ /**
762
+ * `TDeleteOptions` for `table.deleteOne` in a handler: the id is pinned
763
+ * among in-scope rows and the row must match the scope filter (else 404) —
764
+ * the ARBAC DB controllers' `DELETE` enforcement.
765
+ *
766
+ * @since 0.1.72
767
+ */
768
+ removeOptions<Row extends object = Record<string, unknown>>(table: ArbacGuardedTable): TDeleteOptions<Row>;
769
+ }
770
+ /**
771
+ * The current event's merged ARBAC DB scope, for custom routes and
772
+ * `@DbAction` handlers that query the table directly. Resolves the event's
773
+ * scopes once (the ones the authorize interceptor cached, else an ARBAC
774
+ * evaluation of the current resource/action) — a deny, or an allowed
775
+ * evaluation with no scope left, throws 403.
776
+ *
777
+ * @example
778
+ * ```ts
779
+ * const scope = await useArbacDbScope<typeof Task>();
780
+ * await scope.assertRowsInScope(this.table, [id]);
781
+ * await this.table.updateMany(scope.filter({ id }), { ...patch, ...scope.set() });
782
+ * // A handler-side insert with the CRUD endpoints' write enforcement.
783
+ * await this.table.insertOne({ ...row, ...scope.set() }, scope.writeOptions(this.table));
784
+ * ```
785
+ */
786
+ declare function useArbacDbScope<T = unknown>(): Promise<ArbacDbScopeHelpers<T>>;
787
+ //#endregion
338
788
  //#region src/moost-arbac.d.ts
339
789
  /**
340
790
  * A DI-enabled extension of the `Arbac` class for use within Moost.
@@ -347,6 +797,55 @@ declare function collectMethodNames(instance: object): string[];
347
797
  * @template TUserAttrs - The type representing user attributes relevant to access control.
348
798
  * @template TScope - The type representing access control scopes.
349
799
  */
350
- declare class MoostArbac<TUserAttrs extends object, TScope extends object> extends Arbac$1<TUserAttrs, TScope> {}
800
+ declare class MoostArbac<TUserAttrs extends object, TScope extends object> extends Arbac$1<TUserAttrs, TScope> {
801
+ private scopeFieldRules;
802
+ /**
803
+ * Register the conjunction rule of custom (declaration-merged) scope
804
+ * fields — used wherever two scope lists are conjoined into one composite
805
+ * scope (credential attenuation, and the `with` sub-scopes it builds). A
806
+ * custom field present in a conjunction without a rule fails the request
807
+ * with a generic 500 (never dropped: a dropped field would read as
808
+ * unrestricted). A rule's optional `rowFilter` turns the field into a ROW
809
+ * restriction every framework row path enforces (folded into each
810
+ * evaluated scope's `filter`). Each field is registered once; a built-in
811
+ * `ArbacDbScope` key is rejected.
812
+ *
813
+ * @example
814
+ * ```ts
815
+ * arbac.registerScopeFields({
816
+ * teams: {
817
+ * conjoin: (user, cred) => intersectTeams(user, cred),
818
+ * rowFilter: (teams) => ({ teamId: { $in: teams } }),
819
+ * },
820
+ * });
821
+ * ```
822
+ * @since 0.1.72
823
+ */
824
+ registerScopeFields(rules: TScopeFieldRules<TScope>): this;
825
+ /**
826
+ * The engine's evaluation with every custom field's `rowFilter` folded
827
+ * into each evaluated scope's `filter` (both passes of an attenuated
828
+ * evaluation, each with its own scopes) — so every consumer, direct
829
+ * engine calls included, sees the row restriction. A non-built-in scope
830
+ * key without a registered rule is warned about once.
831
+ */
832
+ evaluate<T extends string | undefined>(res: {
833
+ resource: string;
834
+ action: string;
835
+ }, user: {
836
+ id: T;
837
+ roles: string[];
838
+ attrs: TUserAttrs | ((userId: T) => TUserAttrs | Promise<TUserAttrs>);
839
+ attenuate?: {
840
+ roles?: string[];
841
+ attrs?: Partial<TUserAttrs>;
842
+ allowUnheldRoles?: boolean;
843
+ };
844
+ }): Promise<TArbacEvalResult$1<TScope>>;
845
+ private readonly warnedFields;
846
+ private readonly warnUnknownField;
847
+ /** The registered custom scope field rules. @since 0.1.72 */
848
+ getScopeFields(): TScopeFieldRules<TScope>;
849
+ }
351
850
  //#endregion
352
- export { type AoothArbacClaims, Arbac, ArbacAction, ArbacAuthorize, ArbacDbScope, ArbacResource, ArbacUserProvider, ArbacUserProviderToken, AsArbacDbController, AsArbacDbReadableController, type ControlGate, type ControlsOf, type MetaVisibility, MoostArbac, type NavRelationKey, type NavTarget, type OwnFieldKey, type ProjectionOf, type TArbacCompiledRule, type TArbacEvalResult, TArbacMeta, type TArbacRole, type TArbacRoleForResource, type TArbacRule, type VisibilityTableSource, applyAllowedFieldsAndSet, applyArbacMetaOverlay, arbacAuthorizeInterceptor, arbacPatternToRegex, buildScopeVisibility, collectMethodNames, collectWithGrantNames, collectWritableFields, conjoinArbacDbScopes, enforceControlsPolicy, extractUsedControlValues, getArbacMate, getArbacScopes, isMetaFieldVisible, isScopedFieldVisible, metaAlwaysVisibleFields, pruneMetaByVisibility, unionScopeProjection, useArbac };
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 };