better-auth-evp 0.1.0
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/.github/workflows/ci.yml +60 -0
- package/.github/workflows/publish.yml +46 -0
- package/.prettierignore +6 -0
- package/.prettierrc +10 -0
- package/.releaserc.json +27 -0
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/better-auth-evp-0.1.0.tgz +0 -0
- package/dist/client.d.ts +59 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +41 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +156 -0
- package/dist/types.d.ts +90 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/eslint.config.mjs +42 -0
- package/package.json +67 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, master]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main, master]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint-and-build:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
node-version: [20.x, 22.x, 24.x]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- name: Checkout code
|
|
18
|
+
uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Install pnpm
|
|
21
|
+
uses: pnpm/action-setup@v3
|
|
22
|
+
with:
|
|
23
|
+
version: 9
|
|
24
|
+
|
|
25
|
+
- name: Setup Node.js ${{ matrix.node-version }}
|
|
26
|
+
uses: actions/setup-node@v4
|
|
27
|
+
with:
|
|
28
|
+
node-version: ${{ matrix.node-version }}
|
|
29
|
+
cache: "pnpm"
|
|
30
|
+
|
|
31
|
+
- name: Install dependencies
|
|
32
|
+
run: pnpm install --frozen-lockfile
|
|
33
|
+
|
|
34
|
+
- name: Check code formatting
|
|
35
|
+
run: pnpm format:check
|
|
36
|
+
|
|
37
|
+
- name: Run ESLint
|
|
38
|
+
run: pnpm lint:style
|
|
39
|
+
|
|
40
|
+
- name: Type check
|
|
41
|
+
run: pnpm lint:typecheck
|
|
42
|
+
|
|
43
|
+
- name: Build
|
|
44
|
+
run: pnpm build
|
|
45
|
+
|
|
46
|
+
- name: Check build output
|
|
47
|
+
run: |
|
|
48
|
+
if [ ! -d "dist" ]; then
|
|
49
|
+
echo "Build output directory not found"
|
|
50
|
+
exit 1
|
|
51
|
+
fi
|
|
52
|
+
if [ ! -f "dist/index.js" ]; then
|
|
53
|
+
echo "index.js not found in build output"
|
|
54
|
+
exit 1
|
|
55
|
+
fi
|
|
56
|
+
if [ ! -f "dist/index.d.ts" ]; then
|
|
57
|
+
echo "index.d.ts not found in build output"
|
|
58
|
+
exit 1
|
|
59
|
+
fi
|
|
60
|
+
echo "Build output verified successfully"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
name: Semantic Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
- master
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: write
|
|
11
|
+
issues: write
|
|
12
|
+
pull-requests: write
|
|
13
|
+
id-token: write
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
release:
|
|
17
|
+
name: Release
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
steps:
|
|
20
|
+
- name: Checkout
|
|
21
|
+
uses: actions/checkout@v4
|
|
22
|
+
with:
|
|
23
|
+
fetch-depth: 0
|
|
24
|
+
persist-credentials: false
|
|
25
|
+
|
|
26
|
+
- name: Setup Node.js
|
|
27
|
+
uses: actions/setup-node@v4
|
|
28
|
+
with:
|
|
29
|
+
node-version: "lts/*"
|
|
30
|
+
|
|
31
|
+
- name: Setup pnpm
|
|
32
|
+
uses: pnpm/action-setup@v4
|
|
33
|
+
with:
|
|
34
|
+
version: latest
|
|
35
|
+
|
|
36
|
+
- name: Install dependencies
|
|
37
|
+
run: pnpm install --frozen-lockfile
|
|
38
|
+
|
|
39
|
+
- name: Build
|
|
40
|
+
run: pnpm build
|
|
41
|
+
|
|
42
|
+
- name: Release
|
|
43
|
+
env:
|
|
44
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
45
|
+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
46
|
+
run: npx semantic-release
|
package/.prettierignore
ADDED
package/.prettierrc
ADDED
package/.releaserc.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"branches": ["main", "master"],
|
|
3
|
+
"plugins": [
|
|
4
|
+
"@semantic-release/commit-analyzer",
|
|
5
|
+
"@semantic-release/release-notes-generator",
|
|
6
|
+
"@semantic-release/changelog",
|
|
7
|
+
"@semantic-release/npm",
|
|
8
|
+
[
|
|
9
|
+
"@semantic-release/github",
|
|
10
|
+
{
|
|
11
|
+
"assets": [
|
|
12
|
+
{
|
|
13
|
+
"path": "dist/**/*",
|
|
14
|
+
"label": "Distribution files"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
[
|
|
20
|
+
"@semantic-release/git",
|
|
21
|
+
{
|
|
22
|
+
"assets": ["package.json", "CHANGELOG.md"],
|
|
23
|
+
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
]
|
|
27
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kamil Marczak
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# better-auth-evp
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/better-auth-evp)
|
|
4
|
+
[](https://www.better-auth.com)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
|
|
8
|
+
[Email Verification Protocol (EVP)](https://developer.chrome.com/blog/email-verification-protocol-origin-trial) plugin for [Better Auth](https://www.better-auth.com), with automatic fallback to whatever other sign-in method you already have.
|
|
9
|
+
|
|
10
|
+
## What is EVP?
|
|
11
|
+
|
|
12
|
+
EVP is an experimental, **Chrome-only** browser capability (currently gated behind an [origin trial](https://developer.chrome.com/origintrials)) that lets a user prove they own an email address without typing an OTP or clicking a magic link. The browser talks to the user's mailbox provider directly and, if the user is signed in there, hands your form a signed token proving ownership - all triggered by the normal act of filling in and submitting an email field.
|
|
13
|
+
|
|
14
|
+
Because this depends on: the user running an origin-trial build of Chrome, their mailbox provider having implemented the issuer side of the protocol, and the user being signed into that mailbox in the browser - **it will not work for most users today**. This plugin is pure progressive enhancement: wire it up, try it first, and fall back to your existing sign-in method (OTP, magic link, password, ...) whenever it doesn't pan out.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install better-auth-evp email-verification-api
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Peer Dependencies
|
|
23
|
+
|
|
24
|
+
- `better-auth` ^1.5.0
|
|
25
|
+
- `email-verification-api` ^0.1.0 (does the actual SD-JWT/DNS/issuer verification)
|
|
26
|
+
- `zod` ^3.0.0 || ^4.0.0
|
|
27
|
+
|
|
28
|
+
## Server Setup
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { betterAuth } from "better-auth";
|
|
32
|
+
import { emailVerificationProtocol } from "better-auth-evp";
|
|
33
|
+
|
|
34
|
+
export const auth = betterAuth({
|
|
35
|
+
// ...
|
|
36
|
+
plugins: [
|
|
37
|
+
emailVerificationProtocol({
|
|
38
|
+
// Must match the origin your Chrome origin-trial token, DNS
|
|
39
|
+
// `_email-verification` record, etc. were issued for.
|
|
40
|
+
origin: "https://example.com",
|
|
41
|
+
disableSignUp: false,
|
|
42
|
+
userFields: (verified) => ({
|
|
43
|
+
// any additional fields for a newly created user
|
|
44
|
+
}),
|
|
45
|
+
}),
|
|
46
|
+
// Keep your existing sign-in plugin(s) around for the fallback path,
|
|
47
|
+
// e.g. emailOTP(), magicLink(), ...
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Client Setup
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { createAuthClient } from "better-auth/react";
|
|
56
|
+
import { emailVerificationProtocolClient } from "better-auth-evp/client";
|
|
57
|
+
|
|
58
|
+
export const authClient = createAuthClient({
|
|
59
|
+
plugins: [emailVerificationProtocolClient()],
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Origin Trial Token
|
|
64
|
+
|
|
65
|
+
As a participating site you must register for the origin trial and serve the token on any page that renders the email form, either as a meta tag:
|
|
66
|
+
|
|
67
|
+
```html
|
|
68
|
+
<meta http-equiv="origin-trial" content="YOUR_TOKEN" />
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
or as an HTTP response header:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
Origin-Trial: YOUR_TOKEN
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
This plugin does not manage that token for you - it's static per-origin configuration, not something to fetch from an API.
|
|
78
|
+
|
|
79
|
+
## Form Markup
|
|
80
|
+
|
|
81
|
+
```html
|
|
82
|
+
<input name="email" type="email" autocomplete="email" />
|
|
83
|
+
<input type="hidden" name="token" nonce="{nonce}" autocomplete="email-verification-token" />
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`{nonce}` comes from `authClient.evp.getNonce()` and must be re-fetched for every attempt.
|
|
87
|
+
|
|
88
|
+
## Usage Example (progressive enhancement)
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { emailVerificationProtocolClient } from "better-auth-evp/client";
|
|
92
|
+
|
|
93
|
+
async function handleEmailSubmit(email: string, form: HTMLFormElement) {
|
|
94
|
+
const tokenInput = form.elements.namedItem("token") as HTMLInputElement;
|
|
95
|
+
|
|
96
|
+
if (tokenInput.value) {
|
|
97
|
+
const { nonce } = await authClient.evp.getNonce();
|
|
98
|
+
const result = await authClient.evp.verify({
|
|
99
|
+
email,
|
|
100
|
+
token: tokenInput.value,
|
|
101
|
+
nonce,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (result.data?.verified) {
|
|
105
|
+
// User is signed in already - redirect and stop here.
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// EVP unsupported/unavailable/failed - fall back to your normal flow.
|
|
111
|
+
await authClient.emailOtp.sendVerificationOtp({ email, type: "sign-in" });
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## API
|
|
116
|
+
|
|
117
|
+
### Server (`auth.api`)
|
|
118
|
+
|
|
119
|
+
- `evpNonce()` - `GET /evp/nonce` - issues a single-use nonce, valid for `nonceExpiresIn` seconds (default 120).
|
|
120
|
+
- `evpVerify({ email, token, nonce })` - `POST /evp/verify` - verifies the token and, on success, creates a session (and a user, unless `disableSignUp` is set). Returns `{ verified: false, reason }` instead of throwing on any expected failure (invalid/expired nonce, verification failure, email mismatch, sign-up disabled).
|
|
121
|
+
|
|
122
|
+
### Client (`authClient.evp`)
|
|
123
|
+
|
|
124
|
+
- `getNonce()`
|
|
125
|
+
- `verify({ email, token, nonce })`
|
|
126
|
+
|
|
127
|
+
### Options
|
|
128
|
+
|
|
129
|
+
| Option | Type | Default | Description |
|
|
130
|
+
| ---------------- | ------------------------------------------------ | ---------- | --------------------------------------------------------- |
|
|
131
|
+
| `origin` | `string` | (required) | This relying party's absolute origin, used as `audience`. |
|
|
132
|
+
| `nonceExpiresIn` | `number` | `120` | Seconds a nonce stays valid. |
|
|
133
|
+
| `disableSignUp` | `boolean` | `false` | Reject verified emails with no existing account. |
|
|
134
|
+
| `userFields` | `(verified) => T` | - | Extra fields for a newly created user. |
|
|
135
|
+
| `onVerified` | `(verified & { userId }) => void \| Promise<void>` | - | Side-effect hook after a session is created. |
|
|
136
|
+
| `verify` | custom verification function | `verifyEmailToken` from `email-verification-api` | Override for testing. |
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
MIT
|
|
Binary file
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { emailVerificationProtocol } from "./index";
|
|
2
|
+
import type { EvpNonceResponse, EvpVerifyResponse } from "./types";
|
|
3
|
+
/**
|
|
4
|
+
* `autocomplete` value for the visible email `<input>`. Required by the
|
|
5
|
+
* protocol so the browser can offer verified addresses.
|
|
6
|
+
*/
|
|
7
|
+
export declare const EVP_EMAIL_AUTOCOMPLETE = "email";
|
|
8
|
+
/**
|
|
9
|
+
* `autocomplete` value for the hidden token `<input>` the browser fills in
|
|
10
|
+
* with the signed Email Verification Token.
|
|
11
|
+
*/
|
|
12
|
+
export declare const EVP_TOKEN_AUTOCOMPLETE = "email-verification-token";
|
|
13
|
+
export declare function emailVerificationProtocolClient(): {
|
|
14
|
+
id: "email-verification-protocol";
|
|
15
|
+
$InferServerPlugin: ReturnType<typeof emailVerificationProtocol>;
|
|
16
|
+
getActions: ($fetch: import("better-auth/client").BetterFetch) => {
|
|
17
|
+
evp: {
|
|
18
|
+
/**
|
|
19
|
+
* Fetches a fresh, single-use nonce. Call this once per sign-in
|
|
20
|
+
* attempt and bind the result to the hidden token input's `nonce`
|
|
21
|
+
* attribute before the user submits the form.
|
|
22
|
+
*/
|
|
23
|
+
getNonce: () => Promise<{
|
|
24
|
+
data: EvpNonceResponse;
|
|
25
|
+
error: null;
|
|
26
|
+
} | {
|
|
27
|
+
data: null;
|
|
28
|
+
error: {
|
|
29
|
+
message?: string | undefined;
|
|
30
|
+
status: number;
|
|
31
|
+
statusText: string;
|
|
32
|
+
};
|
|
33
|
+
}>;
|
|
34
|
+
/**
|
|
35
|
+
* Verifies the token the browser filled into the hidden input and,
|
|
36
|
+
* on success, signs the user in. Always check `data.verified`
|
|
37
|
+
* (or the presence of `error`) and fall back to a normal sign-in
|
|
38
|
+
* method when it's falsy - this is expected for the vast majority
|
|
39
|
+
* of browsers/users today.
|
|
40
|
+
*/
|
|
41
|
+
verify: (data: {
|
|
42
|
+
email: string;
|
|
43
|
+
token: string;
|
|
44
|
+
nonce: string;
|
|
45
|
+
}) => Promise<{
|
|
46
|
+
data: null;
|
|
47
|
+
error: {
|
|
48
|
+
message?: string | undefined;
|
|
49
|
+
status: number;
|
|
50
|
+
statusText: string;
|
|
51
|
+
};
|
|
52
|
+
} | {
|
|
53
|
+
data: EvpVerifyResponse;
|
|
54
|
+
error: null;
|
|
55
|
+
}>;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,SAAS,CAAC;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEnE;;;GAGG;AACH,eAAO,MAAM,sBAAsB,UAAU,CAAC;AAE9C;;;GAGG;AACH,eAAO,MAAM,sBAAsB,6BAA6B,CAAC;AAEjE,wBAAgB,+BAA+B;;wBAGjB,UAAU,CAAC,OAAO,yBAAyB,CAAC;;;YAGlE;;;;eAIG;;;;;;;;;;;;YAIH;;;;;;eAMG;2BACkB;gBACnB,KAAK,EAAE,MAAM,CAAC;gBACd,KAAK,EAAE,MAAM,CAAC;gBACd,KAAK,EAAE,MAAM,CAAC;aACf;;;;;;;;;;;;;EASR"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `autocomplete` value for the visible email `<input>`. Required by the
|
|
3
|
+
* protocol so the browser can offer verified addresses.
|
|
4
|
+
*/
|
|
5
|
+
export const EVP_EMAIL_AUTOCOMPLETE = "email";
|
|
6
|
+
/**
|
|
7
|
+
* `autocomplete` value for the hidden token `<input>` the browser fills in
|
|
8
|
+
* with the signed Email Verification Token.
|
|
9
|
+
*/
|
|
10
|
+
export const EVP_TOKEN_AUTOCOMPLETE = "email-verification-token";
|
|
11
|
+
export function emailVerificationProtocolClient() {
|
|
12
|
+
return {
|
|
13
|
+
id: "email-verification-protocol",
|
|
14
|
+
$InferServerPlugin: {},
|
|
15
|
+
getActions: ($fetch) => ({
|
|
16
|
+
evp: {
|
|
17
|
+
/**
|
|
18
|
+
* Fetches a fresh, single-use nonce. Call this once per sign-in
|
|
19
|
+
* attempt and bind the result to the hidden token input's `nonce`
|
|
20
|
+
* attribute before the user submits the form.
|
|
21
|
+
*/
|
|
22
|
+
getNonce: async () => {
|
|
23
|
+
return $fetch("/evp/nonce", { method: "GET" });
|
|
24
|
+
},
|
|
25
|
+
/**
|
|
26
|
+
* Verifies the token the browser filled into the hidden input and,
|
|
27
|
+
* on success, signs the user in. Always check `data.verified`
|
|
28
|
+
* (or the presence of `error`) and fall back to a normal sign-in
|
|
29
|
+
* method when it's falsy - this is expected for the vast majority
|
|
30
|
+
* of browsers/users today.
|
|
31
|
+
*/
|
|
32
|
+
verify: async (data) => {
|
|
33
|
+
return $fetch("/evp/verify", {
|
|
34
|
+
method: "POST",
|
|
35
|
+
body: data,
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
41
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import type { EvpPluginOptions } from "./types";
|
|
3
|
+
export * from "./types";
|
|
4
|
+
/**
|
|
5
|
+
* [Email Verification Protocol](https://developer.chrome.com/blog/email-verification-protocol-origin-trial)
|
|
6
|
+
* plugin for Better Auth.
|
|
7
|
+
*
|
|
8
|
+
* EVP lets a browser that supports the (Chrome-only, origin-trial-gated)
|
|
9
|
+
* protocol prove a user owns the email address they typed into a form,
|
|
10
|
+
* without sending an OTP or magic link. It is entirely progressive
|
|
11
|
+
* enhancement: unsupported browsers, mailbox providers that haven't
|
|
12
|
+
* implemented the issuer side, or a user simply not signed into their
|
|
13
|
+
* mailbox all result in an empty/unverifiable token. Callers MUST treat
|
|
14
|
+
* `verified: false` (or the nonce/verify endpoints being unreachable) as a
|
|
15
|
+
* normal, expected outcome and fall back to whatever sign-in method the
|
|
16
|
+
* app already uses (email OTP, magic link, password, ...) - this plugin
|
|
17
|
+
* does not implement a fallback itself, it only adds the EVP fast path.
|
|
18
|
+
*
|
|
19
|
+
* @see https://github.com/philnash/email-verification-api
|
|
20
|
+
*/
|
|
21
|
+
export declare function emailVerificationProtocol<T extends Record<string, any> = {}>(options: EvpPluginOptions<T>): {
|
|
22
|
+
id: "email-verification-protocol";
|
|
23
|
+
endpoints: {
|
|
24
|
+
/**
|
|
25
|
+
* ### Endpoint
|
|
26
|
+
*
|
|
27
|
+
* GET `/evp/nonce`
|
|
28
|
+
*
|
|
29
|
+
* ### API Methods
|
|
30
|
+
*
|
|
31
|
+
* **server:** `auth.api.evpNonce`
|
|
32
|
+
*
|
|
33
|
+
* **client:** `authClient.evp.getNonce`
|
|
34
|
+
*
|
|
35
|
+
* Issues a single-use nonce to bind into the hidden
|
|
36
|
+
* `email-verification-token` input's `nonce` attribute. Call this
|
|
37
|
+
* once per sign-in attempt, right before rendering the form.
|
|
38
|
+
*/
|
|
39
|
+
evpNonce: import("better-auth").StrictEndpoint<"/evp/nonce", {
|
|
40
|
+
method: "GET";
|
|
41
|
+
}, {
|
|
42
|
+
nonce: string;
|
|
43
|
+
expiresIn: number;
|
|
44
|
+
}>;
|
|
45
|
+
/**
|
|
46
|
+
* ### Endpoint
|
|
47
|
+
*
|
|
48
|
+
* POST `/evp/verify`
|
|
49
|
+
*
|
|
50
|
+
* ### API Methods
|
|
51
|
+
*
|
|
52
|
+
* **server:** `auth.api.evpVerify`
|
|
53
|
+
*
|
|
54
|
+
* **client:** `authClient.evp.verify`
|
|
55
|
+
*
|
|
56
|
+
* Verifies the browser-issued Email Verification Token. On success,
|
|
57
|
+
* signs the user in (creating an account first if none exists and
|
|
58
|
+
* sign-up isn't disabled) exactly like any other passwordless method
|
|
59
|
+
* and returns `{ verified: true }`. On any failure it returns
|
|
60
|
+
* `{ verified: false, reason }` instead of throwing, since a failure
|
|
61
|
+
* here is an expected, common outcome that the caller should recover
|
|
62
|
+
* from by falling back to a different sign-in method.
|
|
63
|
+
*/
|
|
64
|
+
evpVerify: import("better-auth").StrictEndpoint<"/evp/verify", {
|
|
65
|
+
method: "POST";
|
|
66
|
+
body: z.ZodObject<{
|
|
67
|
+
email: z.ZodString;
|
|
68
|
+
token: z.ZodString;
|
|
69
|
+
nonce: z.ZodString;
|
|
70
|
+
}, z.core.$strip>;
|
|
71
|
+
}, {
|
|
72
|
+
verified: boolean;
|
|
73
|
+
reason: string;
|
|
74
|
+
} | {
|
|
75
|
+
verified: boolean;
|
|
76
|
+
token: string;
|
|
77
|
+
user: {
|
|
78
|
+
id: string;
|
|
79
|
+
createdAt: Date;
|
|
80
|
+
updatedAt: Date;
|
|
81
|
+
email: string;
|
|
82
|
+
emailVerified: boolean;
|
|
83
|
+
name: string;
|
|
84
|
+
image?: string | null | undefined;
|
|
85
|
+
};
|
|
86
|
+
}>;
|
|
87
|
+
};
|
|
88
|
+
rateLimit: {
|
|
89
|
+
pathMatcher(path: string): boolean;
|
|
90
|
+
window: number;
|
|
91
|
+
max: number;
|
|
92
|
+
}[];
|
|
93
|
+
};
|
|
94
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AAEzB,OAAO,KAAK,EACV,gBAAgB,EAGjB,MAAM,SAAS,CAAC;AAEjB,cAAc,SAAS,CAAC;AAUxB;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,yBAAyB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,EAAE,EAC1E,OAAO,EAAE,gBAAgB,CAAC,CAAC,CAAC;;;QAcxB;;;;;;;;;;;;;;WAcG;;;;;;;QAcH;;;;;;;;;;;;;;;;;;WAkBG;;;;;;;;;;;;;;;;;;;;;;;;;;0BA2FiB,MAAM;;;;EAQ/B"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { APIError, createAuthEndpoint } from "better-auth/api";
|
|
2
|
+
import { setSessionCookie } from "better-auth/cookies";
|
|
3
|
+
import { generateRandomString } from "better-auth/crypto";
|
|
4
|
+
import { verifyEmailToken } from "email-verification-api";
|
|
5
|
+
import * as z from "zod";
|
|
6
|
+
export * from "./types";
|
|
7
|
+
const NONCE_IDENTIFIER_PREFIX = "evp-nonce:";
|
|
8
|
+
const evpVerifyBodySchema = z.object({
|
|
9
|
+
email: z.string(),
|
|
10
|
+
token: z.string(),
|
|
11
|
+
nonce: z.string(),
|
|
12
|
+
});
|
|
13
|
+
/**
|
|
14
|
+
* [Email Verification Protocol](https://developer.chrome.com/blog/email-verification-protocol-origin-trial)
|
|
15
|
+
* plugin for Better Auth.
|
|
16
|
+
*
|
|
17
|
+
* EVP lets a browser that supports the (Chrome-only, origin-trial-gated)
|
|
18
|
+
* protocol prove a user owns the email address they typed into a form,
|
|
19
|
+
* without sending an OTP or magic link. It is entirely progressive
|
|
20
|
+
* enhancement: unsupported browsers, mailbox providers that haven't
|
|
21
|
+
* implemented the issuer side, or a user simply not signed into their
|
|
22
|
+
* mailbox all result in an empty/unverifiable token. Callers MUST treat
|
|
23
|
+
* `verified: false` (or the nonce/verify endpoints being unreachable) as a
|
|
24
|
+
* normal, expected outcome and fall back to whatever sign-in method the
|
|
25
|
+
* app already uses (email OTP, magic link, password, ...) - this plugin
|
|
26
|
+
* does not implement a fallback itself, it only adds the EVP fast path.
|
|
27
|
+
*
|
|
28
|
+
* @see https://github.com/philnash/email-verification-api
|
|
29
|
+
*/
|
|
30
|
+
export function emailVerificationProtocol(options) {
|
|
31
|
+
if (!options.origin) {
|
|
32
|
+
throw new Error("emailVerificationProtocol plugin requires an `origin` option (this relying party's absolute origin)");
|
|
33
|
+
}
|
|
34
|
+
const nonceExpiresIn = options.nonceExpiresIn ?? 120;
|
|
35
|
+
const verify = options.verify ?? verifyEmailToken;
|
|
36
|
+
return {
|
|
37
|
+
id: "email-verification-protocol",
|
|
38
|
+
endpoints: {
|
|
39
|
+
/**
|
|
40
|
+
* ### Endpoint
|
|
41
|
+
*
|
|
42
|
+
* GET `/evp/nonce`
|
|
43
|
+
*
|
|
44
|
+
* ### API Methods
|
|
45
|
+
*
|
|
46
|
+
* **server:** `auth.api.evpNonce`
|
|
47
|
+
*
|
|
48
|
+
* **client:** `authClient.evp.getNonce`
|
|
49
|
+
*
|
|
50
|
+
* Issues a single-use nonce to bind into the hidden
|
|
51
|
+
* `email-verification-token` input's `nonce` attribute. Call this
|
|
52
|
+
* once per sign-in attempt, right before rendering the form.
|
|
53
|
+
*/
|
|
54
|
+
evpNonce: createAuthEndpoint("/evp/nonce", { method: "GET" }, async (ctx) => {
|
|
55
|
+
const nonce = generateRandomString(24, "a-z", "A-Z", "0-9");
|
|
56
|
+
await ctx.context.internalAdapter.createVerificationValue({
|
|
57
|
+
identifier: `${NONCE_IDENTIFIER_PREFIX}${nonce}`,
|
|
58
|
+
value: "pending",
|
|
59
|
+
expiresAt: new Date(Date.now() + nonceExpiresIn * 1000),
|
|
60
|
+
});
|
|
61
|
+
return ctx.json({ nonce, expiresIn: nonceExpiresIn });
|
|
62
|
+
}),
|
|
63
|
+
/**
|
|
64
|
+
* ### Endpoint
|
|
65
|
+
*
|
|
66
|
+
* POST `/evp/verify`
|
|
67
|
+
*
|
|
68
|
+
* ### API Methods
|
|
69
|
+
*
|
|
70
|
+
* **server:** `auth.api.evpVerify`
|
|
71
|
+
*
|
|
72
|
+
* **client:** `authClient.evp.verify`
|
|
73
|
+
*
|
|
74
|
+
* Verifies the browser-issued Email Verification Token. On success,
|
|
75
|
+
* signs the user in (creating an account first if none exists and
|
|
76
|
+
* sign-up isn't disabled) exactly like any other passwordless method
|
|
77
|
+
* and returns `{ verified: true }`. On any failure it returns
|
|
78
|
+
* `{ verified: false, reason }` instead of throwing, since a failure
|
|
79
|
+
* here is an expected, common outcome that the caller should recover
|
|
80
|
+
* from by falling back to a different sign-in method.
|
|
81
|
+
*/
|
|
82
|
+
evpVerify: createAuthEndpoint("/evp/verify", { method: "POST", body: evpVerifyBodySchema }, async (ctx) => {
|
|
83
|
+
const email = ctx.body.email.trim().toLowerCase();
|
|
84
|
+
const { token, nonce } = ctx.body;
|
|
85
|
+
if (!z.email().safeParse(email).success) {
|
|
86
|
+
throw new APIError("BAD_REQUEST", { message: "Invalid email" });
|
|
87
|
+
}
|
|
88
|
+
const nonceRecord = await ctx.context.internalAdapter.consumeVerificationValue(`${NONCE_IDENTIFIER_PREFIX}${nonce}`);
|
|
89
|
+
if (!nonceRecord) {
|
|
90
|
+
return ctx.json({ verified: false, reason: "nonce_invalid" });
|
|
91
|
+
}
|
|
92
|
+
let result;
|
|
93
|
+
try {
|
|
94
|
+
result = await verify({
|
|
95
|
+
token,
|
|
96
|
+
nonce,
|
|
97
|
+
email,
|
|
98
|
+
audience: options.origin,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
ctx.context.logger.error("[email-verification-protocol] verification threw", error);
|
|
103
|
+
return ctx.json({ verified: false, reason: "verification_error" });
|
|
104
|
+
}
|
|
105
|
+
if (!result.ok) {
|
|
106
|
+
return ctx.json({
|
|
107
|
+
verified: false,
|
|
108
|
+
reason: result.error.code,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const verified = result.value;
|
|
112
|
+
if (verified.email.trim().toLowerCase() !== email) {
|
|
113
|
+
return ctx.json({ verified: false, reason: "email_mismatch" });
|
|
114
|
+
}
|
|
115
|
+
const existing = await ctx.context.internalAdapter.findUserByEmail(email);
|
|
116
|
+
let user;
|
|
117
|
+
if (!existing) {
|
|
118
|
+
if (options.disableSignUp) {
|
|
119
|
+
return ctx.json({ verified: false, reason: "sign_up_disabled" });
|
|
120
|
+
}
|
|
121
|
+
const additionalFields = options.userFields
|
|
122
|
+
? options.userFields(verified)
|
|
123
|
+
: {};
|
|
124
|
+
user = await ctx.context.internalAdapter.createUser({ ...additionalFields, email, emailVerified: true, name: "" }, { method: "email-verification-protocol" });
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
user = existing.user;
|
|
128
|
+
if (!user.emailVerified) {
|
|
129
|
+
user = await ctx.context.internalAdapter.updateUser(user.id, {
|
|
130
|
+
emailVerified: true,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const session = await ctx.context.internalAdapter.createSession(user.id);
|
|
135
|
+
await setSessionCookie(ctx, { session, user });
|
|
136
|
+
if (options.onVerified) {
|
|
137
|
+
await options.onVerified({ ...verified, userId: user.id });
|
|
138
|
+
}
|
|
139
|
+
return ctx.json({
|
|
140
|
+
verified: true,
|
|
141
|
+
token: session.token,
|
|
142
|
+
user,
|
|
143
|
+
});
|
|
144
|
+
}),
|
|
145
|
+
},
|
|
146
|
+
rateLimit: [
|
|
147
|
+
{
|
|
148
|
+
pathMatcher(path) {
|
|
149
|
+
return path.startsWith("/evp/");
|
|
150
|
+
},
|
|
151
|
+
window: 60,
|
|
152
|
+
max: 20,
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
};
|
|
156
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export interface EvpVerifiedEmail {
|
|
2
|
+
/** The mailbox address the issuer vouched for. */
|
|
3
|
+
email: string;
|
|
4
|
+
/** The issuer that signed the Email Verification Token. */
|
|
5
|
+
issuer: string;
|
|
6
|
+
/** The relying-party origin the token was bound to. */
|
|
7
|
+
audience: string;
|
|
8
|
+
/** Timestamps reported by the verification library, if any. */
|
|
9
|
+
issuedAt?: unknown;
|
|
10
|
+
/** Any additional claims the issuer included in the token. */
|
|
11
|
+
claims?: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
export interface EvpVerificationError {
|
|
14
|
+
/** Which stage of verification failed (DNS lookup, signature check, ...). */
|
|
15
|
+
stage?: string;
|
|
16
|
+
/** Machine-readable error code. */
|
|
17
|
+
code: string;
|
|
18
|
+
/** Human-readable message, safe to log. */
|
|
19
|
+
message: string;
|
|
20
|
+
cause?: unknown;
|
|
21
|
+
}
|
|
22
|
+
export type EvpVerificationResult = {
|
|
23
|
+
ok: true;
|
|
24
|
+
value: EvpVerifiedEmail;
|
|
25
|
+
} | {
|
|
26
|
+
ok: false;
|
|
27
|
+
error: EvpVerificationError;
|
|
28
|
+
};
|
|
29
|
+
export interface EvpVerifyParams {
|
|
30
|
+
/** The SD-JWT+KB presentation the browser put in the hidden form field. */
|
|
31
|
+
token: string;
|
|
32
|
+
/** The nonce this relying party issued for the current attempt. */
|
|
33
|
+
nonce: string;
|
|
34
|
+
/** The email address the user typed into the form. */
|
|
35
|
+
email: string;
|
|
36
|
+
/** This relying party's absolute HTTP(S) origin. */
|
|
37
|
+
audience: string;
|
|
38
|
+
}
|
|
39
|
+
export interface EvpPluginOptions<T extends Record<string, any> = {}> {
|
|
40
|
+
/**
|
|
41
|
+
* This relying party's absolute origin, e.g. `https://example.com`. Sent
|
|
42
|
+
* to the verifier as the `audience` and must match the origin the Chrome
|
|
43
|
+
* origin-trial token (and the DNS/issuer records) were issued for.
|
|
44
|
+
*/
|
|
45
|
+
origin: string;
|
|
46
|
+
/**
|
|
47
|
+
* How long an issued nonce stays valid for, in seconds.
|
|
48
|
+
* @default 120
|
|
49
|
+
*/
|
|
50
|
+
nonceExpiresIn?: number;
|
|
51
|
+
/**
|
|
52
|
+
* When true, a verified email that has no existing account is rejected
|
|
53
|
+
* instead of provisioning a new user.
|
|
54
|
+
* @default false
|
|
55
|
+
*/
|
|
56
|
+
disableSignUp?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Additional fields to set on a newly created user.
|
|
59
|
+
*/
|
|
60
|
+
userFields?: (verified: EvpVerifiedEmail) => T;
|
|
61
|
+
/**
|
|
62
|
+
* Called after a session has been created for a successfully verified
|
|
63
|
+
* email. Useful for analytics/logging.
|
|
64
|
+
*/
|
|
65
|
+
onVerified?: (verified: EvpVerifiedEmail & {
|
|
66
|
+
userId: string;
|
|
67
|
+
}) => void | Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Overrides the verification call - mainly useful for tests. Defaults to
|
|
70
|
+
* `verifyEmailToken` from the `email-verification-api` package.
|
|
71
|
+
*/
|
|
72
|
+
verify?: (params: EvpVerifyParams) => Promise<EvpVerificationResult>;
|
|
73
|
+
}
|
|
74
|
+
export interface EvpNonceResponse {
|
|
75
|
+
nonce: string;
|
|
76
|
+
expiresIn: number;
|
|
77
|
+
}
|
|
78
|
+
export interface EvpVerifyResponse {
|
|
79
|
+
verified: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Present when `verified` is false. Callers should treat any failure as
|
|
82
|
+
* a signal to fall back to a normal sign-in method (OTP, magic link,
|
|
83
|
+
* password, ...) - this is an experimental, Chrome-only capability and
|
|
84
|
+
* is expected to fail for most users.
|
|
85
|
+
*/
|
|
86
|
+
reason?: string;
|
|
87
|
+
token?: string;
|
|
88
|
+
user?: Record<string, unknown>;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,MAAM,EAAE,MAAM,CAAC;IACf,uDAAuD;IACvD,QAAQ,EAAE,MAAM,CAAC;IACjB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,oBAAoB;IACnC,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,MAAM,qBAAqB,GAC7B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,gBAAgB,CAAA;CAAE,GACrC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,oBAAoB,CAAA;CAAE,CAAC;AAE/C,MAAM,WAAW,eAAe;IAC9B,2EAA2E;IAC3E,KAAK,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAC;IACd,oDAAoD;IACpD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,EAAE;IAClE;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;OAEG;IACH,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,gBAAgB,KAAK,CAAC,CAAC;IAE/C;;;OAGG;IACH,UAAU,CAAC,EAAE,CACX,QAAQ,EAAE,gBAAgB,GAAG;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,KAC5C,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE1B;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAC;CACtE;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import js from "@eslint/js";
|
|
2
|
+
import tseslint from "@typescript-eslint/eslint-plugin";
|
|
3
|
+
import tsparser from "@typescript-eslint/parser";
|
|
4
|
+
import prettier from "eslint-plugin-prettier";
|
|
5
|
+
import prettierConfig from "eslint-config-prettier";
|
|
6
|
+
|
|
7
|
+
export default [
|
|
8
|
+
{
|
|
9
|
+
ignores: ["dist/**", "node_modules/**", "*.js"],
|
|
10
|
+
},
|
|
11
|
+
js.configs.recommended,
|
|
12
|
+
{
|
|
13
|
+
files: ["src/**/*.ts"],
|
|
14
|
+
languageOptions: {
|
|
15
|
+
parser: tsparser,
|
|
16
|
+
parserOptions: {
|
|
17
|
+
ecmaVersion: 2021,
|
|
18
|
+
sourceType: "module",
|
|
19
|
+
project: "./tsconfig.json",
|
|
20
|
+
},
|
|
21
|
+
globals: {
|
|
22
|
+
console: "readonly",
|
|
23
|
+
process: "readonly",
|
|
24
|
+
fetch: "readonly",
|
|
25
|
+
URLSearchParams: "readonly",
|
|
26
|
+
window: "readonly",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
plugins: {
|
|
30
|
+
"@typescript-eslint": tseslint,
|
|
31
|
+
prettier: prettier,
|
|
32
|
+
},
|
|
33
|
+
rules: {
|
|
34
|
+
...tseslint.configs.recommended.rules,
|
|
35
|
+
...prettierConfig.rules,
|
|
36
|
+
"prettier/prettier": "error",
|
|
37
|
+
"@typescript-eslint/no-explicit-any": "off",
|
|
38
|
+
"@typescript-eslint/explicit-module-boundary-types": "off",
|
|
39
|
+
"@typescript-eslint/no-empty-object-type": "off",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
];
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "better-auth-evp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Email Verification Protocol (Chrome origin trial) plugin for Better Auth, with automatic fallback to any other sign-in method",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./client": {
|
|
13
|
+
"types": "./dist/client.d.ts",
|
|
14
|
+
"import": "./dist/client.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"prepublishOnly": "pnpm build",
|
|
20
|
+
"lint": "pnpm lint:style && pnpm lint:typecheck",
|
|
21
|
+
"lint:style": "eslint src --ext .ts",
|
|
22
|
+
"lint:typecheck": "tsc --noEmit",
|
|
23
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
24
|
+
"format:check": "prettier --check \"src/**/*.ts\""
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"better-auth": "^1.5.0",
|
|
28
|
+
"email-verification-api": "^0.1.0",
|
|
29
|
+
"zod": "^3.0.0 || ^4.0.0"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"email-verification-api": "^0.1.0"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"better-auth",
|
|
36
|
+
"better-auth-plugin",
|
|
37
|
+
"email-verification-protocol",
|
|
38
|
+
"email-verification",
|
|
39
|
+
"chrome",
|
|
40
|
+
"origin-trial",
|
|
41
|
+
"authentication",
|
|
42
|
+
"passwordless"
|
|
43
|
+
],
|
|
44
|
+
"author": "Kamil Marczak",
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "https://github.com/qamarq/better-auth-evp"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@eslint/js": "^10.0.1",
|
|
52
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
53
|
+
"@semantic-release/git": "^10.0.1",
|
|
54
|
+
"@semantic-release/github": "^11.0.1",
|
|
55
|
+
"@semantic-release/npm": "^12.0.1",
|
|
56
|
+
"@types/node": "^25.3.5",
|
|
57
|
+
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
|
58
|
+
"@typescript-eslint/parser": "^8.56.1",
|
|
59
|
+
"eslint": "^10.0.3",
|
|
60
|
+
"eslint-config-prettier": "^10.1.8",
|
|
61
|
+
"eslint-plugin-prettier": "^5.5.5",
|
|
62
|
+
"prettier": "^3.8.1",
|
|
63
|
+
"semantic-release": "^24.2.0",
|
|
64
|
+
"typescript": "^5.9.3",
|
|
65
|
+
"zod": "^4.3.6"
|
|
66
|
+
}
|
|
67
|
+
}
|