@ingram-tech/nk-auth 0.12.3 → 0.13.1

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
@@ -70,16 +70,64 @@ the app: the security-sensitive default then lives in exactly one place.
70
70
 
71
71
  ## 1. Apply the schema
72
72
 
73
- ```bash
74
- cp node_modules/@ingram-tech/nk-auth/migrations/0001_better_auth.sql \
75
- migrations/$(date +%Y%m%d%H%M%S)_better_auth.sql
73
+ nk-auth **owns its auth tables as its own migration chain** — the versioned SQL
74
+ files it ships in `migrations/`, journaled separately from your app's `drizzle/`
75
+ migrations. You don't copy anything in; you point the runner at the shipped
76
+ folder. It creates Better Auth's tables (`user`, `session`, `account`,
77
+ `verification`, `jwks`, `passkey`), defaults new user ids to UUIDs, and puts
78
+ **deny-all RLS** on all of them (Better Auth reaches them through its own
79
+ privileged connection). See [db-package.md § "The nk-auth migration
80
+ chain"](../../docs/db-package.md#the-nk-auth-migration-chain) for the full model.
81
+
82
+ The auth chain runs **before** your app chain, because your app tables FK to
83
+ `user`.
84
+
85
+ **Production / deploy** — add the auth chain to your `db:migrate` script,
86
+ auth-first, on its own journal table:
87
+
88
+ ```jsonc
89
+ // package.json
90
+ "scripts": {
91
+ "db:migrate": "nk-pg-migrate --migrations node_modules/@ingram-tech/nk-auth/migrations --table __nkauth_migrations && nk-pg-migrate"
92
+ }
93
+ ```
94
+
95
+ The first invocation applies (and journals) nk-auth's chain; the second applies
96
+ your app's `drizzle/` chain. Both are idempotent and drift-checked, so re-running
97
+ is a no-op — see the [`nk-pg-migrate` runner](../../docs/db-package.md#migrations).
98
+
99
+ **Local dev** — nothing to do. `nk dev` resolves nk-auth and applies its chain to
100
+ the local PGlite database automatically, before your `drizzle/` migrations.
101
+
102
+ **Tests** — when a test needs the auth tables, pass the shipped folder to
103
+ `createTestDb` as a dependency chain (applied first, own journal table):
104
+
105
+ ```ts
106
+ import { createRequire } from "node:module";
107
+ import { dirname, join } from "node:path";
108
+
109
+ const authMigrations = join(
110
+ dirname(createRequire(import.meta.url).resolve("@ingram-tech/nk-auth/package.json")),
111
+ "migrations",
112
+ );
113
+
114
+ const db = await createTestDb({
115
+ dependencyMigrations: [{ folder: authMigrations, table: "__nkauth_migrations" }],
116
+ });
76
117
  ```
77
118
 
78
- It creates Better Auth's tables (`user`, `session`, `account`, `verification`,
79
- `jwks`, `passkey`), defaults new user ids to UUIDs, and puts **deny-all RLS** on
80
- all of them (Better Auth reaches them through its own privileged connection).
81
- Reconcile against your pinned `better-auth` with `npx @better-auth/cli generate`
82
- after upgrades.
119
+ **Upgrading better-auth** never means hand-writing a migration in your site: a
120
+ schema-changing upgrade ships as a new file in nk-auth's chain, so you bump the
121
+ dependency and your next migrate applies it. Reconcile the shipped schema against
122
+ a pinned `better-auth` with `npx @better-auth/cli generate` (a nextkit-maintainer
123
+ task, done once here — not per site).
124
+
125
+ > **Adopting from the old copy-in model?** Earlier docs told sites to `cp` the
126
+ > baseline into their own `drizzle/` chain. If you already did, keep that file
127
+ > (deleting an applied migration causes journal drift) and just add the auth-chain
128
+ > invocation above: its DDL is all `… if not exists`, so it no-ops against your
129
+ > existing tables and simply records the nk-auth journal. New auth migrations then
130
+ > flow through the shipped chain from here on.
83
131
 
84
132
  ## 2. Configure the server
85
133
 
@@ -103,9 +151,13 @@ import { betterAuth } from "better-auth";
103
151
  import { pool } from "@/lib/db"; // the ONE shared createPool() from @ingram-tech/nk-db
104
152
 
105
153
  const env = authEnv();
106
- const email = makeEmailSenders(({ to, subject, url }) =>
107
- sendEmail({ to, from: fromAddress(), subject, text: url, html: url }),
108
- );
154
+ // Render a real template per `kind` these are the first mails a user ever
155
+ // gets from you. `text: url, html: url` ships a bare link that reads as
156
+ // phishing; see "Auth emails" below.
157
+ const email = makeEmailSenders(async ({ kind, to, url, user, newEmail }) => {
158
+ const { subject, html, text } = await renderAuthEmail({ kind, url, user, newEmail });
159
+ sendEmail({ to, from: fromAddress("Example", "no-reply"), subject, html, text });
160
+ });
109
161
 
110
162
  export const auth = betterAuth({
111
163
  database: pool, // inject the shared pool — exactly one pool per process
@@ -125,6 +177,15 @@ export const auth = betterAuth({
125
177
  sendResetPassword: email.sendResetPassword,
126
178
  },
127
179
  emailVerification: { sendVerificationEmail: email.sendVerificationEmail },
180
+ user: {
181
+ // Confirms the move from the CURRENT address. Note the name: it is
182
+ // `sendChangeEmailConfirmation`, and betterAuth() does not
183
+ // excess-property-check, so a wrong name here silently never fires.
184
+ changeEmail: {
185
+ enabled: true,
186
+ sendChangeEmailConfirmation: email.sendChangeEmailConfirmation,
187
+ },
188
+ },
128
189
  socialProviders: {
129
190
  google: {
130
191
  clientId: process.env.GOOGLE_CLIENT_ID ?? "",
@@ -149,6 +210,57 @@ import { auth } from "@/lib/auth";
149
210
  export const { GET, POST } = toNextJsHandler(auth);
150
211
  ```
151
212
 
213
+ ## Auth emails
214
+
215
+ `makeEmailSenders(send)` returns the three callbacks Better Auth needs. Every
216
+ message reaches your `send` with a **`kind`** discriminator — switch on that to
217
+ pick a template. Never switch on `subject`: it is default English copy that a
218
+ localized site is expected to throw away.
219
+
220
+ | `kind` | Better Auth option | Goes to |
221
+ | ---------------- | --------------------------------------------- | --------------------- |
222
+ | `verify-email` | `emailVerification.sendVerificationEmail` | the new signup |
223
+ | `reset-password` | `emailAndPassword.sendResetPassword` | the account address |
224
+ | `change-email` | `user.changeEmail.sendChangeEmailConfirmation` | the **current** address |
225
+
226
+ Each message also carries `user` (`id` for a locale/preferences lookup, `name`
227
+ to personalize), `token`, and the originating `request` — whose `Accept-Language`
228
+ is the only locale signal available for a signup verification, before the user
229
+ has any stored preference.
230
+
231
+ **Render a real template.** Verification, reset and change-email are the first
232
+ mail a user ever gets from you; a bare `<a href=url>url</a>` reads as phishing
233
+ and trains people to distrust your domain. Take the
234
+ [`registry`](https://github.com/ingram-technologies/registry) email components
235
+ (`shadcn add email-verification email-password-reset`) — they accept `heading` /
236
+ `body` / `ctaLabel` / `preview` overrides precisely so you can pass translated
237
+ copy — or write your own. Send auth links from the `no-reply` local part
238
+ (`fromAddress("Example", "no-reply")`), per
239
+ [transactional-email.md](../../docs/transactional-email.md).
240
+
241
+ **Spread all three in; do not hand-write them.** `betterAuth()` takes its
242
+ options through a generic, which switches **off** excess-property checking on
243
+ that object literal. A callback under a wrong-but-plausible name compiles
244
+ perfectly and then never fires:
245
+
246
+ ```ts
247
+ user: {
248
+ changeEmail: {
249
+ enabled: true,
250
+ // WRONG NAME. No type error — and no email, ever. The real option is
251
+ // `sendChangeEmailConfirmation`.
252
+ sendChangeEmailVerification: async ({ user, url }) => { /* dead code */ },
253
+ },
254
+ },
255
+ ```
256
+
257
+ That is not hypothetical — it shipped, and the failure is silent in both
258
+ directions: Better Auth falls through to sending the *verification* mail to the
259
+ **new** address instead, so the current address is never told its account is
260
+ moving. `makeEmailSenders` exists to keep you off that path; `options.ts` pins
261
+ all three names to the real Better Auth option types, so an upstream rename
262
+ fails nk-auth's build rather than quietly disabling your mail.
263
+
152
264
  ## 3. Query data with RLS intact
153
265
 
154
266
  Data access lives in [`@ingram-tech/nk-db`](../nk-db), not here. Query over the
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { type BackendJwtConfig, backendJwtOptions, verifyBackendJwt } from "./jwt.js";
2
2
  export { base58Id, fromPrefixedId, toPrefixedId, uuidGenerateId } from "./id.js";
3
3
  export { type AuthEnv, authEnv, authSecret, isConfigured } from "./keys.js";
4
- export { bcryptPassword, makeEmailSenders, makePasskeyOptions, type PasskeyConfig, passkeyOptionsForBaseUrl, type SendEmail, } from "./options.js";
4
+ export { type AuthEmailKind, type AuthEmailMessage, type AuthEmailUser, bcryptPassword, makeEmailSenders, makePasskeyOptions, type PasskeyConfig, passkeyOptionsForBaseUrl, type SendEmail, } from "./options.js";
5
5
  export { lastActiveOrganizationHooks, lastActiveOrganizationUserField, nkOrganizationDefaults, } from "./organization.js";
6
6
  export { CREDENTIAL_PROVIDER_ID, DEFAULT_MAX_PASSWORD_LENGTH, DEFAULT_MIN_PASSWORD_LENGTH, type PasswordPolicy, passwordSchema, type ResetPasswordError, type ResetPasswordErrorCode, validateNewPassword, } from "./password.js";
7
7
  export { authBasePath } from "./paths.js";
@@ -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,UAAU,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC5E,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"}
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,UAAU,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC5E,OAAO,EACN,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,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.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,UAAU,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC5E,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"}
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,UAAU,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC5E,OAAO,EAIN,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/options.d.ts CHANGED
@@ -57,28 +57,73 @@ export declare const makePasskeyOptions: (cfg: PasskeyConfig) => PasskeyOptions;
57
57
  * for an "app.example.com" origin).
58
58
  */
59
59
  export declare const passkeyOptionsForBaseUrl: (baseURL: string, rpName: string) => PasskeyOptions;
60
- /** Send one transactional email (wire to `@ingram-tech/nk-email`'s `sendEmail`). */
61
- export type SendEmail = (message: {
60
+ /**
61
+ * Which auth mail is being sent. Switch on this to pick a template — never on
62
+ * `subject`, which is English default copy a site is expected to replace.
63
+ */
64
+ export type AuthEmailKind = "verify-email" | "reset-password" | "change-email";
65
+ /** The subset of the Better Auth user these callbacks pass through. */
66
+ export interface AuthEmailUser {
67
+ id: string;
68
+ email: string;
69
+ name?: string;
70
+ }
71
+ /** One outgoing auth mail, handed to the site's sender. */
72
+ export interface AuthEmailMessage {
73
+ /** Discriminator — pick the template off this. */
74
+ kind: AuthEmailKind;
75
+ /**
76
+ * Recipient. For `change-email` this is deliberately the user's CURRENT
77
+ * address: confirming the move from the address that already owns the
78
+ * account is what stops a hijacked session from walking off with it.
79
+ */
62
80
  to: string;
81
+ /**
82
+ * Default English subject, for sites that don't localize. Prefer your own
83
+ * translated copy — see the README.
84
+ */
63
85
  subject: string;
86
+ /** The one-time action link. */
64
87
  url: string;
65
- }) => Promise<unknown>;
88
+ /** The raw token behind `url`, if you need to build your own link. */
89
+ token: string;
90
+ /** `id` lets you look up a locale/preferences; `name` personalizes copy. */
91
+ user: AuthEmailUser;
92
+ /** `change-email` only: the address the user is moving to. */
93
+ newEmail?: string;
94
+ /** The originating request — `Accept-Language` is a locale source. */
95
+ request?: Request;
96
+ }
97
+ /** Send one transactional email (wire to `@ingram-tech/nk-email`'s `sendEmail`). */
98
+ export type SendEmail = (message: AuthEmailMessage) => Promise<unknown>;
66
99
  /**
67
- * Email callbacks for `emailAndPassword.sendResetPassword` and
68
- * `emailVerification.sendVerificationEmail`, routed through your sender.
100
+ * Email callbacks for `emailAndPassword.sendResetPassword`,
101
+ * `emailVerification.sendVerificationEmail` and
102
+ * `user.changeEmail.sendChangeEmailConfirmation`, routed through your sender.
103
+ *
104
+ * Spread all three in. Hand-writing them is a trap: `betterAuth()` infers its
105
+ * options generically, so TypeScript does NOT excess-property-check that object
106
+ * literal — a callback under a misremembered name (`sendChangeEmailVerification`
107
+ * is the one people reach for) compiles clean and simply never fires. The
108
+ * `PinnedEmailSenders` assertion below ties these three names to the real Better
109
+ * Auth options, so a rename upstream breaks this build instead of your site.
69
110
  */
70
111
  export declare const makeEmailSenders: (send: SendEmail) => {
71
- sendResetPassword: ({ user, url, }: {
72
- user: {
73
- email: string;
74
- };
112
+ sendResetPassword: ({ user, url, token }: {
113
+ user: AuthEmailUser;
114
+ url: string;
115
+ token: string;
116
+ }, request?: Request) => Promise<void>;
117
+ sendVerificationEmail: ({ user, url, token }: {
118
+ user: AuthEmailUser;
75
119
  url: string;
76
- }) => Promise<void>;
77
- sendVerificationEmail: ({ user, url, }: {
78
- user: {
79
- email: string;
80
- };
120
+ token: string;
121
+ }, request?: Request) => Promise<void>;
122
+ sendChangeEmailConfirmation: ({ user, newEmail, url, token, }: {
123
+ user: AuthEmailUser;
124
+ newEmail: string;
81
125
  url: string;
82
- }) => Promise<void>;
126
+ token: string;
127
+ }, request?: Request) => Promise<void>;
83
128
  };
84
129
  //# sourceMappingURL=options.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAM3D,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAEzC;;;;;;;;GAQG;AAEH;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,cAAc;IAC1B,IAAI,aAAa,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC;IACzC,MAAM,wBAGH;QACF,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;KACjB,KAAG,OAAO,CAAC,OAAO,CAAC;CACpB,CAAC;AAEF,MAAM,WAAW,aAAa;IAC7B,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CAC1B;AAED,iFAAiF;AACjF,eAAO,MAAM,kBAAkB,QAAS,aAAa,KAAG,cAItD,CAAC;AAEH;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,wBAAwB,YAC3B,MAAM,UACP,MAAM,KACZ,cAKA,CAAC;AAEJ,oFAAoF;AACpF,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,EAAE;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;CACZ,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAEvB;;;GAGG;AACH,eAAO,MAAM,gBAAgB,SAAU,SAAS;IAC/C,iBAAiB,mBAGd;QACF,IAAI,EAAE;YAAE,KAAK,EAAE,MAAM,CAAA;SAAE,CAAC;QACxB,GAAG,EAAE,MAAM,CAAC;KACZ,KAAG,OAAO,CAAC,IAAI,CAAC;IAGjB,qBAAqB,mBAGlB;QACF,IAAI,EAAE;YAAE,KAAK,EAAE,MAAM,CAAA;SAAE,CAAC;QACxB,GAAG,EAAE,MAAM,CAAC;KACZ,KAAG,OAAO,CAAC,IAAI,CAAC;CAGhB,CAAC"}
1
+ {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAO3D,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAEzC;;;;;;;;GAQG;AAEH;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,cAAc;IAC1B,IAAI,aAAa,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC;IACzC,MAAM,wBAGH;QACF,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;KACjB,KAAG,OAAO,CAAC,OAAO,CAAC;CACpB,CAAC;AAEF,MAAM,WAAW,aAAa;IAC7B,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CAC1B;AAED,iFAAiF;AACjF,eAAO,MAAM,kBAAkB,QAAS,aAAa,KAAG,cAItD,CAAC;AAEH;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,wBAAwB,YAC3B,MAAM,UACP,MAAM,KACZ,cAKA,CAAC;AAEJ;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,cAAc,GAAG,gBAAgB,GAAG,cAAc,CAAC;AAE/E,uEAAuE;AACvE,MAAM,WAAW,aAAa;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAED,2DAA2D;AAC3D,MAAM,WAAW,gBAAgB;IAChC,kDAAkD;IAClD,IAAI,EAAE,aAAa,CAAC;IACpB;;;;OAIG;IACH,EAAE,EAAE,MAAM,CAAC;IACX;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB,gCAAgC;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,IAAI,EAAE,aAAa,CAAC;IACpB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,OAAO,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,oFAAoF;AACpF,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,EAAE,gBAAgB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAExE;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gBAAgB,SAAU,SAAS;IAC/C,iBAAiB,yBACM;QAAE,IAAI,EAAE,aAAa,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,YAC/D,OAAO,KACf,OAAO,CAAC,IAAI,CAAC;IAWhB,qBAAqB,yBACE;QAAE,IAAI,EAAE,aAAa,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,YAC/D,OAAO,KACf,OAAO,CAAC,IAAI,CAAC;IAWhB,2BAA2B,oCAMvB;QACF,IAAI,EAAE,aAAa,CAAC;QACpB,QAAQ,EAAE,MAAM,CAAC;QACjB,GAAG,EAAE,MAAM,CAAC;QACZ,KAAK,EAAE,MAAM,CAAC;KACd,YACS,OAAO,KACf,OAAO,CAAC,IAAI,CAAC;CAYf,CAAC"}
package/dist/options.js CHANGED
@@ -58,15 +58,60 @@ export const passkeyOptionsForBaseUrl = (baseURL, rpName) => makePasskeyOptions(
58
58
  origin: baseURL,
59
59
  });
60
60
  /**
61
- * Email callbacks for `emailAndPassword.sendResetPassword` and
62
- * `emailVerification.sendVerificationEmail`, routed through your sender.
61
+ * Email callbacks for `emailAndPassword.sendResetPassword`,
62
+ * `emailVerification.sendVerificationEmail` and
63
+ * `user.changeEmail.sendChangeEmailConfirmation`, routed through your sender.
64
+ *
65
+ * Spread all three in. Hand-writing them is a trap: `betterAuth()` infers its
66
+ * options generically, so TypeScript does NOT excess-property-check that object
67
+ * literal — a callback under a misremembered name (`sendChangeEmailVerification`
68
+ * is the one people reach for) compiles clean and simply never fires. The
69
+ * `PinnedEmailSenders` assertion below ties these three names to the real Better
70
+ * Auth options, so a rename upstream breaks this build instead of your site.
63
71
  */
64
72
  export const makeEmailSenders = (send) => ({
65
- sendResetPassword: async ({ user, url, }) => {
66
- await send({ to: user.email, subject: "Reset your password", url });
73
+ sendResetPassword: async ({ user, url, token }, request) => {
74
+ await send({
75
+ kind: "reset-password",
76
+ to: user.email,
77
+ subject: "Reset your password",
78
+ url,
79
+ token,
80
+ user,
81
+ request,
82
+ });
83
+ },
84
+ sendVerificationEmail: async ({ user, url, token }, request) => {
85
+ await send({
86
+ kind: "verify-email",
87
+ to: user.email,
88
+ subject: "Verify your email",
89
+ url,
90
+ token,
91
+ user,
92
+ request,
93
+ });
67
94
  },
68
- sendVerificationEmail: async ({ user, url, }) => {
69
- await send({ to: user.email, subject: "Verify your email", url });
95
+ sendChangeEmailConfirmation: async ({ user, newEmail, url, token, }, request) => {
96
+ await send({
97
+ kind: "change-email",
98
+ to: user.email, // the CURRENT address — see AuthEmailMessage.to
99
+ subject: "Confirm your email change",
100
+ url,
101
+ token,
102
+ user,
103
+ newEmail,
104
+ request,
105
+ });
70
106
  },
71
107
  });
108
+ const _pinEmailSenders = (send) => {
109
+ const senders = makeEmailSenders(send);
110
+ return {
111
+ reset: senders.sendResetPassword,
112
+ verify: senders.sendVerificationEmail,
113
+ change: senders.sendChangeEmailConfirmation,
114
+ };
115
+ };
116
+ void _pinEmailSenders;
72
117
  //# sourceMappingURL=options.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"options.js","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B,iFAAiF;AACjF,kFAAkF;AAClF,iEAAiE;AACjE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAEzC;;;;;;;;GAQG;AAEH;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;IAC7B,IAAI,EAAE,CAAC,QAAgB,EAAmB,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAC;IACtE,MAAM,EAAE,CAAC,EACR,IAAI,EACJ,QAAQ,GAIR,EAAoB,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC;CACtD,CAAC;AAWF,iFAAiF;AACjF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,GAAkB,EAAkB,EAAE,CAAC,CAAC;IAC1E,IAAI,EAAE,GAAG,CAAC,IAAI;IACd,MAAM,EAAE,GAAG,CAAC,MAAM;IAClB,MAAM,EAAE,GAAG,CAAC,MAAM;CAClB,CAAC,CAAC;AAEH;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CACvC,OAAe,EACf,MAAc,EACG,EAAE,CACnB,kBAAkB,CAAC;IAClB,IAAI,EAAE,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,QAAQ;IAC/B,MAAM;IACN,MAAM,EAAE,OAAO;CACf,CAAC,CAAC;AASJ;;;GAGG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,IAAe,EAAE,EAAE,CAAC,CAAC;IACrD,iBAAiB,EAAE,KAAK,EAAE,EACzB,IAAI,EACJ,GAAG,GAIH,EAAiB,EAAE;QACnB,MAAM,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,qBAAqB,EAAE,GAAG,EAAE,CAAC,CAAC;IACrE,CAAC;IACD,qBAAqB,EAAE,KAAK,EAAE,EAC7B,IAAI,EACJ,GAAG,GAIH,EAAiB,EAAE;QACnB,MAAM,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,mBAAmB,EAAE,GAAG,EAAE,CAAC,CAAC;IACnE,CAAC;CACD,CAAC,CAAC"}
1
+ {"version":3,"file":"options.js","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAEA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B,iFAAiF;AACjF,kFAAkF;AAClF,iEAAiE;AACjE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAEzC;;;;;;;;GAQG;AAEH;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;IAC7B,IAAI,EAAE,CAAC,QAAgB,EAAmB,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAC;IACtE,MAAM,EAAE,CAAC,EACR,IAAI,EACJ,QAAQ,GAIR,EAAoB,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC;CACtD,CAAC;AAWF,iFAAiF;AACjF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,GAAkB,EAAkB,EAAE,CAAC,CAAC;IAC1E,IAAI,EAAE,GAAG,CAAC,IAAI;IACd,MAAM,EAAE,GAAG,CAAC,MAAM;IAClB,MAAM,EAAE,GAAG,CAAC,MAAM;CAClB,CAAC,CAAC;AAEH;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CACvC,OAAe,EACf,MAAc,EACG,EAAE,CACnB,kBAAkB,CAAC;IAClB,IAAI,EAAE,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,QAAQ;IAC/B,MAAM;IACN,MAAM,EAAE,OAAO;CACf,CAAC,CAAC;AA6CJ;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,IAAe,EAAE,EAAE,CAAC,CAAC;IACrD,iBAAiB,EAAE,KAAK,EACvB,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAuD,EACzE,OAAiB,EACD,EAAE;QAClB,MAAM,IAAI,CAAC;YACV,IAAI,EAAE,gBAAgB;YACtB,EAAE,EAAE,IAAI,CAAC,KAAK;YACd,OAAO,EAAE,qBAAqB;YAC9B,GAAG;YACH,KAAK;YACL,IAAI;YACJ,OAAO;SACP,CAAC,CAAC;IACJ,CAAC;IACD,qBAAqB,EAAE,KAAK,EAC3B,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAuD,EACzE,OAAiB,EACD,EAAE;QAClB,MAAM,IAAI,CAAC;YACV,IAAI,EAAE,cAAc;YACpB,EAAE,EAAE,IAAI,CAAC,KAAK;YACd,OAAO,EAAE,mBAAmB;YAC5B,GAAG;YACH,KAAK;YACL,IAAI;YACJ,OAAO;SACP,CAAC,CAAC;IACJ,CAAC;IACD,2BAA2B,EAAE,KAAK,EACjC,EACC,IAAI,EACJ,QAAQ,EACR,GAAG,EACH,KAAK,GAML,EACD,OAAiB,EACD,EAAE;QAClB,MAAM,IAAI,CAAC;YACV,IAAI,EAAE,cAAc;YACpB,EAAE,EAAE,IAAI,CAAC,KAAK,EAAE,gDAAgD;YAChE,OAAO,EAAE,2BAA2B;YACpC,GAAG;YACH,KAAK;YACL,IAAI;YACJ,QAAQ;YACR,OAAO;SACP,CAAC,CAAC;IACJ,CAAC;CACD,CAAC,CAAC;AAgCH,MAAM,gBAAgB,GAAG,CAAC,IAAe,EAAsB,EAAE;IAChE,MAAM,OAAO,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IACvC,OAAO;QACN,KAAK,EAAE,OAAO,CAAC,iBAAiB;QAChC,MAAM,EAAE,OAAO,CAAC,qBAAqB;QACrC,MAAM,EAAE,OAAO,CAAC,2BAA2B;KAC3C,CAAC;AACH,CAAC,CAAC;AACF,KAAK,gBAAgB,CAAC"}
@@ -1,11 +1,20 @@
1
1
  -- @ingram-tech/nk-auth — Better Auth schema, hardened for RLS.
2
2
  --
3
- -- This mirrors Better Auth's core tables (user / session / account /
4
- -- verification), the `jwt` plugin's `jwks` table, and the `passkey` plugin's
5
- -- `passkey` table, for the version of `better-auth` this package depends on.
6
- -- Reconcile against the exact pinned version with:
3
+ -- This is the BASELINE (0001) of nk-auth's own migration chain an append-only
4
+ -- set of SQL files this package ships and versions, journaled independently of a
5
+ -- consuming site's `drizzle/` chain (see docs/db-package.md § "The nk-auth
6
+ -- migration chain"). It mirrors Better Auth's core tables (user / session /
7
+ -- account / verification), the `jwt` plugin's `jwks` table, and the `passkey`
8
+ -- plugin's `passkey` table, for the version of `better-auth` this package pins.
9
+ --
10
+ -- Upgrading better-auth: this file is APPEND-ONLY. Once shipped it must never be
11
+ -- rewritten — the runner hashes each file and a changed baseline drifts every
12
+ -- site that already applied it. When a better-auth upgrade changes the schema,
13
+ -- diff it against the pinned version with:
7
14
  -- npx @better-auth/cli generate
8
- -- and re-run after upgrading better-auth.
15
+ -- and land the delta as a NEW file (0002_*.sql, …) plus a `meta/_journal.json`
16
+ -- entry, written as idempotent `... if not exists` DDL and re-applying the two
17
+ -- hardening steps below to any new table. Never edit 0001.
9
18
  --
10
19
  -- TWO hardening steps the generator does NOT produce, both required:
11
20
  -- 1. New users default to a UUID id, because `auth.uid()`-style RLS policies
@@ -16,6 +25,13 @@
16
25
  -- which bypasses RLS, so denying the app's RLS role any access here costs
17
26
  -- nothing and keeps the auth tables off-limits to user-facing queries.
18
27
 
28
+ -- Statements below are separated by drizzle's per-statement breakpoint marker
29
+ -- (a plain SQL line comment). It is REQUIRED: the PGlite dev/test migrator parses
30
+ -- one statement per command and rejects a multi-statement string, so the runner
31
+ -- splits the file on that marker. Keep one statement per segment when appending
32
+ -- deltas — and never write the literal marker token inside prose like this, or
33
+ -- the splitter will cut here.
34
+
19
35
  create table if not exists "public"."user" (
20
36
  "id" text primary key default gen_random_uuid()::text, -- hardening (1)
21
37
  "name" text not null,
@@ -25,7 +41,7 @@ create table if not exists "public"."user" (
25
41
  "createdAt" timestamptz not null default now(),
26
42
  "updatedAt" timestamptz not null default now()
27
43
  );
28
-
44
+ --> statement-breakpoint
29
45
  create table if not exists "public"."session" (
30
46
  "id" text primary key,
31
47
  "expiresAt" timestamptz not null,
@@ -36,7 +52,7 @@ create table if not exists "public"."session" (
36
52
  "createdAt" timestamptz not null default now(),
37
53
  "updatedAt" timestamptz not null default now()
38
54
  );
39
-
55
+ --> statement-breakpoint
40
56
  create table if not exists "public"."account" (
41
57
  "id" text primary key,
42
58
  "accountId" text not null,
@@ -52,7 +68,7 @@ create table if not exists "public"."account" (
52
68
  "createdAt" timestamptz not null default now(),
53
69
  "updatedAt" timestamptz not null default now()
54
70
  );
55
-
71
+ --> statement-breakpoint
56
72
  create table if not exists "public"."verification" (
57
73
  "id" text primary key,
58
74
  "identifier" text not null,
@@ -61,7 +77,7 @@ create table if not exists "public"."verification" (
61
77
  "createdAt" timestamptz not null default now(),
62
78
  "updatedAt" timestamptz not null default now()
63
79
  );
64
-
80
+ --> statement-breakpoint
65
81
  -- `jwt` plugin: holds the asymmetric keypair used to sign session JWTs.
66
82
  create table if not exists "public"."jwks" (
67
83
  "id" text primary key,
@@ -69,7 +85,7 @@ create table if not exists "public"."jwks" (
69
85
  "privateKey" text not null,
70
86
  "createdAt" timestamptz not null default now()
71
87
  );
72
-
88
+ --> statement-breakpoint
73
89
  -- `passkey` plugin.
74
90
  create table if not exists "public"."passkey" (
75
91
  "id" text primary key,
@@ -84,17 +100,25 @@ create table if not exists "public"."passkey" (
84
100
  "aaguid" text,
85
101
  "createdAt" timestamptz default now()
86
102
  );
87
-
103
+ --> statement-breakpoint
88
104
  create index if not exists "idx_session_userId" on "public"."session" ("userId");
105
+ --> statement-breakpoint
89
106
  create index if not exists "idx_account_userId" on "public"."account" ("userId");
107
+ --> statement-breakpoint
90
108
  create index if not exists "idx_passkey_userId" on "public"."passkey" ("userId");
109
+ --> statement-breakpoint
91
110
  create index if not exists "idx_verification_identifier" on "public"."verification" ("identifier");
92
-
111
+ --> statement-breakpoint
93
112
  -- Hardening (2): deny-all RLS. No policies = no anon/authenticated access.
94
113
  -- Better Auth's privileged connection bypasses RLS, so auth still works.
95
114
  alter table "public"."user" enable row level security;
115
+ --> statement-breakpoint
96
116
  alter table "public"."session" enable row level security;
117
+ --> statement-breakpoint
97
118
  alter table "public"."account" enable row level security;
119
+ --> statement-breakpoint
98
120
  alter table "public"."verification" enable row level security;
121
+ --> statement-breakpoint
99
122
  alter table "public"."jwks" enable row level security;
123
+ --> statement-breakpoint
100
124
  alter table "public"."passkey" enable row level security;
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "postgresql",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "7",
8
+ "when": 1735689600000,
9
+ "tag": "0001_better_auth",
10
+ "breakpoints": true
11
+ }
12
+ ]
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingram-tech/nk-auth",
3
- "version": "0.12.3",
3
+ "version": "0.13.1",
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",
@@ -60,12 +60,12 @@
60
60
  "./migrations/*": "./migrations/*"
61
61
  },
62
62
  "scripts": {
63
- "build": "rm -rf dist tsconfig.tsbuildinfo && tsc -p tsconfig.json",
63
+ "build": "rm -rf dist *.tsbuildinfo && tsc -p tsconfig.build.json",
64
64
  "type-check": "tsc -p tsconfig.json --noEmit",
65
65
  "test": "vitest run"
66
66
  },
67
67
  "dependencies": {
68
- "@ingram-tech/nk-db": "^1.4.0",
68
+ "@ingram-tech/nk-db": "^1.4.2",
69
69
  "bcrypt": "^6.0.0",
70
70
  "jose": "^6.2.3",
71
71
  "zod": "^4.4.3"
@@ -93,7 +93,7 @@
93
93
  },
94
94
  "devDependencies": {
95
95
  "@better-auth/passkey": "^1.6.23",
96
- "@ingram-tech/nk-dev": "0.5.0",
96
+ "@ingram-tech/nk-dev": "0.8.0",
97
97
  "@types/bcrypt": "^6.0.0",
98
98
  "@types/node": "^26.1.1",
99
99
  "@types/pg": "^8.20.0",