@aooth/arbac 0.1.71 → 0.1.72

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
@@ -86,18 +86,6 @@ declare function defineRole<TUserAttrs extends object = object, TScope extends o
86
86
  */
87
87
  declare function definePrivilege<TUserAttrs extends object = object, TScope extends object = object>(): <TArgs extends unknown[]>(factory: (...args: TArgs) => TArbacRule<TUserAttrs, TScope>[]) => ((...args: TArgs) => TPrivilegeFunction<TUserAttrs, TScope>);
88
88
  //#endregion
89
- //#region src/db-privileges.d.ts
90
- type ScopeFn<TUserAttrs, TScope> = (attrs: TUserAttrs, userId: string) => TScope;
91
- interface ScopeOpts<TUserAttrs, TScope> {
92
- scope?: ScopeFn<TUserAttrs, TScope>;
93
- }
94
- /** Read-side actions on an `AsDbController` table: `query`, `pages`, `getOne`, `getOneComposite`, `meta`, `metaForm`. */
95
- declare function allowTableRead<TUserAttrs extends object = object, TScope extends object = object>(resource: string, opts?: ScopeOpts<TUserAttrs, TScope>): TPrivilegeFunction<TUserAttrs, TScope>;
96
- /** All actions on an `AsDbController` table: read + `insert`, `update`, `replace`, `remove`, `removeComposite`. */
97
- declare function allowTableWrite<TUserAttrs extends object = object, TScope extends object = object>(resource: string, opts?: ScopeOpts<TUserAttrs, TScope>): TPrivilegeFunction<TUserAttrs, TScope>;
98
- /** One or more declarative actions on the same table, sharing a scope. */
99
- declare function allowTableAction<TUserAttrs extends object = object, TScope extends object = object>(resource: string, name: string | string[], opts?: ScopeOpts<TUserAttrs, TScope>): TPrivilegeFunction<TUserAttrs, TScope>;
100
- //#endregion
101
89
  //#region src/scope/types.d.ts
102
90
  /**
103
91
  * Mongo-style field projection: `{ field: 0 | 1 }`.
@@ -135,6 +123,98 @@ type TScopeFilter = Record<string, unknown>;
135
123
  */
136
124
  type ControlGate = boolean | readonly string[];
137
125
  //#endregion
126
+ //#region src/db-privileges.d.ts
127
+ /**
128
+ * Read-side handler actions of an `AsDbController` table: `query`, `pages`,
129
+ * `getOne`, `getOneComposite`, `geo`, `meta`, `metaForm`.
130
+ */
131
+ declare const TABLE_READ_ACTIONS: readonly ["query", "pages", "getOne", "getOneComposite", "geo", "meta", "metaForm"];
132
+ /** Write-side handler actions of an `AsDbController` table. */
133
+ declare const TABLE_WRITE_ACTIONS: readonly ["insert", "update", "replace", "remove", "removeComposite"];
134
+ /** The `/meta` + `/meta/form/:name` handler actions (a subset of {@link TABLE_READ_ACTIONS}). */
135
+ declare const TABLE_META_ACTIONS: readonly ["meta", "metaForm"];
136
+ /** Table operation names accepted by {@link allowTableOps} / {@link defineTableAccess}. */
137
+ type TTableOp = "read" | "meta" | "insert" | "update" | "replace" | "remove";
138
+ /**
139
+ * Operation → handler actions. `meta` is the form-metadata subset of `read`,
140
+ * for write-only principals that still render forms from `/meta`.
141
+ */
142
+ declare const TABLE_OP_ACTIONS: Readonly<Record<TTableOp, readonly string[]>>;
143
+ type ScopeFn<TUserAttrs, TScope> = (attrs: TUserAttrs, userId: string) => TScope;
144
+ interface ScopeOpts<TUserAttrs, TScope> {
145
+ scope?: ScopeFn<TUserAttrs, TScope>;
146
+ }
147
+ /** Read-side actions on an `AsDbController` table — see {@link TABLE_READ_ACTIONS}. */
148
+ declare function allowTableRead<TUserAttrs extends object = object, TScope extends object = object>(resource: string, opts?: ScopeOpts<TUserAttrs, TScope>): TPrivilegeFunction<TUserAttrs, TScope>;
149
+ /** All actions on an `AsDbController` table: read + `insert`, `update`, `replace`, `remove`, `removeComposite`. */
150
+ declare function allowTableWrite<TUserAttrs extends object = object, TScope extends object = object>(resource: string, opts?: ScopeOpts<TUserAttrs, TScope>): TPrivilegeFunction<TUserAttrs, TScope>;
151
+ /**
152
+ * Selected operations on an `AsDbController` table, sharing a scope — e.g. an
153
+ * insert-only form principal: `allowTableOps("leads", ["insert", "meta"])`.
154
+ *
155
+ * - `read` → `query`, `pages`, `getOne`, `getOneComposite`, `geo`, `meta`, `metaForm`
156
+ * - `meta` → `meta`, `metaForm` (forms for principals without `read`)
157
+ * - `remove` → `remove`, `removeComposite`
158
+ * - `insert` / `update` / `replace` → the same-named action
159
+ *
160
+ * Throws on an unknown op name.
161
+ */
162
+ declare function allowTableOps<TUserAttrs extends object = object, TScope extends object = object>(resource: string, ops: readonly TTableOp[], opts?: ScopeOpts<TUserAttrs, TScope>): TPrivilegeFunction<TUserAttrs, TScope>;
163
+ /** One or more declarative actions on the same table, sharing a scope. */
164
+ declare function allowTableAction<TUserAttrs extends object = object, TScope extends object = object>(resource: string, name: string | string[], opts?: ScopeOpts<TUserAttrs, TScope>): TPrivilegeFunction<TUserAttrs, TScope>;
165
+ /** The scope keys {@link defineTableAccess} conjoins instead of overriding. */
166
+ interface TTableAccessScope {
167
+ filter?: TScopeFilter;
168
+ check?: TScopeFilter;
169
+ }
170
+ /** Write ops for {@link TTableAccessDef.write}: any {@link TTableOp} but `read`. */
171
+ type TTableWriteOp = Exclude<TTableOp, "read">;
172
+ type PartScopeFn<TUserAttrs, TScope> = ScopeFn<NoInfer<TUserAttrs>, NoInfer<TScope>>;
173
+ /**
174
+ * Table policy for {@link defineTableAccess}. A part's own `scope` merges over
175
+ * the shared one (see there).
176
+ */
177
+ interface TTableAccessDef<TUserAttrs, TScope> {
178
+ /** Shared scope — row filter, projection, controls, `with`, `check`, … — for every part. */
179
+ scope?: PartScopeFn<TUserAttrs, TScope>;
180
+ /** Read side — {@link TABLE_READ_ACTIONS}. */
181
+ read?: boolean | {
182
+ scope?: PartScopeFn<TUserAttrs, TScope>;
183
+ };
184
+ /**
185
+ * Write side: `true` = insert + update + replace + remove. An op list may
186
+ * add `meta` (forms for a principal without `read`).
187
+ */
188
+ write?: boolean | readonly TTableWriteOp[] | {
189
+ ops?: readonly TTableWriteOp[];
190
+ scope?: PartScopeFn<TUserAttrs, TScope>;
191
+ };
192
+ /** Declarative `@DbAction` names. */
193
+ actions?: readonly string[] | {
194
+ names: readonly string[];
195
+ scope?: PartScopeFn<TUserAttrs, TScope>;
196
+ };
197
+ }
198
+ /**
199
+ * One table policy → consistent rules for every endpoint of an
200
+ * `AsDbController` table. The shared `scope` (row filter + projection +
201
+ * controls + `with` + `check` …) applies to every part; a part's own `scope`
202
+ * merges over it — `filter` / `check` conjoined (`$and`), other keys from the
203
+ * part. An action granted by an earlier part (`read` → `write` → `actions`)
204
+ * is not repeated by a later one. Throws on an unknown write op.
205
+ *
206
+ * @example
207
+ * ```ts
208
+ * defineTableAccess<Attrs, ArbacDbScope<typeof Task>>("tasks", {
209
+ * scope: (a) => ({ filter: { tenantId: a.tenantId }, projection: { secret: 0 } }),
210
+ * read: true,
211
+ * write: { ops: ["insert", "update"], scope: () => ({ allowedFields: ["title", "status"] }) },
212
+ * actions: ["markDone"],
213
+ * });
214
+ * ```
215
+ */
216
+ declare function defineTableAccess<TUserAttrs extends object = object, TScope extends TTableAccessScope = TTableAccessScope>(resource: string, def: TTableAccessDef<TUserAttrs, TScope>): TPrivilegeFunction<TUserAttrs, TScope>;
217
+ //#endregion
138
218
  //#region src/scope/projection.d.ts
139
219
  type TProjectionMode = "include" | "exclude" | "empty";
140
220
  /**
@@ -326,6 +406,259 @@ declare function unionControlsPolicy(scopes: ReadonlyArray<{
326
406
  */
327
407
  declare function intersectControlsPolicy(a: Record<string, ControlGate>, b: Record<string, ControlGate>): Record<string, ControlGate>;
328
408
  //#endregion
409
+ //#region src/scope/db-scope.d.ts
410
+ /**
411
+ * The DB scope shape the scope algebra reads — structurally what
412
+ * `ArbacDbScope` (`@aooth/arbac-moost`) carries. Every facet is optional.
413
+ *
414
+ * @since 0.1.72
415
+ */
416
+ interface TDbScope {
417
+ filter?: TScopeFilter;
418
+ check?: TScopeFilter;
419
+ projection?: object;
420
+ controls?: object;
421
+ allowedFields?: readonly string[];
422
+ set?: object;
423
+ nestedWrites?: readonly string[];
424
+ checkRefs?: true | readonly string[];
425
+ with?: object;
426
+ }
427
+ /**
428
+ * The built-in {@link TDbScope} keys — the facets the scope algebra itself
429
+ * combines. Any other key is a custom (declaration-merged) scope field, which
430
+ * {@link conjoinScopes} combines only through a registered
431
+ * {@link TScopeFieldRule}.
432
+ *
433
+ * @since 0.1.72
434
+ */
435
+ declare const DB_SCOPE_KEYS: readonly string[];
436
+ /**
437
+ * The conjunction rule of ONE custom scope field (a key an app adds to its
438
+ * scope type by declaration merging). {@link conjoinScopes} calls `conjoin`
439
+ * only when some scope on either side carries the field.
440
+ *
441
+ * @since 0.1.72
442
+ */
443
+ interface TScopeFieldRule<S extends object = TDbScope> {
444
+ /**
445
+ * Optional ROW restriction the field implies: the filter a scope carrying
446
+ * `value` admits (`undefined` / `{}` = no row restriction). Folded into
447
+ * that scope's `filter` by {@link applyScopeFieldFilters} — ANDed within
448
+ * the scope, so the union across scopes stays an `$or`. An explicit
449
+ * `check` is left as written; an absent one follows the folded filter.
450
+ *
451
+ * Must depend ONLY on `(value, scope)` — never on request context: the
452
+ * result is cached process-wide per scope object. Put user-dependent data
453
+ * into the scope value from the role's scope predicate.
454
+ */
455
+ rowFilter?(value: unknown, scope: S): TScopeFilter | undefined;
456
+ /**
457
+ * Combine the field of two restrict-only sides into the ONE value the
458
+ * composite scope carries — never wider than either side. Each side is a
459
+ * scope LIST the app unions with its own rule (a scope without the field is
460
+ * typically unrestricted for it). In a credential attenuation `a` is the
461
+ * user's full authority and `b` the credential's view. Return `undefined`
462
+ * for "unrestricted" (the key is omitted).
463
+ */
464
+ conjoin(a: readonly S[], b: readonly S[]): unknown;
465
+ }
466
+ /**
467
+ * Thrown by {@link conjoinScopes} for a custom scope field without a
468
+ * conjunction rule — a server CONFIGURATION error (never a client error):
469
+ * the conjunction fails closed instead of dropping the field.
470
+ *
471
+ * @since 0.1.72
472
+ */
473
+ declare class ScopeFieldConfigError extends Error {
474
+ /** The custom scope field without a rule. */
475
+ readonly field: string;
476
+ readonly name = "ScopeFieldConfigError";
477
+ constructor(
478
+ /** The custom scope field without a rule. */
479
+ field: string, message: string);
480
+ }
481
+ /** Custom scope field name → its {@link TScopeFieldRule}. @since 0.1.72 */
482
+ type TScopeFieldRules<S extends object = TDbScope> = Readonly<Record<string, TScopeFieldRule<S>>>;
483
+ /**
484
+ * The effective (unioned) policy of a list of DB scopes — one scope per
485
+ * allowing rule, so each facet unions ADDITIVELY (more roles = more access).
486
+ * Every facet's union rule is defined here once; facets compute lazily (a
487
+ * misconfigured facet throws only where it is used). An empty list is the
488
+ * identity (unrestricted) of every union — denial is decided before, by
489
+ * {@link normalizeScopes}.
490
+ *
491
+ * @since 0.1.72
492
+ */
493
+ interface TEffectiveDbScope<S extends TDbScope = TDbScope> {
494
+ /** Row filter union (`$or`, {@link mergeScopeFilters}); `undefined` = unrestricted. */
495
+ readonly filter: TScopeFilter | undefined;
496
+ /**
497
+ * WITH CHECK union — each scope's `check`, defaulting to its `filter`
498
+ * (`{}` = no check); `undefined` = nothing to check.
499
+ */
500
+ readonly check: TScopeFilter | undefined;
501
+ /** Whether any scope sets an explicit `check`. */
502
+ readonly hasExplicitCheck: boolean;
503
+ /** Projection union ({@link unionProjections}); `{}` = every field. */
504
+ readonly projection: TProjection;
505
+ /** Controls union ({@link unionControlsPolicy}); an absent key = allowed. */
506
+ readonly controls: Record<string, ControlGate>;
507
+ /**
508
+ * Union of the `allowedFields` whitelists; `undefined` when no scope
509
+ * carries one (field-unrestricted writes).
510
+ */
511
+ readonly allowedFields: ReadonlySet<string> | undefined;
512
+ /** The `set` overlays, later scopes winning; `undefined` when none sets one. */
513
+ readonly set: Readonly<Record<string, unknown>> | undefined;
514
+ /** Union of the `nestedWrites` opt-ins. */
515
+ readonly nestedWrites: ReadonlySet<string>;
516
+ /**
517
+ * `checkRefs` entries EVERY scope enables (intersection of enforcement —
518
+ * a scope without the flag grants unconstrained writes of that FK);
519
+ * `true` = all. Names as written (a schema-aware caller resolves them).
520
+ */
521
+ readonly checkRefs: true | ReadonlySet<string>;
522
+ /** Relation names some scope declares a `with.<name>` sub-scope for. */
523
+ readonly withNames: ReadonlySet<string>;
524
+ /** The declared `with.<name>` sub-scopes (identity-stable per name). */
525
+ withScopes(name: string): readonly S[];
526
+ }
527
+ /**
528
+ * The {@link TEffectiveDbScope} of `scopes`, memoized per scopes ARRAY (an
529
+ * identity-stable per-request list reuses every computed facet).
530
+ *
531
+ * @since 0.1.72
532
+ */
533
+ declare function effectiveScope<S extends TDbScope>(scopes: readonly S[]): TEffectiveDbScope<S>;
534
+ /**
535
+ * Intersection of enforcement: an entry is enforced only when EVERY input
536
+ * enables it (`true` enables all). No input → nothing enforced.
537
+ *
538
+ * @since 0.1.72
539
+ */
540
+ declare function intersectEnabled<K>(inputs: Iterable<true | ReadonlySet<K>>): true | Set<K>;
541
+ /**
542
+ * The unified empty-scope rule for an evaluation outcome: denied → `undefined`;
543
+ * allowed with `scopes` absent (a scope-agnostic evaluator) → `[{}]`
544
+ * (unrestricted); a scope predicate that returned nothing contributes nothing
545
+ * (fail closed for that rule); allowed with no scope left → `undefined` (deny).
546
+ *
547
+ * @since 0.1.72
548
+ */
549
+ declare function normalizeScopes<S extends object>(outcome: {
550
+ allowed: boolean;
551
+ scopes?: ReadonlyArray<S | null | undefined>;
552
+ }): S[] | undefined;
553
+ /**
554
+ * Several evaluations served as ONE surface (e.g. the handlers of one crud
555
+ * op): each normalized with {@link normalizeScopes}, the allowed ones'
556
+ * scopes concatenated — `undefined` when none allows.
557
+ *
558
+ * @since 0.1.72
559
+ */
560
+ declare function unionOutcomes<S extends object>(outcomes: ReadonlyArray<{
561
+ allowed: boolean;
562
+ scopes?: ReadonlyArray<S | null | undefined>;
563
+ }>): S[] | undefined;
564
+ /** A row policy: the read / USING `filter` and the WITH CHECK `check` (defaults to `filter`). */
565
+ interface TRowPolicy {
566
+ filter?: TScopeFilter;
567
+ check?: TScopeFilter;
568
+ }
569
+ /**
570
+ * Conjoin two row policies (`$and`, restrict-only): `filter = a.filter ∧
571
+ * b.filter`; the effective check `(a.check ?? a.filter) ∧ (b.check ??
572
+ * b.filter)`. `check` is set only when it differs from the conjoined
573
+ * `filter` (which an absent `check` defaults to) — `{}` when it is
574
+ * unrestricted while the filter is not.
575
+ *
576
+ * @since 0.1.72
577
+ */
578
+ declare function conjoinRowPolicies(a: TRowPolicy, b: TRowPolicy): TRowPolicy;
579
+ /** Options of {@link conjoinScopes}. */
580
+ interface TConjoinScopesOptions<S extends TDbScope> {
581
+ /** Schema lookup to subtract a nested exclusion exactly (without it: fail closed). */
582
+ childrenOf?: TProjectionChildren;
583
+ /**
584
+ * Schema-aware `checkRefs` conjunction; default: the union of each side's
585
+ * {@link TEffectiveDbScope.checkRefs} names.
586
+ */
587
+ checkRefs?: (a: readonly S[], b: readonly S[]) => true | string[] | undefined;
588
+ /**
589
+ * Rules for custom (non-built-in) scope fields, applied at every level
590
+ * (`with` sub-scopes too). A custom field present on either side without a
591
+ * rule throws — it is never dropped (a dropped field reads as absent, i.e.
592
+ * typically unrestricted).
593
+ */
594
+ fields?: TScopeFieldRules<S>;
595
+ }
596
+ /**
597
+ * Conjoin two scope lists (credential attenuation: the user's ceiling ∧ the
598
+ * credential's narrowed view) into ONE composite scope. Each side is first
599
+ * unioned ({@link effectiveScope}), then the two results are CONJOINED facet
600
+ * by facet — never with the additive union helpers, which would widen:
601
+ *
602
+ * - `filter` / `check` — {@link conjoinRowPolicies};
603
+ * - `projection` — {@link intersectProjections} (no common field → the
604
+ * first side's projection plus a match-nothing filter, never `{}`);
605
+ * - `controls` — {@link intersectControlsPolicy} (deny wins);
606
+ * - `allowedFields` — intersection (a side without a whitelist is unrestricted);
607
+ * - `nestedWrites` — intersection (a side without the key allows none);
608
+ * - `set` — combined, the FIRST side winning a key conflict;
609
+ * - `checkRefs` — enforced when EITHER side enforces it;
610
+ * - `with` — per relation, recursively; a relation declared by ONE side only
611
+ * is marked {@link INHERITED_CONJUNCTION} (the silent side's inherited
612
+ * grant on the related table must be conjoined at resolution time);
613
+ * - a custom field — its `opts.fields` rule; none registered → throws
614
+ * (fail closed: never silently dropped).
615
+ *
616
+ * @since 0.1.72
617
+ */
618
+ declare function conjoinScopes<S extends TDbScope>(a: readonly S[], b: readonly S[], opts?: TConjoinScopesOptions<S>): S;
619
+ /** Options of {@link applyScopeFieldFilters}. @since 0.1.72 */
620
+ interface TApplyScopeFieldFiltersOptions {
621
+ /**
622
+ * Called for a non-built-in scope key with no rule in `fields` (once per
623
+ * scope object carrying it) — e.g. to warn about a field that will fail
624
+ * the first attenuated conjunction.
625
+ */
626
+ onUnknownField?: (field: string) => void;
627
+ }
628
+ /**
629
+ * Fold every custom field's {@link TScopeFieldRule.rowFilter} into its
630
+ * scope's `filter` (`filter ∧ rowFilter(value, scope)`), recursively into
631
+ * `with` sub-scopes. Apply it per evaluated scope list — before any union or
632
+ * conjunction — so every row path sees the restriction. The custom value
633
+ * stays on the scope. Scopes are never mutated: a changed scope is a copy.
634
+ * Memoized per rule registry and scope object (process-wide), and
635
+ * idempotent (a folded scope folds to itself); without row rules (and no
636
+ * `onUnknownField`) the input list is returned as is.
637
+ *
638
+ * @since 0.1.72
639
+ */
640
+ declare function applyScopeFieldFilters<S extends TDbScope>(scopes: readonly S[], fields: TScopeFieldRules<S> | undefined, opts?: TApplyScopeFieldFiltersOptions): readonly S[];
641
+ /**
642
+ * Marks a `with.<rel>` sub-scope {@link conjoinScopes} built from ONE side's
643
+ * declaration. The other side is silent on the relation, so ITS policy for
644
+ * the joined rows is its inherited grant on the related table — unknown
645
+ * here. A resolver must conjoin the marked sub-scope with the caller's own
646
+ * grant on the related table (never apply it alone as parent authority);
647
+ * unresolved, the relation is hidden. See {@link needsInheritedConjunction}.
648
+ *
649
+ * @since 0.1.72
650
+ */
651
+ declare const INHERITED_CONJUNCTION: unique symbol;
652
+ /** A `with` sub-scope marked {@link INHERITED_CONJUNCTION}. @since 0.1.72 */
653
+ declare function needsInheritedConjunction(scope: object): boolean;
654
+ /**
655
+ * A stable string key for a JSON-like value (bigint-safe) — for
656
+ * de-duplicating filters / tuples and comparing them structurally.
657
+ *
658
+ * @since 0.1.72
659
+ */
660
+ declare function stableKey(value: unknown): string;
661
+ //#endregion
329
662
  //#region src/codegen/extract.d.ts
330
663
  interface TResourceActionMap {
331
664
  /** Map of resource string → Set of action strings */
@@ -361,4 +694,4 @@ interface TCodegenOptions {
361
694
  */
362
695
  declare function generateResourceTypes(map: TResourceActionMap, options?: TCodegenOptions): string;
363
696
  //#endregion
364
- export { type ControlGate, DENY_FILTER, type RoleBuilder, type TCodegenOptions, type TPrivilegeFunction, type TProjection, type TProjectionChildren, type TProjectionMode, type TResourceActionMap, type TScopeFilter, allowTableAction, allowTableRead, allowTableWrite, conjoinScopeFilters, definePrivilege, defineRole, expandExcludeToLeaves, extractResourceActions, generateResourceTypes, getProjectionMode, intersectControlsPolicy, intersectProjections, isFieldAllowed, mergeScopeFilters, restrictProjection, unionControlsPolicy, unionProjections };
697
+ export { type ControlGate, DB_SCOPE_KEYS, DENY_FILTER, INHERITED_CONJUNCTION, type RoleBuilder, ScopeFieldConfigError, TABLE_META_ACTIONS, TABLE_OP_ACTIONS, TABLE_READ_ACTIONS, TABLE_WRITE_ACTIONS, type TApplyScopeFieldFiltersOptions, type TCodegenOptions, type TConjoinScopesOptions, type TDbScope, type TEffectiveDbScope, type TPrivilegeFunction, type TProjection, type TProjectionChildren, type TProjectionMode, type TResourceActionMap, type TRowPolicy, type TScopeFieldRule, type TScopeFieldRules, type TScopeFilter, type TTableAccessDef, type TTableAccessScope, type TTableOp, type TTableWriteOp, allowTableAction, allowTableOps, allowTableRead, allowTableWrite, applyScopeFieldFilters, conjoinRowPolicies, conjoinScopeFilters, conjoinScopes, definePrivilege, defineRole, defineTableAccess, effectiveScope, expandExcludeToLeaves, extractResourceActions, generateResourceTypes, getProjectionMode, intersectControlsPolicy, intersectEnabled, intersectProjections, isFieldAllowed, mergeScopeFilters, needsInheritedConjunction, normalizeScopes, restrictProjection, stableKey, unionControlsPolicy, unionOutcomes, unionProjections };