@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 +100 -0
- package/build/consts.d.ts +2 -0
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +2 -0
- package/build/consts.js.map +1 -1
- package/build/types.d.ts +4 -4
- package/build/types.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/consts.ts +3 -0
- package/src/types.ts +4 -4
- package/tests/contract.spec.ts +35 -0
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
|
package/build/consts.d.ts.map
CHANGED
|
@@ -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
|
package/build/consts.js.map
CHANGED
|
@@ -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,
|
|
5
|
-
issueChallenge: (email: string) => Promise<
|
|
6
|
-
/** Verify and consume
|
|
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
|
package/build/types.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|
26
|
-
"@owlmeans/error": "^0.1.18
|
|
25
|
+
"@owlmeans/context": "^0.1.18",
|
|
26
|
+
"@owlmeans/error": "^0.1.18"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@owlmeans/dep-config": "
|
|
30
|
-
"@types/bun": "^1.4.
|
|
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
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,
|
|
6
|
-
issueChallenge: (email: string) => Promise<
|
|
7
|
-
/** Verify and consume
|
|
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
|
+
})
|