@aooth/arbac-moost 0.1.1

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.
@@ -0,0 +1,334 @@
1
+ import { n as ArbacUserProviderToken, t as ArbacUserProvider } from "./user.provider-JpI5dD6Z.mjs";
2
+ import { Arbac, Arbac as Arbac$1, TArbacCompiledRule, TArbacEvalResult, TArbacRole, TArbacRoleForResource, TArbacRule, arbacPatternToRegex } from "@aooth/arbac-core";
3
+ import { TAuthGuardDef } from "@moostjs/event-http";
4
+ import { EventContext } from "@wooksjs/event-core";
5
+ import { Mate, TMateParamMeta, TMoostMetadata } from "moost";
6
+ import { ControlGate, ControlGate as ControlGate$1, TProjection, TScopeFilter } from "@aooth/arbac";
7
+ import { AsDbController, AsDbReadableController } from "@atscript/moost-db";
8
+ import { NavPropsOf, OwnPropsOf, TAtscriptAnnotatedType } from "@atscript/typescript/utils";
9
+ import { TMetaResponse } from "@atscript/db";
10
+
11
+ //#region src/arbac.composables.d.ts
12
+ interface ArbacBindings {
13
+ getScopes: <TScope extends object>() => TScope[] | undefined;
14
+ setScopes: <TScope extends object>(scope: TScope[] | undefined) => void;
15
+ evaluate: <TScope extends object>(opts?: {
16
+ resource?: string;
17
+ action?: string;
18
+ }) => Promise<{
19
+ allowed: boolean;
20
+ scopes?: TScope[];
21
+ userId: string;
22
+ }>;
23
+ /**
24
+ * Throw-on-deny variant of {@link evaluate}. Returns the same shape on
25
+ * `allowed: true`; throws `HttpError(403)` otherwise.
26
+ *
27
+ * Use this in handlers/steps that want a hard short-circuit on deny.
28
+ * Use {@link evaluate} when you need to inspect `allowed` (e.g. to merge
29
+ * with another policy, to filter UI metadata, or to fall through to a
30
+ * different authorization path).
31
+ */
32
+ evaluateOrThrow: <TScope extends object>(opts?: {
33
+ resource?: string;
34
+ action?: string;
35
+ }) => Promise<{
36
+ allowed: true;
37
+ scopes?: TScope[];
38
+ userId: string;
39
+ }>;
40
+ resource: string;
41
+ action: string;
42
+ isPublic: boolean;
43
+ }
44
+ /**
45
+ * Composable for ARBAC utilities within Moost handlers and interceptors.
46
+ *
47
+ * Exposes scope read/write, lazy `evaluate`, and the resolved
48
+ * resource/action/public flags derived from the current controller +
49
+ * method metadata.
50
+ *
51
+ * Resource/action/isPublic are recomputed on every call because WF events
52
+ * dispatch multiple step handlers under one `EventContext` — each step
53
+ * mutates the controller-context method via `setControllerContext`, so any
54
+ * per-ctx memo would lock in the first dispatched method's metadata (e.g.
55
+ * the `@Public()` WF_FLOW body) and incorrectly bypass ARBAC on the gated
56
+ * step handlers. Scope read/write goes through `arbacScopesKey` directly
57
+ * so it still lives on the per-event slot.
58
+ */
59
+ declare const useArbac: (_ctx?: EventContext) => ArbacBindings;
60
+ //#endregion
61
+ //#region src/arbac.decorator.d.ts
62
+ /**
63
+ * ARBAC checks authorization, not authentication, so the transport
64
+ * declaration is empty — pair with an upstream auth guard (e.g. JWT
65
+ * bearer) that publishes the principal.
66
+ *
67
+ * Runs for every event kind (HTTP, WF, CLI, WS). The `arbacPublic` mate
68
+ * flag (written by auth-moost's combined `@Public()`) is the only bypass —
69
+ * apply it to controllers/handlers that should remain reachable without an
70
+ * evaluated rule (e.g. login/recovery workflows, health probes).
71
+ */
72
+ declare const arbacAuthorizeInterceptor: TAuthGuardDef;
73
+ /** Wrapped via `Authenticate` so `@moostjs/swagger` picks up the auth-guard metadata. */
74
+ declare const ArbacAuthorize: () => ClassDecorator & MethodDecorator;
75
+ /**
76
+ * Decorator to specify a resource id for ARBAC evaluation. Apply to a
77
+ * controller class or a handler method.
78
+ */
79
+ declare const ArbacResource: (name: string) => MethodDecorator & ClassDecorator & ParameterDecorator & PropertyDecorator;
80
+ /**
81
+ * Decorator to specify an action id for ARBAC evaluation. Typically applied
82
+ * at the method level.
83
+ */
84
+ declare const ArbacAction: (name: string) => MethodDecorator & ClassDecorator & ParameterDecorator & PropertyDecorator;
85
+ //#endregion
86
+ //#region src/arbac.mate.d.ts
87
+ /**
88
+ * ARBAC metadata fields attached to classes and methods by ARBAC decorators.
89
+ *
90
+ * Augments the shared `TMoostMetadata` workspace via TypeScript declaration
91
+ * merging so other moost-aware tooling (and `getArbacMate()` readers) sees
92
+ * these fields with full type safety.
93
+ */
94
+ interface TArbacMeta {
95
+ arbacResourceId?: string;
96
+ arbacActionId?: string;
97
+ arbacPublic?: boolean;
98
+ }
99
+ declare module "moost" {
100
+ interface TMoostMetadata extends TArbacMeta {}
101
+ }
102
+ type ArbacMate = Mate<TMoostMetadata & {
103
+ params: TMateParamMeta[];
104
+ }, TMoostMetadata & {
105
+ params: TMateParamMeta[];
106
+ }>;
107
+ /**
108
+ * Returns the shared `Mate` instance typed with ARBAC metadata fields.
109
+ *
110
+ * All ARBAC decorators write through this typed wrapper so reads and writes
111
+ * stay type-checked against `TArbacMeta`.
112
+ */
113
+ declare function getArbacMate(): ArbacMate;
114
+ //#endregion
115
+ //#region src/db/scope-types.d.ts
116
+ /**
117
+ * Unwrap a `NavPropsOf<T>[K]` value to its target model type:
118
+ * `Comment[]` → `Comment`, `Task` → `Task`. Used to recurse `with.<K>` into
119
+ * the joined model's own type so per-relation scopes get typed against the
120
+ * RIGHT entity (not the array wrapper).
121
+ */
122
+ type NavTarget<U> = U extends readonly (infer V)[] ? V : U;
123
+ /**
124
+ * True only when `T` is `unknown` (or `any`). `unknown extends T` holds for
125
+ * both. Callers use this to pick the legacy untyped shape when no model is
126
+ * annotated.
127
+ */
128
+ type IsUnknown<T> = unknown extends T ? true : false;
129
+ /**
130
+ * Permissive own-field key set. With a typed `T`, autocompletes against
131
+ * `keyof OwnPropsOf<T>` while still accepting dotted-path projections like
132
+ * `mfa.value` via the `(string & {})` escape hatch. With `T = unknown`,
133
+ * `OwnPropsOf<unknown>` collapses to `unknown` so `keyof` is `never` →
134
+ * falls back to plain `string`.
135
+ */
136
+ type OwnFieldKey<T> = keyof OwnPropsOf<T> extends never ? string : (keyof OwnPropsOf<T> & string) | (string & {});
137
+ /**
138
+ * Same idiom for nav relation names. `NavPropsOf<unknown>` is
139
+ * `Record<string, never>`, so its `keyof` is `string` — the typed branch
140
+ * still resolves to plain `string` (the `(string & {})` escape collapses
141
+ * with `string`).
142
+ */
143
+ type NavRelationKey<T> = keyof NavPropsOf<T> extends never ? string : (keyof NavPropsOf<T> & string) | (string & {});
144
+ /**
145
+ * Typed projection keyed by own-field paths (with dotted-path escape).
146
+ * Falls back to the legacy `TProjection = Record<string, 0 | 1>` for
147
+ * untyped scopes so existing callers that pass the result straight to
148
+ * `unionProjections` / `restrictProjection` keep compiling.
149
+ */
150
+ type ProjectionOf<T> = IsUnknown<T> extends true ? TProjection : Partial<Record<OwnFieldKey<T>, 0 | 1>>;
151
+ /**
152
+ * Typed control gates: `$with` against nav names, `$select` against own
153
+ * fields, other `$`-prefixed controls passthrough. Falls back to the
154
+ * legacy untyped `Record<string, ControlGate>` for `T = unknown` so
155
+ * internal helpers (`unionControlsPolicy`) and existing untyped call
156
+ * sites keep compiling.
157
+ */
158
+ type ControlsOf<T> = IsUnknown<T> extends true ? Record<string, ControlGate$1> : {
159
+ $with?: Array<NavRelationKey<T>> | boolean;
160
+ $select?: Array<OwnFieldKey<T>> | boolean;
161
+ $groupBy?: ControlGate$1;
162
+ $having?: ControlGate$1;
163
+ [key: `$${string}`]: ControlGate$1 | Array<string> | undefined;
164
+ };
165
+ //#endregion
166
+ //#region src/db/as-arbac-db-controller.d.ts
167
+ /**
168
+ * Contract returned by an ARBAC role's scope predicate on a DB-backed resource.
169
+ *
170
+ * Apps can extend this interface with their own fields via TypeScript
171
+ * declaration merging — both at role-definition time (returned from
172
+ * `role.allow(...)`) and at consumption time (custom controller overrides
173
+ * reading `scopes[i].myCustomField` get full type safety):
174
+ *
175
+ * @example
176
+ * ```ts
177
+ * declare module '@aooth/arbac-moost' {
178
+ * interface ArbacDbScope {
179
+ * tenantId?: string
180
+ * }
181
+ * }
182
+ * ```
183
+ */
184
+ interface ArbacDbScope<T = unknown> {
185
+ filter?: TScopeFilter;
186
+ projection?: ProjectionOf<T>;
187
+ set?: Partial<Record<OwnFieldKey<T>, unknown>>;
188
+ allowedFields?: Array<OwnFieldKey<T>>;
189
+ /**
190
+ * Per-control gates for Uniquery URL controls (`$with`, `$groupBy`, `$having`, …).
191
+ * Evaluated by {@link AsArbacDbController.validateControls} before query execution;
192
+ * a violation throws `HttpError(403)`.
193
+ *
194
+ * Per-key semantics ({@link ControlGate}):
195
+ * - absent / `true` — allowed.
196
+ * - `false` — denied entirely.
197
+ * - `readonly string[]` — whitelist (e.g. `{ $with: ['comments'] }` allows
198
+ * `?$with=comments`, rejects `?$with=tasks`). Supported only for `$with`
199
+ * (relation names) and `$groupBy` (column names) in v1.
200
+ *
201
+ * Across roles, gates union additively (silence wins) via {@link unionControlsPolicy}.
202
+ *
203
+ * @example `{ controls: { $with: false } }` — disable $with for this role.
204
+ * @example `{ controls: { $with: ['comments', 'owner'] } }` — restrict relations.
205
+ */
206
+ controls?: ControlsOf<T>;
207
+ /**
208
+ * Per-relation sub-scopes applied when the request expands a relation via
209
+ * `?$with=<name>`. Recursive — each sub-scope has the same shape and can
210
+ * declare its own `with` for nested expansions (e.g. tasks → comments → task).
211
+ *
212
+ * **Authority model**: the PARENT scope owns the policy for joined rows.
213
+ * arbac-moost does NOT re-evaluate ARBAC against the joined resource's own
214
+ * scopes — that would be a confusing indirection and a perf hit. Whatever
215
+ * the parent declares here is what surfaces from the expansion.
216
+ *
217
+ * **Union across roles**: when multiple roles allow the same parent table,
218
+ * their `with[name]` sub-scopes are unioned at every nested level using the
219
+ * existing `unionProjections` / `mergeScopeFilters` / `unionControlsPolicy`
220
+ * primitives (additive: broader access wins, same rules as the parent).
221
+ *
222
+ * **Silence wins**: if no role declares `with.<name>`, expansion is
223
+ * unrestricted (matches `controls.$with` whitelist semantics; the gate
224
+ * still applies if declared).
225
+ */
226
+ with?: WithOf<T>;
227
+ }
228
+ /**
229
+ * Per-relation sub-scope map. For `T = unknown` falls back to the legacy
230
+ * untyped `Record<string, ArbacDbScope>`. With a typed `T`, each known
231
+ * relation key gets its scope typed against the joined model (via
232
+ * `NavTarget` to unwrap arrays), while arbitrary `(string & {})` keys
233
+ * keep the untyped escape hatch. Lives here (not in `scope-types.ts`)
234
+ * because it must reference `ArbacDbScope` recursively.
235
+ */
236
+ type WithOf<T> = unknown extends T ? Record<string, ArbacDbScope> : { [K in NavRelationKey<T>]?: K extends keyof NavPropsOf<T> ? ArbacDbScope<NavTarget<NavPropsOf<T>[K]>> : ArbacDbScope };
237
+ declare class AsArbacDbController<T extends TAtscriptAnnotatedType = TAtscriptAnnotatedType> extends AsDbController<T> {
238
+ protected transformFilter(filter: Record<string, unknown> | undefined): Promise<Record<string, unknown>>;
239
+ protected transformProjection(projection?: TProjection): TProjection | undefined | Promise<TProjection | undefined>;
240
+ /**
241
+ * Enforce per-role `ArbacDbScope.controls` gates against the parsed Uniquery
242
+ * controls of a request. Runs after the base validator (which checks the
243
+ * controls DTO shape) and BEFORE the query/aggregation pipeline executes.
244
+ *
245
+ * `transformFilter` runs first on every read endpoint and caches the
246
+ * evaluated scopes via `arbac.setScopes(...)`, so we read those scopes
247
+ * directly here without re-evaluating ARBAC.
248
+ *
249
+ * On a violation we throw `HttpError(403)`. moost-db's `query` / `pages` /
250
+ * `getOne` handlers do NOT wrap `validateParsed` in try/catch, so the
251
+ * thrown error bubbles to moost which translates it to a 403 response.
252
+ */
253
+ protected validateControls(controls: Record<string, unknown>, type: "query" | "pages" | "getOne"): string | undefined;
254
+ protected applyMetaOverlay(meta: TMetaResponse): Promise<TMetaResponse>;
255
+ protected onWrite(action: "insert" | "insertMany" | "replace" | "replaceMany" | "update" | "updateMany", data: unknown): Promise<unknown>;
256
+ protected onRemove(id: unknown): Promise<unknown>;
257
+ private assertInScope;
258
+ private identifierFields;
259
+ }
260
+ /**
261
+ * Test-friendly internal helper — exported for unit tests and helper
262
+ * composition; regular consumers should not call this directly.
263
+ *
264
+ * Enforce a per-control policy against a parsed Uniquery `controls` map.
265
+ *
266
+ * Throws `HttpError(403)` on the first violation. Pure (no DI) so it is
267
+ * trivially unit-testable; `validateControls` wires it up to the controller's
268
+ * cached scopes.
269
+ *
270
+ * Semantics per gate (see {@link ControlGate}):
271
+ * - `true` (or absent — dropped by `unionControlsPolicy`): allow.
272
+ * - `false`: deny if the control is used at all.
273
+ * - `readonly string[]`: allow only the listed values; reject any other.
274
+ *
275
+ * "Used" means the control key is present AND non-empty (an empty array is
276
+ * treated as not used, matching how the parser leaves missing controls).
277
+ */
278
+ declare function enforceControlsPolicy(policy: Record<string, ControlGate$1>, controls: Record<string, unknown>): void;
279
+ /**
280
+ * Test-friendly internal helper — exported for unit tests and helper
281
+ * composition; regular consumers should not call this directly.
282
+ *
283
+ * Extract the set of "named values" from a Uniquery control payload, for
284
+ * use against a whitelist gate.
285
+ *
286
+ * Currently supported (matches `WHITELISTABLE_CONTROLS` in `unionControlsPolicy`):
287
+ * - `$with` — array of `{ name, … }` objects (per `TypedWithRelation`,
288
+ * see `@uniqu/core` parser at `parseWithSegment`); we extract `name`.
289
+ * Bare strings are tolerated for forward compatibility.
290
+ * - `$groupBy` — array of column names (strings). Returned as-is.
291
+ *
292
+ * For unknown controls we return an empty array; the caller then enforces
293
+ * `false`-only semantics (controlled by `unionControlsPolicy`'s whitelist
294
+ * gate, which throws if a non-whitelistable control receives a string[]).
295
+ */
296
+ declare function extractUsedControlValues(key: string, value: unknown): string[];
297
+ /**
298
+ * Test-friendly internal helper — exported for unit tests and helper
299
+ * composition; regular consumers should not call this directly.
300
+ *
301
+ * Apply the union of `allowedFields` whitelists (with `preserveFields`
302
+ * always preserved) and overlay each scope's `set` overrides. Returns a
303
+ * shallow copy; original `data` is not mutated.
304
+ */
305
+ declare function applyAllowedFieldsAndSet(data: unknown, scopes: ArbacDbScope[], preserveFields?: readonly string[]): unknown;
306
+ //#endregion
307
+ //#region src/db/as-arbac-db-readable-controller.d.ts
308
+ /**
309
+ * Read-only mirror of {@link AsArbacDbController} for view-style controllers
310
+ * built on top of `@atscript/moost-db`'s {@link AsDbReadableController}.
311
+ * Applies the same filter / projection / controls overlays — no write-side
312
+ * hooks because the parent exposes none.
313
+ */
314
+ declare class AsArbacDbReadableController<T extends TAtscriptAnnotatedType = TAtscriptAnnotatedType> extends AsDbReadableController<T> {
315
+ protected transformFilter(filter: Record<string, unknown> | undefined): Promise<Record<string, unknown>>;
316
+ protected transformProjection(projection?: TProjection): TProjection | undefined | Promise<TProjection | undefined>;
317
+ protected validateControls(controls: Record<string, unknown>, type: "query" | "pages" | "getOne"): string | undefined;
318
+ }
319
+ //#endregion
320
+ //#region src/moost-arbac.d.ts
321
+ /**
322
+ * A DI-enabled extension of the `Arbac` class for use within Moost.
323
+ *
324
+ * Allows ARBAC (Advanced Role-Based Access Control) to be injected into
325
+ * Moost services and controllers via the framework's dependency-injection
326
+ * container. Defaults to the SINGLETON scope so all handlers share one
327
+ * registry of roles/resources.
328
+ *
329
+ * @template TUserAttrs - The type representing user attributes relevant to access control.
330
+ * @template TScope - The type representing access control scopes.
331
+ */
332
+ declare class MoostArbac<TUserAttrs extends object, TScope extends object> extends Arbac$1<TUserAttrs, TScope> {}
333
+ //#endregion
334
+ export { Arbac, ArbacAction, ArbacAuthorize, ArbacDbScope, ArbacResource, ArbacUserProvider, ArbacUserProviderToken, AsArbacDbController, AsArbacDbReadableController, type ControlGate, type ControlsOf, MoostArbac, type NavRelationKey, type NavTarget, type OwnFieldKey, type ProjectionOf, type TArbacCompiledRule, type TArbacEvalResult, TArbacMeta, type TArbacRole, type TArbacRoleForResource, type TArbacRule, applyAllowedFieldsAndSet, arbacAuthorizeInterceptor, arbacPatternToRegex, enforceControlsPolicy, extractUsedControlValues, getArbacMate, useArbac };