@owlmeans/server-auth-otp 0.1.18-rc.54 → 0.1.18-rc.55
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 +156 -24
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/server-auth-otp/SKILL.md +1 -1
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -1,43 +1,175 @@
|
|
|
1
1
|
# @owlmeans/server-auth-otp
|
|
2
2
|
|
|
3
|
-
Email
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
Email one-time-code login for an OwlMeans server: the `OtpService` implementation that issues and
|
|
4
|
+
verifies codes, the `'email-otp'` `AuthPlugin` for the `@owlmeans/server-auth` plugin registry,
|
|
5
|
+
atomic challenge stores and sliding-window throttles (Redis for production, memory for development
|
|
6
|
+
and tests), and the OTP errors. A server uses it to offer passwordless sign-in by e-mail; the
|
|
7
|
+
verified address is resolved to an account through `@owlmeans/server-auth-identity`. It does not send
|
|
8
|
+
mail itself — a `MailerService` must be registered — and it is not the place for shared constants or
|
|
9
|
+
the `OtpService` type: depend on `@owlmeans/auth-otp` from a package that only names the auth type or
|
|
10
|
+
types the service, and on this package only where the server wires itself up.
|
|
11
11
|
|
|
12
12
|
## Installation
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
bun add @owlmeans/server-auth-otp@^0.1.18-rc.
|
|
15
|
+
bun add @owlmeans/server-auth-otp@^0.1.18-rc.55
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
## Concepts
|
|
19
|
+
|
|
20
|
+
- **Issuance** — `issueChallenge(email)` creates a random 6-digit code (`OTP_CODE_LENGTH`), stores it
|
|
21
|
+
for `OTP_TTL_SECONDS` (600 s) under an opaque issuance id, mails it and returns the id. The id
|
|
22
|
+
never contains the address or the code.
|
|
23
|
+
- **Challenge store** — `OtpChallengeStore`, keyed by digests from `otpKeyHelper`. `verify`
|
|
24
|
+
compares, counts failures and consumes in one operation; after `OTP_MAX_FAILED_ATTEMPTS` (5) the
|
|
25
|
+
issuance is exhausted.
|
|
26
|
+
- **Throttle** — `AuthThrottleService.consume(key, rules, token?)` applies every `ThrottleRule`
|
|
27
|
+
(`{ limit, windowSeconds }`) together. Enabled only when `cfg.otp.throttleAlias` and
|
|
28
|
+
`cfg.otp.throttleRules` are both set.
|
|
29
|
+
- **Plugin** — `appendOtpPlugin` registers the plugin factory under `OTP_AUTH_TYPE` in the
|
|
30
|
+
server-auth registry. `init` issues a challenge `"<email>::<issuanceId>"`; `authenticate`
|
|
31
|
+
verifies the code, links or finds the profile and fills the credential
|
|
32
|
+
(`AuthenticationType.OneTimeToken`).
|
|
33
|
+
|
|
18
34
|
## Usage
|
|
19
35
|
|
|
36
|
+
### Wire the server
|
|
37
|
+
|
|
20
38
|
```typescript
|
|
21
|
-
import { makeRedisResource } from '@owlmeans/redis-resource'
|
|
22
|
-
import { OTP_RESOURCE, OTP_SERVICE } from '@owlmeans/auth-otp'
|
|
23
|
-
import { appendOtpPlugin, makeOtpService } from '@owlmeans/server-auth-otp'
|
|
24
39
|
import { MAILER_SERVICE } from '@owlmeans/mailer'
|
|
25
|
-
import {
|
|
40
|
+
import { makeMailgunMailerService } from '@owlmeans/server-mailer-mailgun'
|
|
41
|
+
import { makeRedisResource } from '@owlmeans/redis-resource'
|
|
42
|
+
import {
|
|
43
|
+
OTP_RESOURCE, appendOtpPlugin, makeOtpService, makeRedisOtpChallengeStore, makeRedisThrottleService,
|
|
44
|
+
} from '@owlmeans/server-auth-otp'
|
|
45
|
+
|
|
46
|
+
context.registerResource(makeRedisResource(OTP_RESOURCE)) // challenge and throttle storage
|
|
47
|
+
context.registerService(makeRedisOtpChallengeStore()) // OTP_CHALLENGE_STORE
|
|
48
|
+
context.registerService(makeRedisThrottleService()) // OTP_THROTTLE_SERVICE
|
|
49
|
+
context.registerService(makeMailgunMailerService(MAILER_SERVICE)) // delivers the code
|
|
50
|
+
context.registerService(makeOtpService()) // OTP_SERVICE
|
|
51
|
+
|
|
52
|
+
appendOtpPlugin(context) // plugin under 'email-otp'
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Configure
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { OTP_EMAIL_THROTTLE_RULES, OTP_THROTTLE_SERVICE } from '@owlmeans/server-auth-otp'
|
|
59
|
+
import type { OtpConfig } from '@owlmeans/server-auth-otp'
|
|
60
|
+
|
|
61
|
+
const otp: OtpConfig['otp'] = {
|
|
62
|
+
throttleAlias: OTP_THROTTLE_SERVICE,
|
|
63
|
+
throttleRules: OTP_EMAIL_THROTTLE_RULES, // 1 per minute, 10 per hour
|
|
64
|
+
// mailerAlias, resourceAlias, challengeStoreAlias, identityAlias override the defaults
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Test with in-memory stores and the console mailer
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import type { OtpService } from '@owlmeans/auth-otp'
|
|
72
|
+
import { makeDefaultConsoleMailerService } from '@owlmeans/mailer'
|
|
73
|
+
import {
|
|
74
|
+
OTP_SERVICE, makeMemoryOtpChallengeStore, makeMemoryThrottleService, makeOtpService,
|
|
75
|
+
} from '@owlmeans/server-auth-otp'
|
|
26
76
|
|
|
27
|
-
|
|
28
|
-
context.registerService(
|
|
29
|
-
context.registerService(
|
|
77
|
+
const mailer = makeDefaultConsoleMailerService()
|
|
78
|
+
context.registerService(mailer)
|
|
79
|
+
context.registerService(makeMemoryOtpChallengeStore())
|
|
80
|
+
context.registerService(makeMemoryThrottleService())
|
|
81
|
+
context.registerService(makeOtpService())
|
|
82
|
+
await context.configure().init()
|
|
30
83
|
|
|
31
|
-
|
|
84
|
+
const otp = context.service<OtpService>(OTP_SERVICE)
|
|
85
|
+
const issuanceId = await otp.issueChallenge('user@example.com')
|
|
86
|
+
const code = mailer.captured[0].text?.match(/\d{6}/)?.[0] ?? ''
|
|
87
|
+
await otp.verifyChallenge('user@example.com', issuanceId, code) // throws AuthenFailed('otp:code') on a mismatch
|
|
32
88
|
```
|
|
33
89
|
|
|
34
|
-
`
|
|
35
|
-
|
|
36
|
-
|
|
90
|
+
`OtpService` and `OTP_SERVICE` come from `@owlmeans/auth-otp` (the constant is also re-exported
|
|
91
|
+
here). The memory factories accept a clock as their second argument for deterministic tests.
|
|
92
|
+
|
|
93
|
+
## API
|
|
94
|
+
|
|
95
|
+
### Services and plugin
|
|
96
|
+
|
|
97
|
+
| Symbol | Purpose |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `makeOtpService(alias = OTP_SERVICE)` | `OtpService`: `issueChallenge(email) → issuanceId`, `verifyChallenge(email, issuanceId, code)` |
|
|
100
|
+
| `appendOtpPlugin(context)` | registers the `'email-otp'` plugin factory; returns the context |
|
|
101
|
+
| `makeRedisOtpChallengeStore(alias?, resourceAlias?)` | atomic Redis store (`SET NX EX` + one Lua verify) |
|
|
102
|
+
| `makeMemoryOtpChallengeStore(alias?, now?)` | in-memory store for a single process or tests |
|
|
103
|
+
| `makeRedisThrottleService(alias?, resourceAlias?)` | every window checked and consumed in one Lua call |
|
|
104
|
+
| `makeMemoryThrottleService(alias?, now?)` | in-memory sliding-window throttle |
|
|
105
|
+
| `otpKeyHelper` (`createOtpKeyHelper()`) | `otpChallengeKey`, `otpEmailKey`, `otpCodeHash` — SHA-256 digests |
|
|
106
|
+
| `throttleKeyHelper` (`createThrottleKeyHelper()`) | `emailThrottleKey`, `ipThrottleKey` — digests, never raw values |
|
|
107
|
+
| `emailThrottleKey`, `ipThrottleKey` | deprecated standalone forms of the helper members |
|
|
108
|
+
|
|
109
|
+
### Constants
|
|
110
|
+
|
|
111
|
+
| Symbol | Value |
|
|
112
|
+
|---|---|
|
|
113
|
+
| `SERVER_AUTH_OTP` | `'server-auth-otp'` |
|
|
114
|
+
| `OTP_CHALLENGE_STORE` | `'auth-otp-challenge-store'` |
|
|
115
|
+
| `OTP_THROTTLE_SERVICE` | `'auth-otp-throttle'` |
|
|
116
|
+
| `OTP_EMAIL_THROTTLE_RULES` | `[{ limit: 1, windowSeconds: 60 }, { limit: 10, windowSeconds: 3600 }]` |
|
|
117
|
+
| `OtpChallengeOutcome` | `Verified`, `Invalid`, `Exhausted`, `Missing` |
|
|
118
|
+
| re-exported from `@owlmeans/auth-otp` | `OTP_AUTH_TYPE` (`'email-otp'`), `OTP_SERVICE`, `OTP_RESOURCE` (`'auth-otp-cache'`), `OTP_TTL_SECONDS`, `OTP_CODE_LENGTH`, `OTP_MAX_FAILED_ATTEMPTS` |
|
|
119
|
+
|
|
120
|
+
### Types
|
|
121
|
+
|
|
122
|
+
`OtpConfig` (`cfg.otp`: `mailerAlias?`, `resourceAlias?`, `challengeStoreAlias?`, `throttleAlias?`,
|
|
123
|
+
`throttleRules?`, `identityAlias?`), `OtpContext`, `OtpChallenge`, `OtpChallengeStore`,
|
|
124
|
+
`AuthThrottleService`, `ThrottleRule`, `ThrottleDecision`, `OtpKeyHelper`, `ThrottleKeyHelper`.
|
|
125
|
+
|
|
126
|
+
### Errors
|
|
127
|
+
|
|
128
|
+
| Class | Status | Meaning |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| `OtpError` | — | base, message `otp:<reason>` |
|
|
131
|
+
| `OtpThrottled` | 429 | issuance refused by the throttle; `retryAfter` in seconds |
|
|
132
|
+
| `OtpUnavailable` | 503 | the store or throttle backend failed, or an issuance id collided |
|
|
133
|
+
|
|
134
|
+
A wrong, expired or exhausted code is `AuthenFailed('otp:code')` from `@owlmeans/auth`; a missing or
|
|
135
|
+
malformed address is `AuthenFailed('otp:email')`.
|
|
136
|
+
|
|
137
|
+
## Auth flow
|
|
138
|
+
|
|
139
|
+
1. **Init** — the client sends `{ type: 'email-otp', userId: <email> }`; the plugin lowercases the
|
|
140
|
+
address, issues a code and returns the challenge in the signed envelope.
|
|
141
|
+
2. **Authenticate** — the client sends the envelope as `challenge`, the code as `credential`, plus
|
|
142
|
+
`type`, `role` and `scopes` (required by `AuthCredentialsSchema`). The plugin verifies the
|
|
143
|
+
issuance, resolves the profile through the `IdentityLinkingService` under
|
|
144
|
+
`cfg.otp.identityAlias ?? AUTH_IDENTITY_LINKING` (linking a new one when absent) and copies
|
|
145
|
+
`userId`, `profileId`, `entitySlug`, `role` and `scopes` from it onto the credential.
|
|
146
|
+
|
|
147
|
+
## Common pitfalls
|
|
148
|
+
|
|
149
|
+
- `makeConsoleMailerService()` registers under `'console-mailer'`; the OTP service resolves
|
|
150
|
+
`cfg.otp.mailerAlias ?? MAILER_SERVICE`. Use `makeDefaultConsoleMailerService()` in tests.
|
|
151
|
+
- Without a registered challenge store the service falls back to a Redis store over `OTP_RESOURCE`;
|
|
152
|
+
register that resource or an explicit store.
|
|
153
|
+
- Throttling is off unless both `throttleAlias` and `throttleRules` are configured.
|
|
154
|
+
- `appendOtpPlugin` writes into a module-level registry — call it once; the context argument is only
|
|
155
|
+
returned for chaining.
|
|
156
|
+
- `credential.entitySlug` on the authenticate request selects nothing: the linked payload's own slug
|
|
157
|
+
overwrites it, so the address alone decides the account and organization entity.
|
|
158
|
+
- The resulting auth token can exceed 1024 characters; a route accepting it must size its schema
|
|
159
|
+
beyond the generic `AuthTokenSchema`.
|
|
160
|
+
- Never encode the address or code into the issuance id, and never replace the attempt counter with
|
|
161
|
+
a bare replay key.
|
|
162
|
+
|
|
163
|
+
## Related packages
|
|
164
|
+
|
|
165
|
+
- [`@owlmeans/auth-otp`](../auth-otp) — `OtpService` type and the shared OTP constants
|
|
166
|
+
- [`@owlmeans/server-auth`](../server-auth) — the auth-manager plugin registry this plugin joins
|
|
167
|
+
- [`@owlmeans/server-auth-identity`](../server-auth-identity) — `IdentityLinkingService`, which maps the address to an account
|
|
168
|
+
- [`@owlmeans/mailer`](../mailer) — `MailerService` and the console transport for tests
|
|
169
|
+
- [`@owlmeans/server-mailer-mailgun`](../server-mailer-mailgun) / [`@owlmeans/mailer-smtp`](../mailer-smtp) — production transports
|
|
170
|
+
- [`@owlmeans/redis-resource`](../redis-resource) — the Redis resource behind the stores and throttle
|
|
37
171
|
|
|
38
|
-
The
|
|
39
|
-
`@owlmeans/mailer-smtp` (SMTP) or `@owlmeans/server-mailer-mailgun` (Mailgun HTTP API),
|
|
40
|
-
registered under `MAILER_SERVICE`. Override the alias with `cfg.otp.mailerAlias`.
|
|
172
|
+
The `server-auth-otp` skill covers the flow and its schema gotchas for agents.
|
|
41
173
|
|
|
42
174
|
<!-- owlmeans:agent-guidance:start -->
|
|
43
175
|
## Agent guidance
|
|
@@ -47,7 +179,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
47
179
|
your project's skill store (`.agents/skills/`):
|
|
48
180
|
|
|
49
181
|
```sh
|
|
50
|
-
npx @owlmeans/agent-skills@^0.1.18-rc.
|
|
182
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.52
|
|
51
183
|
```
|
|
52
184
|
|
|
53
185
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/server-auth-otp",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-10-
|
|
4
|
+
"version": "0.1.18-rc.55",
|
|
5
|
+
"generatedAt": "2026-10-08T21:27:54.299Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -8,7 +8,7 @@ metadata:
|
|
|
8
8
|
|
|
9
9
|
# Using `@owlmeans/server-auth-otp`
|
|
10
10
|
|
|
11
|
-
**Install:** `"@owlmeans/server-auth-otp": "^0.1.18-rc.
|
|
11
|
+
**Install:** `"@owlmeans/server-auth-otp": "^0.1.18-rc.55"` 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
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/server-auth-otp",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.55",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -22,18 +22,18 @@
|
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@owlmeans/auth": "^0.1.18-rc.
|
|
26
|
-
"@owlmeans/auth-otp": "^0.1.18-rc.
|
|
25
|
+
"@owlmeans/auth": "^0.1.18-rc.41",
|
|
26
|
+
"@owlmeans/auth-otp": "^0.1.18-rc.35",
|
|
27
27
|
"@owlmeans/context": "^0.1.18-rc.34",
|
|
28
28
|
"@owlmeans/error": "^0.1.18-rc.38",
|
|
29
|
-
"@owlmeans/log": "^0.1.18-rc.
|
|
30
|
-
"@owlmeans/mailer": "^0.1.18-rc.
|
|
31
|
-
"@owlmeans/oidc": "^0.1.18-rc.
|
|
32
|
-
"@owlmeans/redis-resource": "^0.1.18-rc.
|
|
29
|
+
"@owlmeans/log": "^0.1.18-rc.3",
|
|
30
|
+
"@owlmeans/mailer": "^0.1.18-rc.37",
|
|
31
|
+
"@owlmeans/oidc": "^0.1.18-rc.57",
|
|
32
|
+
"@owlmeans/redis-resource": "^0.1.18-rc.46",
|
|
33
33
|
"@owlmeans/resource": "^0.1.18-rc.40",
|
|
34
|
-
"@owlmeans/server-auth": "^0.1.18-rc.
|
|
35
|
-
"@owlmeans/server-auth-identity": "^0.1.18-rc.
|
|
36
|
-
"@owlmeans/server-context": "^0.1.18-rc.
|
|
34
|
+
"@owlmeans/server-auth": "^0.1.18-rc.56",
|
|
35
|
+
"@owlmeans/server-auth-identity": "^0.1.18-rc.49",
|
|
36
|
+
"@owlmeans/server-context": "^0.1.18-rc.44"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@owlmeans/dep-config": "workspace:*",
|