@ingram-tech/nk-auth 0.9.0 → 0.10.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/README.md +80 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +5 -0
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/jwt.d.ts +9 -1
- package/dist/jwt.d.ts.map +1 -1
- package/dist/jwt.js +24 -4
- package/dist/jwt.js.map +1 -1
- package/dist/password.d.ts +57 -0
- package/dist/password.d.ts.map +1 -0
- package/dist/password.js +68 -0
- package/dist/password.js.map +1 -0
- package/dist/reset-password.d.ts +47 -0
- package/dist/reset-password.d.ts.map +1 -0
- package/dist/reset-password.js +75 -0
- package/dist/reset-password.js.map +1 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +26 -0
- package/dist/server.js.map +1 -1
- package/package.json +16 -12
package/README.md
CHANGED
|
@@ -176,6 +176,8 @@ export const {
|
|
|
176
176
|
requireSession,
|
|
177
177
|
requireUser,
|
|
178
178
|
redirectIfAuthenticated,
|
|
179
|
+
getLinkedProviders, // providerIds linked to the current user
|
|
180
|
+
hasCredentialAccount, // does the current user have an email/password login?
|
|
179
181
|
} = createAuthHelpers(auth);
|
|
180
182
|
```
|
|
181
183
|
|
|
@@ -244,6 +246,84 @@ middleware injects — so the self-heal (next + clearing) needs the middleware.
|
|
|
244
246
|
Sites that skip middleware still get validated gating from the server helpers,
|
|
245
247
|
just without automatic `next`/clearing.
|
|
246
248
|
|
|
249
|
+
## 6. Passwords: change, set, and reset
|
|
250
|
+
|
|
251
|
+
nk-auth owns the reset **sender** (`makeEmailSenders.sendResetPassword`, §2) and
|
|
252
|
+
now closes the loop so a site never touches Better Auth's `account` table or
|
|
253
|
+
endpoint names directly. Three pieces:
|
|
254
|
+
|
|
255
|
+
**Detect what login the user has.** A social-only account (Google, …) has no
|
|
256
|
+
password credential until it sets one. `hasCredentialAccount()` drives the
|
|
257
|
+
"Change password" vs "Set password" choice on a security page; reach for
|
|
258
|
+
`getLinkedProviders()` when you need the full list.
|
|
259
|
+
|
|
260
|
+
```tsx
|
|
261
|
+
// app/settings/security/page.tsx (server component)
|
|
262
|
+
import { hasCredentialAccount } from "@/lib/auth/session";
|
|
263
|
+
export default async function Security() {
|
|
264
|
+
return (await hasCredentialAccount()) ? <ChangePassword /> : <SetPassword />;
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Set a password without a current one.** A social-only user has no current
|
|
269
|
+
password to verify, so the re-auth is an emailed reset link — clicking it proves
|
|
270
|
+
account ownership. Trigger it with the standard client call; there is no separate
|
|
271
|
+
"set password" endpoint:
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
await authClient.requestPasswordReset({
|
|
275
|
+
email,
|
|
276
|
+
redirectTo: "/reset-password", // your token-consumer page (below)
|
|
277
|
+
});
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Better Auth's reset endpoint **creates** the credential when the user has none,
|
|
281
|
+
so the same flow both resets a forgotten password and sets a first one. That
|
|
282
|
+
guarantee is pinned by `reset-password.test.ts` against a real instance, so a
|
|
283
|
+
Better Auth upgrade can't silently break the set-password path.
|
|
284
|
+
|
|
285
|
+
**Consume the token.** The email link lands on your page with `?token=` (or
|
|
286
|
+
`?error=INVALID_TOKEN`). `useResetPassword` is the headless state machine —
|
|
287
|
+
invalid-token, submitting, success, and policy validation (length + match) —
|
|
288
|
+
against the shared `passwordSchema` bounds. You bring the shell:
|
|
289
|
+
|
|
290
|
+
```tsx
|
|
291
|
+
"use client";
|
|
292
|
+
import { useResetPassword } from "@ingram-tech/nk-auth/client";
|
|
293
|
+
import { authClient } from "@/lib/auth/client";
|
|
294
|
+
|
|
295
|
+
export function ResetPasswordForm({ token }: { token: string | null }) {
|
|
296
|
+
const { status, error, submit } = useResetPassword(authClient, { token });
|
|
297
|
+
if (status === "invalid") return <p>This link is invalid or has expired.</p>;
|
|
298
|
+
if (status === "success") return <p>Password set. You can now sign in.</p>;
|
|
299
|
+
// <form onSubmit={() => submit(newPassword, confirm)}>; render `error.code`
|
|
300
|
+
// (stable, for i18n) or `error.message` (English fallback).
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Policy constants live at `@ingram-tech/nk-auth/password` (pure, importable from
|
|
305
|
+
both ends): `DEFAULT_MIN_PASSWORD_LENGTH` / `DEFAULT_MAX_PASSWORD_LENGTH`,
|
|
306
|
+
`passwordSchema()`, and `validateNewPassword()`. Pass your resolved bounds to
|
|
307
|
+
`useResetPassword({ token, minLength, maxLength })` if you override Better Auth's
|
|
308
|
+
defaults, so the form and the server never drift.
|
|
309
|
+
|
|
310
|
+
**`.well-known/change-password`.** Add the [W3C well-known
|
|
311
|
+
redirect](https://w3c.github.io/webappsec-change-password-url/) so password
|
|
312
|
+
managers deep-link straight to your security page. It's a per-app route target,
|
|
313
|
+
so nk-auth documents the convention rather than shipping it — in `next.config`:
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
async redirects() {
|
|
317
|
+
return [
|
|
318
|
+
{
|
|
319
|
+
source: "/.well-known/change-password",
|
|
320
|
+
destination: "/settings/security", // your Change/Set password page
|
|
321
|
+
permanent: false,
|
|
322
|
+
},
|
|
323
|
+
];
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
247
327
|
## Migrating bcrypt passwords to scrypt
|
|
248
328
|
|
|
249
329
|
`bcryptPassword` is **legacy support only** (see its `@deprecated` note). It
|
package/dist/client.d.ts
CHANGED
|
@@ -20,5 +20,7 @@
|
|
|
20
20
|
export { passkeyClient } from "@better-auth/passkey/client";
|
|
21
21
|
export { jwtClient } from "better-auth/client/plugins";
|
|
22
22
|
export { createAuthClient } from "better-auth/react";
|
|
23
|
+
export { CREDENTIAL_PROVIDER_ID, DEFAULT_MAX_PASSWORD_LENGTH, DEFAULT_MIN_PASSWORD_LENGTH, type PasswordPolicy, passwordSchema, type ResetPasswordError, type ResetPasswordErrorCode, validateNewPassword, } from "./password.js";
|
|
23
24
|
export { authBasePath } from "./paths.js";
|
|
25
|
+
export { type ResetPasswordClient, type ResetPasswordStatus, type UseResetPassword, type UseResetPasswordOptions, useResetPassword, } from "./reset-password.js";
|
|
24
26
|
//# sourceMappingURL=client.d.ts.map
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAGrD,OAAO,EACN,sBAAsB,EACtB,2BAA2B,EAC3B,2BAA2B,EAC3B,KAAK,cAAc,EACnB,cAAc,EACd,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,mBAAmB,GACnB,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C,OAAO,EACN,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,EAC5B,gBAAgB,GAChB,MAAM,qBAAqB,CAAC"}
|
package/dist/client.js
CHANGED
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
export { passkeyClient } from "@better-auth/passkey/client";
|
|
21
21
|
export { jwtClient } from "better-auth/client/plugins";
|
|
22
22
|
export { createAuthClient } from "better-auth/react";
|
|
23
|
+
// Password-policy primitives (pure), so a reset form validates against the same
|
|
24
|
+
// bounds the server enforces without a second import.
|
|
25
|
+
export { CREDENTIAL_PROVIDER_ID, DEFAULT_MAX_PASSWORD_LENGTH, DEFAULT_MIN_PASSWORD_LENGTH, passwordSchema, validateNewPassword, } from "./password.js";
|
|
23
26
|
// Re-exported here too so the client can set `basePath` without a server import.
|
|
24
27
|
export { authBasePath } from "./paths.js";
|
|
28
|
+
// Headless reset-/set-password controller (React hook); the site brings the shell.
|
|
29
|
+
export { useResetPassword, } from "./reset-password.js";
|
|
25
30
|
//# sourceMappingURL=client.js.map
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,iFAAiF;AACjF,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,gFAAgF;AAChF,sDAAsD;AACtD,OAAO,EACN,sBAAsB,EACtB,2BAA2B,EAC3B,2BAA2B,EAE3B,cAAc,EAGd,mBAAmB,GACnB,MAAM,eAAe,CAAC;AACvB,iFAAiF;AACjF,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,mFAAmF;AACnF,OAAO,EAKN,gBAAgB,GAChB,MAAM,qBAAqB,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { base58Id, fromPrefixedId, toPrefixedId, uuidGenerateId } from "./id.js"
|
|
|
3
3
|
export { type AuthEnv, authEnv, isConfigured } from "./keys.js";
|
|
4
4
|
export { bcryptPassword, makeEmailSenders, makePasskeyOptions, type PasskeyConfig, passkeyOptionsForBaseUrl, type SendEmail, } from "./options.js";
|
|
5
5
|
export { lastActiveOrganizationHooks, lastActiveOrganizationUserField, nkOrganizationDefaults, } from "./organization.js";
|
|
6
|
+
export { CREDENTIAL_PROVIDER_ID, DEFAULT_MAX_PASSWORD_LENGTH, DEFAULT_MIN_PASSWORD_LENGTH, type PasswordPolicy, passwordSchema, type ResetPasswordError, type ResetPasswordErrorCode, validateNewPassword, } from "./password.js";
|
|
6
7
|
export { authBasePath } from "./paths.js";
|
|
7
8
|
export { createAuthPool } from "./pool.js";
|
|
8
9
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,gBAAgB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACtF,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACjF,OAAO,EAAE,KAAK,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAChE,OAAO,EACN,cAAc,EACd,gBAAgB,EAChB,kBAAkB,EAClB,KAAK,aAAa,EAClB,wBAAwB,EACxB,KAAK,SAAS,GACd,MAAM,cAAc,CAAC;AACtB,OAAO,EACN,2BAA2B,EAC3B,+BAA+B,EAC/B,sBAAsB,GACtB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,gBAAgB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACtF,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACjF,OAAO,EAAE,KAAK,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAChE,OAAO,EACN,cAAc,EACd,gBAAgB,EAChB,kBAAkB,EAClB,KAAK,aAAa,EAClB,wBAAwB,EACxB,KAAK,SAAS,GACd,MAAM,cAAc,CAAC;AACtB,OAAO,EACN,2BAA2B,EAC3B,+BAA+B,EAC/B,sBAAsB,GACtB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACN,sBAAsB,EACtB,2BAA2B,EAC3B,2BAA2B,EAC3B,KAAK,cAAc,EACnB,cAAc,EACd,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,mBAAmB,GACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ export { base58Id, fromPrefixedId, toPrefixedId, uuidGenerateId } from "./id.js"
|
|
|
7
7
|
export { authEnv, isConfigured } from "./keys.js";
|
|
8
8
|
export { bcryptPassword, makeEmailSenders, makePasskeyOptions, passkeyOptionsForBaseUrl, } from "./options.js";
|
|
9
9
|
export { lastActiveOrganizationHooks, lastActiveOrganizationUserField, nkOrganizationDefaults, } from "./organization.js";
|
|
10
|
+
export { CREDENTIAL_PROVIDER_ID, DEFAULT_MAX_PASSWORD_LENGTH, DEFAULT_MIN_PASSWORD_LENGTH, passwordSchema, validateNewPassword, } from "./password.js";
|
|
10
11
|
export { authBasePath } from "./paths.js";
|
|
11
12
|
export { createAuthPool } from "./pool.js";
|
|
12
13
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,+EAA+E;AAC/E,4EAA4E;AAC5E,wEAAwE;AAExE,OAAO,EAAyB,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACtF,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACjF,OAAO,EAAgB,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAChE,OAAO,EACN,cAAc,EACd,gBAAgB,EAChB,kBAAkB,EAElB,wBAAwB,GAExB,MAAM,cAAc,CAAC;AACtB,OAAO,EACN,2BAA2B,EAC3B,+BAA+B,EAC/B,sBAAsB,GACtB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,+EAA+E;AAC/E,4EAA4E;AAC5E,wEAAwE;AAExE,OAAO,EAAyB,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACtF,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACjF,OAAO,EAAgB,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAChE,OAAO,EACN,cAAc,EACd,gBAAgB,EAChB,kBAAkB,EAElB,wBAAwB,GAExB,MAAM,cAAc,CAAC;AACtB,OAAO,EACN,2BAA2B,EAC3B,+BAA+B,EAC/B,sBAAsB,GACtB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACN,sBAAsB,EACtB,2BAA2B,EAC3B,2BAA2B,EAE3B,cAAc,EAGd,mBAAmB,GACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC"}
|
package/dist/jwt.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { type JWTPayload } from "jose";
|
|
|
7
7
|
* exposed at `/api/auth/jwks` so the backend can verify it (`verifyBackendJwt`).
|
|
8
8
|
*/
|
|
9
9
|
export interface BackendJwtConfig {
|
|
10
|
-
/** Expected audience of the site's backend, e.g. "
|
|
10
|
+
/** Expected audience of the site's backend, e.g. "my-app-backend". */
|
|
11
11
|
audience: string;
|
|
12
12
|
/** Token lifetime, e.g. "15m". */
|
|
13
13
|
expirationTime?: string;
|
|
@@ -23,6 +23,14 @@ export declare const backendJwtOptions: (config: BackendJwtConfig) => JwtOptions
|
|
|
23
23
|
* Verify a Better-Auth-minted backend JWT (EdDSA) against the issuer's JWKS.
|
|
24
24
|
* For use by the backend that consumes `backendJwtOptions` tokens. Throws on an
|
|
25
25
|
* invalid/expired token or audience/issuer mismatch; returns the claims.
|
|
26
|
+
*
|
|
27
|
+
* Hardened against Better Auth signing-key rotation: jose's `createRemoteJWKSet`
|
|
28
|
+
* refuses to refetch the JWKS for `cooldownDuration` (30s default) after any
|
|
29
|
+
* fetch, so a token signed with a freshly rotated key whose `kid` isn't yet in
|
|
30
|
+
* the cached set would fail for the *whole* cooldown window — a ~30s burst of
|
|
31
|
+
* auth failures on every request that verifies a token. On a no-matching-key
|
|
32
|
+
* miss we force a single `.reload()` (which bypasses the cooldown) and retry, so
|
|
33
|
+
* a rotation costs one extra fetch instead of a brief outage.
|
|
26
34
|
*/
|
|
27
35
|
export declare const verifyBackendJwt: (params: {
|
|
28
36
|
token: string;
|
package/dist/jwt.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,
|
|
1
|
+
{"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAGN,KAAK,UAAU,EAEf,MAAM,MAAM,CAAC;AAEd;;;;;GAKG;AAEH,MAAM,WAAW,gBAAgB;IAChC,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED,sEAAsE;AACtE,eAAO,MAAM,iBAAiB,GAAI,QAAQ,gBAAgB,KAAG,UAM3D,CAAC;AAIH;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB,GAAU,QAAQ;IAC9C,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,OAAO,EAAE,MAAM,GAAG,GAAG,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CACf,KAAG,OAAO,CAAC,UAAU,CAyBrB,CAAC"}
|
package/dist/jwt.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
1
|
+
import { createRemoteJWKSet, errors as joseErrors, jwtVerify, } from "jose";
|
|
2
2
|
/** `jwt` plugin options for a backend-API token (custom audience). */
|
|
3
3
|
export const backendJwtOptions = (config) => ({
|
|
4
4
|
disableSettingJwtHeader: config.disableSettingJwtHeader,
|
|
@@ -12,6 +12,14 @@ const jwksCache = new Map();
|
|
|
12
12
|
* Verify a Better-Auth-minted backend JWT (EdDSA) against the issuer's JWKS.
|
|
13
13
|
* For use by the backend that consumes `backendJwtOptions` tokens. Throws on an
|
|
14
14
|
* invalid/expired token or audience/issuer mismatch; returns the claims.
|
|
15
|
+
*
|
|
16
|
+
* Hardened against Better Auth signing-key rotation: jose's `createRemoteJWKSet`
|
|
17
|
+
* refuses to refetch the JWKS for `cooldownDuration` (30s default) after any
|
|
18
|
+
* fetch, so a token signed with a freshly rotated key whose `kid` isn't yet in
|
|
19
|
+
* the cached set would fail for the *whole* cooldown window — a ~30s burst of
|
|
20
|
+
* auth failures on every request that verifies a token. On a no-matching-key
|
|
21
|
+
* miss we force a single `.reload()` (which bypasses the cooldown) and retry, so
|
|
22
|
+
* a rotation costs one extra fetch instead of a brief outage.
|
|
15
23
|
*/
|
|
16
24
|
export const verifyBackendJwt = async (params) => {
|
|
17
25
|
const url = typeof params.jwksUrl === "string" ? new URL(params.jwksUrl) : params.jwksUrl;
|
|
@@ -21,11 +29,23 @@ export const verifyBackendJwt = async (params) => {
|
|
|
21
29
|
jwks = createRemoteJWKSet(url);
|
|
22
30
|
jwksCache.set(cacheKey, jwks);
|
|
23
31
|
}
|
|
24
|
-
const
|
|
32
|
+
const options = {
|
|
25
33
|
audience: params.audience,
|
|
26
34
|
issuer: params.issuer,
|
|
27
35
|
algorithms: ["EdDSA"],
|
|
28
|
-
}
|
|
29
|
-
|
|
36
|
+
};
|
|
37
|
+
try {
|
|
38
|
+
const { payload } = await jwtVerify(params.token, jwks, options);
|
|
39
|
+
return payload;
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
if (!(err instanceof joseErrors.JWKSNoMatchingKey))
|
|
43
|
+
throw err;
|
|
44
|
+
// `kid` absent from the cached JWKS — almost always a just-rotated signing
|
|
45
|
+
// key. Force a refresh past the cooldown and retry exactly once.
|
|
46
|
+
await jwks.reload();
|
|
47
|
+
const { payload } = await jwtVerify(params.token, jwks, options);
|
|
48
|
+
return payload;
|
|
49
|
+
}
|
|
30
50
|
};
|
|
31
51
|
//# sourceMappingURL=jwt.js.map
|
package/dist/jwt.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jwt.js","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"jwt.js","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"AACA,OAAO,EACN,kBAAkB,EAClB,MAAM,IAAI,UAAU,EAEpB,SAAS,GACT,MAAM,MAAM,CAAC;AAqBd,sEAAsE;AACtE,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,MAAwB,EAAc,EAAE,CAAC,CAAC;IAC3E,uBAAuB,EAAE,MAAM,CAAC,uBAAuB;IACvD,GAAG,EAAE;QACJ,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,cAAc,EAAE,MAAM,CAAC,cAAc;KACrC;CACD,CAAC,CAAC;AAEH,MAAM,SAAS,GAAG,IAAI,GAAG,EAAiD,CAAC;AAE3E;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,EAAE,MAMtC,EAAuB,EAAE;IACzB,MAAM,GAAG,GACR,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;IAC/E,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC;IAChC,IAAI,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACnC,IAAI,CAAC,IAAI,EAAE,CAAC;QACX,IAAI,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC;QAC/B,SAAS,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC/B,CAAC;IACD,MAAM,OAAO,GAAG;QACf,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,UAAU,EAAE,CAAC,OAAO,CAAC;KACrB,CAAC;IACF,IAAI,CAAC;QACJ,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,SAAS,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;QACjE,OAAO,OAAO,CAAC;IAChB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,IAAI,CAAC,CAAC,GAAG,YAAY,UAAU,CAAC,iBAAiB,CAAC;YAAE,MAAM,GAAG,CAAC;QAC9D,2EAA2E;QAC3E,iEAAiE;QACjE,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;QACpB,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,SAAS,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;QACjE,OAAO,OAAO,CAAC;IAChB,CAAC;AACF,CAAC,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Password-policy primitives, shared by both ends of a site so the client form
|
|
3
|
+
* and the server validate against the *same* numbers. Pure (no `next`, `react`,
|
|
4
|
+
* or server imports) so it resolves identically from "@ingram-tech/nk-auth" and
|
|
5
|
+
* "@ingram-tech/nk-auth/client". Import from the "./password" subpath.
|
|
6
|
+
*
|
|
7
|
+
* The values mirror Better Auth's `emailAndPassword` defaults; if a site
|
|
8
|
+
* overrides `minPasswordLength` / `maxPasswordLength` in its `betterAuth()`
|
|
9
|
+
* config, pass the same numbers to `passwordSchema()` so the two stay in step.
|
|
10
|
+
*/
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
/**
|
|
13
|
+
* The Better Auth `account.providerId` value for an email/password credential
|
|
14
|
+
* (as opposed to a social provider like "google"). This is Better Auth's own
|
|
15
|
+
* table shape, not ours — centralised here so no consuming site hard-codes the
|
|
16
|
+
* string. See {@link import("./server.js").createAuthHelpers}'s
|
|
17
|
+
* `hasCredentialAccount`.
|
|
18
|
+
*/
|
|
19
|
+
export declare const CREDENTIAL_PROVIDER_ID = "credential";
|
|
20
|
+
/** Better Auth's default `emailAndPassword.minPasswordLength`. */
|
|
21
|
+
export declare const DEFAULT_MIN_PASSWORD_LENGTH = 8;
|
|
22
|
+
/** Better Auth's default `emailAndPassword.maxPasswordLength`. */
|
|
23
|
+
export declare const DEFAULT_MAX_PASSWORD_LENGTH = 128;
|
|
24
|
+
export interface PasswordPolicy {
|
|
25
|
+
/** Minimum length. Default {@link DEFAULT_MIN_PASSWORD_LENGTH}. */
|
|
26
|
+
minLength?: number;
|
|
27
|
+
/** Maximum length. Default {@link DEFAULT_MAX_PASSWORD_LENGTH}. */
|
|
28
|
+
maxLength?: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A Zod schema for a new password, honouring the resolved policy. Use it on both
|
|
32
|
+
* the client (form validation) and the server (before calling Better Auth) so
|
|
33
|
+
* neither side drifts from the other or from the instance's configured bounds.
|
|
34
|
+
*/
|
|
35
|
+
export declare function passwordSchema(policy?: PasswordPolicy): z.ZodString;
|
|
36
|
+
/**
|
|
37
|
+
* The failure reasons of a "choose a new password + confirm it" form, as a
|
|
38
|
+
* stable `code` (switch on it for i18n) plus an English `message` fallback.
|
|
39
|
+
* `reset_failed` is the server rejecting the reset token (invalid / expired).
|
|
40
|
+
*/
|
|
41
|
+
export type ResetPasswordErrorCode = "password_too_short" | "password_too_long" | "passwords_do_not_match" | "reset_failed";
|
|
42
|
+
export interface ResetPasswordError {
|
|
43
|
+
code: ResetPasswordErrorCode;
|
|
44
|
+
message: string;
|
|
45
|
+
/** Present on `password_too_short`. */
|
|
46
|
+
minLength?: number;
|
|
47
|
+
/** Present on `password_too_long`. */
|
|
48
|
+
maxLength?: number;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Validate a new-password pair (the field and its confirmation) against the
|
|
52
|
+
* policy. Returns the first {@link ResetPasswordError}, or null when valid. Pure
|
|
53
|
+
* so it is unit-testable and reusable outside React; {@link
|
|
54
|
+
* import("./client.js").useResetPassword} calls it before hitting the server.
|
|
55
|
+
*/
|
|
56
|
+
export declare function validateNewPassword(newPassword: string, confirmPassword: string, policy?: PasswordPolicy): ResetPasswordError | null;
|
|
57
|
+
//# sourceMappingURL=password.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"password.d.ts","sourceRoot":"","sources":["../src/password.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,eAAe,CAAC;AAEnD,kEAAkE;AAClE,eAAO,MAAM,2BAA2B,IAAI,CAAC;AAE7C,kEAAkE;AAClE,eAAO,MAAM,2BAA2B,MAAM,CAAC;AAE/C,MAAM,WAAW,cAAc;IAC9B,mEAAmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,GAAE,cAAmB,GAAG,CAAC,CAAC,SAAS,CAOvE;AAED;;;;GAIG;AACH,MAAM,MAAM,sBAAsB,GAC/B,oBAAoB,GACpB,mBAAmB,GACnB,wBAAwB,GACxB,cAAc,CAAC;AAElB,MAAM,WAAW,kBAAkB;IAClC,IAAI,EAAE,sBAAsB,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,uCAAuC;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sCAAsC;IACtC,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAClC,WAAW,EAAE,MAAM,EACnB,eAAe,EAAE,MAAM,EACvB,MAAM,GAAE,cAAmB,GACzB,kBAAkB,GAAG,IAAI,CAwB3B"}
|
package/dist/password.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Password-policy primitives, shared by both ends of a site so the client form
|
|
3
|
+
* and the server validate against the *same* numbers. Pure (no `next`, `react`,
|
|
4
|
+
* or server imports) so it resolves identically from "@ingram-tech/nk-auth" and
|
|
5
|
+
* "@ingram-tech/nk-auth/client". Import from the "./password" subpath.
|
|
6
|
+
*
|
|
7
|
+
* The values mirror Better Auth's `emailAndPassword` defaults; if a site
|
|
8
|
+
* overrides `minPasswordLength` / `maxPasswordLength` in its `betterAuth()`
|
|
9
|
+
* config, pass the same numbers to `passwordSchema()` so the two stay in step.
|
|
10
|
+
*/
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
/**
|
|
13
|
+
* The Better Auth `account.providerId` value for an email/password credential
|
|
14
|
+
* (as opposed to a social provider like "google"). This is Better Auth's own
|
|
15
|
+
* table shape, not ours — centralised here so no consuming site hard-codes the
|
|
16
|
+
* string. See {@link import("./server.js").createAuthHelpers}'s
|
|
17
|
+
* `hasCredentialAccount`.
|
|
18
|
+
*/
|
|
19
|
+
export const CREDENTIAL_PROVIDER_ID = "credential";
|
|
20
|
+
/** Better Auth's default `emailAndPassword.minPasswordLength`. */
|
|
21
|
+
export const DEFAULT_MIN_PASSWORD_LENGTH = 8;
|
|
22
|
+
/** Better Auth's default `emailAndPassword.maxPasswordLength`. */
|
|
23
|
+
export const DEFAULT_MAX_PASSWORD_LENGTH = 128;
|
|
24
|
+
/**
|
|
25
|
+
* A Zod schema for a new password, honouring the resolved policy. Use it on both
|
|
26
|
+
* the client (form validation) and the server (before calling Better Auth) so
|
|
27
|
+
* neither side drifts from the other or from the instance's configured bounds.
|
|
28
|
+
*/
|
|
29
|
+
export function passwordSchema(policy = {}) {
|
|
30
|
+
const min = policy.minLength ?? DEFAULT_MIN_PASSWORD_LENGTH;
|
|
31
|
+
const max = policy.maxLength ?? DEFAULT_MAX_PASSWORD_LENGTH;
|
|
32
|
+
return z
|
|
33
|
+
.string()
|
|
34
|
+
.min(min, `Password must be at least ${min} characters.`)
|
|
35
|
+
.max(max, `Password must be at most ${max} characters.`);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Validate a new-password pair (the field and its confirmation) against the
|
|
39
|
+
* policy. Returns the first {@link ResetPasswordError}, or null when valid. Pure
|
|
40
|
+
* so it is unit-testable and reusable outside React; {@link
|
|
41
|
+
* import("./client.js").useResetPassword} calls it before hitting the server.
|
|
42
|
+
*/
|
|
43
|
+
export function validateNewPassword(newPassword, confirmPassword, policy = {}) {
|
|
44
|
+
const min = policy.minLength ?? DEFAULT_MIN_PASSWORD_LENGTH;
|
|
45
|
+
const max = policy.maxLength ?? DEFAULT_MAX_PASSWORD_LENGTH;
|
|
46
|
+
if (newPassword.length < min) {
|
|
47
|
+
return {
|
|
48
|
+
code: "password_too_short",
|
|
49
|
+
message: `Password must be at least ${min} characters.`,
|
|
50
|
+
minLength: min,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (newPassword.length > max) {
|
|
54
|
+
return {
|
|
55
|
+
code: "password_too_long",
|
|
56
|
+
message: `Password must be at most ${max} characters.`,
|
|
57
|
+
maxLength: max,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (newPassword !== confirmPassword) {
|
|
61
|
+
return {
|
|
62
|
+
code: "passwords_do_not_match",
|
|
63
|
+
message: "Passwords do not match.",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=password.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"password.js","sourceRoot":"","sources":["../src/password.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,YAAY,CAAC;AAEnD,kEAAkE;AAClE,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,CAAC;AAE7C,kEAAkE;AAClE,MAAM,CAAC,MAAM,2BAA2B,GAAG,GAAG,CAAC;AAS/C;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,SAAyB,EAAE;IACzD,MAAM,GAAG,GAAG,MAAM,CAAC,SAAS,IAAI,2BAA2B,CAAC;IAC5D,MAAM,GAAG,GAAG,MAAM,CAAC,SAAS,IAAI,2BAA2B,CAAC;IAC5D,OAAO,CAAC;SACN,MAAM,EAAE;SACR,GAAG,CAAC,GAAG,EAAE,6BAA6B,GAAG,cAAc,CAAC;SACxD,GAAG,CAAC,GAAG,EAAE,4BAA4B,GAAG,cAAc,CAAC,CAAC;AAC3D,CAAC;AAsBD;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAClC,WAAmB,EACnB,eAAuB,EACvB,SAAyB,EAAE;IAE3B,MAAM,GAAG,GAAG,MAAM,CAAC,SAAS,IAAI,2BAA2B,CAAC;IAC5D,MAAM,GAAG,GAAG,MAAM,CAAC,SAAS,IAAI,2BAA2B,CAAC;IAC5D,IAAI,WAAW,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;QAC9B,OAAO;YACN,IAAI,EAAE,oBAAoB;YAC1B,OAAO,EAAE,6BAA6B,GAAG,cAAc;YACvD,SAAS,EAAE,GAAG;SACd,CAAC;IACH,CAAC;IACD,IAAI,WAAW,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;QAC9B,OAAO;YACN,IAAI,EAAE,mBAAmB;YACzB,OAAO,EAAE,4BAA4B,GAAG,cAAc;YACtD,SAAS,EAAE,GAAG;SACd,CAAC;IACH,CAAC;IACD,IAAI,WAAW,KAAK,eAAe,EAAE,CAAC;QACrC,OAAO;YACN,IAAI,EAAE,wBAAwB;YAC9B,OAAO,EAAE,yBAAyB;SAClC,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type PasswordPolicy, type ResetPasswordError } from "./password.js";
|
|
2
|
+
export type ResetPasswordStatus =
|
|
3
|
+
/** No usable token in the URL — show an "invalid or expired link" message. */
|
|
4
|
+
"invalid"
|
|
5
|
+
/** Token present; awaiting the user's new password. */
|
|
6
|
+
| "idle"
|
|
7
|
+
/** The reset request is in flight. */
|
|
8
|
+
| "submitting"
|
|
9
|
+
/** The password was set; the user can now sign in with it. */
|
|
10
|
+
| "success"
|
|
11
|
+
/** Validation failed, or the server rejected the reset — see `error`. */
|
|
12
|
+
| "error";
|
|
13
|
+
/** The slice of a Better Auth client this hook calls. */
|
|
14
|
+
export interface ResetPasswordClient {
|
|
15
|
+
resetPassword: (input: {
|
|
16
|
+
newPassword: string;
|
|
17
|
+
token: string;
|
|
18
|
+
}) => Promise<{
|
|
19
|
+
error?: {
|
|
20
|
+
message?: string;
|
|
21
|
+
} | null;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
export interface UseResetPasswordOptions extends PasswordPolicy {
|
|
25
|
+
/**
|
|
26
|
+
* The reset token from the URL (`?token=`), or null when it is missing — e.g.
|
|
27
|
+
* the link carried `?error=INVALID_TOKEN` instead. Null puts the controller in
|
|
28
|
+
* the `invalid` state before any submit.
|
|
29
|
+
*/
|
|
30
|
+
token: string | null;
|
|
31
|
+
}
|
|
32
|
+
export interface UseResetPassword {
|
|
33
|
+
status: ResetPasswordStatus;
|
|
34
|
+
/** The current failure, or null. `code` is stable for i18n; `message` is an English fallback. */
|
|
35
|
+
error: ResetPasswordError | null;
|
|
36
|
+
/** Validate the pair against the policy, then call the reset endpoint. */
|
|
37
|
+
submit: (newPassword: string, confirmPassword: string) => Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Drive a reset-password form. Returns the current `status`, the last `error`
|
|
41
|
+
* (with an i18n-friendly `code`), and a `submit(newPassword, confirmPassword)`
|
|
42
|
+
* that runs {@link validateNewPassword} against the policy before hitting the
|
|
43
|
+
* server. Pass the resolved `minLength` / `maxLength` when the site overrides
|
|
44
|
+
* Better Auth's defaults so client and server agree.
|
|
45
|
+
*/
|
|
46
|
+
export declare function useResetPassword(client: ResetPasswordClient, options: UseResetPasswordOptions): UseResetPassword;
|
|
47
|
+
//# sourceMappingURL=reset-password.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reset-password.d.ts","sourceRoot":"","sources":["../src/reset-password.ts"],"names":[],"mappings":"AAoBA,OAAO,EACN,KAAK,cAAc,EACnB,KAAK,kBAAkB,EAEvB,MAAM,eAAe,CAAC;AAEvB,MAAM,MAAM,mBAAmB;AAC9B,8EAA8E;AAC5E,SAAS;AACX,uDAAuD;GACrD,MAAM;AACR,sCAAsC;GACpC,YAAY;AACd,8DAA8D;GAC5D,SAAS;AACX,yEAAyE;GACvE,OAAO,CAAC;AAEX,yDAAyD;AACzD,MAAM,WAAW,mBAAmB;IACnC,aAAa,EAAE,CAAC,KAAK,EAAE;QACtB,WAAW,EAAE,MAAM,CAAC;QACpB,KAAK,EAAE,MAAM,CAAC;KACd,KAAK,OAAO,CAAC;QAAE,KAAK,CAAC,EAAE;YAAE,OAAO,CAAC,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,uBAAwB,SAAQ,cAAc;IAC9D;;;;OAIG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAChC,MAAM,EAAE,mBAAmB,CAAC;IAC5B,iGAAiG;IACjG,KAAK,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACjC,0EAA0E;IAC1E,MAAM,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACxE;AAID;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC/B,MAAM,EAAE,mBAAmB,EAC3B,OAAO,EAAE,uBAAuB,GAC9B,gBAAgB,CAoDlB"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless controller for a "set / reset your password" page — the token
|
|
3
|
+
* consumer at the end of Better Auth's forget-password round trip. nk-auth owns
|
|
4
|
+
* the *sender* (`makeEmailSenders.sendResetPassword`) and the endpoint contract;
|
|
5
|
+
* this closes the loop so a site never touches the `resetPassword` endpoint name
|
|
6
|
+
* or hand-rolls the invalid-token / success / validation state machine. The site
|
|
7
|
+
* brings its own shell (inputs, buttons, i18n) and wires it to what this returns.
|
|
8
|
+
*
|
|
9
|
+
* The same page serves both entry points, because both land on the identical URL
|
|
10
|
+
* with a `?token=`: the classic "I forgot my password" flow, and "set a password
|
|
11
|
+
* for my social-only account" (Better Auth's reset endpoint creates the
|
|
12
|
+
* credential when the user has none — see the package README).
|
|
13
|
+
*
|
|
14
|
+
* "use client";
|
|
15
|
+
* const { status, error, submit } = useResetPassword(authClient, { token });
|
|
16
|
+
* if (status === "invalid") return <InvalidLink />;
|
|
17
|
+
* if (status === "success") return <Done />;
|
|
18
|
+
* // <form onSubmit={() => submit(pw, confirm)}> … {error && t(error.code)} …
|
|
19
|
+
*/
|
|
20
|
+
import { useCallback, useState } from "react";
|
|
21
|
+
import { validateNewPassword, } from "./password.js";
|
|
22
|
+
const RESET_FAILED_FALLBACK = "Could not reset your password.";
|
|
23
|
+
/**
|
|
24
|
+
* Drive a reset-password form. Returns the current `status`, the last `error`
|
|
25
|
+
* (with an i18n-friendly `code`), and a `submit(newPassword, confirmPassword)`
|
|
26
|
+
* that runs {@link validateNewPassword} against the policy before hitting the
|
|
27
|
+
* server. Pass the resolved `minLength` / `maxLength` when the site overrides
|
|
28
|
+
* Better Auth's defaults so client and server agree.
|
|
29
|
+
*/
|
|
30
|
+
export function useResetPassword(client, options) {
|
|
31
|
+
const { token, minLength, maxLength } = options;
|
|
32
|
+
const [status, setStatus] = useState(token ? "idle" : "invalid");
|
|
33
|
+
const [error, setError] = useState(null);
|
|
34
|
+
const submit = useCallback(async (newPassword, confirmPassword) => {
|
|
35
|
+
if (!token) {
|
|
36
|
+
setStatus("invalid");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const validation = validateNewPassword(newPassword, confirmPassword, {
|
|
40
|
+
minLength,
|
|
41
|
+
maxLength,
|
|
42
|
+
});
|
|
43
|
+
if (validation) {
|
|
44
|
+
setError(validation);
|
|
45
|
+
setStatus("error");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
setError(null);
|
|
49
|
+
setStatus("submitting");
|
|
50
|
+
try {
|
|
51
|
+
const { error: resetError } = await client.resetPassword({
|
|
52
|
+
newPassword,
|
|
53
|
+
token,
|
|
54
|
+
});
|
|
55
|
+
if (resetError) {
|
|
56
|
+
setError({
|
|
57
|
+
code: "reset_failed",
|
|
58
|
+
message: resetError.message ?? RESET_FAILED_FALLBACK,
|
|
59
|
+
});
|
|
60
|
+
setStatus("error");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
setStatus("success");
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
setError({
|
|
67
|
+
code: "reset_failed",
|
|
68
|
+
message: err instanceof Error ? err.message : RESET_FAILED_FALLBACK,
|
|
69
|
+
});
|
|
70
|
+
setStatus("error");
|
|
71
|
+
}
|
|
72
|
+
}, [token, minLength, maxLength, client]);
|
|
73
|
+
return { status, error, submit };
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=reset-password.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reset-password.js","sourceRoot":"","sources":["../src/reset-password.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAC9C,OAAO,EAGN,mBAAmB,GACnB,MAAM,eAAe,CAAC;AAuCvB,MAAM,qBAAqB,GAAG,gCAAgC,CAAC;AAE/D;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAC/B,MAA2B,EAC3B,OAAgC;IAEhC,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC;IAChD,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CACnC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAC1B,CAAC;IACF,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAA4B,IAAI,CAAC,CAAC;IAEpE,MAAM,MAAM,GAAG,WAAW,CACzB,KAAK,EAAE,WAAmB,EAAE,eAAuB,EAAiB,EAAE;QACrE,IAAI,CAAC,KAAK,EAAE,CAAC;YACZ,SAAS,CAAC,SAAS,CAAC,CAAC;YACrB,OAAO;QACR,CAAC;QAED,MAAM,UAAU,GAAG,mBAAmB,CAAC,WAAW,EAAE,eAAe,EAAE;YACpE,SAAS;YACT,SAAS;SACT,CAAC,CAAC;QACH,IAAI,UAAU,EAAE,CAAC;YAChB,QAAQ,CAAC,UAAU,CAAC,CAAC;YACrB,SAAS,CAAC,OAAO,CAAC,CAAC;YACnB,OAAO;QACR,CAAC;QAED,QAAQ,CAAC,IAAI,CAAC,CAAC;QACf,SAAS,CAAC,YAAY,CAAC,CAAC;QACxB,IAAI,CAAC;YACJ,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC;gBACxD,WAAW;gBACX,KAAK;aACL,CAAC,CAAC;YACH,IAAI,UAAU,EAAE,CAAC;gBAChB,QAAQ,CAAC;oBACR,IAAI,EAAE,cAAc;oBACpB,OAAO,EAAE,UAAU,CAAC,OAAO,IAAI,qBAAqB;iBACpD,CAAC,CAAC;gBACH,SAAS,CAAC,OAAO,CAAC,CAAC;gBACnB,OAAO;YACR,CAAC;YACD,SAAS,CAAC,SAAS,CAAC,CAAC;QACtB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,QAAQ,CAAC;gBACR,IAAI,EAAE,cAAc;gBACpB,OAAO,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,qBAAqB;aACnE,CAAC,CAAC;YACH,SAAS,CAAC,OAAO,CAAC,CAAC;QACpB,CAAC;IACF,CAAC,EACD,CAAC,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,CAAC,CACrC,CAAC;IAEF,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;AAClC,CAAC"}
|
package/dist/server.d.ts
CHANGED
|
@@ -18,6 +18,17 @@ interface AuthLike<S extends SessionLike> {
|
|
|
18
18
|
getSession: (input: {
|
|
19
19
|
headers: Headers;
|
|
20
20
|
}) => Promise<S | null>;
|
|
21
|
+
/**
|
|
22
|
+
* Better Auth's `/list-accounts`: the auth methods linked to the current
|
|
23
|
+
* session's user, each `{ providerId }` ("credential" for email/password,
|
|
24
|
+
* or a social provider like "google"). Present on every real `betterAuth()`
|
|
25
|
+
* instance; powers `getLinkedProviders` / `hasCredentialAccount` below.
|
|
26
|
+
*/
|
|
27
|
+
listUserAccounts: (input: {
|
|
28
|
+
headers: Headers;
|
|
29
|
+
}) => Promise<Array<{
|
|
30
|
+
providerId: string;
|
|
31
|
+
}>>;
|
|
21
32
|
};
|
|
22
33
|
}
|
|
23
34
|
export interface AuthHelpersOptions {
|
|
@@ -36,5 +47,7 @@ export declare function createAuthHelpers<S extends SessionLike>(auth: AuthLike<
|
|
|
36
47
|
requireSession: () => Promise<S>;
|
|
37
48
|
requireUser: () => Promise<S["user"]>;
|
|
38
49
|
redirectIfAuthenticated: (to: string) => Promise<void>;
|
|
50
|
+
getLinkedProviders: () => Promise<string[]>;
|
|
51
|
+
hasCredentialAccount: () => Promise<boolean>;
|
|
39
52
|
};
|
|
40
53
|
//# sourceMappingURL=server.d.ts.map
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AA6BA;;;;GAIG;AACH,OAAO,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAElE,oEAAoE;AACpE,UAAU,WAAW;IACpB,IAAI,EAAE,OAAO,CAAC;CACd;AAED;;;;GAIG;AACH,UAAU,QAAQ,CAAC,CAAC,SAAS,WAAW;IACvC,GAAG,EAAE;QACJ,UAAU,EAAE,CAAC,KAAK,EAAE;YAAE,OAAO,EAAE,OAAO,CAAA;SAAE,KAAK,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;QAC/D;;;;;WAKG;QACH,gBAAgB,EAAE,CAAC,KAAK,EAAE;YACzB,OAAO,EAAE,OAAO,CAAC;SACjB,KAAK,OAAO,CAAC,KAAK,CAAC;YAAE,UAAU,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC,CAAC;KAC7C,CAAC;CACF;AAED,MAAM,WAAW,kBAAkB;IAClC,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,WAAW,EACtD,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,EACjB,OAAO,GAAE,kBAAuB;sBAMH,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;mBAKpB,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;0BAkClB,OAAO,CAAC,CAAC,CAAC;uBAXb,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;kCAuBL,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;8BAU5B,OAAO,CAAC,MAAM,EAAE,CAAC;gCAcf,OAAO,CAAC,OAAO,CAAC;EAcvD"}
|
package/dist/server.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
import { cookies, headers } from "next/headers";
|
|
26
26
|
import { redirect } from "next/navigation";
|
|
27
27
|
import { NK_AUTH_PATH_HEADER, signInUrl } from "./gating-internals.js";
|
|
28
|
+
import { CREDENTIAL_PROVIDER_ID } from "./password.js";
|
|
28
29
|
/**
|
|
29
30
|
* Validate a `next` redirect param: returns it only if it's an internal,
|
|
30
31
|
* non-protocol-relative path, else null. Use on the login page before honoring
|
|
@@ -87,12 +88,37 @@ export function createAuthHelpers(auth, options = {}) {
|
|
|
87
88
|
if (await getSession())
|
|
88
89
|
redirect(to);
|
|
89
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* The `providerId`s linked to the current session's user — "credential" for
|
|
93
|
+
* email/password, plus any social providers ("google", …). Empty when signed
|
|
94
|
+
* out. Reads Better Auth's own `/list-accounts`, so a site never queries the
|
|
95
|
+
* `account` table or hard-codes provider strings itself.
|
|
96
|
+
*/
|
|
97
|
+
async function getLinkedProviders() {
|
|
98
|
+
const accounts = await auth.api.listUserAccounts({
|
|
99
|
+
headers: await headers(),
|
|
100
|
+
});
|
|
101
|
+
return accounts.map((account) => account.providerId);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Whether the current user has an email/password credential. Drives the
|
|
105
|
+
* "Change password" vs "Set password" decision on a security page: a user who
|
|
106
|
+
* signed up purely through a social provider has none until they set one
|
|
107
|
+
* (typically via the reset-password / forgot-password flow — see the
|
|
108
|
+
* `useResetPassword` client hook). Returns false when signed out.
|
|
109
|
+
*/
|
|
110
|
+
async function hasCredentialAccount() {
|
|
111
|
+
const providers = await getLinkedProviders();
|
|
112
|
+
return providers.includes(CREDENTIAL_PROVIDER_ID);
|
|
113
|
+
}
|
|
90
114
|
return {
|
|
91
115
|
getSession,
|
|
92
116
|
getUser,
|
|
93
117
|
requireSession,
|
|
94
118
|
requireUser,
|
|
95
119
|
redirectIfAuthenticated,
|
|
120
|
+
getLinkedProviders,
|
|
121
|
+
hasCredentialAccount,
|
|
96
122
|
};
|
|
97
123
|
}
|
|
98
124
|
//# sourceMappingURL=server.js.map
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC3C,OAAO,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ingram-tech/nk-auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "The Ingram Better Auth foundation: composable presets (org, dual-shape JWT, active-org hooks, pg pool) for Next.js sites.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -41,6 +41,10 @@
|
|
|
41
41
|
"types": "./dist/paths.d.ts",
|
|
42
42
|
"import": "./dist/paths.js"
|
|
43
43
|
},
|
|
44
|
+
"./password": {
|
|
45
|
+
"types": "./dist/password.d.ts",
|
|
46
|
+
"import": "./dist/password.js"
|
|
47
|
+
},
|
|
44
48
|
"./server": {
|
|
45
49
|
"types": "./dist/server.d.ts",
|
|
46
50
|
"import": "./dist/server.js"
|
|
@@ -61,10 +65,10 @@
|
|
|
61
65
|
"test": "vitest run"
|
|
62
66
|
},
|
|
63
67
|
"dependencies": {
|
|
64
|
-
"@ingram-tech/nk-db": "^1.
|
|
68
|
+
"@ingram-tech/nk-db": "^1.1.0",
|
|
65
69
|
"bcrypt": "^6.0.0",
|
|
66
|
-
"jose": "^6.
|
|
67
|
-
"zod": "^4.
|
|
70
|
+
"jose": "^6.2.3",
|
|
71
|
+
"zod": "^4.4.3"
|
|
68
72
|
},
|
|
69
73
|
"peerDependencies": {
|
|
70
74
|
"@better-auth/passkey": "^1.6.0",
|
|
@@ -88,17 +92,17 @@
|
|
|
88
92
|
}
|
|
89
93
|
},
|
|
90
94
|
"devDependencies": {
|
|
91
|
-
"@better-auth/passkey": "^1.6.
|
|
95
|
+
"@better-auth/passkey": "^1.6.22",
|
|
92
96
|
"@ingram-tech/nk-dev": "0.2.4",
|
|
93
97
|
"@types/bcrypt": "^6.0.0",
|
|
94
|
-
"@types/node": "^
|
|
95
|
-
"@types/pg": "^8.
|
|
96
|
-
"@types/react": "^19.
|
|
97
|
-
"better-auth": "^1.6.
|
|
98
|
+
"@types/node": "^26.0.1",
|
|
99
|
+
"@types/pg": "^8.20.0",
|
|
100
|
+
"@types/react": "^19.2.17",
|
|
101
|
+
"better-auth": "^1.6.22",
|
|
98
102
|
"next": "^16.2.9",
|
|
99
|
-
"pg": "^8.
|
|
100
|
-
"react": "^19.
|
|
103
|
+
"pg": "^8.22.0",
|
|
104
|
+
"react": "^19.2.7",
|
|
101
105
|
"typescript": "^6.0.3",
|
|
102
|
-
"vitest": "^4.1.
|
|
106
|
+
"vitest": "^4.1.9"
|
|
103
107
|
}
|
|
104
108
|
}
|