@aooth/arbac-moost 0.1.3 → 0.1.4
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/README.md +16 -0
- package/dist/atscript/index.d.mts +1 -1
- package/dist/atscript/index.mjs +1 -1
- package/dist/index.d.mts +19 -0
- package/dist/index.mjs +49 -0
- package/dist/user-ChbLPYhG.mjs +15 -0
- package/package.json +15 -14
- package/dist/user-CFXfHtsb.mjs +0 -15
package/README.md
CHANGED
|
@@ -33,6 +33,22 @@ Full docs, API reference, and recipes: **https://aooth.moost.org/moost/**
|
|
|
33
33
|
- [Decorators](https://aooth.moost.org/moost/decorators)
|
|
34
34
|
- [API reference](https://aooth.moost.org/api/arbac-moost)
|
|
35
35
|
|
|
36
|
+
## Custom action handlers — your responsibility
|
|
37
|
+
|
|
38
|
+
The `@ArbacAuthorize` interceptor gates the **entry** to a `@DbAction` handler: if the user lacks the privilege, the handler doesn't run. But what the handler does inside its body is on you — the interceptor doesn't follow secondary database calls or external side effects.
|
|
39
|
+
|
|
40
|
+
If your handler reads or writes rows from the same table the entry-gate scopes, route those calls through the active scope. `useArbac().getScopes<ArbacDbScope>()` exposes the per-event scope array; from there:
|
|
41
|
+
|
|
42
|
+
- `scope.filter` — AND-merge into your WHERE clause (use the union of all scope filters via `mergeScopeFilters` from `@aooth/arbac`, not just the first).
|
|
43
|
+
- `scope.set` — overlay onto any insert/update payload **after** your own body keys, so scope-set wins over caller-supplied values.
|
|
44
|
+
- `scope.projection` — restrict any custom SELECT to the union of allowed fields.
|
|
45
|
+
|
|
46
|
+
The demo at [`packages/e2e-demo/src/controllers/_helpers.ts`](../e2e-demo/src/controllers/_helpers.ts) shows the convention (`scopedFilter`, `scopedSet`); the test `ACT-08` in [`packages/e2e-demo/test/arbac-actions.spec.ts`](../e2e-demo/test/arbac-actions.spec.ts) pins that handlers using this pattern correctly enforce row-level scope.
|
|
47
|
+
|
|
48
|
+
If your handler talks to a different system (audit log, cache, external API, sibling table) or has special logic (cross-tenant aggregations, admin escape hatches), you wire the scope to whatever surface that system exposes — there is no automatic propagation, and the trust decisions stay explicit in the handler.
|
|
49
|
+
|
|
50
|
+
> **Why not enforce structurally?** The framework can't know, at the action-entry point, what the handler intends to read or write. A handler that calls a remote service, spans transactions across tables, or has admin-only branches won't compose cleanly with an automatic row-level gate. Keeping scope enforcement explicit makes the trust decision a code-review-visible artifact. The tradeoff is that a forgetful contributor can bypass it — mitigate via code review and ACT-08-style regression tests.
|
|
51
|
+
|
|
36
52
|
## License
|
|
37
53
|
|
|
38
54
|
MIT
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as AoothArbacUserCredentials } from "../user-
|
|
1
|
+
import { t as AoothArbacUserCredentials } from "../user-ChbLPYhG.mjs";
|
|
2
2
|
import { t as ArbacUserProvider } from "../user.provider-JpI5dD6Z.mjs";
|
|
3
3
|
import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
|
|
4
4
|
|
package/dist/atscript/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { r as __decorate, t as ArbacUserProvider } from "../user.provider-CGWueNjx.mjs";
|
|
2
|
-
import { t as AoothArbacUserCredentials } from "../user-
|
|
2
|
+
import { t as AoothArbacUserCredentials } from "../user-ChbLPYhG.mjs";
|
|
3
3
|
import { defineWook, key } from "@wooksjs/event-core";
|
|
4
4
|
import { Injectable } from "moost";
|
|
5
5
|
//#region \0@oxc-project+runtime@0.129.0/helpers/decorateMetadata.js
|
package/dist/index.d.mts
CHANGED
|
@@ -237,6 +237,25 @@ type WithOf<T> = unknown extends T ? Record<string, ArbacDbScope> : { [K in NavR
|
|
|
237
237
|
declare class AsArbacDbController<T extends TAtscriptAnnotatedType = TAtscriptAnnotatedType> extends AsDbController<T> {
|
|
238
238
|
protected transformFilter(filter: Record<string, unknown> | undefined): Promise<Record<string, unknown>>;
|
|
239
239
|
protected transformProjection(projection?: TProjection): TProjection | undefined | Promise<TProjection | undefined>;
|
|
240
|
+
/**
|
|
241
|
+
* Translate `@uniqu/url` parser failures into HTTP 400 instead of letting them
|
|
242
|
+
* bubble as 500. The base `parseQueryString` calls `parseUrl()` directly with
|
|
243
|
+
* no try/catch, so a malformed `?status=...` (e.g. unquoted single quote at a
|
|
244
|
+
* non-string position) surfaces as a server error — misleading, since the
|
|
245
|
+
* server is fine and the client sent bad input.
|
|
246
|
+
*
|
|
247
|
+
* Narrow targeting: only `SyntaxError` raised from inside the parser is
|
|
248
|
+
* remapped. Other errors (e.g. a programmer-introduced TypeError in a future
|
|
249
|
+
* override) still bubble as 500. SQL-injection-shaped payloads remain safely
|
|
250
|
+
* handled either way — see SEC-17 for the parameterisation invariant; this
|
|
251
|
+
* override is an orthogonal robustness pin around the parse step.
|
|
252
|
+
*/
|
|
253
|
+
protected parseQueryString(url: string): ReturnType<AsDbController<T>["parseQueryString"]>;
|
|
254
|
+
/**
|
|
255
|
+
* Same parser-error → 400 remap as {@link parseQueryString}, applied to the
|
|
256
|
+
* `/one` and `/one?…` code paths which use `parseControlsOnlyFromUrl`.
|
|
257
|
+
*/
|
|
258
|
+
protected parseControlsOnlyFromUrl(url: string): ReturnType<AsDbController<T>["parseControlsOnlyFromUrl"]>;
|
|
240
259
|
/**
|
|
241
260
|
* Enforce per-role `ArbacDbScope.controls` gates against the parsed Uniquery
|
|
242
261
|
* controls of a request. Runs after the base validator (which checks the
|
package/dist/index.mjs
CHANGED
|
@@ -256,6 +256,37 @@ let AsArbacDbController = class AsArbacDbController extends AsDbController {
|
|
|
256
256
|
return applyArbacProjection(projection, readCachedScopes());
|
|
257
257
|
}
|
|
258
258
|
/**
|
|
259
|
+
* Translate `@uniqu/url` parser failures into HTTP 400 instead of letting them
|
|
260
|
+
* bubble as 500. The base `parseQueryString` calls `parseUrl()` directly with
|
|
261
|
+
* no try/catch, so a malformed `?status=...` (e.g. unquoted single quote at a
|
|
262
|
+
* non-string position) surfaces as a server error — misleading, since the
|
|
263
|
+
* server is fine and the client sent bad input.
|
|
264
|
+
*
|
|
265
|
+
* Narrow targeting: only `SyntaxError` raised from inside the parser is
|
|
266
|
+
* remapped. Other errors (e.g. a programmer-introduced TypeError in a future
|
|
267
|
+
* override) still bubble as 500. SQL-injection-shaped payloads remain safely
|
|
268
|
+
* handled either way — see SEC-17 for the parameterisation invariant; this
|
|
269
|
+
* override is an orthogonal robustness pin around the parse step.
|
|
270
|
+
*/
|
|
271
|
+
parseQueryString(url) {
|
|
272
|
+
try {
|
|
273
|
+
return super.parseQueryString(url);
|
|
274
|
+
} catch (err) {
|
|
275
|
+
throw remapUniquUrlSyntaxError(err);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Same parser-error → 400 remap as {@link parseQueryString}, applied to the
|
|
280
|
+
* `/one` and `/one?…` code paths which use `parseControlsOnlyFromUrl`.
|
|
281
|
+
*/
|
|
282
|
+
parseControlsOnlyFromUrl(url) {
|
|
283
|
+
try {
|
|
284
|
+
return super.parseControlsOnlyFromUrl(url);
|
|
285
|
+
} catch (err) {
|
|
286
|
+
throw remapUniquUrlSyntaxError(err);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
259
290
|
* Enforce per-role `ArbacDbScope.controls` gates against the parsed Uniquery
|
|
260
291
|
* controls of a request. Runs after the base validator (which checks the
|
|
261
292
|
* controls DTO shape) and BEFORE the query/aggregation pipeline executes.
|
|
@@ -448,6 +479,24 @@ function applyPreparedOverlay(data, prepared) {
|
|
|
448
479
|
if (prepared.setOverrides) Object.assign(merged, prepared.setOverrides);
|
|
449
480
|
return merged;
|
|
450
481
|
}
|
|
482
|
+
/**
|
|
483
|
+
* Translate a `@uniqu/url` parser `SyntaxError` into `HttpError(400)`. Any
|
|
484
|
+
* other error (including a pre-existing `HttpError`) is returned as-is so the
|
|
485
|
+
* caller's `throw remapUniquUrlSyntaxError(err)` preserves the original
|
|
486
|
+
* status — no double-wrapping.
|
|
487
|
+
*
|
|
488
|
+
* Matching is narrow: only `SyntaxError` whose message carries the parser's
|
|
489
|
+
* "at pos N" / "at N" positional marker (every throw site in @uniqu/url's
|
|
490
|
+
* lexer + parser emits one). This avoids catching unrelated SyntaxErrors that
|
|
491
|
+
* a future override might surface from `JSON.parse` etc.
|
|
492
|
+
*/
|
|
493
|
+
function remapUniquUrlSyntaxError(err) {
|
|
494
|
+
if (err instanceof HttpError) return err;
|
|
495
|
+
if (!(err instanceof SyntaxError)) return err;
|
|
496
|
+
const msg = err.message;
|
|
497
|
+
if (!/at (?:pos )?\d+/.test(msg)) return err;
|
|
498
|
+
return new HttpError(400, `Invalid query string: ${msg}`);
|
|
499
|
+
}
|
|
451
500
|
//#endregion
|
|
452
501
|
//#region src/db/as-arbac-db-readable-controller.ts
|
|
453
502
|
let AsArbacDbReadableController = class AsArbacDbReadableController extends AsDbReadableController {
|
|
@@ -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("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("backupCodes", defineAnnotatedType("array").of(defineAnnotatedType().designType("string").tags("string").$type).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 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aooth/arbac-moost",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Moost RBAC integration for aoothjs (migrated from @moostjs/arbac)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"abac",
|
|
@@ -54,22 +54,22 @@
|
|
|
54
54
|
"access": "public"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@aooth/arbac": "0.1.
|
|
58
|
-
"@aooth/arbac-core": "0.1.
|
|
59
|
-
"@aooth/user": "0.1.
|
|
57
|
+
"@aooth/arbac": "0.1.4",
|
|
58
|
+
"@aooth/arbac-core": "0.1.4",
|
|
59
|
+
"@aooth/user": "0.1.4"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
-
"@atscript/core": "^0.1.
|
|
63
|
-
"@atscript/db": "^0.1.
|
|
64
|
-
"@atscript/db-sql-tools": "^0.1.
|
|
65
|
-
"@atscript/db-sqlite": "^0.1.
|
|
66
|
-
"@atscript/moost-db": "^0.1.
|
|
67
|
-
"@atscript/typescript": "^0.1.
|
|
68
|
-
"@moostjs/event-http": "^0.6.
|
|
62
|
+
"@atscript/core": "^0.1.61",
|
|
63
|
+
"@atscript/db": "^0.1.87",
|
|
64
|
+
"@atscript/db-sql-tools": "^0.1.87",
|
|
65
|
+
"@atscript/db-sqlite": "^0.1.87",
|
|
66
|
+
"@atscript/moost-db": "^0.1.87",
|
|
67
|
+
"@atscript/typescript": "^0.1.61",
|
|
68
|
+
"@moostjs/event-http": "^0.6.17",
|
|
69
69
|
"@types/better-sqlite3": "^7.6.13",
|
|
70
70
|
"@uniqu/core": "^0.1.6",
|
|
71
71
|
"better-sqlite3": "^12.6.2",
|
|
72
|
-
"unplugin-atscript": "^0.1.
|
|
72
|
+
"unplugin-atscript": "^0.1.61"
|
|
73
73
|
},
|
|
74
74
|
"peerDependencies": {
|
|
75
75
|
"@atscript/db": ">=0.1.79",
|
|
@@ -92,10 +92,11 @@
|
|
|
92
92
|
}
|
|
93
93
|
},
|
|
94
94
|
"scripts": {
|
|
95
|
-
"gen:atscript": "
|
|
95
|
+
"gen:atscript": "asc",
|
|
96
96
|
"build": "vp pack",
|
|
97
97
|
"dev": "vp pack --watch",
|
|
98
98
|
"test": "vp test",
|
|
99
|
-
"check": "vp check"
|
|
99
|
+
"check": "vp check",
|
|
100
|
+
"postinstall": "asc"
|
|
100
101
|
}
|
|
101
102
|
}
|
package/dist/user-CFXfHtsb.mjs
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
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 };
|