@c9up/warden 0.1.18 → 0.1.19

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 (55) hide show
  1. package/dist/AuthManager.d.ts +46 -1
  2. package/dist/AuthManager.d.ts.map +1 -1
  3. package/dist/AuthManager.js +71 -10
  4. package/dist/AuthManager.js.map +1 -1
  5. package/dist/Authenticator.d.ts +13 -1
  6. package/dist/Authenticator.d.ts.map +1 -1
  7. package/dist/Authenticator.js +33 -15
  8. package/dist/Authenticator.js.map +1 -1
  9. package/dist/RememberMeToken.d.ts +70 -0
  10. package/dist/RememberMeToken.d.ts.map +1 -1
  11. package/dist/RememberMeToken.js +102 -0
  12. package/dist/RememberMeToken.js.map +1 -1
  13. package/dist/Secret.d.ts +11 -0
  14. package/dist/Secret.d.ts.map +1 -0
  15. package/dist/Secret.js +36 -0
  16. package/dist/Secret.js.map +1 -0
  17. package/dist/bouncer/Bouncer.d.ts +14 -0
  18. package/dist/bouncer/Bouncer.d.ts.map +1 -1
  19. package/dist/bouncer/Bouncer.js +23 -4
  20. package/dist/bouncer/Bouncer.js.map +1 -1
  21. package/dist/bouncer/PolicyAuthorizer.d.ts +9 -1
  22. package/dist/bouncer/PolicyAuthorizer.d.ts.map +1 -1
  23. package/dist/bouncer/PolicyAuthorizer.js +17 -1
  24. package/dist/bouncer/PolicyAuthorizer.js.map +1 -1
  25. package/dist/bouncer/evaluate.d.ts +14 -2
  26. package/dist/bouncer/evaluate.d.ts.map +1 -1
  27. package/dist/bouncer/evaluate.js +16 -5
  28. package/dist/bouncer/evaluate.js.map +1 -1
  29. package/dist/bouncer/types.d.ts +4 -2
  30. package/dist/bouncer/types.d.ts.map +1 -1
  31. package/dist/index.d.ts +3 -2
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +3 -2
  34. package/dist/index.js.map +1 -1
  35. package/dist/sanitize.d.ts +10 -0
  36. package/dist/sanitize.d.ts.map +1 -0
  37. package/dist/sanitize.js +15 -0
  38. package/dist/sanitize.js.map +1 -0
  39. package/dist/strategies/SessionStrategy.d.ts +26 -0
  40. package/dist/strategies/SessionStrategy.d.ts.map +1 -1
  41. package/dist/strategies/SessionStrategy.js +49 -0
  42. package/dist/strategies/SessionStrategy.js.map +1 -1
  43. package/index.win32-x64-msvc.node +0 -0
  44. package/package.json +1 -1
  45. package/src/AuthManager.ts +86 -10
  46. package/src/Authenticator.ts +49 -20
  47. package/src/RememberMeToken.ts +134 -0
  48. package/src/Secret.ts +41 -0
  49. package/src/bouncer/Bouncer.ts +32 -3
  50. package/src/bouncer/PolicyAuthorizer.ts +20 -1
  51. package/src/bouncer/evaluate.ts +26 -6
  52. package/src/bouncer/types.ts +4 -2
  53. package/src/index.ts +7 -1
  54. package/src/sanitize.ts +17 -0
  55. package/src/strategies/SessionStrategy.ts +54 -0
@@ -16,6 +16,7 @@ import {
16
16
  type Action,
17
17
  evaluate,
18
18
  isAction,
19
+ type ResponseBuilder,
19
20
  throwAuthorizationFailure,
20
21
  } from "./evaluate.js";
21
22
  import { emptyPermissions, setPolicyContext } from "./policyContext.js";
@@ -69,7 +70,8 @@ export class PolicyAuthorizer {
69
70
  readonly #factory: () => Promise<BasePolicy>;
70
71
  readonly #scope: Scope;
71
72
  readonly #resolvePermissions: () => Promise<EffectivePermissions>;
72
- readonly #emitter: BouncerEmitter | undefined;
73
+ #emitter: BouncerEmitter | undefined;
74
+ readonly #responseBuilder: ResponseBuilder | undefined;
73
75
 
74
76
  constructor(
75
77
  user: UserPayload | null,
@@ -78,12 +80,14 @@ export class PolicyAuthorizer {
78
80
  resolvePermissions: () => Promise<EffectivePermissions> = () =>
79
81
  Promise.resolve(emptyPermissions(scope)),
80
82
  emitter?: BouncerEmitter,
83
+ responseBuilder?: ResponseBuilder,
81
84
  ) {
82
85
  this.#user = user;
83
86
  this.#factory = factory;
84
87
  this.#scope = scope;
85
88
  this.#resolvePermissions = resolvePermissions;
86
89
  this.#emitter = emitter;
90
+ this.#responseBuilder = responseBuilder;
87
91
  }
88
92
 
89
93
  /** Run a check and resolve to the full response (D8 — fresh policy per check). */
@@ -106,15 +110,30 @@ export class PolicyAuthorizer {
106
110
  args,
107
111
  before: policy.before?.bind(policy),
108
112
  after: policy.after?.bind(policy),
113
+ responseBuilder: this.#responseBuilder,
109
114
  });
110
115
  this.#emitter?.emit("authorization:finished", {
111
116
  user: this.#user,
112
117
  action,
118
+ // Same payload as an ability check: which resource the decision was
119
+ // about, not just that a decision happened.
120
+ parameters: args,
113
121
  response,
114
122
  });
115
123
  return response;
116
124
  }
117
125
 
126
+ /**
127
+ * Swap the event sink after construction (AdonisJS `setEmitter`).
128
+ *
129
+ * The Bouncer passes its own down, but an authorizer built directly — a
130
+ * test, a console command — had no way to be given one.
131
+ */
132
+ setEmitter(emitter?: BouncerEmitter): this {
133
+ this.#emitter = emitter;
134
+ return this;
135
+ }
136
+
118
137
  /** True iff the action is authorized. Never throws on denial. */
119
138
  async allows(action: string, ...args: unknown[]): Promise<boolean> {
120
139
  return (await this.execute(action, ...args)).authorized;
@@ -19,14 +19,31 @@ export type Action = (
19
19
  ...args: unknown[]
20
20
  ) => AuthorizerResponse;
21
21
 
22
- /** Boolean sugar response; an explicit response passes through (D7). */
23
- export function normalizeResponse(
22
+ /** Turns whatever an ability returned into an {@link AuthorizationResponse}. */
23
+ export type ResponseBuilder = (
24
24
  value: boolean | AuthorizationResponse,
25
- ): AuthorizationResponse {
25
+ ) => AuthorizationResponse;
26
+
27
+ /** The default: boolean sugar → response; an explicit response passes through (D7). */
28
+ export const defaultResponseBuilder: ResponseBuilder = (value) => {
26
29
  if (value instanceof AuthorizationResponse) {
27
30
  return value;
28
31
  }
29
32
  return value ? AuthorizationResponse.allow() : AuthorizationResponse.deny();
33
+ };
34
+
35
+ /**
36
+ * Boolean sugar → response, through whatever builder is installed.
37
+ *
38
+ * `Bouncer.responseBuilder` replaces it app-wide, which is how you give every
39
+ * bare `return false` a house message and status instead of a naked 403
40
+ * (AdonisJS `Bouncer.responseBuilder`).
41
+ */
42
+ export function normalizeResponse(
43
+ value: boolean | AuthorizationResponse,
44
+ builder: ResponseBuilder = defaultResponseBuilder,
45
+ ): AuthorizationResponse {
46
+ return builder(value);
30
47
  }
31
48
 
32
49
  /**
@@ -71,8 +88,11 @@ export async function evaluate(params: {
71
88
  result: AuthorizationResponse,
72
89
  ...args: unknown[]
73
90
  ) => HookResponse;
91
+ /** Overrides how a bare boolean becomes a response. */
92
+ responseBuilder?: ResponseBuilder;
74
93
  }): Promise<AuthorizationResponse> {
75
94
  const { user, action, allowGuest, run, args, before, after } = params;
95
+ const builder = params.responseBuilder ?? defaultResponseBuilder;
76
96
 
77
97
  let response: AuthorizationResponse | undefined;
78
98
 
@@ -80,7 +100,7 @@ export async function evaluate(params: {
80
100
  if (before) {
81
101
  const early = await before(user, action, ...args);
82
102
  if (early !== undefined) {
83
- response = normalizeResponse(early);
103
+ response = normalizeResponse(early, builder);
84
104
  }
85
105
  }
86
106
 
@@ -89,7 +109,7 @@ export async function evaluate(params: {
89
109
  if (user === null && !allowGuest) {
90
110
  response = AuthorizationResponse.deny();
91
111
  } else {
92
- response = normalizeResponse(await run(user));
112
+ response = normalizeResponse(await run(user), builder);
93
113
  }
94
114
  }
95
115
 
@@ -98,7 +118,7 @@ export async function evaluate(params: {
98
118
  if (after) {
99
119
  const override = await after(user, action, response, ...args);
100
120
  if (override !== undefined) {
101
- response = normalizeResponse(override);
121
+ response = normalizeResponse(override, builder);
102
122
  }
103
123
  }
104
124
 
@@ -23,8 +23,10 @@ export interface PolicyContainerResolver {
23
23
 
24
24
  /**
25
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.
26
+ * `authorization:finished` event fires after every ability/policy evaluation
27
+ * with `{ user, action, parameters, response }` (AdonisJS `BouncerEvents`).
28
+ * `parameters` are the arguments the check ran against — an audit log needs
29
+ * them to say WHICH resource was refused. No-op when absent.
28
30
  */
29
31
  export interface BouncerEmitter {
30
32
  emit(event: string, payload: unknown): void;
package/src/index.ts CHANGED
@@ -17,7 +17,11 @@ export type {
17
17
  AuthStrategy,
18
18
  UserPayload,
19
19
  } from "./AuthManager.js";
20
- export { AuthManager, type WardenEmitter } from "./AuthManager.js";
20
+ export {
21
+ AuthManager,
22
+ authEventPrefix,
23
+ type WardenEmitter,
24
+ } from "./AuthManager.js";
21
25
  export type { AuthRateLimiterConfig } from "./AuthRateLimiter.js";
22
26
  export { AuthRateLimiter } from "./AuthRateLimiter.js";
23
27
  export { AbilitiesBuilder } from "./bouncer/AbilitiesBuilder.js";
@@ -140,6 +144,7 @@ export {
140
144
  MemoryRememberMeTokenDriver,
141
145
  type MintedRememberMeToken,
142
146
  mintRememberMeToken,
147
+ RememberMeToken,
143
148
  type RememberMeTokenDriver,
144
149
  type StoredRememberMeToken,
145
150
  safeCompareHashes,
@@ -158,6 +163,7 @@ export type {
158
163
  Scope,
159
164
  } from "./rights/types.js";
160
165
  export { scopeKey } from "./rights/types.js";
166
+ export { Secret } from "./Secret.js";
161
167
  export type { ApiKeyConfig } from "./strategies/ApiKeyStrategy.js";
162
168
  export { ApiKeyStrategy } from "./strategies/ApiKeyStrategy.js";
163
169
  export type { BasicAuthConfig } from "./strategies/BasicAuthStrategy.js";
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Strip the prototype-pollution keys from a user payload.
3
+ *
4
+ * Lives in its own module so `AuthManager` and `Authenticator` do not have to
5
+ * import each other's values: the manager builds an authenticator, and the
6
+ * authenticator sanitises — putting both in one file made a runtime cycle.
7
+ */
8
+
9
+ import type { UserPayload } from "./AuthManager.js";
10
+
11
+ export function sanitizePayload(user: UserPayload): void {
12
+ for (const key of ["__proto__", "constructor", "prototype"]) {
13
+ if (key in user) {
14
+ delete (user as Record<string, unknown>)[key];
15
+ }
16
+ }
17
+ }
@@ -69,6 +69,9 @@ export class SessionStrategy implements AuthStrategy {
69
69
  name = "session";
70
70
  #config: SessionStrategyConfig;
71
71
  #sessionKey: string;
72
+ #viaRemember = false;
73
+ #loggedOut = false;
74
+ #attemptedViaRemember = false;
72
75
 
73
76
  constructor(config: SessionStrategyConfig) {
74
77
  this.#config = config;
@@ -85,6 +88,47 @@ export class SessionStrategy implements AuthStrategy {
85
88
  return this.#config.rememberMeTokens !== undefined;
86
89
  }
87
90
 
91
+ /**
92
+ * Whether the current user was revived from a remember-me cookie rather
93
+ * than signing in (AdonisJS `viaRemember`).
94
+ *
95
+ * This is the distinction that lets an app demand the password again before
96
+ * something sensitive — changing an email, spending money, deleting an
97
+ * account. Nothing reported it, so a session restored from a cookie looked
98
+ * exactly like one where the user had just typed their password.
99
+ */
100
+ get viaRemember(): boolean {
101
+ return this.#viaRemember;
102
+ }
103
+
104
+ /**
105
+ * Whether a remember-me token was even tried on this request (AdonisJS
106
+ * `attemptedViaRemember`) — true whether or not it worked.
107
+ */
108
+ get attemptedViaRemember(): boolean {
109
+ return this.#attemptedViaRemember;
110
+ }
111
+
112
+ /**
113
+ * Whether `logout()` ran on this guard (AdonisJS `isLoggedOut`).
114
+ *
115
+ * A handler that logs out and then keeps working — clearing a cart, writing
116
+ * an audit line — could not tell that the session was already gone.
117
+ */
118
+ get isLoggedOut(): boolean {
119
+ return this.#loggedOut;
120
+ }
121
+
122
+ /** The session key the user id is stored under (AdonisJS `sessionKeyName`). */
123
+ get sessionKeyName(): string {
124
+ return this.#sessionKey;
125
+ }
126
+
127
+ /** The cookie key the remember-me token is stored under (AdonisJS `rememberMeKeyName`). */
128
+ get rememberMeKeyName(): string {
129
+ return this.rememberMeCookieName;
130
+ }
131
+
88
132
  #rememberMeAge(): number {
89
133
  return this.#config.rememberMeAge ?? DEFAULT_REMEMBER_AGE_SECONDS;
90
134
  }
@@ -114,6 +158,7 @@ export class SessionStrategy implements AuthStrategy {
114
158
  ): Promise<{ user: UserPayload; cookieValue: string } | null> {
115
159
  const driver = this.#config.rememberMeTokens;
116
160
  if (!driver) return null;
161
+ this.#attemptedViaRemember = true;
117
162
 
118
163
  const recycled = await verifyAndRecycleRememberMeToken(
119
164
  driver,
@@ -125,6 +170,7 @@ export class SessionStrategy implements AuthStrategy {
125
170
  const user = await this.#config.findUser(recycled.userId);
126
171
  if (!user) return null;
127
172
 
173
+ this.#viaRemember = true;
128
174
  return { user, cookieValue: recycled.value };
129
175
  }
130
176
 
@@ -197,6 +243,11 @@ export class SessionStrategy implements AuthStrategy {
197
243
  async login(user: UserPayload, session: SessionStore): Promise<void> {
198
244
  session.regenerate();
199
245
  session.put(this.#sessionKey, user.id);
246
+ this.#loggedOut = false;
247
+ // A password was typed: this session is no longer "via remember", even
248
+ // if a cookie was tried earlier in the same request. Without the reset
249
+ // the flag would stay true and a re-auth prompt would never fire.
250
+ this.#viaRemember = false;
200
251
  }
201
252
 
202
253
  /**
@@ -218,5 +269,8 @@ export class SessionStrategy implements AuthStrategy {
218
269
  */
219
270
  async logout(session: SessionStore): Promise<void> {
220
271
  session.forget(this.#sessionKey);
272
+ this.#viaRemember = false;
273
+ this.#attemptedViaRemember = false;
274
+ this.#loggedOut = true;
221
275
  }
222
276
  }