@owlmeans/server-auth-otp 0.1.18-rc.34 → 0.1.18-rc.35

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 (46) hide show
  1. package/README.md +2 -2
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/server-auth-otp/SKILL.md +20 -24
  4. package/build/challenge.d.ts +8 -0
  5. package/build/challenge.d.ts.map +1 -0
  6. package/build/challenge.js +98 -0
  7. package/build/challenge.js.map +1 -0
  8. package/build/consts.d.ts +10 -1
  9. package/build/consts.d.ts.map +1 -1
  10. package/build/consts.js +7 -1
  11. package/build/consts.js.map +1 -1
  12. package/build/errors.d.ts +18 -0
  13. package/build/errors.d.ts.map +1 -0
  14. package/build/errors.js +32 -0
  15. package/build/errors.js.map +1 -0
  16. package/build/index.d.ts +4 -1
  17. package/build/index.d.ts.map +1 -1
  18. package/build/index.js +4 -0
  19. package/build/index.js.map +1 -1
  20. package/build/plugin.d.ts.map +1 -1
  21. package/build/plugin.js +11 -13
  22. package/build/plugin.js.map +1 -1
  23. package/build/service.d.ts +3 -0
  24. package/build/service.d.ts.map +1 -1
  25. package/build/service.js +45 -22
  26. package/build/service.js.map +1 -1
  27. package/build/throttle.d.ts +12 -0
  28. package/build/throttle.d.ts.map +1 -0
  29. package/build/throttle.js +93 -0
  30. package/build/throttle.js.map +1 -0
  31. package/build/types.d.ts +34 -0
  32. package/build/types.d.ts.map +1 -1
  33. package/build/types.js +7 -1
  34. package/build/types.js.map +1 -1
  35. package/package.json +12 -12
  36. package/src/challenge.ts +114 -0
  37. package/src/consts.ts +11 -1
  38. package/src/errors.ts +37 -0
  39. package/src/index.ts +4 -1
  40. package/src/plugin.ts +11 -13
  41. package/src/service.ts +59 -34
  42. package/src/throttle.ts +120 -0
  43. package/src/types.ts +42 -0
  44. package/tests/context.ts +43 -0
  45. package/tests/keys.spec.ts +23 -0
  46. package/tests/service.spec.ts +88 -0
package/README.md CHANGED
@@ -12,7 +12,7 @@ Email OTP `AuthPlugin` for OwlMeans servers — passwordless login via time-limi
12
12
  ## Installation
13
13
 
14
14
  ```bash
15
- bun add @owlmeans/server-auth-otp@^0.1.18-rc.34
15
+ bun add @owlmeans/server-auth-otp@^0.1.18-rc.35
16
16
  ```
17
17
 
18
18
  ## Usage
@@ -47,7 +47,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
47
47
  your project's skill store (`.agents/skills/`):
48
48
 
49
49
  ```sh
50
- npx @owlmeans/agent-skills@^0.1.18-rc.28
50
+ npx @owlmeans/agent-skills@^0.1.18-rc.30
51
51
  ```
52
52
 
53
53
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/server-auth-otp",
4
- "version": "0.1.18-rc.34",
5
- "generatedAt": "2026-09-17T08:16:11.220Z",
4
+ "version": "0.1.18-rc.35",
5
+ "generatedAt": "2026-09-19T13:42:56.249Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -8,18 +8,17 @@ metadata:
8
8
 
9
9
  # Using `@owlmeans/server-auth-otp`
10
10
 
11
- **Install:** `"@owlmeans/server-auth-otp": "^0.1.18-rc.34"` in `dependencies`
11
+ **Install:** `"@owlmeans/server-auth-otp": "^0.1.18-rc.35"` in `dependencies`
12
12
 
13
13
  Email OTP authentication plugin for the OwlMeans auth-manager plugin system. Relies on `@owlmeans/auth-otp` for the OTP service interface, a Redis resource for code storage, and a `MailerService` to send codes.
14
14
 
15
15
  ## `@owlmeans/auth-otp` — the contracts
16
16
 
17
- `@owlmeans/auth-otp` has no skill of its own because it has no behaviour: it is the contract half of
18
- this pair, and everything it declares is implemented here. It carries the `OtpService` interface —
19
- `issueChallenge(email)`, which generates a code, persists it with a TTL and mails it, and
20
- `verifyChallenge(email, code)`, which verifies and consumes it — plus the names both halves agree
21
- on: `OTP_SERVICE`, `OTP_AUTH_TYPE` (`'email-otp'`), `OTP_RESOURCE`, `OTP_TTL_SECONDS` (600) and
22
- `OTP_CODE_LENGTH` (6). Nothing else; no Redis, no mailer, no plugin.
17
+ `@owlmeans/auth-otp` has no skill of its own because it is the contracts half of this pair. Its
18
+ `OtpService` issues an opaque issuance id, persists and mails a code, and verifies that exact
19
+ issuance; it also owns `OTP_SERVICE`, `OTP_AUTH_TYPE` (`'email-otp'`), `OTP_RESOURCE`,
20
+ `OTP_TTL_SECONDS` (600), `OTP_CODE_LENGTH` (6), and the five-attempt policy. The server package
21
+ provides the challenge stores, throttles, mailer integration and plugin.
23
22
 
24
23
  Depend on it from a shared package that must name the auth type or type the service, and depend on
25
24
  `@owlmeans/server-auth-otp` only where the server wires itself up — the same producer/consumer split
@@ -67,13 +66,11 @@ appendOtpPlugin(context)
67
66
 
68
67
  **Init** — client sends `{ type: 'email-otp', userId: 'user@email.com' }`:
69
68
  - OTP service generates a 6-digit code, stores it in Redis with 10 min TTL, emails it.
70
- - Returns `{ challenge: '<email>::<nonce>' }` in a signed envelope — see Gotchas below for why the
71
- nonce is required, not optional.
69
+ - Returns an opaque issuance id in a signed envelope. The id never contains the email or code.
72
70
 
73
71
  **Authenticate** — client sends `{ challenge: <signed-envelope>, userId: email, credential: '123456', type: 'email-otp', role: AuthRole.User, scopes: [ALL_SCOPES] }`:
74
- - Envelope is opened → `email::nonce` is extracted, split on `::` to recover the email (the nonce
75
- itself is discarded — it only exists to make the challenge unique, see Gotchas).
76
- - OTP service verifies the code (throws `AuthenFailed` if wrong or expired), then deletes it.
72
+ - Envelope is opened → the plugin verifies the opaque issuance and code atomically. Five failed
73
+ attempts invalidate it; one concurrent correct verification can consume it.
77
74
  - `IdentityLinkingService` finds the linked profile, or links this email to the person's platform
78
75
  identity — registering an account, a profile and an organization entity only when the address is
79
76
  new to the platform.
@@ -98,7 +95,9 @@ cfg.otp = {
98
95
 
99
96
  ## Rules
100
97
 
101
- - Always register the Redis resource AND the mailer service BEFORE the OTP service.
98
+ - Always register the Redis challenge store, Redis throttle service, and mailer service BEFORE the
99
+ OTP service. Production uses `makeRedisOtpChallengeStore` and `makeRedisThrottleService`; memory
100
+ implementations are local/test defaults only.
102
101
  - Call `appendOtpPlugin(context)` once per context — it adds to the shared plugin registry singleton.
103
102
  - `credential.entitySlug` on the authenticate request **selects nothing**. The plugin copies it into
104
103
  the linking details as `clientId` (defaulting to `'default'`) and `entityId`, and
@@ -109,23 +108,20 @@ cfg.otp = {
109
108
  slug before the envelope is signed, so the address alone decides which identity and which
110
109
  organization the token names.
111
110
  - Errors from this plugin are `AuthenFailed` (from `@owlmeans/auth`) — callers catch that, not raw `Error`.
111
+ - Use the email throttle for the public integrated-IAM flow: one issuance per minute and ten per
112
+ hour, with a 429 and Retry-After. The companion IP throttle is available for an ingress that
113
+ safely supplies a normalized client address; throttle keys hash email/IP values and never store
114
+ the raw identifier.
112
115
  - For tests, register `makeDefaultConsoleMailerService()` and read `svc.captured[n].text` to extract
113
116
  the code. Bare `makeConsoleMailerService()` registers under `CONSOLE_MAILER` (`'console-mailer'`),
114
117
  which is not the alias `makeOtpService` resolves — it looks up `cfg.otp?.mailerAlias ?? MAILER_SERVICE`.
115
118
 
116
119
  ## Gotchas
117
120
 
118
- - **The challenge must never be just the plaintext email.** The auth manager's anti-replay guard
119
- (`AUTH_CACHE` in `@owlmeans/server-auth`) burns the *decoded* challenge into a create-once record
120
- before the plugin's own credential check runs. `AUTH_CACHE` defaults to a **static, in-memory**
121
- resource — the manager's context appends it, and nothing here upgrades it to Redis; the OTP codes
122
- are the only thing this package keeps in Redis. If `init()` returned the bare email, that decoded
123
- value would be identical across every independent login attempt for the same address, so a second
124
- legitimate login within the cache TTL (`AUTHEN_TIMEFRAME`, 15 min) — right code or wrong —
125
- collides with the still-cached prior attempt and throws `AuthenFailed('challenge')` (the
126
- resource's `RecordExists` underneath), not an OTP-specific error. Fix: `init()` appends a fresh
127
- `createIdOfLength(16, IdStyle.Base58)` nonce (`'<email>::<nonce>'`); `authenticate()` splits it
128
- back apart. Never revert to a bare-email challenge.
121
+ - **The OTP challenge must be opaque and plugin-owned.** The generic auth-manager replay policy
122
+ does not consume it before the OTP store can count attempts; the plugin atomically owns verify
123
+ and consume. Never encode the email or code in the issuance id, and never replace the bounded
124
+ attempt counter with a bare auth-cache replay key.
129
125
  - **`AuthCredentialsSchema.credential` has a `minLength` floor** (from `@owlmeans/auth`) sized for
130
126
  long tokens/signatures from other plugins (Ed25519 signature, OAuth code). A 6-digit OTP code is
131
127
  legitimately shorter — the floor is `minLength: 1` and must stay low enough to admit it, or every
@@ -0,0 +1,8 @@
1
+ import type { OtpChallengeStore } from './types.js';
2
+ type Clock = () => number;
3
+ /** Real in-memory challenge store with synchronous consume/update semantics. */
4
+ export declare const makeMemoryOtpChallengeStore: (alias?: string, now?: Clock) => OtpChallengeStore;
5
+ /** Redis challenge store: compare, attempt increment, exhaustion, and consume are one operation. */
6
+ export declare const makeRedisOtpChallengeStore: (alias?: string, resourceAlias?: string) => OtpChallengeStore;
7
+ export {};
8
+ //# sourceMappingURL=challenge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"challenge.d.ts","sourceRoot":"","sources":["../src/challenge.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACyB,iBAAiB,EACrD,MAAM,YAAY,CAAA;AAGnB,KAAK,KAAK,GAAG,MAAM,MAAM,CAAA;AAGzB,gFAAgF;AAChF,eAAO,MAAM,2BAA2B,yBACJ,KAAK,KACtC,iBA+BF,CAAA;AA0BD,oGAAoG;AACpG,eAAO,MAAM,0BAA0B,8CAEpC,iBAqCF,CAAA"}
@@ -0,0 +1,98 @@
1
+ import { appendContextual, createService } from '@owlmeans/context';
2
+ import { OtpUnavailable } from './errors.js';
3
+ import { OTP_CHALLENGE_STORE, OTP_RESOURCE } from './consts.js';
4
+ import { OtpChallengeOutcome as Outcome } from './types.js';
5
+ /** Real in-memory challenge store with synchronous consume/update semantics. */
6
+ export const makeMemoryOtpChallengeStore = (alias = OTP_CHALLENGE_STORE, now = Date.now) => {
7
+ const records = new Map();
8
+ return createService(alias, {
9
+ issue: async (challenge, ttlSeconds) => {
10
+ const current = records.get(challenge.id);
11
+ if (current != null && current.expiresAt > now())
12
+ return false;
13
+ records.set(challenge.id, {
14
+ ...challenge, failedAttempts: 0, expiresAt: now() + ttlSeconds * 1000,
15
+ });
16
+ return true;
17
+ },
18
+ verify: async (id, emailKey, codeHash, maxFailedAttempts) => {
19
+ const record = records.get(id);
20
+ if (record == null || record.expiresAt <= now()) {
21
+ records.delete(id);
22
+ return Outcome.Missing;
23
+ }
24
+ if (record.emailKey === emailKey && record.codeHash === codeHash) {
25
+ records.delete(id);
26
+ return Outcome.Verified;
27
+ }
28
+ record.failedAttempts += 1;
29
+ if (record.failedAttempts >= maxFailedAttempts) {
30
+ records.delete(id);
31
+ return Outcome.Exhausted;
32
+ }
33
+ return Outcome.Invalid;
34
+ },
35
+ });
36
+ };
37
+ const REDIS_VERIFY_SCRIPT = `
38
+ local raw = redis.call('GET', KEYS[1])
39
+ if not raw then return 0 end
40
+ local record = cjson.decode(raw)
41
+ if record.emailKey == ARGV[1] and record.codeHash == ARGV[2] then
42
+ redis.call('DEL', KEYS[1])
43
+ return 1
44
+ end
45
+ record.failedAttempts = tonumber(record.failedAttempts or 0) + 1
46
+ if record.failedAttempts >= tonumber(ARGV[3]) then
47
+ redis.call('DEL', KEYS[1])
48
+ return -2
49
+ end
50
+ local ttl = redis.call('PTTL', KEYS[1])
51
+ if ttl <= 0 then
52
+ redis.call('DEL', KEYS[1])
53
+ return 0
54
+ end
55
+ redis.call('SET', KEYS[1], cjson.encode(record), 'XX', 'PX', ttl)
56
+ return -1
57
+ `;
58
+ /** Redis challenge store: compare, attempt increment, exhaustion, and consume are one operation. */
59
+ export const makeRedisOtpChallengeStore = (alias = OTP_CHALLENGE_STORE, resourceAlias = OTP_RESOURCE) => {
60
+ const service = appendContextual(alias, {
61
+ issue: async (challenge, ttlSeconds) => {
62
+ const ctx = service.ctx;
63
+ const resource = ctx.resource(resourceAlias);
64
+ try {
65
+ const result = await resource.db.client.set(resource.key(challenge.id), JSON.stringify({ ...challenge, failedAttempts: 0 }), 'EX', ttlSeconds, 'NX');
66
+ return result === 'OK';
67
+ }
68
+ catch (error) {
69
+ const unavailable = new OtpUnavailable('challenge');
70
+ if (error instanceof Error)
71
+ unavailable.oiriginalStack = `${error}: ${error.stack}`;
72
+ throw unavailable;
73
+ }
74
+ },
75
+ verify: async (id, emailKey, codeHash, maxFailedAttempts) => {
76
+ const ctx = service.ctx;
77
+ const resource = ctx.resource(resourceAlias);
78
+ try {
79
+ const result = Number(await resource.db.client.eval(REDIS_VERIFY_SCRIPT, 1, resource.key(id), emailKey, codeHash, String(maxFailedAttempts)));
80
+ if (result === 1)
81
+ return Outcome.Verified;
82
+ if (result === -1)
83
+ return Outcome.Invalid;
84
+ if (result === -2)
85
+ return Outcome.Exhausted;
86
+ return Outcome.Missing;
87
+ }
88
+ catch (error) {
89
+ const unavailable = new OtpUnavailable('challenge');
90
+ if (error instanceof Error)
91
+ unavailable.oiriginalStack = `${error}: ${error.stack}`;
92
+ throw unavailable;
93
+ }
94
+ },
95
+ });
96
+ return service;
97
+ };
98
+ //# sourceMappingURL=challenge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"challenge.js","sourceRoot":"","sources":["../src/challenge.ts"],"names":[],"mappings":"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"}
package/build/consts.d.ts CHANGED
@@ -1,4 +1,13 @@
1
1
  export declare const SERVER_AUTH_OTP = "server-auth-otp";
2
+ export declare const OTP_CHALLENGE_STORE = "auth-otp-challenge-store";
3
+ export declare const OTP_THROTTLE_SERVICE = "auth-otp-throttle";
4
+ export declare const OTP_EMAIL_THROTTLE_RULES: readonly (Readonly<{
5
+ limit: 1;
6
+ windowSeconds: 60;
7
+ }> | Readonly<{
8
+ limit: 10;
9
+ windowSeconds: number;
10
+ }>)[];
2
11
  /** The OTP auth plugin type key — passed as credential.type in init requests */
3
- export { OTP_AUTH_TYPE, OTP_SERVICE, OTP_RESOURCE, OTP_TTL_SECONDS, OTP_CODE_LENGTH } from '@owlmeans/auth-otp';
12
+ export { OTP_AUTH_TYPE, OTP_SERVICE, OTP_RESOURCE, OTP_TTL_SECONDS, OTP_CODE_LENGTH, OTP_MAX_FAILED_ATTEMPTS, } from '@owlmeans/auth-otp';
4
13
  //# sourceMappingURL=consts.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe,oBAAoB,CAAA;AAEhD,gFAAgF;AAChF,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA"}
1
+ {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe,oBAAoB,CAAA;AAChD,eAAO,MAAM,mBAAmB,6BAA6B,CAAA;AAC7D,eAAO,MAAM,oBAAoB,sBAAsB,CAAA;AAEvD,eAAO,MAAM,wBAAwB;;;;;;KAGnC,CAAA;AAEF,gFAAgF;AAChF,OAAO,EACL,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,eAAe,EAAE,eAAe,EAC1E,uBAAuB,GACxB,MAAM,oBAAoB,CAAA"}
package/build/consts.js CHANGED
@@ -1,4 +1,10 @@
1
1
  export const SERVER_AUTH_OTP = 'server-auth-otp';
2
+ export const OTP_CHALLENGE_STORE = 'auth-otp-challenge-store';
3
+ export const OTP_THROTTLE_SERVICE = 'auth-otp-throttle';
4
+ export const OTP_EMAIL_THROTTLE_RULES = Object.freeze([
5
+ Object.freeze({ limit: 1, windowSeconds: 60 }),
6
+ Object.freeze({ limit: 10, windowSeconds: 60 * 60 }),
7
+ ]);
2
8
  /** The OTP auth plugin type key — passed as credential.type in init requests */
3
- export { OTP_AUTH_TYPE, OTP_SERVICE, OTP_RESOURCE, OTP_TTL_SECONDS, OTP_CODE_LENGTH } from '@owlmeans/auth-otp';
9
+ export { OTP_AUTH_TYPE, OTP_SERVICE, OTP_RESOURCE, OTP_TTL_SECONDS, OTP_CODE_LENGTH, OTP_MAX_FAILED_ATTEMPTS, } from '@owlmeans/auth-otp';
4
10
  //# sourceMappingURL=consts.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,eAAe,GAAG,iBAAiB,CAAA;AAEhD,gFAAgF;AAChF,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA"}
1
+ {"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,eAAe,GAAG,iBAAiB,CAAA;AAChD,MAAM,CAAC,MAAM,mBAAmB,GAAG,0BAA0B,CAAA;AAC7D,MAAM,CAAC,MAAM,oBAAoB,GAAG,mBAAmB,CAAA;AAEvD,MAAM,CAAC,MAAM,wBAAwB,GAAG,MAAM,CAAC,MAAM,CAAC;IACpD,MAAM,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,aAAa,EAAE,EAAE,EAAE,CAAC;IAC9C,MAAM,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,aAAa,EAAE,EAAE,GAAG,EAAE,EAAE,CAAC;CACrD,CAAC,CAAA;AAEF,gFAAgF;AAChF,OAAO,EACL,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,eAAe,EAAE,eAAe,EAC1E,uBAAuB,GACxB,MAAM,oBAAoB,CAAA"}
@@ -0,0 +1,18 @@
1
+ import { ResilientError } from '@owlmeans/error';
2
+ export declare class OtpError extends ResilientError {
3
+ static typeName: string;
4
+ constructor(message?: string);
5
+ }
6
+ export declare class OtpThrottled extends OtpError {
7
+ static typeName: string;
8
+ static httpStatus: number;
9
+ readonly retryAfter: number;
10
+ constructor(retryAfter?: number | string, stack?: string);
11
+ }
12
+ export declare class OtpUnavailable extends OtpError {
13
+ static typeName: string;
14
+ static httpStatus: number;
15
+ static allowServerErrorStatus: boolean;
16
+ constructor(message?: string);
17
+ }
18
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,qBAAa,QAAS,SAAQ,cAAc;IAC1C,OAAuB,QAAQ,SAAa;IAE5C,YAAY,OAAO,GAAE,MAAgB,EAEpC;CACF;AAED,qBAAa,YAAa,SAAQ,QAAQ;IACxC,OAAuB,QAAQ,SAAkC;IACjE,OAAc,UAAU,SAAM;IAC9B,SAAgB,UAAU,EAAE,MAAM,CAAA;IAElC,YAAY,UAAU,GAAE,MAAM,GAAG,MAAU,EAAE,KAAK,CAAC,EAAE,MAAM,EAK1D;CACF;AAED,qBAAa,cAAe,SAAQ,QAAQ;IAC1C,OAAuB,QAAQ,SAAoC;IACnE,OAAc,UAAU,SAAM;IAC9B,OAAc,sBAAsB,UAAO;IAE3C,YAAY,OAAO,GAAE,MAAgB,EAGpC;CACF"}
@@ -0,0 +1,32 @@
1
+ import { ResilientError } from '@owlmeans/error';
2
+ export class OtpError extends ResilientError {
3
+ static typeName = 'OtpError';
4
+ constructor(message = 'error') {
5
+ super(OtpError.typeName, `otp:${message}`);
6
+ }
7
+ }
8
+ export class OtpThrottled extends OtpError {
9
+ static typeName = `${OtpError.typeName}Throttled`;
10
+ static httpStatus = 429;
11
+ retryAfter;
12
+ constructor(retryAfter = 1, stack) {
13
+ super(typeof retryAfter === 'string' ? retryAfter : 'throttled');
14
+ this.type = OtpThrottled.typeName;
15
+ this.retryAfter = typeof retryAfter === 'number' ? Math.max(1, Math.ceil(retryAfter)) : 1;
16
+ if (stack != null)
17
+ this.oiriginalStack = stack;
18
+ }
19
+ }
20
+ export class OtpUnavailable extends OtpError {
21
+ static typeName = `${OtpError.typeName}Unavailable`;
22
+ static httpStatus = 503;
23
+ static allowServerErrorStatus = true;
24
+ constructor(message = 'store') {
25
+ super(`unavailable:${message}`);
26
+ this.type = OtpUnavailable.typeName;
27
+ }
28
+ }
29
+ ResilientError.registerErrorClass(OtpError);
30
+ ResilientError.registerErrorClass(OtpThrottled);
31
+ ResilientError.registerErrorClass(OtpUnavailable);
32
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,MAAM,OAAO,QAAS,SAAQ,cAAc;IACnC,MAAM,CAAU,QAAQ,GAAG,UAAU,CAAA;IAE5C,YAAY,OAAO,GAAW,OAAO;QACnC,KAAK,CAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,OAAO,EAAE,CAAC,CAAA;IAC5C,CAAC;CACF;AAED,MAAM,OAAO,YAAa,SAAQ,QAAQ;IACjC,MAAM,CAAU,QAAQ,GAAG,GAAG,QAAQ,CAAC,QAAQ,WAAW,CAAA;IAC1D,MAAM,CAAC,UAAU,GAAG,GAAG,CAAA;IACd,UAAU,CAAQ;IAElC,YAAY,UAAU,GAAoB,CAAC,EAAE,KAAc;QACzD,KAAK,CAAC,OAAO,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,CAAA;QAChE,IAAI,CAAC,IAAI,GAAG,YAAY,CAAC,QAAQ,CAAA;QACjC,IAAI,CAAC,UAAU,GAAG,OAAO,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QACzF,IAAI,KAAK,IAAI,IAAI;YAAE,IAAI,CAAC,cAAc,GAAG,KAAK,CAAA;IAChD,CAAC;CACF;AAED,MAAM,OAAO,cAAe,SAAQ,QAAQ;IACnC,MAAM,CAAU,QAAQ,GAAG,GAAG,QAAQ,CAAC,QAAQ,aAAa,CAAA;IAC5D,MAAM,CAAC,UAAU,GAAG,GAAG,CAAA;IACvB,MAAM,CAAC,sBAAsB,GAAG,IAAI,CAAA;IAE3C,YAAY,OAAO,GAAW,OAAO;QACnC,KAAK,CAAC,eAAe,OAAO,EAAE,CAAC,CAAA;QAC/B,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC,QAAQ,CAAA;IACrC,CAAC;CACF;AAED,cAAc,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAA;AAC3C,cAAc,CAAC,kBAAkB,CAAC,YAAY,CAAC,CAAA;AAC/C,cAAc,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAA"}
package/build/index.d.ts CHANGED
@@ -1,5 +1,8 @@
1
- export type * from './types.js';
1
+ export * from './types.js';
2
2
  export * from './consts.js';
3
3
  export * from './service.js';
4
4
  export * from './plugin.js';
5
+ export * from './errors.js';
6
+ export * from './throttle.js';
7
+ export * from './challenge.js';
5
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,gBAAgB,CAAA"}
package/build/index.js CHANGED
@@ -1,4 +1,8 @@
1
+ export * from './types.js';
1
2
  export * from './consts.js';
2
3
  export * from './service.js';
3
4
  export * from './plugin.js';
5
+ export * from './errors.js';
6
+ export * from './throttle.js';
7
+ export * from './challenge.js';
4
8
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,gBAAgB,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAsEvD,sGAAsG;AACtG,eAAO,MAAM,eAAe,GAAI,CAAC,SAAS,SAAS,EAAE,CAAC,SAAS,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,KAAG,CAG1F,CAAA"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAqEvD,sGAAsG;AACtG,eAAO,MAAM,eAAe,GAAI,CAAC,SAAS,SAAS,EAAE,CAAC,SAAS,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,KAAG,CAG1F,CAAA"}
package/build/plugin.js CHANGED
@@ -1,38 +1,36 @@
1
1
  import { ALL_SCOPES, AuthenFailed, AuthRole, AuthenticationType } from '@owlmeans/auth';
2
- import { registerPlugin } from '@owlmeans/server-auth/manager/plugins';
2
+ import { AuthChallengeReplayPolicy, registerPlugin } from '@owlmeans/server-auth/manager/plugins';
3
3
  import { OTP_AUTH_TYPE, OTP_SERVICE } from '@owlmeans/auth-otp';
4
4
  import { AUTH_IDENTITY_LINKING } from '@owlmeans/server-auth-identity';
5
- import { createIdOfLength, IdStyle } from '@owlmeans/basic-ids';
6
- // Separates the email from the per-request nonce inside the challenge (see init() below).
5
+ // Separates the email from the opaque issuance id inside the signed challenge.
7
6
  const CHALLENGE_DELIMITER = '::';
8
7
  /** Factory that creates the email-OTP AuthPlugin bound to the given context. */
9
8
  const otpPlugin = (context) => ({
10
9
  type: OTP_AUTH_TYPE,
10
+ challengeReplayPolicy: AuthChallengeReplayPolicy.Plugin,
11
11
  init: async (request) => {
12
12
  if (request.userId == null || !request.userId.includes('@')) {
13
13
  throw new AuthenFailed('otp:email');
14
14
  }
15
15
  const email = request.userId.trim().toLowerCase();
16
16
  const otp = context.service(OTP_SERVICE);
17
- await otp.issueChallenge(email);
18
- // The auth manager's anti-replay cache (AUTH_CACHE) keys on this decoded challenge, so it
19
- // must be unique per request — the plaintext email alone would be identical across every
20
- // independent login attempt for that address and collide with a still-cached prior one.
21
- const nonce = createIdOfLength(16, IdStyle.Base58);
22
- return { challenge: `${email}${CHALLENGE_DELIMITER}${nonce}` };
17
+ const issuanceId = await otp.issueChallenge(email);
18
+ return { challenge: `${email}${CHALLENGE_DELIMITER}${issuanceId}` };
23
19
  },
24
20
  authenticate: async (credential) => {
25
- // credential.challenge is "email::nonce" (opened from the signed envelope in makeAuthModel).
26
- const email = credential.challenge?.split(CHALLENGE_DELIMITER)[0];
21
+ // credential.challenge is "email::issuanceId" (opened from the signed envelope).
22
+ const parts = credential.challenge?.split(CHALLENGE_DELIMITER) ?? [];
23
+ const email = parts.length === 2 ? parts[0] : undefined;
24
+ const issuanceId = parts.length === 2 ? parts[1] : undefined;
27
25
  const code = credential.credential;
28
- if (!email || !email.includes('@')) {
26
+ if (!email || !email.includes('@') || !issuanceId) {
29
27
  throw new AuthenFailed('otp:email');
30
28
  }
31
29
  if (!code) {
32
30
  throw new AuthenFailed('otp:code');
33
31
  }
34
32
  const otp = context.service(OTP_SERVICE);
35
- await otp.verifyChallenge(email, code);
33
+ await otp.verifyChallenge(email, issuanceId, code);
36
34
  const identityAlias = context.cfg.otp?.identityAlias ?? AUTH_IDENTITY_LINKING;
37
35
  const identity = context.service(identityAlias);
38
36
  // What arrives on the credential is the organization SLUG the user is logging into — it is
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,6 @@
1
1
  import type { OtpService } from '@owlmeans/auth-otp';
2
+ export declare const otpChallengeKey: (issuanceId: string) => string;
3
+ export declare const otpEmailKey: (email: string) => string;
4
+ export declare const otpCodeHash: (issuanceId: string, code: string) => string;
2
5
  export declare const makeOtpService: (alias?: string) => OtpService;
3
6
  //# sourceMappingURL=service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAgBpD,eAAO,MAAM,cAAc,sBAA0B,UA+CpD,CAAA"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAkBpD,eAAO,MAAM,eAAe,eAAgB,MAAM,KAAG,MAAuD,CAAA;AAC5G,eAAO,MAAM,WAAW,UAAW,MAAM,KAAG,MAAqD,CAAA;AACjG,eAAO,MAAM,WAAW,eAAgB,MAAM,QAAQ,MAAM,KAAG,MACd,CAAA;AAEjD,eAAO,MAAM,cAAc,sBAA0B,UAiEpD,CAAA"}
package/build/service.js CHANGED
@@ -1,51 +1,74 @@
1
1
  import { appendContextual } from '@owlmeans/context';
2
2
  import { AuthenFailed } from '@owlmeans/auth';
3
- import { OTP_SERVICE, OTP_RESOURCE, OTP_TTL_SECONDS, OTP_CODE_LENGTH } from '@owlmeans/auth-otp';
3
+ import { OTP_SERVICE, OTP_TTL_SECONDS, OTP_CODE_LENGTH, OTP_MAX_FAILED_ATTEMPTS, OTP_RESOURCE, } from '@owlmeans/auth-otp';
4
4
  import { MAILER_SERVICE } from '@owlmeans/mailer';
5
- const codeKey = (email) => email.toLowerCase().replace(/[^a-z0-9]/g, '_');
5
+ import { createHash, randomBytes, randomInt } from 'node:crypto';
6
+ import { OtpChallengeOutcome } from './types.js';
7
+ import { OTP_CHALLENGE_STORE } from './consts.js';
8
+ import { emailThrottleKey } from './throttle.js';
9
+ import { OtpThrottled, OtpUnavailable } from './errors.js';
10
+ import { makeRedisOtpChallengeStore } from './challenge.js';
11
+ const digest = (scope, value) => createHash('sha256').update(`owlmeans:otp:${scope}\0${value}`).digest('hex');
12
+ export const otpChallengeKey = (issuanceId) => `challenge:${digest('issuance', issuanceId)}`;
13
+ export const otpEmailKey = (email) => digest('email', email.trim().toLowerCase());
14
+ export const otpCodeHash = (issuanceId, code) => digest('code', `${issuanceId}\0${code.trim()}`);
6
15
  export const makeOtpService = (alias = OTP_SERVICE) => {
16
+ let fallbackChallenges;
17
+ const challengeStore = (ctx) => {
18
+ const challengeAlias = ctx.cfg.otp?.challengeStoreAlias ?? OTP_CHALLENGE_STORE;
19
+ if (ctx.hasService(challengeAlias))
20
+ return ctx.service(challengeAlias);
21
+ // Backwards-compatible secure default: existing consumers that registered only OTP_RESOURCE
22
+ // still get the atomic Redis store. New consumers can inject the real memory implementation.
23
+ if (fallbackChallenges == null) {
24
+ fallbackChallenges = makeRedisOtpChallengeStore(`${alias}:redis-challenge`, ctx.cfg.otp?.resourceAlias ?? OTP_RESOURCE);
25
+ fallbackChallenges.registerContext(ctx);
26
+ }
27
+ return fallbackChallenges;
28
+ };
7
29
  const service = appendContextual(alias, {
8
30
  issueChallenge: async (email) => {
9
31
  const ctx = service.ctx;
10
- const resourceAlias = ctx.cfg.otp?.resourceAlias ?? OTP_RESOURCE;
11
32
  const mailerAlias = ctx.cfg.otp?.mailerAlias ?? MAILER_SERVICE;
12
- const resource = ctx.resource(resourceAlias);
33
+ const challenges = challengeStore(ctx);
13
34
  const mailer = ctx.service(mailerAlias);
14
35
  const code = generateCode();
15
- const id = codeKey(email);
16
- // Upsert: delete previous code if any, then create fresh.
17
- try {
18
- await resource.delete(id);
36
+ const issuanceId = randomBytes(18).toString('base64url');
37
+ const throttleAlias = ctx.cfg.otp?.throttleAlias;
38
+ const throttleRules = ctx.cfg.otp?.throttleRules;
39
+ if (throttleAlias != null && throttleRules != null) {
40
+ const decision = await ctx.service(throttleAlias)
41
+ .consume(emailThrottleKey(email), throttleRules, issuanceId);
42
+ if (!decision.allowed)
43
+ throw new OtpThrottled(decision.retryAfter);
19
44
  }
20
- catch { /* key may not exist */ }
21
- await resource.create({ id, code }, { ttl: OTP_TTL_SECONDS });
45
+ const issued = await challenges.issue({
46
+ id: otpChallengeKey(issuanceId),
47
+ emailKey: otpEmailKey(email),
48
+ codeHash: otpCodeHash(issuanceId, code),
49
+ }, OTP_TTL_SECONDS);
50
+ if (!issued)
51
+ throw new OtpUnavailable('issuance-collision');
22
52
  await mailer.send({
23
53
  to: email,
24
54
  subject: 'Your login code',
25
55
  text: `Your one-time login code is: ${code}\n\nIt expires in ${OTP_TTL_SECONDS / 60} minutes.`,
26
56
  html: `<p>Your one-time login code is: <strong>${code}</strong></p><p>It expires in ${OTP_TTL_SECONDS / 60} minutes.</p>`,
27
57
  });
58
+ return issuanceId;
28
59
  },
29
- verifyChallenge: async (email, code) => {
60
+ verifyChallenge: async (email, issuanceId, code) => {
30
61
  const ctx = service.ctx;
31
- const resourceAlias = ctx.cfg.otp?.resourceAlias ?? OTP_RESOURCE;
32
- const resource = ctx.resource(resourceAlias);
33
- const id = codeKey(email);
34
- const record = await resource.load(id);
35
- if (record == null || record.code !== code.trim()) {
62
+ const outcome = await challengeStore(ctx).verify(otpChallengeKey(issuanceId), otpEmailKey(email), otpCodeHash(issuanceId, code), OTP_MAX_FAILED_ATTEMPTS);
63
+ if (outcome !== OtpChallengeOutcome.Verified) {
36
64
  throw new AuthenFailed('otp:code');
37
65
  }
38
- // Consume: delete so the code cannot be reused.
39
- try {
40
- await resource.delete(id);
41
- }
42
- catch { /* already expired */ }
43
66
  },
44
67
  });
45
68
  return service;
46
69
  };
47
70
  const generateCode = () => {
48
- const digits = Math.floor(Math.random() * Math.pow(10, OTP_CODE_LENGTH));
71
+ const digits = randomInt(0, Math.pow(10, OTP_CODE_LENGTH));
49
72
  return String(digits).padStart(OTP_CODE_LENGTH, '0');
50
73
  };
51
74
  //# sourceMappingURL=service.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { AuthThrottleService } from './types.js';
2
+ type Clock = () => number;
3
+ /** Collision-resistant, non-raw key for case-insensitive email throttles. */
4
+ export declare const emailThrottleKey: (email: string) => string;
5
+ /** Non-raw key for request-source throttles. Callers remain responsible for trusted-proxy parsing. */
6
+ export declare const ipThrottleKey: (ip: string) => string;
7
+ /** Real in-memory implementation for single-process development and deterministic unit tests. */
8
+ export declare const makeMemoryThrottleService: (alias?: string, now?: Clock) => AuthThrottleService;
9
+ /** Redis implementation: every configured window is checked and consumed in one Lua operation. */
10
+ export declare const makeRedisThrottleService: (alias?: string, resourceAlias?: string) => AuthThrottleService;
11
+ export {};
12
+ //# sourceMappingURL=throttle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"throttle.d.ts","sourceRoot":"","sources":["../src/throttle.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,mBAAmB,EAAyD,MAAM,YAAY,CAAA;AAG5G,KAAK,KAAK,GAAG,MAAM,MAAM,CAAA;AAKzB,6EAA6E;AAC7E,eAAO,MAAM,gBAAgB,UAAW,MAAM,KAAG,MACU,CAAA;AAE3D,sGAAsG;AACtG,eAAO,MAAM,aAAa,OAAQ,MAAM,KAAG,MACO,CAAA;AASlD,iGAAiG;AACjG,eAAO,MAAM,yBAAyB,yBACD,KAAK,KACvC,mBAyBF,CAAA;AA+BD,kGAAkG;AAClG,eAAO,MAAM,wBAAwB,8CAElC,mBA4BF,CAAA"}