@owlmeans/auth-otp 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.
package/README.md CHANGED
@@ -11,7 +11,7 @@ stores, throttles and the auth plugin.
11
11
  ## Installation
12
12
 
13
13
  ```bash
14
- bun add @owlmeans/auth-otp@^0.1.18
14
+ bun add @owlmeans/auth-otp@^0.1.19
15
15
  ```
16
16
 
17
17
  ## Concepts
@@ -23,6 +23,12 @@ bun add @owlmeans/auth-otp@^0.1.18
23
23
  a TTL, mails it and returns an **opaque issuance id** that contains neither the email nor the code.
24
24
  - **Verification** — `verifyChallenge(email, issuanceId, code)` checks and atomically consumes that
25
25
  exact issuance; it resolves on success and throws otherwise.
26
+ - **Message** — `issueChallenge(email, { message })` mails an `OtpMessage` (`subject`, `text`,
27
+ `html` templates with `{{code}}` and `{{minutes}}` placeholders) instead of the default English
28
+ mail, e.g. a localized one; without it the default mail is sent unchanged.
29
+ - **Rewrite seam** — the optional `rewriteChallenge(email, issuanceId, code)` replaces the code of a
30
+ pending issuance for developer tooling (lifetime kept, failed attempts reset); it answers `false`
31
+ for a gone issuance, another address or a code that is not `OTP_CODE_LENGTH` digits.
26
32
  - **Attempt limit** — a challenge is invalidated on its `OTP_MAX_FAILED_ATTEMPTS`-th (fifth) failed
27
33
  guess.
28
34
 
@@ -62,8 +68,11 @@ await otpService(ctx).verifyChallenge('user@example.com', issuanceId, '123456')
62
68
  | Symbol | Kind | Purpose |
63
69
  |---|---|---|
64
70
  | `OtpService` | interface | extends `InitializedService` (`@owlmeans/context`) |
65
- | `OtpService.issueChallenge(email)` | method | `→ Promise<string>` — generate, persist with TTL, email; returns the opaque issuance id |
71
+ | `OtpService.issueChallenge(email, opts?)` | method | `→ Promise<string>` — generate, persist with TTL, email (`opts.message` overrides the mail); returns the opaque issuance id |
66
72
  | `OtpService.verifyChallenge(email, issuanceId, code)` | method | `→ Promise<void>` — verify and atomically consume one issuance |
73
+ | `OtpService.rewriteChallenge?(email, issuanceId, code)` | optional method | `→ Promise<boolean>` — developer seam: replace a pending code, keep its TTL, reset attempts |
74
+ | `OtpMessage` | interface | `{ subject, text, html }` templates; `{{code}}` and `{{minutes}}` are substituted |
75
+ | `OtpIssueOptions` | interface | `{ message?: OtpMessage }` |
67
76
 
68
77
  ### Constants
69
78
 
@@ -76,7 +85,7 @@ await otpService(ctx).verifyChallenge('user@example.com', issuanceId, '123456')
76
85
  | `OTP_CODE_LENGTH` | `6` | numeric digits per code |
77
86
  | `OTP_MAX_FAILED_ATTEMPTS` | `5` | a challenge is invalidated on its fifth failed guess |
78
87
 
79
- The barrel exports only these string and number constants plus the type; the package tests pin every
88
+ The barrel exports only these string and number constants plus the types; the package tests pin every
80
89
  value, because changing one changes what server and clients agree on.
81
90
 
82
91
  ## Common pitfalls
package/build/types.d.ts CHANGED
@@ -1,9 +1,28 @@
1
1
  import type { InitializedService } from '@owlmeans/context';
2
+ /**
3
+ * The mail a login code travels in, as templates: every part may carry `{{code}}` (the code) and
4
+ * `{{minutes}}` (its lifetime in minutes), substituted at issuance.
5
+ */
6
+ export interface OtpMessage {
7
+ subject: string;
8
+ text: string;
9
+ html: string;
10
+ }
11
+ export interface OtpIssueOptions {
12
+ /** Replaces the default English login-code mail; omitted keeps it. */
13
+ message?: OtpMessage;
14
+ }
2
15
  /** Passwordless email OTP service — generates, stores, and verifies short-lived one-time codes */
3
16
  export interface OtpService extends InitializedService {
4
17
  /** Generate a code, persist it with TTL, email it, and return its opaque issuance id. */
5
- issueChallenge: (email: string) => Promise<string>;
18
+ issueChallenge: (email: string, opts?: OtpIssueOptions) => Promise<string>;
6
19
  /** Verify and atomically consume one issuance. */
7
20
  verifyChallenge: (email: string, issuanceId: string, code: string) => Promise<void>;
21
+ /**
22
+ * Developer seam: replace the code of a pending issuance, keeping its lifetime and resetting its
23
+ * failed attempts. `false` when the issuance is gone, names another address, the code is not
24
+ * `OTP_CODE_LENGTH` digits, or the store cannot rewrite. Never expose it to the public.
25
+ */
26
+ rewriteChallenge?: (email: string, issuanceId: string, code: string) => Promise<boolean>;
8
27
  }
9
28
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAE3D,kGAAkG;AAClG,MAAM,WAAW,UAAW,SAAQ,kBAAkB;IACpD,yFAAyF;IACzF,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IAClD,kDAAkD;IAClD,eAAe,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CACpF"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAE3D;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,eAAe;IAC9B,sEAAsE;IACtE,OAAO,CAAC,EAAE,UAAU,CAAA;CACrB;AAED,kGAAkG;AAClG,MAAM,WAAW,UAAW,SAAQ,kBAAkB;IACpD,yFAAyF;IACzF,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,eAAe,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IAC1E,kDAAkD;IAClD,eAAe,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACnF;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CACzF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/auth-otp",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -22,12 +22,12 @@
22
22
  }
23
23
  },
24
24
  "dependencies": {
25
- "@owlmeans/context": "^0.1.18",
26
- "@owlmeans/error": "^0.1.18"
25
+ "@owlmeans/context": "^0.1.19",
26
+ "@owlmeans/error": "^0.1.19"
27
27
  },
28
28
  "devDependencies": {
29
- "@owlmeans/dep-config": "^0.1.18",
30
- "@types/bun": "^1.4.2",
29
+ "@owlmeans/dep-config": "^0.1.19",
30
+ "@types/bun": "^1.4.4",
31
31
  "nodemon": "^3.1.14",
32
32
  "typescript": "^7.0.2"
33
33
  },
package/src/types.ts CHANGED
@@ -1,9 +1,30 @@
1
1
  import type { InitializedService } from '@owlmeans/context'
2
2
 
3
+ /**
4
+ * The mail a login code travels in, as templates: every part may carry `{{code}}` (the code) and
5
+ * `{{minutes}}` (its lifetime in minutes), substituted at issuance.
6
+ */
7
+ export interface OtpMessage {
8
+ subject: string
9
+ text: string
10
+ html: string
11
+ }
12
+
13
+ export interface OtpIssueOptions {
14
+ /** Replaces the default English login-code mail; omitted keeps it. */
15
+ message?: OtpMessage
16
+ }
17
+
3
18
  /** Passwordless email OTP service — generates, stores, and verifies short-lived one-time codes */
4
19
  export interface OtpService extends InitializedService {
5
20
  /** Generate a code, persist it with TTL, email it, and return its opaque issuance id. */
6
- issueChallenge: (email: string) => Promise<string>
21
+ issueChallenge: (email: string, opts?: OtpIssueOptions) => Promise<string>
7
22
  /** Verify and atomically consume one issuance. */
8
23
  verifyChallenge: (email: string, issuanceId: string, code: string) => Promise<void>
24
+ /**
25
+ * Developer seam: replace the code of a pending issuance, keeping its lifetime and resetting its
26
+ * failed attempts. `false` when the issuance is gone, names another address, the code is not
27
+ * `OTP_CODE_LENGTH` digits, or the store cannot rewrite. Never expose it to the public.
28
+ */
29
+ rewriteChallenge?: (email: string, issuanceId: string, code: string) => Promise<boolean>
9
30
  }
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, expectTypeOf, test } from 'bun:test'
2
2
  import * as otp from '../src/index.js'
3
- import type { OtpService } from '../src/index.js'
3
+ import type { OtpIssueOptions, OtpService } from '../src/index.js'
4
4
 
5
5
  /**
6
6
  * This package is the contract between the OTP server (`@owlmeans/server-auth-otp`) and the clients
@@ -27,9 +27,14 @@ describe('@owlmeans/auth-otp — the shared contract', () => {
27
27
  })
28
28
 
29
29
  test('the service issues an opaque id and verifies by email, id and code', () => {
30
- expectTypeOf<OtpService['issueChallenge']>().parameters.toEqualTypeOf<[string]>()
30
+ expectTypeOf<OtpService['issueChallenge']>().parameters.toEqualTypeOf<[string, OtpIssueOptions?]>()
31
31
  expectTypeOf<OtpService['issueChallenge']>().returns.toEqualTypeOf<Promise<string>>()
32
32
  expectTypeOf<OtpService['verifyChallenge']>().parameters.toEqualTypeOf<[string, string, string]>()
33
33
  expectTypeOf<OtpService['verifyChallenge']>().returns.toEqualTypeOf<Promise<void>>()
34
34
  })
35
+
36
+ test('the optional developer seam rewrites a pending code by email, id and code', () => {
37
+ expectTypeOf<NonNullable<OtpService['rewriteChallenge']>>().parameters.toEqualTypeOf<[string, string, string]>()
38
+ expectTypeOf<NonNullable<OtpService['rewriteChallenge']>>().returns.toEqualTypeOf<Promise<boolean>>()
39
+ })
35
40
  })