@owlmeans/auth-otp 0.1.18-rc.9 → 0.1.18

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 ADDED
@@ -0,0 +1,100 @@
1
+ # @owlmeans/auth-otp
2
+
3
+ The shared contract of OwlMeans passwordless email login: the `OtpService` interface and the
4
+ constants both sides of an email-OTP flow agree on — the service alias, the auth type a client
5
+ selects, the code-store alias, the code length, its lifetime and the failed-attempt limit. A shared
6
+ (common) package depends on it when it must name the `'email-otp'` auth type or type the service. It
7
+ contains no runtime code beyond those constants: it generates, stores and mails nothing. A server
8
+ wires the actual flow with `@owlmeans/server-auth-otp`, which implements `OtpService`, the challenge
9
+ stores, throttles and the auth plugin.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ bun add @owlmeans/auth-otp@^0.1.18
15
+ ```
16
+
17
+ ## Concepts
18
+
19
+ - **Contracts half** — this package is the producer/consumer contract; `@owlmeans/server-auth-otp`
20
+ is the implementation. A new constant or method signature is added here, never only in the
21
+ implementation.
22
+ - **Challenge** — one issuance of a code. `issueChallenge(email)` generates a code, persists it with
23
+ a TTL, mails it and returns an **opaque issuance id** that contains neither the email nor the code.
24
+ - **Verification** — `verifyChallenge(email, issuanceId, code)` checks and atomically consumes that
25
+ exact issuance; it resolves on success and throws otherwise.
26
+ - **Attempt limit** — a challenge is invalidated on its `OTP_MAX_FAILED_ATTEMPTS`-th (fifth) failed
27
+ guess.
28
+
29
+ ## Usage
30
+
31
+ ### Select the email-OTP auth type on a client
32
+
33
+ A client names the plugin by its auth type when it starts an authentication; the constant keeps the
34
+ string identical on both sides.
35
+
36
+ ```typescript
37
+ import { OTP_AUTH_TYPE, OTP_CODE_LENGTH } from '@owlmeans/auth-otp'
38
+
39
+ const init = { type: OTP_AUTH_TYPE, userId: email } // 'email-otp'
40
+
41
+ const isComplete = (code: string) => code.length === OTP_CODE_LENGTH && /^\d+$/.test(code)
42
+ ```
43
+
44
+ ### Type the service in shared code
45
+
46
+ ```typescript
47
+ import type { BasicContext, BasicConfig } from '@owlmeans/context'
48
+ import { OTP_SERVICE } from '@owlmeans/auth-otp'
49
+ import type { OtpService } from '@owlmeans/auth-otp'
50
+
51
+ export const otpService = (ctx: BasicContext<BasicConfig>) => ctx.service<OtpService>(OTP_SERVICE)
52
+
53
+ // server side, after @owlmeans/server-auth-otp registered the implementation
54
+ const issuanceId = await otpService(ctx).issueChallenge('user@example.com')
55
+ await otpService(ctx).verifyChallenge('user@example.com', issuanceId, '123456')
56
+ ```
57
+
58
+ ## API
59
+
60
+ ### Types
61
+
62
+ | Symbol | Kind | Purpose |
63
+ |---|---|---|
64
+ | `OtpService` | interface | extends `InitializedService` (`@owlmeans/context`) |
65
+ | `OtpService.issueChallenge(email)` | method | `→ Promise<string>` — generate, persist with TTL, email; returns the opaque issuance id |
66
+ | `OtpService.verifyChallenge(email, issuanceId, code)` | method | `→ Promise<void>` — verify and atomically consume one issuance |
67
+
68
+ ### Constants
69
+
70
+ | Symbol | Value | Purpose |
71
+ |---|---|---|
72
+ | `OTP_SERVICE` | `'auth-otp-service'` | service alias the implementation registers under |
73
+ | `OTP_AUTH_TYPE` | `'email-otp'` | auth plugin type string a client sends |
74
+ | `OTP_RESOURCE` | `'auth-otp-cache'` | resource alias for pending codes |
75
+ | `OTP_TTL_SECONDS` | `600` | code lifetime (10 minutes) |
76
+ | `OTP_CODE_LENGTH` | `6` | numeric digits per code |
77
+ | `OTP_MAX_FAILED_ATTEMPTS` | `5` | a challenge is invalidated on its fifth failed guess |
78
+
79
+ The barrel exports only these string and number constants plus the type; the package tests pin every
80
+ value, because changing one changes what server and clients agree on.
81
+
82
+ ## Common pitfalls
83
+
84
+ - Do not import this package expecting an implementation — `makeOtpService`, `appendOtpPlugin` and
85
+ the stores live in `@owlmeans/server-auth-otp`.
86
+ - Never encode the email or the code into the issuance id; it stays opaque and owned by the OTP
87
+ store, which counts attempts and consumes it.
88
+ - Do not hard-code `'email-otp'` or `6` in clients — import `OTP_AUTH_TYPE` and `OTP_CODE_LENGTH`.
89
+ - Depend on `@owlmeans/server-auth-otp` only where the server wires itself up; shared packages depend
90
+ on this one.
91
+
92
+ The `server-auth-otp` skill covers this package together with its implementation: registration
93
+ order, the auth flow, config overrides and throttling.
94
+
95
+ ## Related packages
96
+
97
+ - [`@owlmeans/server-auth-otp`](../server-auth-otp) — the `OtpService` implementation, challenge stores, throttles and the `AuthPlugin`
98
+ - [`@owlmeans/context`](../context) — `InitializedService`, the base of `OtpService`
99
+ - [`@owlmeans/auth`](../auth) — the auth payload and credential types the OTP flow produces
100
+ - [`@owlmeans/mailer`](../mailer) — the `MailerService` that delivers the codes
package/build/consts.d.ts CHANGED
@@ -7,4 +7,6 @@ export declare const OTP_RESOURCE = "auth-otp-cache";
7
7
  export declare const OTP_TTL_SECONDS = 600;
8
8
  /** OTP code length (numeric digits) */
9
9
  export declare const OTP_CODE_LENGTH = 6;
10
+ /** A challenge is invalidated on its fifth failed guess. */
11
+ export declare const OTP_MAX_FAILED_ATTEMPTS = 5;
10
12
  //# sourceMappingURL=consts.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,WAAW,qBAAqB,CAAA;AAE7C,yEAAyE;AACzE,eAAO,MAAM,aAAa,cAAc,CAAA;AAExC,yDAAyD;AACzD,eAAO,MAAM,YAAY,mBAAmB,CAAA;AAE5C,uCAAuC;AACvC,eAAO,MAAM,eAAe,MAAM,CAAA;AAElC,uCAAuC;AACvC,eAAO,MAAM,eAAe,IAAI,CAAA"}
1
+ {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,WAAW,qBAAqB,CAAA;AAE7C,yEAAyE;AACzE,eAAO,MAAM,aAAa,cAAc,CAAA;AAExC,yDAAyD;AACzD,eAAO,MAAM,YAAY,mBAAmB,CAAA;AAE5C,uCAAuC;AACvC,eAAO,MAAM,eAAe,MAAM,CAAA;AAElC,uCAAuC;AACvC,eAAO,MAAM,eAAe,IAAI,CAAA;AAEhC,4DAA4D;AAC5D,eAAO,MAAM,uBAAuB,IAAI,CAAA"}
package/build/consts.js CHANGED
@@ -7,4 +7,6 @@ export const OTP_RESOURCE = 'auth-otp-cache';
7
7
  export const OTP_TTL_SECONDS = 600;
8
8
  /** OTP code length (numeric digits) */
9
9
  export const OTP_CODE_LENGTH = 6;
10
+ /** A challenge is invalidated on its fifth failed guess. */
11
+ export const OTP_MAX_FAILED_ATTEMPTS = 5;
10
12
  //# sourceMappingURL=consts.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG,kBAAkB,CAAA;AAE7C,yEAAyE;AACzE,MAAM,CAAC,MAAM,aAAa,GAAG,WAAW,CAAA;AAExC,yDAAyD;AACzD,MAAM,CAAC,MAAM,YAAY,GAAG,gBAAgB,CAAA;AAE5C,uCAAuC;AACvC,MAAM,CAAC,MAAM,eAAe,GAAG,GAAG,CAAA;AAElC,uCAAuC;AACvC,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAA"}
1
+ {"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG,kBAAkB,CAAA;AAE7C,yEAAyE;AACzE,MAAM,CAAC,MAAM,aAAa,GAAG,WAAW,CAAA;AAExC,yDAAyD;AACzD,MAAM,CAAC,MAAM,YAAY,GAAG,gBAAgB,CAAA;AAE5C,uCAAuC;AACvC,MAAM,CAAC,MAAM,eAAe,GAAG,GAAG,CAAA;AAElC,uCAAuC;AACvC,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAA;AAEhC,4DAA4D;AAC5D,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAA"}
package/build/types.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import type { InitializedService } from '@owlmeans/context';
2
2
  /** Passwordless email OTP service — generates, stores, and verifies short-lived one-time codes */
3
3
  export interface OtpService extends InitializedService {
4
- /** Generate a code, persist it with TTL, and email it to the user. */
5
- issueChallenge: (email: string) => Promise<void>;
6
- /** Verify and consume the code. Throws OtpInvalidError if the code is wrong or expired. */
7
- verifyChallenge: (email: string, code: string) => Promise<void>;
4
+ /** Generate a code, persist it with TTL, email it, and return its opaque issuance id. */
5
+ issueChallenge: (email: string) => Promise<string>;
6
+ /** Verify and atomically consume one issuance. */
7
+ verifyChallenge: (email: string, issuanceId: string, code: string) => Promise<void>;
8
8
  }
9
9
  //# 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,sEAAsE;IACtE,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAChD,2FAA2F;IAC3F,eAAe,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAChE"}
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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/auth-otp",
3
- "version": "0.1.18-rc.9",
3
+ "version": "0.1.18",
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-rc.9",
26
- "@owlmeans/error": "^0.1.18-rc.9"
25
+ "@owlmeans/context": "^0.1.18",
26
+ "@owlmeans/error": "^0.1.18"
27
27
  },
28
28
  "devDependencies": {
29
- "@owlmeans/dep-config": "workspace:*",
30
- "@types/bun": "^1.4.0",
29
+ "@owlmeans/dep-config": "^0.1.18",
30
+ "@types/bun": "^1.4.2",
31
31
  "nodemon": "^3.1.14",
32
32
  "typescript": "^7.0.2"
33
33
  },
package/src/consts.ts CHANGED
@@ -11,3 +11,6 @@ export const OTP_TTL_SECONDS = 600
11
11
 
12
12
  /** OTP code length (numeric digits) */
13
13
  export const OTP_CODE_LENGTH = 6
14
+
15
+ /** A challenge is invalidated on its fifth failed guess. */
16
+ export const OTP_MAX_FAILED_ATTEMPTS = 5
package/src/types.ts CHANGED
@@ -2,8 +2,8 @@ import type { InitializedService } from '@owlmeans/context'
2
2
 
3
3
  /** Passwordless email OTP service — generates, stores, and verifies short-lived one-time codes */
4
4
  export interface OtpService extends InitializedService {
5
- /** Generate a code, persist it with TTL, and email it to the user. */
6
- issueChallenge: (email: string) => Promise<void>
7
- /** Verify and consume the code. Throws OtpInvalidError if the code is wrong or expired. */
8
- verifyChallenge: (email: string, code: string) => Promise<void>
5
+ /** Generate a code, persist it with TTL, email it, and return its opaque issuance id. */
6
+ issueChallenge: (email: string) => Promise<string>
7
+ /** Verify and atomically consume one issuance. */
8
+ verifyChallenge: (email: string, issuanceId: string, code: string) => Promise<void>
9
9
  }
@@ -0,0 +1,35 @@
1
+ import { describe, expect, expectTypeOf, test } from 'bun:test'
2
+ import * as otp from '../src/index.js'
3
+ import type { OtpService } from '../src/index.js'
4
+
5
+ /**
6
+ * This package is the contract between the OTP server (`@owlmeans/server-auth-otp`) and the clients
7
+ * that select it: a value changed here changes what both sides agree on, so each one is pinned.
8
+ */
9
+ describe('@owlmeans/auth-otp — the shared contract', () => {
10
+ test('names the service, the auth type and the code store', () => {
11
+ expect(otp.OTP_SERVICE).toBe('auth-otp-service')
12
+ expect(otp.OTP_AUTH_TYPE).toBe('email-otp')
13
+ expect(otp.OTP_RESOURCE).toBe('auth-otp-cache')
14
+ })
15
+
16
+ test('a code is six digits, lives ten minutes and survives four wrong guesses', () => {
17
+ expect(otp.OTP_CODE_LENGTH).toBe(6)
18
+ expect(otp.OTP_TTL_SECONDS).toBe(600)
19
+ expect(otp.OTP_MAX_FAILED_ATTEMPTS).toBe(5)
20
+ })
21
+
22
+ test('the barrel exports constants only — no runtime code to drift', () => {
23
+ expect(Object.keys(otp).sort()).toEqual([
24
+ 'OTP_AUTH_TYPE', 'OTP_CODE_LENGTH', 'OTP_MAX_FAILED_ATTEMPTS', 'OTP_RESOURCE', 'OTP_SERVICE', 'OTP_TTL_SECONDS',
25
+ ])
26
+ expect(Object.values(otp).every(value => typeof value === 'string' || typeof value === 'number')).toBe(true)
27
+ })
28
+
29
+ test('the service issues an opaque id and verifies by email, id and code', () => {
30
+ expectTypeOf<OtpService['issueChallenge']>().parameters.toEqualTypeOf<[string]>()
31
+ expectTypeOf<OtpService['issueChallenge']>().returns.toEqualTypeOf<Promise<string>>()
32
+ expectTypeOf<OtpService['verifyChallenge']>().parameters.toEqualTypeOf<[string, string, string]>()
33
+ expectTypeOf<OtpService['verifyChallenge']>().returns.toEqualTypeOf<Promise<void>>()
34
+ })
35
+ })