@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 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
@@ -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;AAErD,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,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
@@ -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
@@ -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;
@@ -1 +1 @@
1
- {"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAUH,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,OAc1B,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,YAAY,QAAO,OAAgD,CAAC"}
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
- const schema = z.object({
16
- BETTER_AUTH_SECRET: z.string().min(1),
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 = schema.safeParse(process.env);
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 = () => schema.safeParse(process.env).success;
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;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC;IACvB,kBAAkB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACrC,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;AAQH;;;;GAIG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,GAAY,EAAE;IACpC,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC7C,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,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,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC"}
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"}
@@ -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
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AA4BA;;;;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;KAC/D,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;EAWjE"}
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
@@ -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;AAEvE;;;;GAIG;AACH,OAAO,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AA6BlE,MAAM,UAAU,iBAAiB,CAChC,IAAiB,EACjB,UAA8B,EAAE;IAEhC,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,QAAQ,CAAC;IAClD,MAAM,YAAY,GAAG,OAAO,CAAC,mBAAmB,IAAI,aAAa,CAAC;IAElE,4DAA4D;IAC5D,KAAK,UAAU,UAAU;QACxB,OAAO,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,MAAM,OAAO,EAAE,EAAE,CAAC,CAAC;IAC1D,CAAC;IAED,uCAAuC;IACvC,KAAK,UAAU,OAAO;QACrB,MAAM,OAAO,GAAG,MAAM,UAAU,EAAE,CAAC;QACnC,OAAO,OAAO,EAAE,IAAI,IAAI,IAAI,CAAC;IAC9B,CAAC;IAED;;;;;OAKG;IACH,KAAK,UAAU,YAAY;QAC1B,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;QACzD,MAAM,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;QACxC,MAAM,KAAK,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC;QACtE,OAAO,SAAS,CAAC,UAAU,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC/C,CAAC;IAED;;;;OAIG;IACH,KAAK,UAAU,WAAW;QACzB,MAAM,IAAI,GAAG,MAAM,OAAO,EAAE,CAAC;QAC7B,oEAAoE;QACpE,IAAI,CAAC,IAAI;YAAE,QAAQ,CAAC,MAAM,YAAY,EAAE,CAAC,CAAC;QAC1C,OAAO,IAAI,CAAC;IACb,CAAC;IAED;;;OAGG;IACH,KAAK,UAAU,cAAc;QAC5B,MAAM,OAAO,GAAG,MAAM,UAAU,EAAE,CAAC;QACnC,IAAI,CAAC,OAAO;YAAE,QAAQ,CAAC,MAAM,YAAY,EAAE,CAAC,CAAC;QAC7C,OAAO,OAAO,CAAC;IAChB,CAAC;IAED;;;;;OAKG;IACH,KAAK,UAAU,uBAAuB,CAAC,EAAU;QAChD,IAAI,MAAM,UAAU,EAAE;YAAE,QAAQ,CAAC,EAAE,CAAC,CAAC;IACtC,CAAC;IAED,OAAO;QACN,UAAU;QACV,OAAO;QACP,cAAc;QACd,WAAW;QACX,uBAAuB;KACvB,CAAC;AACH,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.9.1",
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"