@aooth/arbac-moost 0.1.7 → 0.1.9

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.mjs CHANGED
@@ -1,10 +1,102 @@
1
- import { n as ArbacUserProviderToken, r as __decorate, t as ArbacUserProvider } from "./user.provider-CGWueNjx.mjs";
1
+ import { n as ArbacUserProviderToken, r as __decorate, t as ArbacUserProvider } from "./user.provider-BiM5P8fl.mjs";
2
2
  import { Arbac, Arbac as Arbac$1, arbacPatternToRegex } from "@aooth/arbac-core";
3
3
  import { Authenticate, HttpError } from "@moostjs/event-http";
4
4
  import { current, key } from "@wooksjs/event-core";
5
5
  import { Inherit, Injectable, TInterceptorPriority, defineBeforeInterceptor, getConstructor, getInstanceOwnMethods, getMoostMate, useControllerContext, useLogger } from "moost";
6
- import { mergeScopeFilters, restrictProjection, unionControlsPolicy, unionProjections } from "@aooth/arbac";
6
+ import { conjoinScopeFilters, intersectControlsPolicy, mergeScopeFilters, restrictProjection, unionControlsPolicy, unionProjections } from "@aooth/arbac";
7
7
  import { AsDbController, AsDbReadableController } from "@atscript/moost-db";
8
+ //#region src/attenuation.ts
9
+ /**
10
+ * Conjoin the full-authority scopes (`userScopes`, the ceiling) and the
11
+ * credential's narrowed scopes (`credScopes`) into ONE composite
12
+ * `ArbacDbScope`. A row/field/control is admitted only if BOTH passes admit
13
+ * it — the normative restrict-only clip that closes the attr-widen hole.
14
+ *
15
+ * Each side is first UNIONed with the existing additive helpers (today's
16
+ * machinery), then the two RESULTS are CONJOINED with the dedicated combiners
17
+ * ({@link conjoinScopeFilters} `$and`, {@link restrictProjection} field ∩,
18
+ * {@link intersectControlsPolicy} deny-wins, `with` recursion) — never the
19
+ * additive union helpers, which would silently widen.
20
+ *
21
+ * Returned as a single-element list so every downstream scope-application
22
+ * site (which UNIONs the cached scope list per facet) sees the identity of a
23
+ * one-element union — i.e. the conjunction — with no change to those sites.
24
+ */
25
+ function conjoinArbacDbScopes(userScopes, credScopes) {
26
+ const filter = conjoinScopeFilters(mergeScopeFilters(userScopes.map((s) => s.filter ?? {})), mergeScopeFilters(credScopes.map((s) => s.filter ?? {})));
27
+ const projection = restrictProjection(unionProjections(...userScopes.map((s) => s.projection ?? {})), unionProjections(...credScopes.map((s) => s.projection ?? {})));
28
+ const controls = intersectControlsPolicy(unionControlsPolicy(userScopes), unionControlsPolicy(credScopes));
29
+ const allowedFields = intersectAllowedFields(userScopes, credScopes);
30
+ const set = combineSet(userScopes, credScopes);
31
+ const withMap = conjoinWith(userScopes, credScopes);
32
+ const s = {};
33
+ if (filter && Object.keys(filter).length > 0) s.filter = filter;
34
+ if (Object.keys(projection).length > 0) s.projection = projection;
35
+ if (Object.keys(controls).length > 0) s.controls = controls;
36
+ if (allowedFields) s.allowedFields = allowedFields;
37
+ if (set) s.set = set;
38
+ if (withMap) s.with = withMap;
39
+ return [s];
40
+ }
41
+ /**
42
+ * Intersect the two sides' `allowedFields` write-whitelists (the credential
43
+ * may write FEWER fields). A side with no whitelist is unrestricted (all
44
+ * fields), so the other side wins; both restricting → set intersection.
45
+ */
46
+ function intersectAllowedFields(userScopes, credScopes) {
47
+ const u = unionAllowedFields(userScopes);
48
+ const c = unionAllowedFields(credScopes);
49
+ if (u === void 0) return c;
50
+ if (c === void 0) return u;
51
+ const cset = new Set(c);
52
+ return [...new Set(u)].filter((f) => cset.has(f)).toSorted();
53
+ }
54
+ function unionAllowedFields(scopes) {
55
+ let set;
56
+ for (const s of scopes) if (Array.isArray(s.allowedFields)) {
57
+ set ??= /* @__PURE__ */ new Set();
58
+ for (const f of s.allowedFields) set.add(f);
59
+ }
60
+ return set ? [...set] : void 0;
61
+ }
62
+ /**
63
+ * Combine the two sides' `set` overlays. `set` forces field values on writes;
64
+ * both sides' forced constraints apply, and on a key conflict the USER's value
65
+ * wins so the credential can only ADD constraints, never override the owner's.
66
+ */
67
+ function combineSet(userScopes, credScopes) {
68
+ const u = unionSet(userScopes);
69
+ const c = unionSet(credScopes);
70
+ if (!u && !c) return void 0;
71
+ return {
72
+ ...c,
73
+ ...u
74
+ };
75
+ }
76
+ function unionSet(scopes) {
77
+ let out;
78
+ for (const s of scopes) if (s.set) {
79
+ out ??= {};
80
+ Object.assign(out, s.set);
81
+ }
82
+ return out;
83
+ }
84
+ /**
85
+ * Recurse the conjunction into joined-resource sub-scopes. A relation declared
86
+ * on only one side is conjoined against the other side's silence (unrestricted
87
+ * = identity), so a credential can narrow a relation the user left open but
88
+ * never widen one the user restricted.
89
+ */
90
+ function conjoinWith(userScopes, credScopes) {
91
+ const relNames = /* @__PURE__ */ new Set();
92
+ for (const s of userScopes) if (s.with) for (const k of Object.keys(s.with)) relNames.add(k);
93
+ for (const s of credScopes) if (s.with) for (const k of Object.keys(s.with)) relNames.add(k);
94
+ if (relNames.size === 0) return void 0;
95
+ const out = {};
96
+ for (const rel of relNames) out[rel] = conjoinArbacDbScopes(userScopes.map((s) => s.with?.[rel]).filter(Boolean), credScopes.map((s) => s.with?.[rel]).filter(Boolean))[0];
97
+ return out;
98
+ }
99
+ //#endregion
8
100
  //#region src/moost-arbac.ts
9
101
  let MoostArbac = class MoostArbac extends Arbac$1 {};
10
102
  MoostArbac = __decorate([Injectable()], MoostArbac);
@@ -52,15 +144,28 @@ const useArbac = (_ctx) => {
52
144
  if (!effectiveAction) throw new Error("useArbac().evaluate(): `action` is required — could not be resolved from controller/method metadata. Pass it explicitly.");
53
145
  const [user, arbac] = await Promise.all([cc.instantiate(ArbacUserProviderToken), cc.instantiate(MoostArbac)]);
54
146
  const userId = await user.getUserId();
147
+ const att = user.getAttenuation ? await user.getAttenuation() : void 0;
148
+ const attenuate = att && (att.roles !== void 0 || att.attrs !== void 0) ? {
149
+ roles: att.roles,
150
+ attrs: att.attrs
151
+ } : void 0;
152
+ const result = await arbac.evaluate({
153
+ resource: effectiveResource,
154
+ action: effectiveAction
155
+ }, {
156
+ id: userId,
157
+ roles: await user.getRoles(userId),
158
+ attrs: (id) => user.getAttrs(id),
159
+ attenuate
160
+ });
161
+ if (result.allowed && result.credScopes !== void 0) return {
162
+ allowed: true,
163
+ scopes: conjoinArbacDbScopes(result.scopes ?? [], result.credScopes),
164
+ userId
165
+ };
55
166
  return {
56
- ...await arbac.evaluate({
57
- resource: effectiveResource,
58
- action: effectiveAction
59
- }, {
60
- id: userId,
61
- roles: await user.getRoles(userId),
62
- attrs: (id) => user.getAttrs(id)
63
- }),
167
+ allowed: result.allowed,
168
+ scopes: result.scopes,
64
169
  userId
65
170
  };
66
171
  };
@@ -516,4 +621,4 @@ let AsArbacDbReadableController = class AsArbacDbReadableController extends AsDb
516
621
  };
517
622
  AsArbacDbReadableController = __decorate([Inherit()], AsArbacDbReadableController);
518
623
  //#endregion
519
- export { Arbac, ArbacAction, ArbacAuthorize, ArbacResource, ArbacUserProvider, ArbacUserProviderToken, AsArbacDbController, AsArbacDbReadableController, MoostArbac, applyAllowedFieldsAndSet, arbacAuthorizeInterceptor, arbacPatternToRegex, enforceControlsPolicy, extractUsedControlValues, getArbacMate, useArbac };
624
+ export { Arbac, ArbacAction, ArbacAuthorize, ArbacResource, ArbacUserProvider, ArbacUserProviderToken, AsArbacDbController, AsArbacDbReadableController, MoostArbac, applyAllowedFieldsAndSet, arbacAuthorizeInterceptor, arbacPatternToRegex, conjoinArbacDbScopes, enforceControlsPolicy, extractUsedControlValues, getArbacMate, useArbac };
package/dist/plugin.d.mts CHANGED
@@ -16,6 +16,29 @@ import { TAtscriptPlugin } from "@atscript/core";
16
16
  * (1) `@arbac.userId`, (2) the single field of the `@db.table.preferredId.uniqueIndex`
17
17
  * group, (3) `@meta.id` — first match wins.
18
18
  *
19
+ * Credential-side (restrict-only attenuation — a credential authorizes for
20
+ * strictly LESS than its owning user; soundness is the engine's scope
21
+ * conjunction, these only mark WHERE the narrowing inputs live):
22
+ *
23
+ * - `@arbac.attenuate.role` — marks the ONE credential field holding the
24
+ * assumed-role SUBSET (`string[]`). Intersected with the user's roles
25
+ * (fail-closed: a claimed role the user lacks is dropped). Exactly one per
26
+ * type — multiple declarations throw at boot.
27
+ * - `@arbac.attenuate.attr "userAttrName"` — marks a credential field whose
28
+ * value narrows the named USER attribute. The string argument is the target
29
+ * key in the user model's `@arbac.attribute` keyspace (validated to exist at
30
+ * boot). Multiple fields may each carry it, targeting different attrs.
31
+ *
32
+ * This plugin also registers the `@aooth.user.*` identity-handle namespace
33
+ * (read by `getAoothUserHandleSpec`) so `.as` user models can mark their login
34
+ * handles name-agnostically:
35
+ *
36
+ * - `@aooth.user.email` — the field is the EMAIL login/recovery handle. Resolved
37
+ * by annotation (not by being literally named `email`). MUST carry
38
+ * `@db.index.unique` (unique-when-present); without it the handle is disabled
39
+ * with a warning. At most one per type.
40
+ * - `@aooth.user.phone` — the PHONE login/recovery handle, same contract.
41
+ *
19
42
  * Install in `atscript.config.ts`:
20
43
  *
21
44
  * ```ts
package/dist/plugin.mjs CHANGED
@@ -15,6 +15,29 @@ import { AnnotationSpec } from "@atscript/core";
15
15
  * (1) `@arbac.userId`, (2) the single field of the `@db.table.preferredId.uniqueIndex`
16
16
  * group, (3) `@meta.id` — first match wins.
17
17
  *
18
+ * Credential-side (restrict-only attenuation — a credential authorizes for
19
+ * strictly LESS than its owning user; soundness is the engine's scope
20
+ * conjunction, these only mark WHERE the narrowing inputs live):
21
+ *
22
+ * - `@arbac.attenuate.role` — marks the ONE credential field holding the
23
+ * assumed-role SUBSET (`string[]`). Intersected with the user's roles
24
+ * (fail-closed: a claimed role the user lacks is dropped). Exactly one per
25
+ * type — multiple declarations throw at boot.
26
+ * - `@arbac.attenuate.attr "userAttrName"` — marks a credential field whose
27
+ * value narrows the named USER attribute. The string argument is the target
28
+ * key in the user model's `@arbac.attribute` keyspace (validated to exist at
29
+ * boot). Multiple fields may each carry it, targeting different attrs.
30
+ *
31
+ * This plugin also registers the `@aooth.user.*` identity-handle namespace
32
+ * (read by `getAoothUserHandleSpec`) so `.as` user models can mark their login
33
+ * handles name-agnostically:
34
+ *
35
+ * - `@aooth.user.email` — the field is the EMAIL login/recovery handle. Resolved
36
+ * by annotation (not by being literally named `email`). MUST carry
37
+ * `@db.index.unique` (unique-when-present); without it the handle is disabled
38
+ * with a warning. At most one per type.
39
+ * - `@aooth.user.phone` — the PHONE login/recovery handle, same contract.
40
+ *
18
41
  * Install in `atscript.config.ts`:
19
42
  *
20
43
  * ```ts
@@ -26,23 +49,54 @@ function arbacPlugin() {
26
49
  return {
27
50
  name: "aoothjs-arbac",
28
51
  config() {
29
- return { annotations: { arbac: {
30
- role: new AnnotationSpec({
31
- description: "Marks this field as THE source of role identifiers for ARBAC evaluation. Two shapes are supported: inline (string | string[]) or @db.rel.from nav prop. Exactly one @arbac.role field per type — multiple declarations throw at boot.",
32
- nodeType: ["prop"],
33
- multiple: false
34
- }),
35
- attribute: new AnnotationSpec({
36
- description: "Marks this field as a user attribute used by ARBAC scope evaluation. The field name becomes the attribute key. Multiple fields are merged.",
37
- nodeType: ["prop"],
38
- multiple: false
39
- }),
40
- userId: new AnnotationSpec({
41
- description: "Overrides which field provides the user identifier for ARBAC. Resolution chain: @arbac.userId → @db.table.preferredId.uniqueIndex field → @meta.id.",
42
- nodeType: ["prop"],
43
- multiple: false
44
- })
45
- } } };
52
+ return { annotations: {
53
+ arbac: {
54
+ role: new AnnotationSpec({
55
+ description: "Marks this field as THE source of role identifiers for ARBAC evaluation. Two shapes are supported: inline (string | string[]) or @db.rel.from nav prop. Exactly one @arbac.role field per type — multiple declarations throw at boot.",
56
+ nodeType: ["prop"],
57
+ multiple: false
58
+ }),
59
+ attribute: new AnnotationSpec({
60
+ description: "Marks this field as a user attribute used by ARBAC scope evaluation. The field name becomes the attribute key. Multiple fields are merged.",
61
+ nodeType: ["prop"],
62
+ multiple: false
63
+ }),
64
+ userId: new AnnotationSpec({
65
+ description: "Overrides which field provides the user identifier for ARBAC. Resolution chain: @arbac.userId → @db.table.preferredId.uniqueIndex field → @meta.id.",
66
+ nodeType: ["prop"],
67
+ multiple: false
68
+ }),
69
+ attenuate: {
70
+ role: new AnnotationSpec({
71
+ description: "Marks the credential field holding the assumed-role SUBSET (string[]) for restrict-only ARBAC attenuation. Intersected with the user's roles (fail-closed). Exactly one @arbac.attenuate.role field per type — multiple declarations throw at boot.",
72
+ nodeType: ["prop"],
73
+ multiple: false
74
+ }),
75
+ attr: new AnnotationSpec({
76
+ description: "Marks a credential field whose value narrows the named USER attribute for restrict-only ARBAC attenuation. The argument is the target key in the user model's @arbac.attribute keyspace (validated to exist at boot). Multiple fields may each carry it, targeting different attrs.",
77
+ nodeType: ["prop"],
78
+ multiple: false,
79
+ argument: {
80
+ name: "userAttr",
81
+ type: "string",
82
+ description: "Target user-attribute key (a field annotated @arbac.attribute on the user model)."
83
+ }
84
+ })
85
+ }
86
+ },
87
+ aooth: { user: {
88
+ email: new AnnotationSpec({
89
+ description: "Marks this field as the EMAIL login/recovery handle. The framework resolves the field by this annotation (name-agnostic) for findByHandle / recovery / signup. The field MUST carry @db.index.unique (unique-when-present) — a handle must resolve to at most one row; without a unique index the email handle is DISABLED with a warning. At most one @aooth.user.email field per type — multiple declarations throw at boot.",
90
+ nodeType: ["prop"],
91
+ multiple: false
92
+ }),
93
+ phone: new AnnotationSpec({
94
+ description: "Marks this field as the PHONE login/recovery handle. Same resolution + unique-index contract as @aooth.user.email (disabled with a warning when the field lacks @db.index.unique). At most one @aooth.user.phone field per type — multiple throw at boot.",
95
+ nodeType: ["prop"],
96
+ multiple: false
97
+ })
98
+ } }
99
+ } };
46
100
  }
47
101
  };
48
102
  }
@@ -0,0 +1,15 @@
1
+ import { defineAnnotatedType, throwFeatureDisabled } from "@atscript/typescript/utils";
2
+ import "@aooth/user/atscript-db/model.as";
3
+ //#region src/atscript/models/user.as
4
+ var AoothArbacUserCredentials = class {
5
+ static __is_atscript_annotated_type = true;
6
+ static type = {};
7
+ static metadata = /* @__PURE__ */ new Map();
8
+ static id = "AoothArbacUserCredentials";
9
+ static toJsonSchema() {
10
+ throwFeatureDisabled("JSON Schema", "jsonSchema", "emit.jsonSchema");
11
+ }
12
+ };
13
+ defineAnnotatedType("object", AoothArbacUserCredentials).prop("id", defineAnnotatedType().designType("string").tags("string").annotate("meta.id", true).annotate("db.default.uuid", true).$type).prop("username", defineAnnotatedType().designType("string").tags("string").annotate("db.index.unique", "username_idx", true).$type).prop("version", defineAnnotatedType().designType("number").tags("int", "number").annotate("db.column.version", true).annotate("expect.int", true).$type).prop("password", defineAnnotatedType("object").prop("hash", defineAnnotatedType().designType("string").tags("string").$type).prop("history", defineAnnotatedType("array").of(defineAnnotatedType().designType("string").tags("string").$type).$type).prop("lastChanged", defineAnnotatedType().designType("number").tags("timestamp", "number").annotate("expect.int", true).$type).prop("isInitial", defineAnnotatedType().designType("boolean").tags("boolean").$type).annotate("db.patch.strategy", "merge").$type).prop("account", defineAnnotatedType("object").prop("active", defineAnnotatedType().designType("boolean").tags("boolean").$type).prop("locked", defineAnnotatedType().designType("boolean").tags("boolean").$type).prop("lockReason", defineAnnotatedType().designType("string").tags("string").$type).prop("lockEnds", defineAnnotatedType().designType("number").tags("timestamp", "number").annotate("expect.int", true).$type).prop("failedLoginAttempts", defineAnnotatedType().designType("number").tags("number").$type).prop("lastLogin", defineAnnotatedType().designType("number").tags("timestamp", "number").annotate("expect.int", true).$type).prop("pendingInvitation", defineAnnotatedType().designType("boolean").tags("boolean").optional().$type).annotate("db.patch.strategy", "merge").$type).prop("mfa", defineAnnotatedType("object").prop("methods", defineAnnotatedType("array").of(defineAnnotatedType("object").prop("name", defineAnnotatedType().designType("string").tags("string").$type).prop("confirmed", defineAnnotatedType().designType("boolean").tags("boolean").$type).prop("value", defineAnnotatedType().designType("string").tags("string").$type).prop("lastUsedWindow", defineAnnotatedType().designType("number").tags("int", "number").annotate("expect.int", true).optional().$type).$type).$type).prop("defaultMethod", defineAnnotatedType().designType("string").tags("string").$type).prop("autoSend", defineAnnotatedType().designType("boolean").tags("boolean").$type).annotate("db.patch.strategy", "merge").$type).prop("trustedDevices", defineAnnotatedType("array").of(defineAnnotatedType("object").prop("token", defineAnnotatedType().designType("string").tags("string").$type).prop("ip", defineAnnotatedType().designType("string").tags("string").optional().$type).prop("issuedAt", defineAnnotatedType().designType("number").tags("timestamp", "number").annotate("expect.int", true).$type).prop("expiresAt", defineAnnotatedType().designType("number").tags("timestamp", "number").annotate("expect.int", true).$type).prop("name", defineAnnotatedType().designType("string").tags("string").optional().$type).$type).annotate("db.patch.strategy", "merge").optional().$type).prop("roles", defineAnnotatedType("array").of(defineAnnotatedType().designType("string").tags("string").$type).annotate("arbac.role", true).$type);
14
+ //#endregion
15
+ export { AoothArbacUserCredentials as t };
@@ -1,4 +1,4 @@
1
- //#region \0@oxc-project+runtime@0.129.0/helpers/decorate.js
1
+ //#region \0@oxc-project+runtime@0.133.0/helpers/esm/decorate.js
2
2
  function __decorate(decorators, target, key, desc) {
3
3
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
4
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -0,0 +1,314 @@
1
+ import { TClassConstructor } from "moost";
2
+ import { ControlGate, TProjection, TScopeFilter } from "@aooth/arbac";
3
+ import { AsDbController } from "@atscript/moost-db";
4
+ import { NavPropsOf, OwnPropsOf, TAtscriptAnnotatedType } from "@atscript/typescript/utils";
5
+ import { TMetaResponse } from "@atscript/db";
6
+
7
+ //#region src/db/scope-types.d.ts
8
+ /**
9
+ * Unwrap a `NavPropsOf<T>[K]` value to its target model type:
10
+ * `Comment[]` → `Comment`, `Task` → `Task`. Used to recurse `with.<K>` into
11
+ * the joined model's own type so per-relation scopes get typed against the
12
+ * RIGHT entity (not the array wrapper).
13
+ */
14
+ type NavTarget<U> = U extends readonly (infer V)[] ? V : U;
15
+ /**
16
+ * True only when `T` is `unknown` (or `any`). `unknown extends T` holds for
17
+ * both. Callers use this to pick the legacy untyped shape when no model is
18
+ * annotated.
19
+ */
20
+ type IsUnknown<T> = unknown extends T ? true : false;
21
+ /**
22
+ * Permissive own-field key set. With a typed `T`, autocompletes against
23
+ * `keyof OwnPropsOf<T>` while still accepting dotted-path projections like
24
+ * `mfa.value` via the `(string & {})` escape hatch. With `T = unknown`,
25
+ * `OwnPropsOf<unknown>` collapses to `unknown` so `keyof` is `never` →
26
+ * falls back to plain `string`.
27
+ */
28
+ type OwnFieldKey<T> = keyof OwnPropsOf<T> extends never ? string : (keyof OwnPropsOf<T> & string) | (string & {});
29
+ /**
30
+ * Same idiom for nav relation names. `NavPropsOf<unknown>` is
31
+ * `Record<string, never>`, so its `keyof` is `string` — the typed branch
32
+ * still resolves to plain `string` (the `(string & {})` escape collapses
33
+ * with `string`).
34
+ */
35
+ type NavRelationKey<T> = keyof NavPropsOf<T> extends never ? string : (keyof NavPropsOf<T> & string) | (string & {});
36
+ /**
37
+ * Typed projection keyed by own-field paths (with dotted-path escape).
38
+ * Falls back to the legacy `TProjection = Record<string, 0 | 1>` for
39
+ * untyped scopes so existing callers that pass the result straight to
40
+ * `unionProjections` / `restrictProjection` keep compiling.
41
+ */
42
+ type ProjectionOf<T> = IsUnknown<T> extends true ? TProjection : Partial<Record<OwnFieldKey<T>, 0 | 1>>;
43
+ /**
44
+ * Typed control gates: `$with` against nav names, `$select` against own
45
+ * fields, other `$`-prefixed controls passthrough. Falls back to the
46
+ * legacy untyped `Record<string, ControlGate>` for `T = unknown` so
47
+ * internal helpers (`unionControlsPolicy`) and existing untyped call
48
+ * sites keep compiling.
49
+ */
50
+ type ControlsOf<T> = IsUnknown<T> extends true ? Record<string, ControlGate> : {
51
+ $with?: Array<NavRelationKey<T>> | boolean;
52
+ $select?: Array<OwnFieldKey<T>> | boolean;
53
+ $groupBy?: ControlGate;
54
+ $having?: ControlGate;
55
+ [key: `$${string}`]: ControlGate | Array<string> | undefined;
56
+ };
57
+ //#endregion
58
+ //#region src/db/as-arbac-db-controller.d.ts
59
+ /**
60
+ * Contract returned by an ARBAC role's scope predicate on a DB-backed resource.
61
+ *
62
+ * Apps can extend this interface with their own fields via TypeScript
63
+ * declaration merging — both at role-definition time (returned from
64
+ * `role.allow(...)`) and at consumption time (custom controller overrides
65
+ * reading `scopes[i].myCustomField` get full type safety):
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * declare module '@aooth/arbac-moost' {
70
+ * interface ArbacDbScope {
71
+ * tenantId?: string
72
+ * }
73
+ * }
74
+ * ```
75
+ */
76
+ interface ArbacDbScope<T = unknown> {
77
+ filter?: TScopeFilter;
78
+ projection?: ProjectionOf<T>;
79
+ set?: Partial<Record<OwnFieldKey<T>, unknown>>;
80
+ allowedFields?: Array<OwnFieldKey<T>>;
81
+ /**
82
+ * Per-control gates for Uniquery URL controls (`$with`, `$groupBy`, `$having`, …).
83
+ * Evaluated by {@link AsArbacDbController.validateControls} before query execution;
84
+ * a violation throws `HttpError(403)`.
85
+ *
86
+ * Per-key semantics ({@link ControlGate}):
87
+ * - absent / `true` — allowed.
88
+ * - `false` — denied entirely.
89
+ * - `readonly string[]` — whitelist (e.g. `{ $with: ['comments'] }` allows
90
+ * `?$with=comments`, rejects `?$with=tasks`). Supported only for `$with`
91
+ * (relation names) and `$groupBy` (column names) in v1.
92
+ *
93
+ * Across roles, gates union additively (silence wins) via {@link unionControlsPolicy}.
94
+ *
95
+ * @example `{ controls: { $with: false } }` — disable $with for this role.
96
+ * @example `{ controls: { $with: ['comments', 'owner'] } }` — restrict relations.
97
+ */
98
+ controls?: ControlsOf<T>;
99
+ /**
100
+ * Per-relation sub-scopes applied when the request expands a relation via
101
+ * `?$with=<name>`. Recursive — each sub-scope has the same shape and can
102
+ * declare its own `with` for nested expansions (e.g. tasks → comments → task).
103
+ *
104
+ * **Authority model**: the PARENT scope owns the policy for joined rows.
105
+ * arbac-moost does NOT re-evaluate ARBAC against the joined resource's own
106
+ * scopes — that would be a confusing indirection and a perf hit. Whatever
107
+ * the parent declares here is what surfaces from the expansion.
108
+ *
109
+ * **Union across roles**: when multiple roles allow the same parent table,
110
+ * their `with[name]` sub-scopes are unioned at every nested level using the
111
+ * existing `unionProjections` / `mergeScopeFilters` / `unionControlsPolicy`
112
+ * primitives (additive: broader access wins, same rules as the parent).
113
+ *
114
+ * **Silence wins**: if no role declares `with.<name>`, expansion is
115
+ * unrestricted (matches `controls.$with` whitelist semantics; the gate
116
+ * still applies if declared).
117
+ */
118
+ with?: WithOf<T>;
119
+ }
120
+ /**
121
+ * Per-relation sub-scope map. For `T = unknown` falls back to the legacy
122
+ * untyped `Record<string, ArbacDbScope>`. With a typed `T`, each known
123
+ * relation key gets its scope typed against the joined model (via
124
+ * `NavTarget` to unwrap arrays), while arbitrary `(string & {})` keys
125
+ * keep the untyped escape hatch. Lives here (not in `scope-types.ts`)
126
+ * because it must reference `ArbacDbScope` recursively.
127
+ */
128
+ type WithOf<T> = unknown extends T ? Record<string, ArbacDbScope> : { [K in NavRelationKey<T>]?: K extends keyof NavPropsOf<T> ? ArbacDbScope<NavTarget<NavPropsOf<T>[K]>> : ArbacDbScope };
129
+ declare class AsArbacDbController<T extends TAtscriptAnnotatedType = TAtscriptAnnotatedType> extends AsDbController<T> {
130
+ protected transformFilter(filter: Record<string, unknown> | undefined): Promise<Record<string, unknown>>;
131
+ protected transformProjection(projection?: TProjection): TProjection | undefined | Promise<TProjection | undefined>;
132
+ /**
133
+ * Translate `@uniqu/url` parser failures into HTTP 400 instead of letting them
134
+ * bubble as 500. The base `parseQueryString` calls `parseUrl()` directly with
135
+ * no try/catch, so a malformed `?status=...` (e.g. unquoted single quote at a
136
+ * non-string position) surfaces as a server error — misleading, since the
137
+ * server is fine and the client sent bad input.
138
+ *
139
+ * Narrow targeting: only `SyntaxError` raised from inside the parser is
140
+ * remapped. Other errors (e.g. a programmer-introduced TypeError in a future
141
+ * override) still bubble as 500. SQL-injection-shaped payloads remain safely
142
+ * handled either way — see SEC-17 for the parameterisation invariant; this
143
+ * override is an orthogonal robustness pin around the parse step.
144
+ */
145
+ protected parseQueryString(url: string): ReturnType<AsDbController<T>["parseQueryString"]>;
146
+ /**
147
+ * Same parser-error → 400 remap as {@link parseQueryString}, applied to the
148
+ * `/one` and `/one?…` code paths which use `parseControlsOnlyFromUrl`.
149
+ */
150
+ protected parseControlsOnlyFromUrl(url: string): ReturnType<AsDbController<T>["parseControlsOnlyFromUrl"]>;
151
+ /**
152
+ * Enforce per-role `ArbacDbScope.controls` gates against the parsed Uniquery
153
+ * controls of a request. Runs after the base validator (which checks the
154
+ * controls DTO shape) and BEFORE the query/aggregation pipeline executes.
155
+ *
156
+ * `transformFilter` runs first on every read endpoint and caches the
157
+ * evaluated scopes via `arbac.setScopes(...)`, so we read those scopes
158
+ * directly here without re-evaluating ARBAC.
159
+ *
160
+ * On a violation we throw `HttpError(403)`. moost-db's `query` / `pages` /
161
+ * `getOne` handlers do NOT wrap `validateParsed` in try/catch, so the
162
+ * thrown error bubbles to moost which translates it to a 403 response.
163
+ */
164
+ protected validateControls(controls: Record<string, unknown>, type: "query" | "pages" | "getOne"): string | undefined;
165
+ protected applyMetaOverlay(meta: TMetaResponse): Promise<TMetaResponse>;
166
+ protected onWrite(action: "insert" | "insertMany" | "replace" | "replaceMany" | "update" | "updateMany", data: unknown): Promise<unknown>;
167
+ protected onRemove(id: unknown): Promise<unknown>;
168
+ private assertInScope;
169
+ private identifierFields;
170
+ }
171
+ /**
172
+ * Test-friendly internal helper — exported for unit tests and helper
173
+ * composition; regular consumers should not call this directly.
174
+ *
175
+ * Enforce a per-control policy against a parsed Uniquery `controls` map.
176
+ *
177
+ * Throws `HttpError(403)` on the first violation. Pure (no DI) so it is
178
+ * trivially unit-testable; `validateControls` wires it up to the controller's
179
+ * cached scopes.
180
+ *
181
+ * Semantics per gate (see {@link ControlGate}):
182
+ * - `true` (or absent — dropped by `unionControlsPolicy`): allow.
183
+ * - `false`: deny if the control is used at all.
184
+ * - `readonly string[]`: allow only the listed values; reject any other.
185
+ *
186
+ * "Used" means the control key is present AND non-empty (an empty array is
187
+ * treated as not used, matching how the parser leaves missing controls).
188
+ */
189
+ declare function enforceControlsPolicy(policy: Record<string, ControlGate>, controls: Record<string, unknown>): void;
190
+ /**
191
+ * Test-friendly internal helper — exported for unit tests and helper
192
+ * composition; regular consumers should not call this directly.
193
+ *
194
+ * Extract the set of "named values" from a Uniquery control payload, for
195
+ * use against a whitelist gate.
196
+ *
197
+ * Currently supported (matches `WHITELISTABLE_CONTROLS` in `unionControlsPolicy`):
198
+ * - `$with` — array of `{ name, … }` objects (per `TypedWithRelation`,
199
+ * see `@uniqu/core` parser at `parseWithSegment`); we extract `name`.
200
+ * Bare strings are tolerated for forward compatibility.
201
+ * - `$groupBy` — array of column names (strings). Returned as-is.
202
+ *
203
+ * For unknown controls we return an empty array; the caller then enforces
204
+ * `false`-only semantics (controlled by `unionControlsPolicy`'s whitelist
205
+ * gate, which throws if a non-whitelistable control receives a string[]).
206
+ */
207
+ declare function extractUsedControlValues(key: string, value: unknown): string[];
208
+ /**
209
+ * Test-friendly internal helper — exported for unit tests and helper
210
+ * composition; regular consumers should not call this directly.
211
+ *
212
+ * Apply the union of `allowedFields` whitelists (with `preserveFields`
213
+ * always preserved) and overlay each scope's `set` overrides. Returns a
214
+ * shallow copy; original `data` is not mutated.
215
+ */
216
+ declare function applyAllowedFieldsAndSet(data: unknown, scopes: ArbacDbScope[], preserveFields?: readonly string[]): unknown;
217
+ //#endregion
218
+ //#region src/attenuation.d.ts
219
+ /**
220
+ * The restrict-only ARBAC attenuation carried by a credential — its assumed
221
+ * role SUBSET and narrowing attribute overrides. Sourced into evaluation via
222
+ * the optional `ArbacUserProvider.getAttenuation()` hook (typically built by
223
+ * walking the credential model's `@arbac.attenuate.*`-annotated typed root
224
+ * fields with {@link extractAttenuation}), it NARROWS (never expands) the
225
+ * principal — see the engine's `evaluate({ attenuate })` for the restrict-only
226
+ * outcome-intersection.
227
+ *
228
+ * - `roles` — assume a SUBSET of the user's roles. `[]` = no roles (deny-all,
229
+ * fail-closed); an OMITTED key = keep all the user's roles (attrs-only
230
+ * narrowing); a role the user lacks is dropped by the intersection.
231
+ * - `attrs` — extra/overriding inputs to scope predicates, keyed by the target
232
+ * user-attribute name, intended to narrow scopes. They are merged LOCALLY
233
+ * into the credential pass only and clipped by the scope conjunction, so they
234
+ * can never widen beyond the user.
235
+ */
236
+ interface AoothArbacClaims {
237
+ roles?: string[];
238
+ attrs?: Record<string, unknown>;
239
+ }
240
+ /**
241
+ * Conjoin the full-authority scopes (`userScopes`, the ceiling) and the
242
+ * credential's narrowed scopes (`credScopes`) into ONE composite
243
+ * `ArbacDbScope`. A row/field/control is admitted only if BOTH passes admit
244
+ * it — the normative restrict-only clip that closes the attr-widen hole.
245
+ *
246
+ * Each side is first UNIONed with the existing additive helpers (today's
247
+ * machinery), then the two RESULTS are CONJOINED with the dedicated combiners
248
+ * ({@link conjoinScopeFilters} `$and`, {@link restrictProjection} field ∩,
249
+ * {@link intersectControlsPolicy} deny-wins, `with` recursion) — never the
250
+ * additive union helpers, which would silently widen.
251
+ *
252
+ * Returned as a single-element list so every downstream scope-application
253
+ * site (which UNIONs the cached scope list per facet) sees the identity of a
254
+ * one-element union — i.e. the conjunction — with no change to those sites.
255
+ */
256
+ declare function conjoinArbacDbScopes(userScopes: ArbacDbScope[], credScopes: ArbacDbScope[]): ArbacDbScope[];
257
+ //#endregion
258
+ //#region src/user.provider.d.ts
259
+ /**
260
+ * Abstract base class for providing user data required for ARBAC evaluations.
261
+ *
262
+ * Consumers must extend this class and implement the three abstract methods
263
+ * to plug their own authentication/identity layer into the ARBAC authorize
264
+ * interceptor. The subclass must re-apply `@Injectable()` (moost does not
265
+ * inherit decorator metadata) and be registered via Moost's
266
+ * `setReplaceRegistry` (or `@Replace`) so DI resolves the concrete provider.
267
+ *
268
+ * @template TUserAttrs - The type representing user attributes relevant to access control.
269
+ */
270
+ declare abstract class ArbacUserProvider<TUserAttrs extends object = object> {
271
+ /**
272
+ * Retrieves the unique identifier of the current user.
273
+ */
274
+ abstract getUserId(): string | Promise<string>;
275
+ /**
276
+ * Retrieves the roles assigned to a user based on their ID.
277
+ *
278
+ * @param id - The user ID.
279
+ */
280
+ abstract getRoles(id: string): string[] | Promise<string[]>;
281
+ /**
282
+ * Retrieves the attributes associated with a user based on their ID.
283
+ *
284
+ * @param id - The user ID.
285
+ */
286
+ abstract getAttrs(id: string): TUserAttrs | Promise<TUserAttrs>;
287
+ /**
288
+ * OPTIONAL: source the credential's restrict-only ARBAC attenuation for the
289
+ * current request. When it returns claims, `useArbac().evaluate()` runs the
290
+ * engine's dual-pass outcome-intersection so the credential can do/see/affect
291
+ * strictly LESS than its owning user; returning `undefined` (the default —
292
+ * this method is unimplemented on the base/atscript providers) means "no
293
+ * narrowing", i.e. the credential authorizes with the user's full authority.
294
+ *
295
+ * `arbac-moost` is auth-agnostic, so the base class does NOT read the auth
296
+ * layer. A consumer wires it up by overriding this to read the credential's
297
+ * typed root fields and return their attenuation — typically
298
+ * `extractAttenuation(CredentialModel, useAuth().getAuthContext())`, which
299
+ * walks the model's `@arbac.attenuate.*`-annotated fields (from
300
+ * `@aooth/arbac-moost/atscript`). The restrict-only intersection happens in
301
+ * the engine regardless, so an app cannot get the safety wrong here.
302
+ */
303
+ getAttenuation?(): AoothArbacClaims | undefined | Promise<AoothArbacClaims | undefined>;
304
+ }
305
+ /**
306
+ * DI token form of {@link ArbacUserProvider}. The class itself is `abstract`,
307
+ * so it does not satisfy moost's `TClassConstructor` (`new (...) => T`)
308
+ * structurally; this re-typed alias is the supported way to pass the base
309
+ * class to `cc.instantiate(...)`, `createReplaceRegistry(...)`, `@Replace(...)`,
310
+ * etc. Runtime resolves to the concrete subclass registered via DI.
311
+ */
312
+ declare const ArbacUserProviderToken: TClassConstructor<ArbacUserProvider>;
313
+ //#endregion
314
+ export { ArbacDbScope as a, enforceControlsPolicy as c, NavRelationKey as d, NavTarget as f, conjoinArbacDbScopes as i, extractUsedControlValues as l, ProjectionOf as m, ArbacUserProviderToken as n, AsArbacDbController as o, OwnFieldKey as p, AoothArbacClaims as r, applyAllowedFieldsAndSet as s, ArbacUserProvider as t, ControlsOf as u };