@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.
package/dist/index.mjs ADDED
@@ -0,0 +1,470 @@
1
+ import { n as ArbacUserProviderToken, r as __decorate, t as ArbacUserProvider } from "./user.provider-CGWueNjx.mjs";
2
+ import { Arbac, Arbac as Arbac$1, arbacPatternToRegex } from "@aooth/arbac-core";
3
+ import { Authenticate, HttpError } from "@moostjs/event-http";
4
+ import { current, key } from "@wooksjs/event-core";
5
+ import { Inherit, Injectable, TInterceptorPriority, defineBeforeInterceptor, getConstructor, getInstanceOwnMethods, getMoostMate, useControllerContext, useLogger } from "moost";
6
+ import { mergeScopeFilters, restrictProjection, unionControlsPolicy, unionProjections } from "@aooth/arbac";
7
+ import { AsDbController, AsDbReadableController } from "@atscript/moost-db";
8
+ //#region src/moost-arbac.ts
9
+ let MoostArbac = class MoostArbac extends Arbac$1 {};
10
+ MoostArbac = __decorate([Injectable()], MoostArbac);
11
+ //#endregion
12
+ //#region src/arbac.composables.ts
13
+ /**
14
+ * Writable slot holding the evaluated scopes for the current event.
15
+ *
16
+ * Module-level `key()` per the `@wooksjs/event-core` slot system. The
17
+ * authorize interceptor calls `setScopes(...)` after a successful evaluate;
18
+ * downstream `@ArbacScopes()` resolvers read it back from the same event context.
19
+ */
20
+ const arbacScopesKey = key("arbac.scopes");
21
+ /**
22
+ * Composable for ARBAC utilities within Moost handlers and interceptors.
23
+ *
24
+ * Exposes scope read/write, lazy `evaluate`, and the resolved
25
+ * resource/action/public flags derived from the current controller +
26
+ * method metadata.
27
+ *
28
+ * Resource/action/isPublic are recomputed on every call because WF events
29
+ * dispatch multiple step handlers under one `EventContext` — each step
30
+ * mutates the controller-context method via `setControllerContext`, so any
31
+ * per-ctx memo would lock in the first dispatched method's metadata (e.g.
32
+ * the `@Public()` WF_FLOW body) and incorrectly bypass ARBAC on the gated
33
+ * step handlers. Scope read/write goes through `arbacScopesKey` directly
34
+ * so it still lives on the per-event slot.
35
+ */
36
+ const useArbac = (_ctx) => {
37
+ const ctx = _ctx ?? current();
38
+ const cc = useControllerContext(ctx);
39
+ const getScopes = () => ctx.has(arbacScopesKey) ? ctx.get(arbacScopesKey) : void 0;
40
+ const setScopes = (scope) => {
41
+ ctx.set(arbacScopesKey, scope);
42
+ };
43
+ const cMeta = cc.getControllerMeta();
44
+ const mMeta = cc.getMethodMeta();
45
+ const resource = mMeta?.arbacResourceId || cMeta?.arbacResourceId || cMeta?.id || getConstructor(cc.getController()).name;
46
+ const action = mMeta?.arbacActionId || mMeta?.atscript_db_action?.name || cMeta?.arbacActionId || mMeta?.id || (cc.getMethod() ?? "");
47
+ const isPublic = mMeta?.arbacPublic || cMeta?.arbacPublic || false;
48
+ const evaluate = async (opts) => {
49
+ const effectiveResource = opts?.resource || resource;
50
+ const effectiveAction = opts?.action || action;
51
+ if (!effectiveResource) throw new Error("useArbac().evaluate(): `resource` is required — could not be resolved from controller/method metadata. Pass it explicitly.");
52
+ if (!effectiveAction) throw new Error("useArbac().evaluate(): `action` is required — could not be resolved from controller/method metadata. Pass it explicitly.");
53
+ const [user, arbac] = await Promise.all([cc.instantiate(ArbacUserProviderToken), cc.instantiate(MoostArbac)]);
54
+ const userId = await user.getUserId();
55
+ 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
+ }),
64
+ userId
65
+ };
66
+ };
67
+ const evaluateOrThrow = async (opts) => {
68
+ const result = await evaluate(opts);
69
+ if (!result.allowed) throw new HttpError(403, `Forbidden: ${opts?.resource || resource}/${opts?.action || action}`);
70
+ return {
71
+ ...result,
72
+ allowed: true
73
+ };
74
+ };
75
+ return {
76
+ getScopes,
77
+ setScopes,
78
+ evaluate,
79
+ evaluateOrThrow,
80
+ resource,
81
+ action,
82
+ isPublic
83
+ };
84
+ };
85
+ //#endregion
86
+ //#region src/arbac.mate.ts
87
+ /**
88
+ * Returns the shared `Mate` instance typed with ARBAC metadata fields.
89
+ *
90
+ * All ARBAC decorators write through this typed wrapper so reads and writes
91
+ * stay type-checked against `TArbacMeta`.
92
+ */
93
+ function getArbacMate() {
94
+ return getMoostMate();
95
+ }
96
+ //#endregion
97
+ //#region src/arbac.decorator.ts
98
+ /**
99
+ * ARBAC checks authorization, not authentication, so the transport
100
+ * declaration is empty — pair with an upstream auth guard (e.g. JWT
101
+ * bearer) that publishes the principal.
102
+ *
103
+ * Runs for every event kind (HTTP, WF, CLI, WS). The `arbacPublic` mate
104
+ * flag (written by auth-moost's combined `@Public()`) is the only bypass —
105
+ * apply it to controllers/handlers that should remain reachable without an
106
+ * evaluated rule (e.g. login/recovery workflows, health probes).
107
+ */
108
+ const arbacAuthorizeInterceptor = Object.assign(defineBeforeInterceptor(async () => {
109
+ const ctx = current();
110
+ const { setScopes, evaluate, resource, action, isPublic } = useArbac(ctx);
111
+ if (!action || !resource || isPublic) return;
112
+ const logger = useLogger("arbac", ctx);
113
+ try {
114
+ const { allowed, scopes, userId } = await evaluate();
115
+ logger.debug(`[${userId}] ${allowed ? "Authorized" : "Blocked"} "${resource}" : "${action}"`);
116
+ if (!allowed) throw new HttpError(403, `Insufficient privileges for action "${action}" on resource "${resource}"`);
117
+ setScopes(scopes);
118
+ } catch (error) {
119
+ if (error instanceof HttpError) throw error;
120
+ logger.warn(String(error));
121
+ throw new HttpError(401, error instanceof Error ? error.message : String(error));
122
+ }
123
+ }, TInterceptorPriority.GUARD), { __authTransports: {} });
124
+ /** Wrapped via `Authenticate` so `@moostjs/swagger` picks up the auth-guard metadata. */
125
+ const ArbacAuthorize = () => Authenticate(arbacAuthorizeInterceptor);
126
+ /**
127
+ * Decorator to specify a resource id for ARBAC evaluation. Apply to a
128
+ * controller class or a handler method.
129
+ */
130
+ const ArbacResource = (name) => getArbacMate().decorate("arbacResourceId", name);
131
+ /**
132
+ * Decorator to specify an action id for ARBAC evaluation. Typically applied
133
+ * at the method level.
134
+ */
135
+ const ArbacAction = (name) => getArbacMate().decorate("arbacActionId", name);
136
+ //#endregion
137
+ //#region src/db/shared-read-helpers.ts
138
+ /** Filter that matches no rows; used when ARBAC denies the request. */
139
+ const DENY_FILTER = { $or: [] };
140
+ /** Single point of access to the per-event scope cache. */
141
+ function readCachedScopes() {
142
+ return useArbac().getScopes() ?? [];
143
+ }
144
+ /**
145
+ * Full body of `transformFilter` for both ARBAC DB controllers: evaluate
146
+ * ARBAC once, cache the scopes for the per-event hooks that follow
147
+ * (`transformProjection`, `validateControls`), and merge the user filter
148
+ * with the union of scope filters.
149
+ *
150
+ * Wraps the merge with `$and` (not object spread) so a top-level
151
+ * `$or`/`$and`/`$not` in `filter` cannot drop the scope's sibling field
152
+ * keys when @uniqu/core's `walkFilter` short-circuits on logical
153
+ * operators (see BUG-2). Returns a match-nothing filter on denial so the
154
+ * downstream pipeline returns an empty result set without leaking rows.
155
+ */
156
+ async function transformArbacFilter(filter) {
157
+ const arbac = useArbac();
158
+ const { allowed, scopes } = await arbac.evaluate();
159
+ if (!allowed) return DENY_FILTER;
160
+ arbac.setScopes(scopes);
161
+ const scopeList = scopes ?? [];
162
+ const merged = scopeList.length > 0 ? mergeScopeFilters(scopeList.map((s) => s.filter ?? {})) : void 0;
163
+ const userFilter = filter && Object.keys(filter).length > 0 ? filter : void 0;
164
+ if (!merged) return userFilter ?? {};
165
+ if (!userFilter) return merged;
166
+ return { $and: [merged, userFilter] };
167
+ }
168
+ /**
169
+ * Union the per-scope `projection` whitelists and restrict the user-supplied
170
+ * projection to that union. Returns the original projection untouched when
171
+ * no scope declares a projection (so caller sees the unrestricted shape).
172
+ */
173
+ function applyArbacProjection(projection, scopes) {
174
+ if (scopes.length === 0) return projection;
175
+ const allowed = unionProjections(...scopes.map((s) => s.projection ?? {}));
176
+ if (Object.keys(allowed).length === 0) return projection;
177
+ return restrictProjection(projection ?? {}, allowed);
178
+ }
179
+ /**
180
+ * Enforce the union of per-scope `controls` gates against the parsed
181
+ * Uniquery controls. Throws `HttpError(403)` on the first violation.
182
+ */
183
+ function applyArbacControls(controls, scopes) {
184
+ if (scopes.length === 0) return;
185
+ enforceControlsPolicy(unionControlsPolicy(scopes), controls);
186
+ }
187
+ /**
188
+ * Walk the user-supplied `$with` items in `controls` and inject per-relation
189
+ * filter/projection/controls/nested-$with from the role scopes' `with` field.
190
+ *
191
+ * Mutates `controls` in place (matches the `validateControls` contract).
192
+ * Throws `HttpError(403)` if a per-relation control policy is violated
193
+ * (delegates to `enforceControlsPolicy` like top-level `applyArbacControls`).
194
+ *
195
+ * Silence wins: if no role declares `with.<name>` for a relation, that entry
196
+ * passes through unchanged. The outermost check skips the entire walk when no
197
+ * scope declares `with` at all — this is the common case on every read.
198
+ */
199
+ function applyArbacRelationScopes(controls, scopes) {
200
+ if (scopes.length === 0) return;
201
+ const withArr = controls.$with;
202
+ if (!Array.isArray(withArr) || withArr.length === 0) return;
203
+ if (!scopes.some((s) => s.with)) return;
204
+ for (const raw of withArr) {
205
+ if (!raw || typeof raw !== "object") continue;
206
+ const entry = raw;
207
+ if (typeof entry.name !== "string") continue;
208
+ const subScopes = collectSubScopes(scopes, entry.name);
209
+ if (subScopes.length === 0) continue;
210
+ const subFilter = mergeScopeFilters(subScopes.map((s) => s.filter ?? {}));
211
+ if (subFilter) {
212
+ const userFilter = entry.filter && Object.keys(entry.filter).length > 0 ? entry.filter : void 0;
213
+ entry.filter = userFilter ? { $and: [subFilter, userFilter] } : subFilter;
214
+ }
215
+ const entryControls = entry.controls ?? {};
216
+ const restricted = applyArbacProjection(normalizeSelect(entryControls.$select), subScopes);
217
+ if (restricted !== void 0) {
218
+ entryControls.$select = restricted;
219
+ entry.controls = entryControls;
220
+ }
221
+ enforceControlsPolicy(unionControlsPolicy(subScopes), entryControls);
222
+ applyArbacRelationScopes(entryControls, subScopes);
223
+ }
224
+ }
225
+ /** Pick `scopes[i].with?.[name]`, dropping undefined. */
226
+ function collectSubScopes(scopes, name) {
227
+ const out = [];
228
+ for (const s of scopes) {
229
+ const sub = s.with?.[name];
230
+ if (sub) out.push(sub);
231
+ }
232
+ return out;
233
+ }
234
+ /**
235
+ * `$select` may be an array of field names (uniquery inclusion list) or a
236
+ * `TProjection` object. `applyArbacProjection` / `restrictProjection` operate
237
+ * on the object form, so normalize the array to `{ field: 1 }` first. Returns
238
+ * undefined if the input is empty (caller treats as "no projection set").
239
+ */
240
+ function normalizeSelect(value) {
241
+ if (value === void 0 || value === null) return void 0;
242
+ if (Array.isArray(value)) {
243
+ const out = {};
244
+ for (const item of value) if (typeof item === "string") out[item] = 1;
245
+ return Object.keys(out).length > 0 ? out : void 0;
246
+ }
247
+ if (typeof value === "object") return value;
248
+ }
249
+ //#endregion
250
+ //#region src/db/as-arbac-db-controller.ts
251
+ let AsArbacDbController = class AsArbacDbController extends AsDbController {
252
+ transformFilter(filter) {
253
+ return transformArbacFilter(filter);
254
+ }
255
+ transformProjection(projection) {
256
+ return applyArbacProjection(projection, readCachedScopes());
257
+ }
258
+ /**
259
+ * Enforce per-role `ArbacDbScope.controls` gates against the parsed Uniquery
260
+ * controls of a request. Runs after the base validator (which checks the
261
+ * controls DTO shape) and BEFORE the query/aggregation pipeline executes.
262
+ *
263
+ * `transformFilter` runs first on every read endpoint and caches the
264
+ * evaluated scopes via `arbac.setScopes(...)`, so we read those scopes
265
+ * directly here without re-evaluating ARBAC.
266
+ *
267
+ * On a violation we throw `HttpError(403)`. moost-db's `query` / `pages` /
268
+ * `getOne` handlers do NOT wrap `validateParsed` in try/catch, so the
269
+ * thrown error bubbles to moost which translates it to a 403 response.
270
+ */
271
+ validateControls(controls, type) {
272
+ const baseErr = super.validateControls(controls, type);
273
+ if (baseErr) return baseErr;
274
+ const scopes = readCachedScopes();
275
+ applyArbacControls(controls, scopes);
276
+ applyArbacRelationScopes(controls, scopes);
277
+ }
278
+ async applyMetaOverlay(meta) {
279
+ const arbac = useArbac();
280
+ const actionToMethodMeta = collectActionMetaByName();
281
+ const crudKeys = Object.keys(meta.crud);
282
+ const [actionResults, crudResults] = await Promise.all([Promise.all(meta.actions.map((entry) => {
283
+ const methodMeta = actionToMethodMeta.get(entry.name);
284
+ const arbacAction = methodMeta?.arbacActionId ?? methodMeta?.id ?? entry.name;
285
+ return arbac.evaluate({ action: arbacAction });
286
+ })), Promise.all(crudKeys.map((key) => arbac.evaluate({ action: key })))]);
287
+ const filteredActions = [];
288
+ for (let i = 0; i < meta.actions.length; i++) if (actionResults[i].allowed) filteredActions.push(meta.actions[i]);
289
+ const filteredCrud = {};
290
+ for (let i = 0; i < crudKeys.length; i++) if (crudResults[i].allowed) filteredCrud[crudKeys[i]] = meta.crud[crudKeys[i]];
291
+ return {
292
+ ...meta,
293
+ actions: filteredActions,
294
+ crud: filteredCrud
295
+ };
296
+ }
297
+ async onWrite(action, data) {
298
+ const scopes = readCachedScopes();
299
+ if (action !== "insert" && action !== "insertMany") await this.assertInScope(data, scopes);
300
+ return applyAllowedFieldsAndSet(data, scopes, this.identifierFields());
301
+ }
302
+ async onRemove(id) {
303
+ await this.assertInScope(id, readCachedScopes());
304
+ return id;
305
+ }
306
+ async assertInScope(idOrIds, scopes) {
307
+ const scopeFilter = mergeScopeFilters(scopes.map((s) => s.filter ?? {}));
308
+ if (!scopeFilter) return;
309
+ const ids = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
310
+ const idFilters = ids.map((id) => this.table.resolveIdFilter(id));
311
+ if (idFilters.some((f) => !f)) throw new HttpError(404, "Not found");
312
+ const idFilter = idFilters.length === 1 ? idFilters[0] : { $or: idFilters };
313
+ if (await this.table.count({ filter: { $and: [idFilter, scopeFilter] } }) < ids.length) throw new HttpError(404, "Not found");
314
+ }
315
+ identifierFields() {
316
+ const ctor = this.constructor;
317
+ const cached = identifierFieldsCache.get(ctor);
318
+ if (cached) return cached;
319
+ const out = /* @__PURE__ */ new Set();
320
+ for (const ident of this.table.identifications) for (const f of ident.fields) out.add(f);
321
+ const arr = [...out];
322
+ identifierFieldsCache.set(ctor, arr);
323
+ return arr;
324
+ }
325
+ };
326
+ AsArbacDbController = __decorate([Inherit()], AsArbacDbController);
327
+ const identifierFieldsCache = /* @__PURE__ */ new WeakMap();
328
+ const actionMetaByClassCache = /* @__PURE__ */ new WeakMap();
329
+ function collectActionMetaByName() {
330
+ const cc = useControllerContext();
331
+ const instance = cc.getController();
332
+ const ctor = getConstructor(instance);
333
+ const cached = actionMetaByClassCache.get(ctor);
334
+ if (cached) return cached;
335
+ const map = /* @__PURE__ */ new Map();
336
+ const ctrlMeta = cc.getControllerMeta();
337
+ for (const entry of ctrlMeta?.atscript_db_actions ?? []) map.set(entry.name, {});
338
+ for (const methodName of getInstanceOwnMethods(instance)) {
339
+ if (typeof methodName !== "string") continue;
340
+ const m = cc.getMethodMeta(methodName);
341
+ if (!m) continue;
342
+ const actionMeta = m.atscript_db_action;
343
+ if (actionMeta?.name) map.set(actionMeta.name, {
344
+ arbacActionId: m.arbacActionId,
345
+ id: m.id
346
+ });
347
+ }
348
+ actionMetaByClassCache.set(ctor, map);
349
+ return map;
350
+ }
351
+ /**
352
+ * Test-friendly internal helper — exported for unit tests and helper
353
+ * composition; regular consumers should not call this directly.
354
+ *
355
+ * Enforce a per-control policy against a parsed Uniquery `controls` map.
356
+ *
357
+ * Throws `HttpError(403)` on the first violation. Pure (no DI) so it is
358
+ * trivially unit-testable; `validateControls` wires it up to the controller's
359
+ * cached scopes.
360
+ *
361
+ * Semantics per gate (see {@link ControlGate}):
362
+ * - `true` (or absent — dropped by `unionControlsPolicy`): allow.
363
+ * - `false`: deny if the control is used at all.
364
+ * - `readonly string[]`: allow only the listed values; reject any other.
365
+ *
366
+ * "Used" means the control key is present AND non-empty (an empty array is
367
+ * treated as not used, matching how the parser leaves missing controls).
368
+ */
369
+ function enforceControlsPolicy(policy, controls) {
370
+ if (Object.keys(policy).length === 0) return;
371
+ for (const [key, gate] of Object.entries(policy)) {
372
+ const used = controls[key];
373
+ if (used === void 0 || used === null) continue;
374
+ if (Array.isArray(used) && used.length === 0) continue;
375
+ if (gate === false) throw new HttpError(403, `Control "${key}" is not allowed for your role`);
376
+ if (Array.isArray(gate)) {
377
+ const usedValues = extractUsedControlValues(key, used);
378
+ for (const v of usedValues) if (!gate.includes(v)) throw new HttpError(403, `Control "${key}=${v}" is not allowed for your role`);
379
+ }
380
+ }
381
+ }
382
+ /**
383
+ * Test-friendly internal helper — exported for unit tests and helper
384
+ * composition; regular consumers should not call this directly.
385
+ *
386
+ * Extract the set of "named values" from a Uniquery control payload, for
387
+ * use against a whitelist gate.
388
+ *
389
+ * Currently supported (matches `WHITELISTABLE_CONTROLS` in `unionControlsPolicy`):
390
+ * - `$with` — array of `{ name, … }` objects (per `TypedWithRelation`,
391
+ * see `@uniqu/core` parser at `parseWithSegment`); we extract `name`.
392
+ * Bare strings are tolerated for forward compatibility.
393
+ * - `$groupBy` — array of column names (strings). Returned as-is.
394
+ *
395
+ * For unknown controls we return an empty array; the caller then enforces
396
+ * `false`-only semantics (controlled by `unionControlsPolicy`'s whitelist
397
+ * gate, which throws if a non-whitelistable control receives a string[]).
398
+ */
399
+ function extractUsedControlValues(key, value) {
400
+ if (!Array.isArray(value)) return [];
401
+ if (key === "$with") {
402
+ const out = [];
403
+ for (const entry of value) if (typeof entry === "string") out.push(entry);
404
+ else if (typeof entry?.name === "string") out.push(entry.name);
405
+ return out;
406
+ }
407
+ if (key === "$groupBy") return value.filter((x) => typeof x === "string");
408
+ return [];
409
+ }
410
+ /**
411
+ * Test-friendly internal helper — exported for unit tests and helper
412
+ * composition; regular consumers should not call this directly.
413
+ *
414
+ * Apply the union of `allowedFields` whitelists (with `preserveFields`
415
+ * always preserved) and overlay each scope's `set` overrides. Returns a
416
+ * shallow copy; original `data` is not mutated.
417
+ */
418
+ function applyAllowedFieldsAndSet(data, scopes, preserveFields = []) {
419
+ if (scopes.length === 0) return data;
420
+ const prepared = prepareScopeOverlay(scopes, preserveFields);
421
+ if (Array.isArray(data)) return data.map((row) => applyPreparedOverlay(row, prepared));
422
+ return applyPreparedOverlay(data, prepared);
423
+ }
424
+ function prepareScopeOverlay(scopes, preserveFields) {
425
+ let union = null;
426
+ for (const s of scopes) if (Array.isArray(s.allowedFields)) {
427
+ if (!union) union = /* @__PURE__ */ new Set();
428
+ for (const f of s.allowedFields) union.add(f);
429
+ }
430
+ if (union) for (const f of preserveFields) union.add(f);
431
+ let setOverrides = null;
432
+ for (const s of scopes) if (s.set) {
433
+ if (!setOverrides) setOverrides = {};
434
+ Object.assign(setOverrides, s.set);
435
+ }
436
+ return {
437
+ union,
438
+ setOverrides
439
+ };
440
+ }
441
+ function applyPreparedOverlay(data, prepared) {
442
+ if (Array.isArray(data)) return data.map((row) => applyPreparedOverlay(row, prepared));
443
+ if (!data || typeof data !== "object") return data;
444
+ const merged = { ...data };
445
+ if (prepared.union) {
446
+ for (const k of Object.keys(merged)) if (!prepared.union.has(k)) delete merged[k];
447
+ }
448
+ if (prepared.setOverrides) Object.assign(merged, prepared.setOverrides);
449
+ return merged;
450
+ }
451
+ //#endregion
452
+ //#region src/db/as-arbac-db-readable-controller.ts
453
+ let AsArbacDbReadableController = class AsArbacDbReadableController extends AsDbReadableController {
454
+ transformFilter(filter) {
455
+ return transformArbacFilter(filter);
456
+ }
457
+ transformProjection(projection) {
458
+ return applyArbacProjection(projection, readCachedScopes());
459
+ }
460
+ validateControls(controls, type) {
461
+ const baseErr = super.validateControls(controls, type);
462
+ if (baseErr) return baseErr;
463
+ const scopes = readCachedScopes();
464
+ applyArbacControls(controls, scopes);
465
+ applyArbacRelationScopes(controls, scopes);
466
+ }
467
+ };
468
+ AsArbacDbReadableController = __decorate([Inherit()], AsArbacDbReadableController);
469
+ //#endregion
470
+ export { Arbac, ArbacAction, ArbacAuthorize, ArbacResource, ArbacUserProvider, ArbacUserProviderToken, AsArbacDbController, AsArbacDbReadableController, MoostArbac, applyAllowedFieldsAndSet, arbacAuthorizeInterceptor, arbacPatternToRegex, enforceControlsPolicy, extractUsedControlValues, getArbacMate, useArbac };
@@ -0,0 +1,28 @@
1
+ import { TAtscriptPlugin } from "@atscript/core";
2
+
3
+ //#region src/plugin.d.ts
4
+ /**
5
+ * Registers the `@arbac.*` annotation namespace for `@aooth/arbac-moost`:
6
+ *
7
+ * - `@arbac.role` — field is the source of role identifiers. Exactly one
8
+ * field per type. Two valid shapes:
9
+ * - inline `string | string[]` — values are read directly,
10
+ * - `@db.rel.from` nav prop (1:N to a role table) — provider auto-injects
11
+ * `controls.$with` and pulls role names off the joined records.
12
+ * Multiple `@arbac.role` declarations throw at spec computation time.
13
+ * - `@arbac.attribute` — field becomes a user attribute keyed by its prop name.
14
+ * Multiple `@arbac.attribute` fields are merged into the `UserAttrs` map.
15
+ * - `@arbac.userId` — overrides the user-id source. Resolution order is
16
+ * (1) `@arbac.userId`, (2) the single field of the `@db.table.preferredId.uniqueIndex`
17
+ * group, (3) `@meta.id` — first match wins.
18
+ *
19
+ * Install in `atscript.config.ts`:
20
+ *
21
+ * ```ts
22
+ * import arbacPlugin from '@aooth/arbac-moost/plugin'
23
+ * export default { plugins: [arbacPlugin()] }
24
+ * ```
25
+ */
26
+ declare function arbacPlugin(): TAtscriptPlugin;
27
+ //#endregion
28
+ export { arbacPlugin as default };
@@ -0,0 +1,50 @@
1
+ import { AnnotationSpec } from "@atscript/core";
2
+ //#region src/plugin.ts
3
+ /**
4
+ * Registers the `@arbac.*` annotation namespace for `@aooth/arbac-moost`:
5
+ *
6
+ * - `@arbac.role` — field is the source of role identifiers. Exactly one
7
+ * field per type. Two valid shapes:
8
+ * - inline `string | string[]` — values are read directly,
9
+ * - `@db.rel.from` nav prop (1:N to a role table) — provider auto-injects
10
+ * `controls.$with` and pulls role names off the joined records.
11
+ * Multiple `@arbac.role` declarations throw at spec computation time.
12
+ * - `@arbac.attribute` — field becomes a user attribute keyed by its prop name.
13
+ * Multiple `@arbac.attribute` fields are merged into the `UserAttrs` map.
14
+ * - `@arbac.userId` — overrides the user-id source. Resolution order is
15
+ * (1) `@arbac.userId`, (2) the single field of the `@db.table.preferredId.uniqueIndex`
16
+ * group, (3) `@meta.id` — first match wins.
17
+ *
18
+ * Install in `atscript.config.ts`:
19
+ *
20
+ * ```ts
21
+ * import arbacPlugin from '@aooth/arbac-moost/plugin'
22
+ * export default { plugins: [arbacPlugin()] }
23
+ * ```
24
+ */
25
+ function arbacPlugin() {
26
+ return {
27
+ name: "aoothjs-arbac",
28
+ 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
+ } } };
46
+ }
47
+ };
48
+ }
49
+ //#endregion
50
+ export { arbacPlugin as default };
@@ -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("username", defineAnnotatedType().designType("string").tags("string").annotate("db.index.unique", "username_idx", 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).$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 };
@@ -0,0 +1,31 @@
1
+ //#region \0@oxc-project+runtime@0.129.0/helpers/decorate.js
2
+ function __decorate(decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ }
8
+ //#endregion
9
+ //#region src/user.provider.ts
10
+ /**
11
+ * Abstract base class for providing user data required for ARBAC evaluations.
12
+ *
13
+ * Consumers must extend this class and implement the three abstract methods
14
+ * to plug their own authentication/identity layer into the ARBAC authorize
15
+ * interceptor. The subclass must re-apply `@Injectable()` (moost does not
16
+ * inherit decorator metadata) and be registered via Moost's
17
+ * `setReplaceRegistry` (or `@Replace`) so DI resolves the concrete provider.
18
+ *
19
+ * @template TUserAttrs - The type representing user attributes relevant to access control.
20
+ */
21
+ var ArbacUserProvider = class {};
22
+ /**
23
+ * DI token form of {@link ArbacUserProvider}. The class itself is `abstract`,
24
+ * so it does not satisfy moost's `TClassConstructor` (`new (...) => T`)
25
+ * structurally; this re-typed alias is the supported way to pass the base
26
+ * class to `cc.instantiate(...)`, `createReplaceRegistry(...)`, `@Replace(...)`,
27
+ * etc. Runtime resolves to the concrete subclass registered via DI.
28
+ */
29
+ const ArbacUserProviderToken = ArbacUserProvider;
30
+ //#endregion
31
+ export { ArbacUserProviderToken as n, __decorate as r, ArbacUserProvider as t };
@@ -0,0 +1,42 @@
1
+ import { TClassConstructor } from "moost";
2
+
3
+ //#region src/user.provider.d.ts
4
+ /**
5
+ * Abstract base class for providing user data required for ARBAC evaluations.
6
+ *
7
+ * Consumers must extend this class and implement the three abstract methods
8
+ * to plug their own authentication/identity layer into the ARBAC authorize
9
+ * interceptor. The subclass must re-apply `@Injectable()` (moost does not
10
+ * inherit decorator metadata) and be registered via Moost's
11
+ * `setReplaceRegistry` (or `@Replace`) so DI resolves the concrete provider.
12
+ *
13
+ * @template TUserAttrs - The type representing user attributes relevant to access control.
14
+ */
15
+ declare abstract class ArbacUserProvider<TUserAttrs extends object = object> {
16
+ /**
17
+ * Retrieves the unique identifier of the current user.
18
+ */
19
+ abstract getUserId(): string | Promise<string>;
20
+ /**
21
+ * Retrieves the roles assigned to a user based on their ID.
22
+ *
23
+ * @param id - The user ID.
24
+ */
25
+ abstract getRoles(id: string): string[] | Promise<string[]>;
26
+ /**
27
+ * Retrieves the attributes associated with a user based on their ID.
28
+ *
29
+ * @param id - The user ID.
30
+ */
31
+ abstract getAttrs(id: string): TUserAttrs | Promise<TUserAttrs>;
32
+ }
33
+ /**
34
+ * DI token form of {@link ArbacUserProvider}. The class itself is `abstract`,
35
+ * so it does not satisfy moost's `TClassConstructor` (`new (...) => T`)
36
+ * structurally; this re-typed alias is the supported way to pass the base
37
+ * class to `cc.instantiate(...)`, `createReplaceRegistry(...)`, `@Replace(...)`,
38
+ * etc. Runtime resolves to the concrete subclass registered via DI.
39
+ */
40
+ declare const ArbacUserProviderToken: TClassConstructor<ArbacUserProvider>;
41
+ //#endregion
42
+ export { ArbacUserProviderToken as n, ArbacUserProvider as t };