@ingram-tech/nk-auth 0.9.1 → 0.11.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 +84 -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/keys.d.ts +6 -0
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +28 -4
- package/dist/keys.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 +5 -1
package/README.md
CHANGED
|
@@ -48,6 +48,10 @@ BETTER_AUTH_URL=https://example.com
|
|
|
48
48
|
DATABASE_URL=… # direct Postgres connection (:5432)
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
+
Outside production, `BETTER_AUTH_SECRET` falls back to a well-known insecure
|
|
52
|
+
placeholder, so local dev and tests run without setting it (a warning is logged).
|
|
53
|
+
In production it stays required — a missing secret throws at startup.
|
|
54
|
+
|
|
51
55
|
## 1. Apply the schema
|
|
52
56
|
|
|
53
57
|
```bash
|
|
@@ -176,6 +180,8 @@ export const {
|
|
|
176
180
|
requireSession,
|
|
177
181
|
requireUser,
|
|
178
182
|
redirectIfAuthenticated,
|
|
183
|
+
getLinkedProviders, // providerIds linked to the current user
|
|
184
|
+
hasCredentialAccount, // does the current user have an email/password login?
|
|
179
185
|
} = createAuthHelpers(auth);
|
|
180
186
|
```
|
|
181
187
|
|
|
@@ -244,6 +250,84 @@ middleware injects — so the self-heal (next + clearing) needs the middleware.
|
|
|
244
250
|
Sites that skip middleware still get validated gating from the server helpers,
|
|
245
251
|
just without automatic `next`/clearing.
|
|
246
252
|
|
|
253
|
+
## 6. Passwords: change, set, and reset
|
|
254
|
+
|
|
255
|
+
nk-auth owns the reset **sender** (`makeEmailSenders.sendResetPassword`, §2) and
|
|
256
|
+
now closes the loop so a site never touches Better Auth's `account` table or
|
|
257
|
+
endpoint names directly. Three pieces:
|
|
258
|
+
|
|
259
|
+
**Detect what login the user has.** A social-only account (Google, …) has no
|
|
260
|
+
password credential until it sets one. `hasCredentialAccount()` drives the
|
|
261
|
+
"Change password" vs "Set password" choice on a security page; reach for
|
|
262
|
+
`getLinkedProviders()` when you need the full list.
|
|
263
|
+
|
|
264
|
+
```tsx
|
|
265
|
+
// app/settings/security/page.tsx (server component)
|
|
266
|
+
import { hasCredentialAccount } from "@/lib/auth/session";
|
|
267
|
+
export default async function Security() {
|
|
268
|
+
return (await hasCredentialAccount()) ? <ChangePassword /> : <SetPassword />;
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**Set a password without a current one.** A social-only user has no current
|
|
273
|
+
password to verify, so the re-auth is an emailed reset link — clicking it proves
|
|
274
|
+
account ownership. Trigger it with the standard client call; there is no separate
|
|
275
|
+
"set password" endpoint:
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
await authClient.requestPasswordReset({
|
|
279
|
+
email,
|
|
280
|
+
redirectTo: "/reset-password", // your token-consumer page (below)
|
|
281
|
+
});
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Better Auth's reset endpoint **creates** the credential when the user has none,
|
|
285
|
+
so the same flow both resets a forgotten password and sets a first one. That
|
|
286
|
+
guarantee is pinned by `reset-password.test.ts` against a real instance, so a
|
|
287
|
+
Better Auth upgrade can't silently break the set-password path.
|
|
288
|
+
|
|
289
|
+
**Consume the token.** The email link lands on your page with `?token=` (or
|
|
290
|
+
`?error=INVALID_TOKEN`). `useResetPassword` is the headless state machine —
|
|
291
|
+
invalid-token, submitting, success, and policy validation (length + match) —
|
|
292
|
+
against the shared `passwordSchema` bounds. You bring the shell:
|
|
293
|
+
|
|
294
|
+
```tsx
|
|
295
|
+
"use client";
|
|
296
|
+
import { useResetPassword } from "@ingram-tech/nk-auth/client";
|
|
297
|
+
import { authClient } from "@/lib/auth/client";
|
|
298
|
+
|
|
299
|
+
export function ResetPasswordForm({ token }: { token: string | null }) {
|
|
300
|
+
const { status, error, submit } = useResetPassword(authClient, { token });
|
|
301
|
+
if (status === "invalid") return <p>This link is invalid or has expired.</p>;
|
|
302
|
+
if (status === "success") return <p>Password set. You can now sign in.</p>;
|
|
303
|
+
// <form onSubmit={() => submit(newPassword, confirm)}>; render `error.code`
|
|
304
|
+
// (stable, for i18n) or `error.message` (English fallback).
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Policy constants live at `@ingram-tech/nk-auth/password` (pure, importable from
|
|
309
|
+
both ends): `DEFAULT_MIN_PASSWORD_LENGTH` / `DEFAULT_MAX_PASSWORD_LENGTH`,
|
|
310
|
+
`passwordSchema()`, and `validateNewPassword()`. Pass your resolved bounds to
|
|
311
|
+
`useResetPassword({ token, minLength, maxLength })` if you override Better Auth's
|
|
312
|
+
defaults, so the form and the server never drift.
|
|
313
|
+
|
|
314
|
+
**`.well-known/change-password`.** Add the [W3C well-known
|
|
315
|
+
redirect](https://w3c.github.io/webappsec-change-password-url/) so password
|
|
316
|
+
managers deep-link straight to your security page. It's a per-app route target,
|
|
317
|
+
so nk-auth documents the convention rather than shipping it — in `next.config`:
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
async redirects() {
|
|
321
|
+
return [
|
|
322
|
+
{
|
|
323
|
+
source: "/.well-known/change-password",
|
|
324
|
+
destination: "/settings/security", // your Change/Set password page
|
|
325
|
+
permanent: false,
|
|
326
|
+
},
|
|
327
|
+
];
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
247
331
|
## Migrating bcrypt passwords to scrypt
|
|
248
332
|
|
|
249
333
|
`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/keys.d.ts
CHANGED
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
* BETTER_AUTH_SECRET — session/CSRF signing key (`openssl rand -hex 32`)
|
|
11
11
|
* BETTER_AUTH_URL — canonical site origin, e.g. "https://example.com"
|
|
12
12
|
* DATABASE_URL — direct Postgres connection for Better Auth (:5432)
|
|
13
|
+
*
|
|
14
|
+
* Outside production, BETTER_AUTH_SECRET falls back to a well-known insecure
|
|
15
|
+
* placeholder so local dev and tests run without hand-setting it (`nk dev` and
|
|
16
|
+
* plain `next dev` alike). In production it stays strictly required — a missing
|
|
17
|
+
* secret throws at startup rather than silently signing sessions with a value
|
|
18
|
+
* anyone could guess.
|
|
13
19
|
*/
|
|
14
20
|
export interface AuthEnv {
|
|
15
21
|
secret: string;
|
package/dist/keys.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AA6BH,MAAM,WAAW,OAAO;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,eAAO,MAAM,OAAO,QAAO,OAoB1B,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,YAAY,QAAO,OAAuD,CAAC"}
|
package/dist/keys.js
CHANGED
|
@@ -10,20 +10,40 @@
|
|
|
10
10
|
* BETTER_AUTH_SECRET — session/CSRF signing key (`openssl rand -hex 32`)
|
|
11
11
|
* BETTER_AUTH_URL — canonical site origin, e.g. "https://example.com"
|
|
12
12
|
* DATABASE_URL — direct Postgres connection for Better Auth (:5432)
|
|
13
|
+
*
|
|
14
|
+
* Outside production, BETTER_AUTH_SECRET falls back to a well-known insecure
|
|
15
|
+
* placeholder so local dev and tests run without hand-setting it (`nk dev` and
|
|
16
|
+
* plain `next dev` alike). In production it stays strictly required — a missing
|
|
17
|
+
* secret throws at startup rather than silently signing sessions with a value
|
|
18
|
+
* anyone could guess.
|
|
13
19
|
*/
|
|
14
20
|
import { z } from "zod";
|
|
15
|
-
|
|
16
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Well-known, deliberately insecure secret used ONLY when NODE_ENV is not
|
|
23
|
+
* "production". Never applied to a production build — see `secretField`.
|
|
24
|
+
*/
|
|
25
|
+
const DEV_SECRET_PLACEHOLDER = "nk-dev-insecure-placeholder-not-for-production";
|
|
26
|
+
/**
|
|
27
|
+
* In production the secret is required; everywhere else it defaults to the
|
|
28
|
+
* insecure placeholder. Read NODE_ENV at parse time (not module load) so the
|
|
29
|
+
* same process can be exercised under either mode.
|
|
30
|
+
*/
|
|
31
|
+
const secretField = () => process.env.NODE_ENV === "production"
|
|
32
|
+
? z.string().min(1)
|
|
33
|
+
: z.string().min(1).default(DEV_SECRET_PLACEHOLDER);
|
|
34
|
+
const buildSchema = () => z.object({
|
|
35
|
+
BETTER_AUTH_SECRET: secretField(),
|
|
17
36
|
BETTER_AUTH_URL: z.string().url(),
|
|
18
37
|
DATABASE_URL: z.string().min(1),
|
|
19
38
|
});
|
|
39
|
+
let warnedPlaceholder = false;
|
|
20
40
|
/**
|
|
21
41
|
* Read and validate all nk-auth env vars at once. Throws a single error listing
|
|
22
42
|
* everything missing/invalid, so a misconfigured site fails fast at startup
|
|
23
43
|
* rather than at first sign-in.
|
|
24
44
|
*/
|
|
25
45
|
export const authEnv = () => {
|
|
26
|
-
const result =
|
|
46
|
+
const result = buildSchema().safeParse(process.env);
|
|
27
47
|
if (!result.success) {
|
|
28
48
|
const issues = result.error.issues
|
|
29
49
|
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
|
@@ -31,6 +51,10 @@ export const authEnv = () => {
|
|
|
31
51
|
throw new Error(`@ingram-tech/nk-auth: invalid environment — ${issues}`);
|
|
32
52
|
}
|
|
33
53
|
const env = result.data;
|
|
54
|
+
if (env.BETTER_AUTH_SECRET === DEV_SECRET_PLACEHOLDER && !warnedPlaceholder) {
|
|
55
|
+
warnedPlaceholder = true;
|
|
56
|
+
console.warn("@ingram-tech/nk-auth: BETTER_AUTH_SECRET is unset — using an insecure dev placeholder. Set it before deploying.");
|
|
57
|
+
}
|
|
34
58
|
return {
|
|
35
59
|
secret: env.BETTER_AUTH_SECRET,
|
|
36
60
|
baseURL: env.BETTER_AUTH_URL,
|
|
@@ -38,5 +62,5 @@ export const authEnv = () => {
|
|
|
38
62
|
};
|
|
39
63
|
};
|
|
40
64
|
/** Whether nk-auth is fully configured (lets callers degrade in local/dev). */
|
|
41
|
-
export const isConfigured = () =>
|
|
65
|
+
export const isConfigured = () => buildSchema().safeParse(process.env).success;
|
|
42
66
|
//# sourceMappingURL=keys.js.map
|
package/dist/keys.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keys.js","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"keys.js","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;GAGG;AACH,MAAM,sBAAsB,GAAG,gDAAgD,CAAC;AAEhF;;;;GAIG;AACH,MAAM,WAAW,GAAG,GAAG,EAAE,CACxB,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY;IACpC,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACnB,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAC;AAEtD,MAAM,WAAW,GAAG,GAAG,EAAE,CACxB,CAAC,CAAC,MAAM,CAAC;IACR,kBAAkB,EAAE,WAAW,EAAE;IACjC,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IACjC,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CAC/B,CAAC,CAAC;AAEJ,IAAI,iBAAiB,GAAG,KAAK,CAAC;AAQ9B;;;;GAIG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,GAAY,EAAE;IACpC,MAAM,MAAM,GAAG,WAAW,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACpD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACrB,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM;aAChC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC;aAC3D,IAAI,CAAC,IAAI,CAAC,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,+CAA+C,MAAM,EAAE,CAAC,CAAC;IAC1E,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC;IACxB,IAAI,GAAG,CAAC,kBAAkB,KAAK,sBAAsB,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAC7E,iBAAiB,GAAG,IAAI,CAAC;QACzB,OAAO,CAAC,IAAI,CACX,iHAAiH,CACjH,CAAC;IACH,CAAC;IACD,OAAO;QACN,MAAM,EAAE,GAAG,CAAC,kBAAkB;QAC9B,OAAO,EAAE,GAAG,CAAC,eAAe;QAC5B,WAAW,EAAE,GAAG,CAAC,YAAY;KAC7B,CAAC;AACH,CAAC,CAAC;AAEF,+EAA+E;AAC/E,MAAM,CAAC,MAAM,YAAY,GAAG,GAAY,EAAE,CAAC,WAAW,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,OAAO,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.11.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"
|