@furystack/security 7.0.9 → 8.0.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.
Files changed (90) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +9 -3
  3. package/esm/crypto-password-hasher.d.ts +10 -12
  4. package/esm/crypto-password-hasher.d.ts.map +1 -1
  5. package/esm/crypto-password-hasher.js +43 -36
  6. package/esm/crypto-password-hasher.js.map +1 -1
  7. package/esm/errors/password-complexity-error.d.ts +5 -4
  8. package/esm/errors/password-complexity-error.d.ts.map +1 -1
  9. package/esm/errors/password-complexity-error.js +5 -4
  10. package/esm/errors/password-complexity-error.js.map +1 -1
  11. package/esm/errors/unauthenticated-error.d.ts +3 -1
  12. package/esm/errors/unauthenticated-error.d.ts.map +1 -1
  13. package/esm/errors/unauthenticated-error.js +3 -1
  14. package/esm/errors/unauthenticated-error.js.map +1 -1
  15. package/esm/helpers.d.ts +20 -7
  16. package/esm/helpers.d.ts.map +1 -1
  17. package/esm/helpers.js +27 -13
  18. package/esm/helpers.js.map +1 -1
  19. package/esm/index.d.ts +3 -1
  20. package/esm/index.d.ts.map +1 -1
  21. package/esm/index.js +3 -1
  22. package/esm/index.js.map +1 -1
  23. package/esm/models/password-credential.d.ts +3 -13
  24. package/esm/models/password-credential.d.ts.map +1 -1
  25. package/esm/models/password-credential.js +3 -1
  26. package/esm/models/password-credential.js.map +1 -1
  27. package/esm/models/password-reset-token.d.ts +5 -9
  28. package/esm/models/password-reset-token.d.ts.map +1 -1
  29. package/esm/models/password-reset-token.js +5 -0
  30. package/esm/models/password-reset-token.js.map +1 -1
  31. package/esm/password-authenticator.d.ts +26 -15
  32. package/esm/password-authenticator.d.ts.map +1 -1
  33. package/esm/password-authenticator.js +73 -109
  34. package/esm/password-authenticator.js.map +1 -1
  35. package/esm/password-authenticator.spec.js +122 -212
  36. package/esm/password-authenticator.spec.js.map +1 -1
  37. package/esm/password-complexity-rules/contains-lowercase.d.ts +2 -2
  38. package/esm/password-complexity-rules/contains-lowercase.js +2 -2
  39. package/esm/password-complexity-rules/contains-uppercase.d.ts +2 -2
  40. package/esm/password-complexity-rules/contains-uppercase.js +2 -2
  41. package/esm/password-complexity-rules/max-length.d.ts +1 -4
  42. package/esm/password-complexity-rules/max-length.d.ts.map +1 -1
  43. package/esm/password-complexity-rules/max-length.js +1 -4
  44. package/esm/password-complexity-rules/max-length.js.map +1 -1
  45. package/esm/password-complexity-rules/min-length.d.ts +1 -4
  46. package/esm/password-complexity-rules/min-length.d.ts.map +1 -1
  47. package/esm/password-complexity-rules/min-length.js +1 -4
  48. package/esm/password-complexity-rules/min-length.js.map +1 -1
  49. package/esm/password-credential-store.d.ts +50 -0
  50. package/esm/password-credential-store.d.ts.map +1 -0
  51. package/esm/password-credential-store.js +73 -0
  52. package/esm/password-credential-store.js.map +1 -0
  53. package/esm/password-credential-store.spec.d.ts +2 -0
  54. package/esm/password-credential-store.spec.d.ts.map +1 -0
  55. package/esm/password-credential-store.spec.js +64 -0
  56. package/esm/password-credential-store.spec.js.map +1 -0
  57. package/esm/password-hasher.d.ts +21 -3
  58. package/esm/password-hasher.d.ts.map +1 -1
  59. package/esm/password-hasher.js +1 -19
  60. package/esm/password-hasher.js.map +1 -1
  61. package/esm/security-policy-manager.d.ts +46 -14
  62. package/esm/security-policy-manager.d.ts.map +1 -1
  63. package/esm/security-policy-manager.js +48 -52
  64. package/esm/security-policy-manager.js.map +1 -1
  65. package/esm/security-policy-manager.spec.js +58 -7
  66. package/esm/security-policy-manager.spec.js.map +1 -1
  67. package/esm/security-policy.d.ts +35 -9
  68. package/esm/security-policy.d.ts.map +1 -1
  69. package/esm/security-policy.js +23 -33
  70. package/esm/security-policy.js.map +1 -1
  71. package/package.json +7 -7
  72. package/src/crypto-password-hasher.ts +46 -39
  73. package/src/errors/password-complexity-error.ts +7 -4
  74. package/src/errors/unauthenticated-error.ts +3 -1
  75. package/src/helpers.ts +27 -15
  76. package/src/index.ts +3 -2
  77. package/src/models/password-credential.ts +5 -15
  78. package/src/models/password-reset-token.ts +5 -9
  79. package/src/password-authenticator.spec.ts +134 -254
  80. package/src/password-authenticator.ts +92 -86
  81. package/src/password-complexity-rules/contains-lowercase.ts +2 -2
  82. package/src/password-complexity-rules/contains-uppercase.ts +2 -2
  83. package/src/password-complexity-rules/max-length.ts +1 -4
  84. package/src/password-complexity-rules/min-length.ts +1 -4
  85. package/src/password-credential-store.spec.ts +77 -0
  86. package/src/password-credential-store.ts +79 -0
  87. package/src/password-hasher.ts +21 -9
  88. package/src/security-policy-manager.spec.ts +63 -7
  89. package/src/security-policy-manager.ts +91 -42
  90. package/src/security-policy.ts +50 -18
@@ -1,35 +1,25 @@
1
- var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
- 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;
5
- return c > 3 && r && Object.defineProperty(target, key, r), r;
6
- };
7
- import { Injectable } from '@furystack/inject';
1
+ import { defineService } from '@furystack/inject';
8
2
  import { CryptoPasswordHasher } from './crypto-password-hasher.js';
9
- let SecurityPolicy = class SecurityPolicy {
10
- /**
11
- * A Hasher instance for password hash generation
12
- */
13
- hasher = CryptoPasswordHasher;
14
- /**
15
- * Array of password complexity rules to apply
16
- */
17
- passwordComplexityRules = [];
18
- /**
19
- * The password expiration in days
20
- */
21
- passwordExpirationDays = 0;
22
- /**
23
- * Defines if the login needs a second factor authentication
24
- */
25
- requireSecondFactor = 'never'; // TODO - Challenge based on trusted devices
26
- /**
27
- * The Password Reset Token expiration in seconds
28
- */
29
- resetTokenExpirationSeconds = 15;
30
- };
31
- SecurityPolicy = __decorate([
32
- Injectable({ lifetime: 'singleton' })
33
- ], SecurityPolicy);
34
- export { SecurityPolicy };
3
+ /**
4
+ * Returns a fresh copy of the default {@link SecurityPolicy}. Useful as a
5
+ * starting point for `Object.assign`-style overrides or spec fixtures.
6
+ */
7
+ export const defaultSecurityPolicy = () => ({
8
+ hasher: CryptoPasswordHasher,
9
+ passwordComplexityRules: [],
10
+ passwordExpirationDays: 0,
11
+ requireSecondFactor: 'never',
12
+ resetTokenExpirationSeconds: 15,
13
+ });
14
+ /**
15
+ * DI token that resolves the current {@link SecurityPolicy}. Rebind this
16
+ * token via {@link usePasswordPolicy} (preferred) or directly through
17
+ * {@link Injector.bind} to customise the policy per-application or
18
+ * per-scope.
19
+ */
20
+ export const SecurityPolicy = defineService({
21
+ name: 'furystack/security/SecurityPolicy',
22
+ lifetime: 'singleton',
23
+ factory: () => defaultSecurityPolicy(),
24
+ });
35
25
  //# sourceMappingURL=security-policy.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"security-policy.js","sourceRoot":"","sources":["../src/security-policy.ts"],"names":[],"mappings":";;;;;;AACA,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAC9C,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAA;AAK3D,IAAM,cAAc,GAApB,MAAM,cAAc;IACzB;;OAEG;IACH,MAAM,GAAkC,oBAAoB,CAAA;IAE5D;;OAEG;IACH,uBAAuB,GAA6B,EAAE,CAAA;IAEtD;;OAEG;IACH,sBAAsB,GAAG,CAAC,CAAA;IAE1B;;OAEG;IACH,mBAAmB,GAAuB,OAAO,CAAA,CAAC,4CAA4C;IAE9F;;OAEG;IACH,2BAA2B,GAAG,EAAE,CAAA;CACjC,CAAA;AAzBY,cAAc;IAD1B,UAAU,CAAC,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;GACzB,cAAc,CAyB1B"}
1
+ {"version":3,"file":"security-policy.js","sourceRoot":"","sources":["../src/security-policy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAc,MAAM,mBAAmB,CAAA;AAC7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAA;AAyClE;;;GAGG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAmB,EAAE,CAAC,CAAC;IAC1D,MAAM,EAAE,oBAAoB;IAC5B,uBAAuB,EAAE,EAAE;IAC3B,sBAAsB,EAAE,CAAC;IACzB,mBAAmB,EAAE,OAAO;IAC5B,2BAA2B,EAAE,EAAE;CAChC,CAAC,CAAA;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,cAAc,GAAuC,aAAa,CAAC;IAC9E,IAAI,EAAE,mCAAmC;IACzC,QAAQ,EAAE,WAAW;IACrB,OAAO,EAAE,GAAG,EAAE,CAAC,qBAAqB,EAAE;CACvC,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@furystack/security",
3
- "version": "7.0.9",
3
+ "version": "8.0.1",
4
4
  "description": "Security, password management and authorization related stuff for FuryStack",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -37,16 +37,16 @@
37
37
  "url": "https://github.com/furystack/furystack/issues"
38
38
  },
39
39
  "dependencies": {
40
- "@furystack/core": "^16.0.4",
41
- "@furystack/inject": "^12.0.36",
42
- "@furystack/repository": "^10.1.11"
40
+ "@furystack/core": "^17.1.0",
41
+ "@furystack/inject": "^13.0.1",
42
+ "@furystack/repository": "^11.0.1"
43
43
  },
44
44
  "homepage": "https://github.com/furystack/furystack",
45
45
  "devDependencies": {
46
- "@furystack/utils": "^8.2.5",
47
- "@types/node": "^25.6.0",
46
+ "@furystack/utils": "^9.0.1",
47
+ "@types/node": "^25.9.1",
48
48
  "typescript": "^6.0.3",
49
- "vitest": "^4.1.4"
49
+ "vitest": "^4.1.7"
50
50
  },
51
51
  "engines": {
52
52
  "node": ">=22.0.0"
@@ -1,44 +1,51 @@
1
- import { Injectable } from '@furystack/inject'
1
+ import { defineService, type Token } from '@furystack/inject'
2
+ import { pbkdf2, randomBytes } from 'crypto'
3
+ import type { PasswordCheckResult } from './models/index.js'
2
4
  import type { PasswordHasher } from './password-hasher.js'
3
- import { randomBytes, pbkdf2 } from 'crypto'
4
- import type { PasswordCheckResult, PasswordCredential } from './models/index.js'
5
5
 
6
- @Injectable({ lifetime: 'singleton' })
7
- export class CryptoPasswordHasher implements PasswordHasher {
8
- private readonly iterations = 1000
9
- private readonly keylen = 64
6
+ const ITERATIONS = 1000
7
+ const KEY_LENGTH = 64
8
+ const SALT_BYTES = 16
10
9
 
11
- public async verifyCredential(
12
- password: string,
13
- { passwordHash, salt }: PasswordCredential,
14
- ): Promise<PasswordCheckResult> {
15
- const newHash = await new Promise((resolve, reject) =>
16
- pbkdf2(password, salt, this.iterations, this.keylen, 'sha512', (err, key) =>
17
- err ? reject(err) : resolve(key.toString('hex')),
18
- ),
19
- )
20
- if (newHash === passwordHash) {
10
+ const deriveHash = (password: string, salt: string): Promise<string> =>
11
+ new Promise<string>((resolve, reject) => {
12
+ pbkdf2(password, salt, ITERATIONS, KEY_LENGTH, 'sha512', (err, key) => {
13
+ if (err) {
14
+ reject(err)
15
+ return
16
+ }
17
+ resolve(key.toString('hex'))
18
+ })
19
+ })
20
+
21
+ /**
22
+ * Default PBKDF2-based {@link PasswordHasher} implementation. Uses SHA-512
23
+ * with 1000 iterations and a 64-byte derived key length.
24
+ *
25
+ * Swap this token in {@link SecurityPolicy.hasher} to replace the hashing
26
+ * algorithm — for example with a stronger Argon2 implementation or a
27
+ * delegated hashing service.
28
+ */
29
+ export const CryptoPasswordHasher: Token<PasswordHasher, 'singleton'> = defineService({
30
+ name: 'furystack/security/CryptoPasswordHasher',
31
+ lifetime: 'singleton',
32
+ factory: (): PasswordHasher => ({
33
+ verifyCredential: async (password, { passwordHash, salt }): Promise<PasswordCheckResult> => {
34
+ const candidate = await deriveHash(password, salt)
35
+ if (candidate === passwordHash) {
36
+ return { isValid: true }
37
+ }
38
+ return { isValid: false, reason: 'badUsernameOrPassword' }
39
+ },
40
+ createCredential: async (userName, password) => {
41
+ const salt = randomBytes(SALT_BYTES).toString('hex')
42
+ const passwordHash = await deriveHash(password, salt)
21
43
  return {
22
- isValid: true,
44
+ userName,
45
+ salt,
46
+ passwordHash,
47
+ creationDate: new Date().toISOString(),
23
48
  }
24
- }
25
- return {
26
- isValid: false,
27
- reason: 'badUsernameOrPassword',
28
- }
29
- }
30
- public async createCredential(userName: string, password: string) {
31
- const salt = randomBytes(16).toString('hex')
32
- const passwordHash = await new Promise<string>((resolve, reject) =>
33
- pbkdf2(password, salt, this.iterations, this.keylen, 'sha512', (err, key) =>
34
- err ? reject(err) : resolve(key.toString('hex')),
35
- ),
36
- )
37
- return {
38
- userName,
39
- salt,
40
- passwordHash,
41
- creationDate: new Date().toISOString(),
42
- }
43
- }
44
- }
49
+ },
50
+ }),
51
+ })
@@ -1,8 +1,11 @@
1
+ import type { PasswordComplexityRule } from '../models/password-complexity-rule.js'
2
+
3
+ /**
4
+ * Thrown by password-setting flows when the candidate password fails one or
5
+ * more configured {@link PasswordComplexityRule}s. `result` carries the
6
+ * per-rule failures so callers can render targeted user feedback.
7
+ */
1
8
  export class PasswordComplexityError extends Error {
2
- /**
3
- * @param result A detailed error result array
4
- * @param message The Error message
5
- */
6
9
  constructor(
7
10
  public readonly result: Array<{ message: string; rule: string }>,
8
11
  message?: string,
@@ -1,4 +1,6 @@
1
1
  /**
2
- * Error that can be thrown if an authentication is required but not provided
2
+ * Thrown when credential verification fails: bad username/password, expired
3
+ * password, or expired/invalid reset token. REST handlers should map this
4
+ * to a 401 response.
3
5
  */
4
6
  export class UnauthenticatedError extends Error {}
package/src/helpers.ts CHANGED
@@ -1,22 +1,34 @@
1
1
  import type { Injector } from '@furystack/inject'
2
- import { SecurityPolicy } from './security-policy.js'
2
+ import { PasswordAuthenticator } from './password-authenticator.js'
3
+ import { SecurityPolicyManager } from './security-policy-manager.js'
4
+ import { defaultSecurityPolicy, SecurityPolicy } from './security-policy.js'
3
5
 
4
6
  /**
5
- * Sets up the @furystack/security with the provided settings.
7
+ * Rebinds the {@link SecurityPolicy} on the given injector with the default
8
+ * policy merged with `overrides`.
6
9
  *
7
- * **Prerequisite:** DataSets for `PasswordCredential` and `PasswordResetToken` must be registered
8
- * via `getRepository(injector).createDataSet()` before `PasswordAuthenticator` is instantiated,
9
- * as it resolves these DataSets through `@Injected`.
10
+ * Apply this at application setup before resolving any service that reads
11
+ * the policy — {@link PasswordAuthenticator}, {@link SecurityPolicyManager}
12
+ * or direct `injector.get(SecurityPolicy)` callers. Because those services
13
+ * capture the policy at resolve time, calling `usePasswordPolicy` after
14
+ * they have been resolved has no effect unless you also invalidate them.
10
15
  *
11
- * @param injector The Injector instance
12
- * @param policy The security policy to use
16
+ * **Prerequisite:** bind persistent implementations of
17
+ * `PasswordCredentialStore` and `PasswordResetTokenStore` before resolving
18
+ * {@link PasswordAuthenticator}. Their default factories throw on purpose
19
+ * so password data is never silently kept in-memory.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * usePasswordPolicy(injector, {
24
+ * passwordExpirationDays: 90,
25
+ * passwordComplexityRules: [createMinLengthComplexityRule(8)],
26
+ * })
27
+ * ```
13
28
  */
14
- export const usePasswordPolicy = (injector: Injector, policy?: Partial<SecurityPolicy>) => {
15
- const plainPolicy = new SecurityPolicy()
16
-
17
- if (policy) {
18
- Object.assign(plainPolicy, policy)
19
- }
20
-
21
- injector.setExplicitInstance(plainPolicy, SecurityPolicy)
29
+ export const usePasswordPolicy = (injector: Injector, overrides?: Partial<SecurityPolicy>): void => {
30
+ injector.bind(SecurityPolicy, () => ({ ...defaultSecurityPolicy(), ...overrides }))
31
+ injector.invalidate(SecurityPolicy)
32
+ injector.invalidate(SecurityPolicyManager)
33
+ injector.invalidate(PasswordAuthenticator)
22
34
  }
package/src/index.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  export * from './crypto-password-hasher.js'
2
2
  export * from './errors/index.js'
3
+ export * from './helpers.js'
3
4
  export * from './models/index.js'
4
5
  export * from './password-authenticator.js'
5
6
  export * from './password-complexity-rules/index.js'
7
+ export * from './password-credential-store.js'
6
8
  export * from './password-hasher.js'
7
9
  export * from './security-policy-manager.js'
8
-
9
- export * from './helpers.js'
10
+ export * from './security-policy.js'
@@ -1,23 +1,13 @@
1
+ import type { PasswordHasher } from '../password-hasher.js'
2
+
1
3
  /**
2
- * Entry that stores credential info for password login
4
+ * Persisted password credential. `passwordHash` and `salt` are produced by
5
+ * the configured {@link PasswordHasher}; `creationDate` (ISO 8601 string)
6
+ * is the source of truth for password-expiration policies.
3
7
  */
4
8
  export class PasswordCredential {
5
- /**
6
- * The unique name of the user
7
- */
8
9
  declare userName: string
9
- /**
10
- * The hashed password value
11
- */
12
10
  declare passwordHash: string
13
-
14
- /**
15
- * Salt value for password hashing // TODO
16
- */
17
11
  declare salt: string
18
-
19
- /**
20
- * The Creation date in ISO String format
21
- */
22
12
  declare creationDate: string
23
13
  }
@@ -1,14 +1,10 @@
1
+ /**
2
+ * Single-use password reset token. `token` is the URL-safe identifier
3
+ * shared with the user (e.g. via email). `createdAt` (ISO 8601) is the
4
+ * expiration anchor honoured by `SecurityPolicyManager.hasTokenExpired`.
5
+ */
1
6
  export class PasswordResetToken {
2
- /**
3
- * The related user name
4
- */
5
7
  declare userName: string
6
- /**
7
- * The Token value - should be some kind of generated UUID that can be included e.g. in URLs
8
- */
9
8
  declare token: string
10
- /**
11
- * The creation date in an ISO datetime format
12
- */
13
9
  declare createdAt: string
14
10
  }