@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 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-CFXfHtsb.mjs";
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
 
@@ -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-CFXfHtsb.mjs";
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",
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.3",
58
- "@aooth/arbac-core": "0.1.3",
59
- "@aooth/user": "0.1.3"
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.56",
63
- "@atscript/db": "^0.1.80",
64
- "@atscript/db-sql-tools": "^0.1.80",
65
- "@atscript/db-sqlite": "^0.1.80",
66
- "@atscript/moost-db": "^0.1.80",
67
- "@atscript/typescript": "^0.1.56",
68
- "@moostjs/event-http": "^0.6.10",
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.56"
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": "node --experimental-strip-types scripts/gen-as.mjs",
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
  }
@@ -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 };