@c9up/warden 0.1.12 → 0.1.14

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.
Files changed (96) hide show
  1. package/dist/AuthManager.d.ts +45 -9
  2. package/dist/AuthManager.d.ts.map +1 -1
  3. package/dist/AuthManager.js +75 -23
  4. package/dist/AuthManager.js.map +1 -1
  5. package/dist/Authenticator.d.ts +128 -0
  6. package/dist/Authenticator.d.ts.map +1 -0
  7. package/dist/Authenticator.js +296 -0
  8. package/dist/Authenticator.js.map +1 -0
  9. package/dist/WardenProvider.d.ts.map +1 -1
  10. package/dist/WardenProvider.js +51 -23
  11. package/dist/WardenProvider.js.map +1 -1
  12. package/dist/bouncer/AbilitiesBuilder.d.ts +20 -0
  13. package/dist/bouncer/AbilitiesBuilder.d.ts.map +1 -0
  14. package/dist/bouncer/AbilitiesBuilder.js +32 -0
  15. package/dist/bouncer/AbilitiesBuilder.js.map +1 -0
  16. package/dist/bouncer/AuthorizationResponse.d.ts +18 -8
  17. package/dist/bouncer/AuthorizationResponse.d.ts.map +1 -1
  18. package/dist/bouncer/AuthorizationResponse.js +20 -8
  19. package/dist/bouncer/AuthorizationResponse.js.map +1 -1
  20. package/dist/bouncer/BasePolicy.d.ts +4 -3
  21. package/dist/bouncer/BasePolicy.d.ts.map +1 -1
  22. package/dist/bouncer/BasePolicy.js.map +1 -1
  23. package/dist/bouncer/Bouncer.d.ts +9 -2
  24. package/dist/bouncer/Bouncer.d.ts.map +1 -1
  25. package/dist/bouncer/Bouncer.js +59 -10
  26. package/dist/bouncer/Bouncer.js.map +1 -1
  27. package/dist/bouncer/PolicyAuthorizer.d.ts +2 -1
  28. package/dist/bouncer/PolicyAuthorizer.d.ts.map +1 -1
  29. package/dist/bouncer/PolicyAuthorizer.js +11 -3
  30. package/dist/bouncer/PolicyAuthorizer.js.map +1 -1
  31. package/dist/bouncer/evaluate.d.ts +1 -1
  32. package/dist/bouncer/evaluate.d.ts.map +1 -1
  33. package/dist/bouncer/evaluate.js +3 -2
  34. package/dist/bouncer/evaluate.js.map +1 -1
  35. package/dist/bouncer/types.d.ts +22 -1
  36. package/dist/bouncer/types.d.ts.map +1 -1
  37. package/dist/config.d.ts +34 -3
  38. package/dist/config.d.ts.map +1 -1
  39. package/dist/config.js +15 -0
  40. package/dist/config.js.map +1 -1
  41. package/dist/configure.d.ts.map +1 -1
  42. package/dist/configure.js +15 -13
  43. package/dist/configure.js.map +1 -1
  44. package/dist/errors.d.ts +27 -0
  45. package/dist/errors.d.ts.map +1 -1
  46. package/dist/errors.js +37 -1
  47. package/dist/errors.js.map +1 -1
  48. package/dist/index.d.ts +7 -3
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +4 -2
  51. package/dist/index.js.map +1 -1
  52. package/dist/mfa/OtpProvider.d.ts.map +1 -1
  53. package/dist/mfa/OtpProvider.js +21 -3
  54. package/dist/mfa/OtpProvider.js.map +1 -1
  55. package/dist/mfa/WebauthnProvider.d.ts +24 -2
  56. package/dist/mfa/WebauthnProvider.d.ts.map +1 -1
  57. package/dist/mfa/WebauthnProvider.js +28 -8
  58. package/dist/mfa/WebauthnProvider.js.map +1 -1
  59. package/dist/middleware.d.ts +29 -6
  60. package/dist/middleware.d.ts.map +1 -1
  61. package/dist/middleware.js +85 -166
  62. package/dist/middleware.js.map +1 -1
  63. package/dist/standalone.d.ts.map +1 -1
  64. package/dist/standalone.js +21 -13
  65. package/dist/standalone.js.map +1 -1
  66. package/dist/strategies/ApiKeyStrategy.d.ts.map +1 -1
  67. package/dist/strategies/ApiKeyStrategy.js +4 -1
  68. package/dist/strategies/ApiKeyStrategy.js.map +1 -1
  69. package/dist/strategies/SessionStrategy.js +2 -2
  70. package/dist/strategies/SessionStrategy.js.map +1 -1
  71. package/index.darwin-arm64.node +0 -0
  72. package/index.darwin-x64.node +0 -0
  73. package/index.linux-arm64-gnu.node +0 -0
  74. package/index.linux-x64-gnu.node +0 -0
  75. package/index.win32-x64-msvc.node +0 -0
  76. package/package.json +2 -1
  77. package/src/AuthManager.ts +125 -28
  78. package/src/Authenticator.ts +385 -0
  79. package/src/WardenProvider.ts +51 -25
  80. package/src/bouncer/AbilitiesBuilder.ts +41 -0
  81. package/src/bouncer/AuthorizationResponse.ts +24 -16
  82. package/src/bouncer/BasePolicy.ts +4 -2
  83. package/src/bouncer/Bouncer.ts +77 -14
  84. package/src/bouncer/PolicyAuthorizer.ts +14 -4
  85. package/src/bouncer/evaluate.ts +4 -2
  86. package/src/bouncer/types.ts +24 -1
  87. package/src/config.ts +47 -3
  88. package/src/configure.ts +15 -13
  89. package/src/errors.ts +43 -1
  90. package/src/index.ts +22 -2
  91. package/src/mfa/OtpProvider.ts +31 -3
  92. package/src/mfa/WebauthnProvider.ts +58 -12
  93. package/src/middleware.ts +112 -207
  94. package/src/standalone.ts +22 -14
  95. package/src/strategies/ApiKeyStrategy.ts +4 -1
  96. package/src/strategies/SessionStrategy.ts +2 -2
@@ -39,35 +39,60 @@ export default class WardenProvider {
39
39
  // AuthManager with `strategies: {}` and `defaultStrategy: 'jwt'`,
40
40
  // passing the (then-permissive) constructor and erroring deep in the
41
41
  // middleware loop.
42
- if (!config?.jwt && !config?.session && !config?.apiKey) {
42
+ const hasGuards = config?.guards && Object.keys(config.guards).length > 0;
43
+ if (!hasGuards && !config?.jwt && !config?.session && !config?.apiKey) {
43
44
  throw new WardenError(
44
45
  "WARDEN_NO_AUTH_CONFIG",
45
- `@c9up/warden: no authentication strategies configured. Set at least one of config.warden.auth.jwt / .session / .apiKey in your reamrc.ts before registering WardenProvider.`,
46
+ `@c9up/warden: no authentication guards configured. Set config.warden.auth.guards (AdonisJS form) or at least one of .jwt / .session / .apiKey in your reamrc.ts before registering WardenProvider.`,
46
47
  );
47
48
  }
48
49
 
49
- // Build the strategy table from whatever the app configured. Each strategy
50
- // is also exposed by its class token (mirrors the jwt convention) so apps
51
- // can resolve a specific one. The `revoke()` blacklist flows through
52
- // `config.jwt.blacklist` into JwtStrategy.
50
+ // Build the guard table. Two accepted forms:
51
+ // - AdonisJS: `config.guards` (name AuthStrategy, via *Guard() factories).
52
+ // - Legacy driver-keyed: `config.jwt` / `.session` / `.apiKey`.
53
+ // Each strategy is also exposed by its class token (so apps can resolve a
54
+ // specific one). The `revoke()` blacklist flows through `config.jwt.blacklist`.
53
55
  const strategies: Record<string, AuthStrategy> = {};
54
56
 
55
- if (config.jwt) {
56
- const jwt = new JwtStrategy(config.jwt);
57
- this.app.container.singleton(JwtStrategy, () => jwt);
58
- strategies.jwt = jwt;
59
- }
60
- if (config.session) {
61
- const session = new SessionStrategy(config.session);
62
- this.app.container.singleton(SessionStrategy, () => session);
63
- strategies.session = session;
64
- }
65
- if (config.apiKey) {
66
- const apiKey = new ApiKeyStrategy(config.apiKey);
67
- this.app.container.singleton(ApiKeyStrategy, () => apiKey);
68
- strategies["api-key"] = apiKey;
57
+ if (hasGuards && config.guards) {
58
+ for (const [name, strategy] of Object.entries(config.guards)) {
59
+ strategies[name] = strategy;
60
+ // Expose recognised driver instances by their class token too.
61
+ if (strategy instanceof JwtStrategy) {
62
+ this.app.container.singleton(JwtStrategy, () => strategy);
63
+ } else if (strategy instanceof SessionStrategy) {
64
+ this.app.container.singleton(SessionStrategy, () => strategy);
65
+ } else if (strategy instanceof ApiKeyStrategy) {
66
+ this.app.container.singleton(ApiKeyStrategy, () => strategy);
67
+ }
68
+ }
69
+ } else {
70
+ if (config.jwt) {
71
+ const jwt = new JwtStrategy(config.jwt);
72
+ this.app.container.singleton(JwtStrategy, () => jwt);
73
+ strategies.jwt = jwt;
74
+ }
75
+ if (config.session) {
76
+ const session = new SessionStrategy(config.session);
77
+ this.app.container.singleton(SessionStrategy, () => session);
78
+ strategies.session = session;
79
+ }
80
+ if (config.apiKey) {
81
+ const apiKey = new ApiKeyStrategy(config.apiKey);
82
+ this.app.container.singleton(ApiKeyStrategy, () => apiKey);
83
+ // AdonisJS names this guard driver "access_tokens"; keep "api-key"
84
+ // as an accepted alias so existing `@Guard('api-key')` routes work.
85
+ strategies.access_tokens = apiKey;
86
+ strategies["api-key"] = apiKey;
87
+ }
69
88
  }
70
89
 
90
+ // Login route (session-guard HTML redirect target) — resolved by the
91
+ // enforcing middleware via the "warden:loginRoute" token. Registered even
92
+ // when undefined so the lookup is a clean resolve, not a throw.
93
+ const loginRoute = config.loginRoute;
94
+ this.app.container.singleton("warden:loginRoute", () => loginRoute);
95
+
71
96
  // Rights layer (Epic 56): one resolver singleton backs BOTH the coarse
72
97
  // RBAC helpers (injected into AuthManager below) and — once 56.6 lands —
73
98
  // the Bouncer construction, so a coarse question and a policy question
@@ -93,13 +118,14 @@ export default class WardenProvider {
93
118
  };
94
119
  this.app.container.singleton("bouncer:registry", () => bouncerRegistry);
95
120
 
96
- // Default to the configured strategy, else the first one registered.
97
- const defaultStrategy =
98
- config.defaultStrategy ?? Object.keys(strategies)[0];
121
+ // Default to the configured guard (AdonisJS `default`, then legacy
122
+ // `defaultStrategy`), else the first one registered.
123
+ const defaultGuard =
124
+ config.default ?? config.defaultStrategy ?? Object.keys(strategies)[0];
99
125
  this.app.container.singleton(AuthManager, () => {
100
126
  return new AuthManager({
101
- defaultStrategy,
102
- strategies,
127
+ default: defaultGuard,
128
+ guards: strategies,
103
129
  rights: rightsResolver,
104
130
  });
105
131
  });
@@ -0,0 +1,41 @@
1
+ /**
2
+ * AbilitiesBuilder — a chainable builder that fluently assembles an object of
3
+ * named abilities (Adonis `AbilitiesBuilder`). Each `.define(...)` delegates to
4
+ * `Bouncer.ability` and stores the result on `.abilities`, returning `this` so
5
+ * calls chain: `new AbilitiesBuilder().define(a).define(b).abilities`.
6
+ */
7
+
8
+ import type { UserPayload } from "../AuthManager.js";
9
+ import { Bouncer } from "./Bouncer.js";
10
+ import type { Ability, AbilityOptions, AuthorizerResponse } from "./types.js";
11
+
12
+ export class AbilitiesBuilder {
13
+ /** The abilities accumulated so far, keyed by name. */
14
+ readonly abilities: Record<string, Ability<never[]>>;
15
+
16
+ constructor(abilities: Record<string, Ability<never[]>> = {}) {
17
+ this.abilities = abilities;
18
+ }
19
+
20
+ /**
21
+ * Define a named ability (Adonis `.define(name, authorizer, options?)`). The
22
+ * callback receives a non-null user (a guest is denied by the evaluator before
23
+ * it runs); pass `{ allowGuest: true }` to let the callback run for a guest.
24
+ */
25
+ define<Args extends unknown[]>(
26
+ name: string,
27
+ authorizer: (user: UserPayload, ...args: Args) => AuthorizerResponse,
28
+ options?: AbilityOptions,
29
+ ): this {
30
+ // Delegate the callback wrapping to `Bouncer.ability` (guest-denied form),
31
+ // then apply the `allowGuest` option. Store without the `__args` phantom —
32
+ // its concrete-args brand cannot widen to the `never[]` storage slot, while
33
+ // `execute` is already non-generic.
34
+ const built = Bouncer.ability(authorizer);
35
+ this.abilities[name] = {
36
+ allowGuest: options?.allowGuest ?? built.allowGuest,
37
+ execute: built.execute,
38
+ };
39
+ return this;
40
+ }
41
+ }
@@ -3,27 +3,22 @@
3
3
  *
4
4
  * Layer 2 (EVALUATION) of Warden's unified authorization (Epic 56). Faithful
5
5
  * to AdonisJS Bouncer's `AuthorizationResponse`: instances are produced only by
6
- * the `allow()` / `deny()` static constructors (the constructor is private), a
7
- * denial defaults to HTTP 403, and a `translation` field is carried for shape
8
- * parity with Adonis's i18n hook (always `undefined` in 56.2 D6).
6
+ * the `allow()` / `deny()` static constructors (the constructor is private). A
7
+ * denial carries no `status` unless one is passed the HTTP 403 default is
8
+ * applied only at the throw/HTTP boundary (Adonis parity) and a `translation`
9
+ * field carries the i18n binding set via {@link t}.
9
10
  */
10
11
  export class AuthorizationResponse {
11
12
  readonly authorized: boolean;
12
- readonly message?: string;
13
- readonly status?: number;
14
- /** Parity placeholder for an i18n binding (always undefined in 56.2 — D6). */
15
- readonly translation?: { identifier: string; data?: Record<string, unknown> };
13
+ message?: string;
14
+ status?: number;
15
+ /** i18n binding set via {@link t} (Adonis `AuthorizationResponse.t`). */
16
+ translation?: { identifier: string; data?: Record<string, unknown> };
16
17
 
17
- private constructor(
18
- authorized: boolean,
19
- message?: string,
20
- status?: number,
21
- translation?: { identifier: string; data?: Record<string, unknown> },
22
- ) {
18
+ private constructor(authorized: boolean, message?: string, status?: number) {
23
19
  this.authorized = authorized;
24
20
  this.message = message;
25
21
  this.status = status;
26
- this.translation = translation;
27
22
  }
28
23
 
29
24
  /** Authorized response (no status). */
@@ -31,8 +26,21 @@ export class AuthorizationResponse {
31
26
  return new AuthorizationResponse(true);
32
27
  }
33
28
 
34
- /** Denied response; `status` defaults to 403 (D6). */
35
- static deny(message?: string, status = 403): AuthorizationResponse {
29
+ /**
30
+ * Denied response. `status` is left `undefined` unless passed — the 403
31
+ * default is applied only when the denial is thrown / mapped to HTTP (Adonis
32
+ * parity).
33
+ */
34
+ static deny(message?: string, status?: number): AuthorizationResponse {
36
35
  return new AuthorizationResponse(false, message, status);
37
36
  }
37
+
38
+ /**
39
+ * Set the i18n translation binding and return `this` for chaining (Adonis
40
+ * `AuthorizationResponse.t`), e.g. `AuthorizationResponse.deny().t('errors.forbidden')`.
41
+ */
42
+ t(identifier: string, data?: Record<string, unknown>): this {
43
+ this.translation = { identifier, data };
44
+ return this;
45
+ }
38
46
  }
@@ -27,13 +27,15 @@ export abstract class BasePolicy {
27
27
 
28
28
  /**
29
29
  * Runs after the action (or after a `before` short-circuit), receiving the
30
- * resolved response. A non-`undefined` return overrides it; `undefined`
31
- * keeps it.
30
+ * resolved response and the same resource args passed to the action (Adonis
31
+ * `after(user, action, result, ...args)`). A non-`undefined` return overrides
32
+ * the response; `undefined` keeps it.
32
33
  */
33
34
  after?(
34
35
  user: UserPayload | null,
35
36
  action: string,
36
37
  result: AuthorizationResponse,
38
+ ...args: unknown[]
37
39
  ): HookResponse;
38
40
 
39
41
  /**
@@ -15,6 +15,7 @@ import type { UserPayload } from "../AuthManager.js";
15
15
  import { WardenError } from "../errors.js";
16
16
  import type { RightsResolver } from "../rights/RightsResolver.js";
17
17
  import type { EffectivePermissions, Scope } from "../rights/types.js";
18
+ import { AbilitiesBuilder } from "./AbilitiesBuilder.js";
18
19
  import { AuthorizationResponse } from "./AuthorizationResponse.js";
19
20
  import type { BasePolicy } from "./BasePolicy.js";
20
21
  import { evaluate, isAction, throwAuthorizationFailure } from "./evaluate.js";
@@ -25,28 +26,63 @@ import type {
25
26
  AbilityOptions,
26
27
  AuthorizerResponse,
27
28
  BouncerContext,
29
+ BouncerEmitter,
30
+ PolicyContainerResolver,
28
31
  } from "./types.js";
29
32
 
30
33
  export class Bouncer {
31
- readonly #user: UserPayload | null;
34
+ readonly #userOrResolver: UserPayload | (() => UserPayload | null) | null;
35
+ /** Lazily-resolved user cache (`undefined` until `#getUser` runs). */
36
+ #user: UserPayload | null | undefined;
32
37
  readonly #abilities: Record<string, Ability<never[]>>;
33
38
  readonly #policies: Record<string, new () => BasePolicy>;
34
39
  readonly #scope: Scope;
35
40
  readonly #resolver: RightsResolver | undefined;
41
+ readonly #containerResolver: PolicyContainerResolver | undefined;
42
+ readonly #emitter: BouncerEmitter | undefined;
36
43
  /** Memoized resolution — a Bouncer is fixed per `(user, scope)`, so resolve once (D3). */
37
44
  #resolved: Promise<EffectivePermissions> | undefined;
38
45
 
39
46
  constructor(
40
- user: UserPayload | null,
47
+ user: UserPayload | (() => UserPayload | null) | null,
41
48
  abilities?: Record<string, Ability<never[]>>,
42
49
  policies?: Record<string, new () => BasePolicy>,
43
50
  context?: BouncerContext,
44
51
  ) {
45
- this.#user = user;
52
+ this.#userOrResolver = user;
46
53
  this.#abilities = abilities ?? {};
47
54
  this.#policies = policies ?? {};
48
55
  this.#scope = context?.scope ?? "global";
49
56
  this.#resolver = context?.resolver;
57
+ this.#containerResolver = context?.containerResolver;
58
+ this.#emitter = context?.emitter;
59
+ }
60
+
61
+ /**
62
+ * Resolve the user, lazily invoking a resolver callback once and memoizing
63
+ * the result (Adonis `#getUser`). A plain `UserPayload | null` passes through.
64
+ */
65
+ #getUser(): UserPayload | null {
66
+ if (this.#user === undefined) {
67
+ this.#user =
68
+ typeof this.#userOrResolver === "function"
69
+ ? this.#userOrResolver()
70
+ : this.#userOrResolver;
71
+ }
72
+ return this.#user;
73
+ }
74
+
75
+ /**
76
+ * Define an ability and open a chainable {@link AbilitiesBuilder} (Adonis
77
+ * `Bouncer.define`). Read `.abilities` off the returned builder to pass into a
78
+ * `new Bouncer(user, abilities)`.
79
+ */
80
+ static define<Args extends unknown[]>(
81
+ name: string,
82
+ authorizer: (user: UserPayload, ...args: Args) => AuthorizerResponse,
83
+ options?: AbilityOptions,
84
+ ): AbilitiesBuilder {
85
+ return new AbilitiesBuilder().define(name, authorizer, options);
50
86
  }
51
87
 
52
88
  /** The active resolution scope (default `"global"`). */
@@ -62,9 +98,10 @@ export class Bouncer {
62
98
  */
63
99
  #resolvePermissions(): Promise<EffectivePermissions> {
64
100
  if (this.#resolved === undefined) {
101
+ const user = this.#getUser();
65
102
  this.#resolved =
66
- this.#resolver !== undefined && this.#user !== null
67
- ? this.#resolver.resolve(this.#user, this.#scope)
103
+ this.#resolver !== undefined && user !== null
104
+ ? this.#resolver.resolve(user, this.#scope)
68
105
  : Promise.resolve(emptyPermissions(this.#scope));
69
106
  }
70
107
  return this.#resolved;
@@ -120,19 +157,42 @@ export class Bouncer {
120
157
  return AuthorizationResponse.deny(message, status);
121
158
  }
122
159
 
123
- /** Open a policy for checks (D8 — fresh `new PolicyClass()` per check). */
160
+ /** Open a policy for checks (D8 — fresh policy instance per check). */
124
161
  with(policy: (new () => BasePolicy) | string): PolicyAuthorizer {
125
162
  const factory =
126
163
  typeof policy === "string"
127
164
  ? this.#resolvePolicyClass(policy)
128
- : () => new policy();
165
+ : () => this.#construct(policy);
129
166
  // The PolicyAuthorizer inherits the Bouncer's scope + the shared memoized
130
167
  // resolve so every policy check sees the active `(user, scope)` (AC1/AC3).
131
- return new PolicyAuthorizer(this.#user, factory, this.#scope, () =>
132
- this.#resolvePermissions(),
168
+ return new PolicyAuthorizer(
169
+ this.#getUser(),
170
+ factory,
171
+ this.#scope,
172
+ () => this.#resolvePermissions(),
173
+ this.#emitter,
133
174
  );
134
175
  }
135
176
 
177
+ /**
178
+ * Construct a policy instance via the container resolver when present (Adonis
179
+ * DI parity), else a plain `new Policy()` (D8 — a fresh instance per check).
180
+ */
181
+ #construct(ctor: new () => BasePolicy): Promise<BasePolicy> {
182
+ return this.#containerResolver !== undefined
183
+ ? this.#containerResolver.make(ctor)
184
+ : Promise.resolve(new ctor());
185
+ }
186
+
187
+ /** Emit `authorization:finished` when an emitter is wired (no-op otherwise). */
188
+ #emit(action: string, response: AuthorizationResponse): void {
189
+ this.#emitter?.emit("authorization:finished", {
190
+ user: this.#getUser(),
191
+ action,
192
+ response,
193
+ });
194
+ }
195
+
136
196
  /** Run an ability check and resolve to the full response. */
137
197
  execute<Args extends unknown[]>(
138
198
  ability: Ability<Args>,
@@ -194,13 +254,16 @@ export class Bouncer {
194
254
  ): Promise<AuthorizationResponse> {
195
255
  const resolved =
196
256
  typeof ability === "string" ? this.#resolveAbility(ability) : ability;
197
- return evaluate({
198
- user: this.#user,
199
- action: typeof ability === "string" ? ability : "(ability)",
257
+ const action = typeof ability === "string" ? ability : "(ability)";
258
+ const response = await evaluate({
259
+ user: this.#getUser(),
260
+ action,
200
261
  allowGuest: resolved.allowGuest,
201
262
  run: (user) => resolved.execute(user, ...args),
202
263
  args,
203
264
  });
265
+ this.#emit(action, response);
266
+ return response;
204
267
  }
205
268
 
206
269
  #resolveAbility(name: string): Ability<never[]> {
@@ -215,7 +278,7 @@ export class Bouncer {
215
278
  return found;
216
279
  }
217
280
 
218
- #resolvePolicyClass(name: string): () => BasePolicy {
281
+ #resolvePolicyClass(name: string): () => Promise<BasePolicy> {
219
282
  // Lazy: the lookup runs when a verb constructs the policy, so an unknown
220
283
  // name surfaces as a promise rejection (like every other verb error),
221
284
  // not a synchronous throw at `with()` time.
@@ -230,7 +293,7 @@ export class Bouncer {
230
293
  },
231
294
  );
232
295
  }
233
- return new ctor();
296
+ return this.#construct(ctor);
234
297
  };
235
298
  }
236
299
  }
@@ -19,6 +19,7 @@ import {
19
19
  throwAuthorizationFailure,
20
20
  } from "./evaluate.js";
21
21
  import { emptyPermissions, setPolicyContext } from "./policyContext.js";
22
+ import type { BouncerEmitter } from "./types.js";
22
23
 
23
24
  /**
24
25
  * Resolve a named action method declared anywhere on the policy's own subclass
@@ -65,21 +66,24 @@ function resolveActionMethod(policy: BasePolicy, action: string): Action {
65
66
 
66
67
  export class PolicyAuthorizer {
67
68
  readonly #user: UserPayload | null;
68
- readonly #factory: () => BasePolicy;
69
+ readonly #factory: () => Promise<BasePolicy>;
69
70
  readonly #scope: Scope;
70
71
  readonly #resolvePermissions: () => Promise<EffectivePermissions>;
72
+ readonly #emitter: BouncerEmitter | undefined;
71
73
 
72
74
  constructor(
73
75
  user: UserPayload | null,
74
- factory: () => BasePolicy,
76
+ factory: () => Promise<BasePolicy>,
75
77
  scope: Scope = "global",
76
78
  resolvePermissions: () => Promise<EffectivePermissions> = () =>
77
79
  Promise.resolve(emptyPermissions(scope)),
80
+ emitter?: BouncerEmitter,
78
81
  ) {
79
82
  this.#user = user;
80
83
  this.#factory = factory;
81
84
  this.#scope = scope;
82
85
  this.#resolvePermissions = resolvePermissions;
86
+ this.#emitter = emitter;
83
87
  }
84
88
 
85
89
  /** Run a check and resolve to the full response (D8 — fresh policy per check). */
@@ -87,14 +91,14 @@ export class PolicyAuthorizer {
87
91
  action: string,
88
92
  ...args: unknown[]
89
93
  ): Promise<AuthorizationResponse> {
90
- const policy = this.#factory();
94
+ const policy = await this.#factory();
91
95
  // Attach the active scope + resolved permissions BEFORE dispatch so the
92
96
  // before/method/after pipeline all read `this.scope` / `this.permissions`.
93
97
  const permissions = await this.#resolvePermissions();
94
98
  setPolicyContext(policy, { scope: this.#scope, permissions });
95
99
  const method = resolveActionMethod(policy, action);
96
100
  const options = getActionMetadata(policy, action);
97
- return evaluate({
101
+ const response = await evaluate({
98
102
  user: this.#user,
99
103
  action,
100
104
  allowGuest: options.allowGuest ?? false,
@@ -103,6 +107,12 @@ export class PolicyAuthorizer {
103
107
  before: policy.before?.bind(policy),
104
108
  after: policy.after?.bind(policy),
105
109
  });
110
+ this.#emitter?.emit("authorization:finished", {
111
+ user: this.#user,
112
+ action,
113
+ response,
114
+ });
115
+ return response;
106
116
  }
107
117
 
108
118
  /** True iff the action is authorized. Never throws on denial. */
@@ -69,6 +69,7 @@ export async function evaluate(params: {
69
69
  user: UserPayload | null,
70
70
  action: string,
71
71
  result: AuthorizationResponse,
72
+ ...args: unknown[]
72
73
  ) => HookResponse;
73
74
  }): Promise<AuthorizationResponse> {
74
75
  const { user, action, allowGuest, run, args, before, after } = params;
@@ -92,9 +93,10 @@ export async function evaluate(params: {
92
93
  }
93
94
  }
94
95
 
95
- // (4) after — non-undefined overrides.
96
+ // (4) after — non-undefined overrides. Receives the resource args (Adonis
97
+ // `policy.after(user, action, result, ...args)`).
96
98
  if (after) {
97
- const override = await after(user, action, response);
99
+ const override = await after(user, action, response, ...args);
98
100
  if (override !== undefined) {
99
101
  response = normalizeResponse(override);
100
102
  }
@@ -11,9 +11,28 @@ import type { RightsResolver } from "../rights/RightsResolver.js";
11
11
  import type { Scope } from "../rights/types.js";
12
12
  import type { AuthorizationResponse } from "./AuthorizationResponse.js";
13
13
 
14
+ /**
15
+ * Agnostic IoC-container resolver used to construct policy instances (Adonis
16
+ * `PolicyAuthorizer`'s `#containerResolver`). Same shape as `ctx.containerResolver`
17
+ * so a Ream request context is passed through directly — warden never imports the
18
+ * container. Absent ⇒ policies are built with a plain `new Policy()`.
19
+ */
20
+ export interface PolicyContainerResolver {
21
+ make<T>(ctor: new (...args: never[]) => T): Promise<T>;
22
+ }
23
+
24
+ /**
25
+ * Agnostic event sink (Adonis `Bouncer.emitter`). When present, an
26
+ * `authorization:finished` event fires after every ability/policy evaluation with
27
+ * `{ user, action, response }`. No-op when absent.
28
+ */
29
+ export interface BouncerEmitter {
30
+ emit(event: string, payload: unknown): void;
31
+ }
32
+
14
33
  /**
15
34
  * Optional 4th `Bouncer` ctor argument (56.3, D1) — the scope dimension + the
16
- * Layer-1 resolver. Both optional; omitting it ⇒ the implicit `global` scope
35
+ * Layer-1 resolver. All optional; omitting it ⇒ the implicit `global` scope
17
36
  * with no resolver (single-tenant zero-config, D7). Additive: every 56.2 call
18
37
  * site keeps compiling.
19
38
  */
@@ -22,6 +41,10 @@ export interface BouncerContext {
22
41
  readonly scope?: Scope;
23
42
  /** Layer-1 resolver consulted for this Bouncer's `(user, scope)`. */
24
43
  readonly resolver?: RightsResolver;
44
+ /** IoC-container resolver used to construct policies (Adonis DI parity). */
45
+ readonly containerResolver?: PolicyContainerResolver;
46
+ /** Event sink for `authorization:finished` (Adonis `Bouncer.emitter`). */
47
+ readonly emitter?: BouncerEmitter;
25
48
  }
26
49
 
27
50
  /** A predicate's return — bool sugar or an explicit response, sync or async (D7). */
package/src/config.ts CHANGED
@@ -15,13 +15,16 @@
15
15
  * })
16
16
  */
17
17
 
18
- import type { UserPayload } from "./AuthManager.js";
18
+ import type { AuthStrategy, UserPayload } from "./AuthManager.js";
19
19
  import type { BasePolicy } from "./bouncer/BasePolicy.js";
20
20
  import type { Ability } from "./bouncer/types.js";
21
21
  import type { MfaManager } from "./mfa/MfaManager.js";
22
22
  import type { RightsStore, Scope } from "./rights/types.js";
23
23
  import type { ApiKeyConfig } from "./strategies/ApiKeyStrategy.js";
24
+ import { ApiKeyStrategy } from "./strategies/ApiKeyStrategy.js";
25
+ import { JwtStrategy } from "./strategies/JwtStrategy.js";
24
26
  import type { SessionStrategyConfig } from "./strategies/SessionStrategy.js";
27
+ import { SessionStrategy } from "./strategies/SessionStrategy.js";
25
28
  import type { TokenBlacklist } from "./TokenBlacklist.js";
26
29
 
27
30
  /**
@@ -50,10 +53,51 @@ export interface JwtConfig {
50
53
  blacklist?: TokenBlacklist;
51
54
  }
52
55
 
56
+ /**
57
+ * A guard entry in the AdonisJS-style config — an {@link AuthStrategy} instance,
58
+ * built via {@link jwtGuard}/{@link sessionGuard}/{@link apiKeyGuard}. Named a
59
+ * "factory" for AdonisJS symmetry (`sessionGuard({...})`), though Warden guards
60
+ * are shared per-app instances (the per-request state lives on the Authenticator).
61
+ */
62
+ export type GuardFactory = AuthStrategy;
63
+
64
+ /** Build a JWT guard from its config (AdonisJS `jwtGuard()` shape). */
65
+ export function jwtGuard(config: JwtConfig): GuardFactory {
66
+ return new JwtStrategy(config);
67
+ }
68
+
69
+ /** Build a session guard from its config (AdonisJS `sessionGuard()` shape). */
70
+ export function sessionGuard(config: SessionStrategyConfig): GuardFactory {
71
+ return new SessionStrategy(config);
72
+ }
73
+
74
+ /** Build an API-key / access-tokens guard from its config. */
75
+ export function apiKeyGuard(config: ApiKeyConfig): GuardFactory {
76
+ return new ApiKeyStrategy(config);
77
+ }
78
+
53
79
  export interface WardenConfig {
54
- /** Default auth strategy (default: 'jwt'). */
80
+ /**
81
+ * AdonisJS-style default guard NAME — the key in {@link WardenConfig.guards}
82
+ * used when a route/call names none. Preferred over {@link
83
+ * WardenConfig.defaultStrategy}.
84
+ */
85
+ default?: string;
86
+ /**
87
+ * AdonisJS-style named guard map, e.g.
88
+ * `{ web: sessionGuard({...}), api: jwtGuard({...}) }`. Guards are named by
89
+ * YOU (multiple instances of the same driver are allowed). When supplied,
90
+ * this takes precedence over the driver-keyed `jwt`/`session`/`apiKey` fields.
91
+ */
92
+ guards?: Record<string, GuardFactory>;
93
+ /**
94
+ * Login route an HTML client is redirected to when a session-guarded request
95
+ * is unauthenticated (AdonisJS session renderer parity). Absent ⇒ 401 JSON.
96
+ */
97
+ loginRoute?: string;
98
+ /** Legacy default auth strategy name (default: 'jwt'). Prefer {@link WardenConfig.default}. */
55
99
  defaultStrategy?: string;
56
- /** JWT strategy configuration. */
100
+ /** JWT strategy configuration (legacy driver-keyed form). */
57
101
  jwt?: JwtConfig;
58
102
  /**
59
103
  * Session strategy configuration. Supply `findUser` so `@Guard('session')`
package/src/configure.ts CHANGED
@@ -19,21 +19,23 @@ export async function configure(codemods: Codemods): Promise<void> {
19
19
  });
20
20
  await codemods.writeFile(
21
21
  "config/auth.ts",
22
- `import { defineConfig } from '@c9up/warden'
22
+ `import { defineConfig, jwtGuard } from '@c9up/warden'
23
23
 
24
24
  export default defineConfig({
25
- defaultStrategy: 'jwt',
26
- jwt: {
27
- secret: process.env.JWT_SECRET ?? '',
28
- expiresInSeconds: Number(process.env.JWT_EXPIRY ?? '3600'),
29
- // TODO: wire these to your user model (e.g. via your ORM).
30
- // The JWT strategy needs both to issue and verify tokens.
31
- findUser: async (_id) => {
32
- throw new Error('TODO: implement findUser(id) for the JWT strategy in config/auth.ts')
33
- },
34
- verifyCredentials: async (_email, _password) => {
35
- throw new Error('TODO: implement verifyCredentials(email, password) in config/auth.ts')
36
- },
25
+ default: 'jwt',
26
+ guards: {
27
+ jwt: jwtGuard({
28
+ secret: process.env.JWT_SECRET ?? '',
29
+ expiresInSeconds: Number(process.env.JWT_EXPIRY ?? '3600'),
30
+ // TODO: wire these to your user model (e.g. via your ORM).
31
+ // The JWT guard needs both to issue and verify tokens.
32
+ findUser: async (_id) => {
33
+ throw new Error('TODO: implement findUser(id) for the JWT guard in config/auth.ts')
34
+ },
35
+ verifyCredentials: async (_email, _password) => {
36
+ throw new Error('TODO: implement verifyCredentials(email, password) in config/auth.ts')
37
+ },
38
+ }),
37
39
  },
38
40
  })
39
41
  `,