@ingram-tech/nk-auth 0.13.6 → 0.14.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
@@ -1,32 +1,28 @@
1
1
  # @ingram-tech/nk-auth
2
2
 
3
- The Ingram **[Better Auth](https://better-auth.com) foundation**: a toolkit of
4
- composable presets each Ingram Next.js site spreads into its *own*
5
- `betterAuth()` call. It is **not** a `betterAuth()` wrapper — the site stays
6
- plain Better Auth (prime directive), keeping full plugin type inference. Import
7
- only what you need from focused subpaths.
3
+ Composable [Better Auth](https://better-auth.com) presets for Next.js apps, spread
4
+ into your *own* `betterAuth()` call. This is not a `betterAuth()` wrapper: your app
5
+ stays plain Better Auth, so plugin type inference survives. Import only what you
6
+ need from focused subpaths.
8
7
 
9
- `better-auth`, `pg`, `@better-auth/passkey` are **peer dependencies** so there's
8
+ `better-auth`, `pg`, `@better-auth/passkey` are peer dependencies, so there is
10
9
  exactly one Better Auth copy in the app.
11
10
 
12
11
  | Export (subpath) | For |
13
12
  | --- | --- |
14
- | `authEnv`, `authSecret`, `isConfigured` (`./`) | env validation — `authEnv()` returns the full `{ secret, baseURL, databaseUrl }` bundle; `authSecret()` resolves just the secret (with the prod/dev rule) for sites that derive their own `baseURL` / DB connection |
15
- | `backendJwtOptions` / `verifyBackendJwt` (`./jwt`) | a JWT for the site's own backend API (custom `audience`) |
13
+ | `authEnv`, `authSecret`, `isConfigured` (`./`) | env validation — `authEnv()` returns the full `{ secret, baseURL, databaseUrl }` bundle; `authSecret()` resolves just the secret (with the prod/dev rule) for apps that derive their own `baseURL` / DB connection |
14
+ | `backendJwtOptions` / `verifyBackendJwt` (`./jwt`) | a JWT for your own backend API (custom `audience`; `ids` mints `sub` as the public user id) |
16
15
  | `nkOrganizationDefaults`, `lastActiveOrganizationHooks`, `lastActiveOrganizationUserField` (`./organization`) | org-plugin defaults + active-org restore/persist |
17
- | `createAuthPool` (`./pool`) | **deprecated** — alias of `createPool` from [`@ingram-tech/nk-db`](../nk-db); inject the site's shared pool instead |
16
+ | `createAuthPool` (`./pool`) | **deprecated** — alias of `createPool` from [`@ingram-tech/nk-db`](../nk-db); inject your app's shared pool instead |
18
17
  | `makeEmailSenders`, `makePasskeyOptions`, `passkeyOptionsForBaseUrl`, `uuidGenerateId` (`./`) | email hooks, passkeys (`passkeyOptionsForBaseUrl` derives `rpID`/`origin` from a single base URL), UUID ids |
19
- | `bcryptPassword` (`./`) | **legacy only** — bcrypt verifier for sites with pre-existing bcrypt hashes. New sites omit it (Better Auth defaults to scrypt). See [Migrating bcrypt passwords to scrypt](#migrating-bcrypt-passwords-to-scrypt) |
18
+ | `bcryptPassword` (`./`) | **legacy only** — bcrypt verifier for apps with pre-existing bcrypt hashes. New apps omit it (Better Auth defaults to scrypt). See [Migrating bcrypt passwords to scrypt](#migrating-bcrypt-passwords-to-scrypt) |
20
19
  | `createAuthHelpers`, `safeNext` (`./server`) | validated App Router session helpers (`getSession` / `getUser` / `requireSession` / `requireUser` / `redirectIfAuthenticated`), request-memoized via React `cache()`, with automatic `next` + stale-cookie signalling; `safeNext` validates a `?next=` param |
21
20
  | `createAuthMiddleware` (`./middleware`) | loop-safe edge middleware: gates unauthenticated users off protected paths, preserves `next`, and clears a stale session cookie so a bad session self-heals |
22
21
 
23
- > **Data access + RLS** is owned by [`@ingram-tech/nk-db`](../nk-db): query over a
24
- > direct `pg` connection and enforce per-request Row Level Security with
25
- > `withRls` / `withRlsTransaction` (claims taken from the Better Auth session — no
26
- > JWT minting, no REST proxy).
22
+ > **Data access + RLS** is owned by [`@ingram-tech/nk-db`](../nk-db); see §3.
27
23
  >
28
- > **Backend-JWT + org sites** (a backend API plus the org plugin): compose the
29
- > site's shared nk-db pool, `backendJwtOptions({ audience })`,
24
+ > **Backend-JWT + org sites** (a backend API plus the org plugin): compose your
25
+ > app's shared nk-db pool, `backendJwtOptions({ audience })`,
30
26
  > `nkOrganizationDefaults`, and `lastActiveOrganizationHooks(pool)` in your
31
27
  > `betterAuth()`; verify backend tokens with `verifyBackendJwt`. Keep
32
28
  > app-specific bits (SSO restrictions, permissions/roles, connectors) in the app.
@@ -49,7 +45,7 @@ Outside production, `BETTER_AUTH_SECRET` falls back to a well-known insecure
49
45
  placeholder, so local dev and tests run without setting it (a warning is logged).
50
46
  In production it stays required — a missing secret throws at startup.
51
47
 
52
- If your site derives its own `baseURL` and opens its own database connection, it
48
+ If your app derives its own `baseURL` and opens its own database connection, it
53
49
  may not want `authEnv()`'s all-or-nothing bundle (which also requires
54
50
  `BETTER_AUTH_URL` and `DATABASE_URL`). Take just the secret — same prod/dev rule —
55
51
  with `authSecret()`:
@@ -116,14 +112,14 @@ const db = await createTestDb({
116
112
  });
117
113
  ```
118
114
 
119
- **Upgrading better-auth** never means hand-writing a migration in your site: a
115
+ **Upgrading better-auth** never means hand-writing a migration in your app: a
120
116
  schema-changing upgrade ships as a new file in nk-auth's chain, so you bump the
121
117
  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).
118
+ a pinned `better-auth` with `npx @better-auth/cli generate` (a maintainer task,
119
+ done once in this package).
124
120
 
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
121
+ > **Adopting from the old copy-in model?** Earlier versions told you to `cp` the
122
+ > baseline into your own `drizzle/` chain. If you already did, keep that file
127
123
  > (deleting an applied migration causes journal drift) and just add the auth-chain
128
124
  > invocation above: its DDL is all `… if not exists`, so it no-ops against your
129
125
  > existing tables and simply records the nk-auth journal. New auth migrations then
@@ -131,10 +127,8 @@ task, done once here — not per site).
131
127
 
132
128
  ## 2. Configure the server
133
129
 
134
- This package is **not** a `betterAuth()` wrapper — your site calls `betterAuth`
135
- itself and spreads in our presets. That keeps full Better Auth type inference at
136
- the call site (so `auth.api.*` stays typed) and keeps the site plain Better Auth,
137
- per the prime directive.
130
+ Your app calls `betterAuth` itself and spreads in the presets, which keeps full
131
+ Better Auth type inference at the call site, so `auth.api.*` stays typed.
138
132
 
139
133
  ```ts
140
134
  // lib/auth.ts
@@ -166,13 +160,13 @@ export const auth = betterAuth({
166
160
  basePath: authBasePath, // mount at /auth, not the framework default /api/auth
167
161
  advanced: { database: { generateId: uuidGenerateId } }, // UUIDv7 ids
168
162
  // ^ stored as hyphenated UUIDv7 (uuid columns / RLS policies stay valid).
169
- // To show those same ids as prefixed base58 on the wire/UI — `team_…`,
170
- // matching the Ingram Cloud API's `agt_`/`smt_` ids — skin them with
163
+ // To show those same ids as prefixed base58 on the wire/UI — `team_…`, the
164
+ // way an API exposes prefixed ids like `agt_`/`smt_` — skin them with
171
165
  // `toPrefixedId(uuid, "team")` / recover with `fromPrefixedId`. `base58Id`
172
- // mints a fresh one directly for text-id sites. All from `@ingram-tech/nk-auth`.
166
+ // mints a fresh one directly for text-id apps. All from `@ingram-tech/nk-auth`.
173
167
  emailAndPassword: {
174
168
  enabled: true,
175
- // Better Auth hashes with scrypt by default. Only sites carrying
169
+ // Better Auth hashes with scrypt by default. Only apps carrying
176
170
  // pre-existing bcrypt hashes set `password: bcryptPassword` (legacy).
177
171
  sendResetPassword: email.sendResetPassword,
178
172
  },
@@ -232,11 +226,11 @@ has any stored preference.
232
226
  mail a user ever gets from you; a bare `<a href=url>url</a>` reads as phishing
233
227
  and trains people to distrust your domain. Take the
234
228
  [`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 default `notifications` local
238
- part (`fromAddress("Example")`) — **not** `no-reply`: this is the mail users are
239
- most likely to reply to, and dropping that reply is hostile. See
229
+ (`shadcn add email-verification email-password-reset`), which accept `heading` /
230
+ `body` / `ctaLabel` / `preview` overrides so you can pass translated copy, or
231
+ write your own. Send auth links from the default `notifications` local part
232
+ (`fromAddress("Example")`), **not** `no-reply`: this is the mail users are most
233
+ likely to reply to, and dropping that reply is hostile. See
240
234
  [transactional-email.md](../../docs/transactional-email.md).
241
235
 
242
236
  **Spread all three in; do not hand-write them.** `betterAuth()` takes its
@@ -268,12 +262,14 @@ Data access lives in [`@ingram-tech/nk-db`](../nk-db), not here. Query over the
268
262
  direct `pg` connection and wrap reads/writes in `withRls` / `withRlsTransaction`,
269
263
  which set `request.jwt.claims` + `SET LOCAL ROLE` per transaction from the Better
270
264
  Auth session — so `auth.uid()` policies fire unchanged, with no JWT minting and
271
- no REST proxy. See its README for the pattern.
265
+ no REST proxy. See its README for the pattern. Pass `user.id` as it comes from
266
+ the helpers: a public `usr_…` is decoded to the uuid before it reaches the
267
+ claims GUC, so a policy's `auth.uid()` still compares to a `uuid` column.
272
268
 
273
269
  ## 4. Client
274
270
 
275
- Assemble the client in a `"use client"` module (full plugin inference is
276
- preserved here too):
271
+ Assemble the client in a `"use client"` module (plugin inference is preserved
272
+ here too):
277
273
 
278
274
  ```tsx
279
275
  "use client";
@@ -312,9 +308,23 @@ export const {
312
308
  redirectIfAuthenticated,
313
309
  getLinkedProviders, // providerIds linked to the current user
314
310
  hasCredentialAccount, // does the current user have an email/password login?
315
- } = createAuthHelpers(auth);
311
+ } = createAuthHelpers(auth, {
312
+ // Session ids in public form (`usr_…`, `org_…`), the same form `idColumn`
313
+ // reads return, so `row.organization_id === session.activeOrganizationId`
314
+ // holds. Omit on a site with no public id form.
315
+ ids: { user: ids.user, organization: ids.organization },
316
+ });
316
317
  ```
317
318
 
319
+ With `ids`, every read through the helpers presents `user.id`,
320
+ `session.userId` and `session.activeOrganizationId` as public ids; Better Auth
321
+ itself keeps working on raw uuids underneath, and anything read directly from
322
+ `auth.api.*` is raw — the same rule as raw SQL vs `idColumn`. Pass the same
323
+ helper to `backendJwtOptions({ audience, ids: { user } })` and the backend
324
+ JWT's `sub` (and payload `id`) is the public id too. RLS needs nothing: nk-db's
325
+ `withRls*` decode a prefixed `sub` before it reaches `request.jwt.claims`, so
326
+ `auth.uid()` policies hold either way.
327
+
318
328
  ```tsx
319
329
  // app/dashboard/page.tsx — gate a protected page (validated, DB-backed).
320
330
  import { requireUser } from "@/lib/auth/session";
@@ -378,13 +388,13 @@ marker and bounces to a clean `/login?next=…`; signing in returns them to
378
388
  `next`. `next` for the cookie-less case is filled in by middleware directly; for
379
389
  the cookie-present case the guard reads it from the `x-nk-auth-path` header
380
390
  middleware injects — so the self-heal (next + clearing) needs the middleware.
381
- Sites that skip middleware still get validated gating from the server helpers,
391
+ An app that skips middleware still gets validated gating from the server helpers,
382
392
  just without automatic `next`/clearing.
383
393
 
384
394
  ## 6. Passwords: change, set, and reset
385
395
 
386
396
  nk-auth owns the reset **sender** (`makeEmailSenders.sendResetPassword`, §2) and
387
- now closes the loop so a site never touches Better Auth's `account` table or
397
+ closes the loop so your app never touches Better Auth's `account` table or
388
398
  endpoint names directly. Three pieces:
389
399
 
390
400
  **Detect what login the user has.** A social-only account (Google, …) has no
@@ -462,11 +472,11 @@ async redirects() {
462
472
  ## Migrating bcrypt passwords to scrypt
463
473
 
464
474
  `bcryptPassword` is **legacy support only** (see its `@deprecated` note). It
465
- exists so sites whose `account.password` hashes are bcrypt keep verifying —
475
+ exists so apps whose `account.password` hashes are bcrypt keep verifying:
466
476
  that is all nk-auth ships for the bcrypt case.
467
477
 
468
- The path to move a site fully onto scrypt is a **dual-format verifier**, wired
469
- in the site (nk-auth does not ship it). Better Auth's default hasher is scrypt
478
+ The path to move fully onto scrypt is a **dual-format verifier**, wired in your
479
+ app (nk-auth does not ship it). Better Auth's default hasher is scrypt
470
480
  (`<salt-hex>:<key-hex>`) and bcrypt hashes are trivially distinguishable (they
471
481
  start with `$2a$`/`$2b$`/`$2y$`), so override only
472
482
  `emailAndPassword.password.verify` to branch on `hash.startsWith("$2")` →
package/dist/jwt.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { IdHelper } from "@ingram-tech/nk-db/id";
1
2
  import type { JwtOptions } from "better-auth/plugins/jwt";
2
3
  import { type JWTPayload } from "jose";
3
4
  /**
@@ -16,6 +17,14 @@ export interface BackendJwtConfig {
16
17
  * the site mints tokens on demand via `auth.api.signJWT` instead.
17
18
  */
18
19
  disableSettingJwtHeader?: boolean;
20
+ /**
21
+ * Mint the token's `sub` (and the `id` in its payload) as the user's public
22
+ * id rather than the raw uuid, so a backend that also talks to the app sees
23
+ * ids in one form. The same helper the site passes to `createAuthHelpers`.
24
+ */
25
+ ids?: {
26
+ user: IdHelper;
27
+ };
19
28
  }
20
29
  /** `jwt` plugin options for a backend-API token (custom audience). */
21
30
  export declare const backendJwtOptions: (config: BackendJwtConfig) => JwtOptions;
package/dist/jwt.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAGN,KAAK,UAAU,EAEf,MAAM,MAAM,CAAC;AAEd;;;;;GAKG;AAEH,MAAM,WAAW,gBAAgB;IAChC,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED,sEAAsE;AACtE,eAAO,MAAM,iBAAiB,WAAY,gBAAgB,KAAG,UAM3D,CAAC;AAWH;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB,WAAkB;IAC9C,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,OAAO,EAAE,MAAM,GAAG,GAAG,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CACf,KAAG,OAAO,CAAC,UAAU,CAmCrB,CAAC"}
1
+ {"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAGN,KAAK,UAAU,EAEf,MAAM,MAAM,CAAC;AAEd;;;;;GAKG;AAEH,MAAM,WAAW,gBAAgB;IAChC,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC;;;;OAIG;IACH,GAAG,CAAC,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAA;KAAE,CAAC;CACzB;AAED,sEAAsE;AACtE,eAAO,MAAM,iBAAiB,WAAY,gBAAgB,KAAG,UAgB3D,CAAC;AAWH;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB,WAAkB;IAC9C,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,OAAO,EAAE,MAAM,GAAG,GAAG,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CACf,KAAG,OAAO,CAAC,UAAU,CAmCrB,CAAC"}
package/dist/jwt.js CHANGED
@@ -3,6 +3,15 @@ import { createRemoteJWKSet, errors as joseErrors, jwtVerify, } from "jose";
3
3
  export const backendJwtOptions = (config) => ({
4
4
  disableSettingJwtHeader: config.disableSettingJwtHeader,
5
5
  jwt: {
6
+ ...(config.ids
7
+ ? {
8
+ getSubject: ({ user }) => config.ids?.user.encode(user.id) ?? user.id,
9
+ definePayload: ({ user }) => ({
10
+ ...user,
11
+ id: config.ids?.user.encode(user.id) ?? user.id,
12
+ }),
13
+ }
14
+ : {}),
6
15
  audience: config.audience,
7
16
  expirationTime: config.expirationTime,
8
17
  },
package/dist/jwt.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"jwt.js","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"AACA,OAAO,EACN,kBAAkB,EAClB,MAAM,IAAI,UAAU,EAEpB,SAAS,GACT,MAAM,MAAM,CAAC;AAqBd,sEAAsE;AACtE,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,MAAwB,EAAc,EAAE,CAAC,CAAC;IAC3E,uBAAuB,EAAE,MAAM,CAAC,uBAAuB;IACvD,GAAG,EAAE;QACJ,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,cAAc,EAAE,MAAM,CAAC,cAAc;KACrC;CACD,CAAC,CAAC;AAEH,MAAM,SAAS,GAAG,IAAI,GAAG,EAAiD,CAAC;AAE3E,+EAA+E;AAC/E,yEAAyE;AACzE,4EAA4E;AAC5E,mDAAmD;AACnD,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAClC,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAAkB,CAAC;AAEnD;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,EAAE,MAMtC,EAAuB,EAAE;IACzB,MAAM,GAAG,GACR,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;IAC/E,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC;IAChC,IAAI,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACnC,IAAI,CAAC,IAAI,EAAE,CAAC;QACX,IAAI,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC;QAC/B,SAAS,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC/B,CAAC;IACD,MAAM,OAAO,GAAG;QACf,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,UAAU,EAAE,CAAC,OAAO,CAAC;QACrB,yEAAyE;QACzE,4EAA4E;QAC5E,cAAc,EAAE,CAAC;KACjB,CAAC;IACF,IAAI,CAAC;QACJ,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,SAAS,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;QACjE,OAAO,OAAO,CAAC;IAChB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,IAAI,CAAC,CAAC,GAAG,YAAY,UAAU,CAAC,iBAAiB,CAAC;YAAE,MAAM,GAAG,CAAC;QAC9D,2EAA2E;QAC3E,yEAAyE;QACzE,qEAAqE;QACrE,UAAU;QACV,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,IAAI,GAAG,GAAG,CAAC,gBAAgB,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,kBAAkB,EAAE,CAAC;YACtE,MAAM,GAAG,CAAC;QACX,CAAC;QACD,gBAAgB,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;QACpC,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;QACpB,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,SAAS,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;QACjE,OAAO,OAAO,CAAC;IAChB,CAAC;AACF,CAAC,CAAC"}
1
+ {"version":3,"file":"jwt.js","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"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"}
package/dist/server.d.ts CHANGED
@@ -1,3 +1,30 @@
1
+ /**
2
+ * Server-side session helpers for the App Router, at
3
+ * "@ingram-tech/nk-auth/server" so the framework-agnostic core entry never
4
+ * pulls in `next`. A site binds these once to its own Better Auth instance:
5
+ *
6
+ * // lib/auth/session.ts
7
+ * import { createAuthHelpers } from "@ingram-tech/nk-auth/server";
8
+ * import { auth } from "@/lib/auth";
9
+ *
10
+ * export const { getSession, getUser, requireSession, requireUser,
11
+ * redirectIfAuthenticated } = createAuthHelpers(auth);
12
+ *
13
+ * These are the **validated** authority: they resolve through
14
+ * `auth.api.getSession`, which checks the session against the database — not
15
+ * merely the presence of a cookie. The read is memoized per request with React
16
+ * `cache()`, so a render fanning out many `getUser()` calls validates once,
17
+ * not once per call. When a guard finds no valid session it redirects to the sign-in page,
18
+ * preserving the requested path as `next` (read from the header the middleware
19
+ * injects) and, when a session cookie is present-but-invalid, adding `stale=1`
20
+ * so the middleware clears the dead cookie. The optimistic, cookie-presence
21
+ * check belongs in middleware (see "@ingram-tech/nk-auth/middleware"); it may
22
+ * only ever push *unauthenticated* users off protected routes. The decision to
23
+ * send a signed-in user *away* from the sign-in page must run through
24
+ * `redirectIfAuthenticated` here, so a stale cookie resolves to "no session"
25
+ * and falls through to the form instead of ping-ponging forever.
26
+ */
27
+ import type { IdHelper } from "@ingram-tech/nk-db/id";
1
28
  /**
2
29
  * Validate a `next` redirect param: returns it only if it's an internal,
3
30
  * non-protocol-relative path, else null. Use on the login page before honoring
@@ -31,9 +58,26 @@ interface AuthLike<S extends SessionLike> {
31
58
  }>>;
32
59
  };
33
60
  }
61
+ /**
62
+ * The registry helpers for the ids a session carries. With these, every
63
+ * session read through the helpers presents `user.id` / `session.userId` as
64
+ * the user's public id and `session.activeOrganizationId` as the
65
+ * organization's, so app code compares session ids with `idColumn` reads in
66
+ * one form. Better Auth itself keeps working on raw uuids underneath.
67
+ */
68
+ export interface SessionIds {
69
+ user: IdHelper;
70
+ organization?: IdHelper;
71
+ }
34
72
  export interface AuthHelpersOptions {
35
73
  /** Where guards send unauthenticated users. Default `/login`. */
36
74
  signInPath?: string;
75
+ /**
76
+ * Encode session ids to their public form on every read (see
77
+ * {@link SessionIds}). Omit on a site that has no public id form: the
78
+ * helpers then return Better Auth's raw uuids.
79
+ */
80
+ ids?: SessionIds;
37
81
  /**
38
82
  * Cookie-name fragment that identifies the Better Auth session cookie, used
39
83
  * to detect a present-but-invalid (stale) cookie. Default `better-auth`,
@@ -41,6 +85,13 @@ export interface AuthHelpersOptions {
41
85
  */
42
86
  sessionCookiePrefix?: string;
43
87
  }
88
+ /**
89
+ * A session with its ids in public form: `user.id`, `session.userId` and
90
+ * `session.activeOrganizationId` (each only when present and a string). Pure —
91
+ * Better Auth's own object is left untouched, since it is what the adapter
92
+ * writes back. `encode` is idempotent, so an already-public id is unchanged.
93
+ */
94
+ export declare function encodeSessionIds<S extends SessionLike>(session: S, ids: SessionIds): S;
44
95
  export declare function createAuthHelpers<S extends SessionLike>(auth: AuthLike<S>, options?: AuthHelpersOptions): {
45
96
  getSession: () => Promise<S | null>;
46
97
  getUser: () => Promise<S["user"] | null>;
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAgCA;;;;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;sBAiBG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;mBAK1B,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"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAOtD;;;;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;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,QAAQ,CAAC;IACf,YAAY,CAAC,EAAE,QAAQ,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB;IAClC,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAKD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,WAAW,EACrD,OAAO,EAAE,CAAC,EACV,GAAG,EAAE,UAAU,GACb,CAAC,CAyBH;AAED,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,WAAW,EACtD,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,EACjB,OAAO,GAAE,kBAAuB;sBAiBG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;mBAQ1B,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
@@ -1,29 +1,3 @@
1
- /**
2
- * Server-side session helpers for the App Router, at
3
- * "@ingram-tech/nk-auth/server" so the framework-agnostic core entry never
4
- * pulls in `next`. A site binds these once to its own Better Auth instance:
5
- *
6
- * // lib/auth/session.ts
7
- * import { createAuthHelpers } from "@ingram-tech/nk-auth/server";
8
- * import { auth } from "@/lib/auth";
9
- *
10
- * export const { getSession, getUser, requireSession, requireUser,
11
- * redirectIfAuthenticated } = createAuthHelpers(auth);
12
- *
13
- * These are the **validated** authority: they resolve through
14
- * `auth.api.getSession`, which checks the session against the database — not
15
- * merely the presence of a cookie. The read is memoized per request with React
16
- * `cache()`, so a render fanning out many `getUser()` calls validates once,
17
- * not once per call. When a guard finds no valid session it redirects to the sign-in page,
18
- * preserving the requested path as `next` (read from the header the middleware
19
- * injects) and, when a session cookie is present-but-invalid, adding `stale=1`
20
- * so the middleware clears the dead cookie. The optimistic, cookie-presence
21
- * check belongs in middleware (see "@ingram-tech/nk-auth/middleware"); it may
22
- * only ever push *unauthenticated* users off protected routes. The decision to
23
- * send a signed-in user *away* from the sign-in page must run through
24
- * `redirectIfAuthenticated` here, so a stale cookie resolves to "no session"
25
- * and falls through to the form instead of ping-ponging forever.
26
- */
27
1
  import { cookies, headers } from "next/headers";
28
2
  import { redirect } from "next/navigation";
29
3
  import { cache } from "react";
@@ -35,6 +9,36 @@ import { CREDENTIAL_PROVIDER_ID } from "./password.js";
35
9
  * `?next=` so it can't become an open redirect.
36
10
  */
37
11
  export { safeNextParam as safeNext } from "./gating-internals.js";
12
+ const encodeIfString = (helper, value) => helper && typeof value === "string" ? helper.encode(value) : value;
13
+ /**
14
+ * A session with its ids in public form: `user.id`, `session.userId` and
15
+ * `session.activeOrganizationId` (each only when present and a string). Pure —
16
+ * Better Auth's own object is left untouched, since it is what the adapter
17
+ * writes back. `encode` is idempotent, so an already-public id is unchanged.
18
+ */
19
+ export function encodeSessionIds(session, ids) {
20
+ const out = { ...session };
21
+ const user = session.user;
22
+ if (user && typeof user === "object" && "id" in user) {
23
+ out.user = { ...user, id: encodeIfString(ids.user, user.id) };
24
+ }
25
+ const inner = "session" in session ? session.session : undefined;
26
+ if (inner && typeof inner === "object") {
27
+ const rec = inner;
28
+ out.session = {
29
+ ...rec,
30
+ ...("userId" in rec
31
+ ? { userId: encodeIfString(ids.user, rec.userId) }
32
+ : {}),
33
+ ...("activeOrganizationId" in rec
34
+ ? {
35
+ activeOrganizationId: encodeIfString(ids.organization, rec.activeOrganizationId),
36
+ }
37
+ : {}),
38
+ };
39
+ }
40
+ return out;
41
+ }
38
42
  export function createAuthHelpers(auth, options = {}) {
39
43
  const signInPath = options.signInPath ?? "/login";
40
44
  const cookiePrefix = options.sessionCookiePrefix ?? "better-auth";
@@ -50,7 +54,12 @@ export function createAuthHelpers(auth, options = {}) {
50
54
  * pre-mutation snapshot; re-read through `auth.api.getSession` directly if
51
55
  * that ever matters.
52
56
  */
53
- const getSession = cache(async () => auth.api.getSession({ headers: await headers() }));
57
+ const getSession = cache(async () => {
58
+ const session = await auth.api.getSession({ headers: await headers() });
59
+ return session && options.ids
60
+ ? encodeSessionIds(session, options.ids)
61
+ : session;
62
+ });
54
63
  /** The authenticated user, or null. */
55
64
  async function getUser() {
56
65
  const session = await getSession();
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
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.13.6",
3
+ "version": "0.14.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",
@@ -68,7 +68,7 @@
68
68
  "test": "vitest run"
69
69
  },
70
70
  "dependencies": {
71
- "@ingram-tech/nk-db": "^2.0.0",
71
+ "@ingram-tech/nk-db": "^2.1.1",
72
72
  "bcrypt": "^6.0.0",
73
73
  "jose": "^6.2.9",
74
74
  "zod": "^4.4.3"
@@ -96,7 +96,7 @@
96
96
  },
97
97
  "devDependencies": {
98
98
  "@better-auth/passkey": "^1.7.0",
99
- "@ingram-tech/nk-dev": "0.12.0",
99
+ "@ingram-tech/nk-dev": "0.13.1",
100
100
  "@types/bcrypt": "^6.0.0",
101
101
  "@types/node": "^26.2.0",
102
102
  "@types/pg": "^8.23.1",
package/src/jwt.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { IdHelper } from "@ingram-tech/nk-db/id";
1
2
  import type { JwtOptions } from "better-auth/plugins/jwt";
2
3
  import {
3
4
  createRemoteJWKSet,
@@ -23,12 +24,28 @@ export interface BackendJwtConfig {
23
24
  * the site mints tokens on demand via `auth.api.signJWT` instead.
24
25
  */
25
26
  disableSettingJwtHeader?: boolean;
27
+ /**
28
+ * Mint the token's `sub` (and the `id` in its payload) as the user's public
29
+ * id rather than the raw uuid, so a backend that also talks to the app sees
30
+ * ids in one form. The same helper the site passes to `createAuthHelpers`.
31
+ */
32
+ ids?: { user: IdHelper };
26
33
  }
27
34
 
28
35
  /** `jwt` plugin options for a backend-API token (custom audience). */
29
36
  export const backendJwtOptions = (config: BackendJwtConfig): JwtOptions => ({
30
37
  disableSettingJwtHeader: config.disableSettingJwtHeader,
31
38
  jwt: {
39
+ ...(config.ids
40
+ ? {
41
+ getSubject: ({ user }) =>
42
+ config.ids?.user.encode(user.id) ?? user.id,
43
+ definePayload: ({ user }) => ({
44
+ ...user,
45
+ id: config.ids?.user.encode(user.id) ?? user.id,
46
+ }),
47
+ }
48
+ : {}),
32
49
  audience: config.audience,
33
50
  expirationTime: config.expirationTime,
34
51
  },
package/src/server.ts CHANGED
@@ -24,6 +24,7 @@
24
24
  * `redirectIfAuthenticated` here, so a stale cookie resolves to "no session"
25
25
  * and falls through to the form instead of ping-ponging forever.
26
26
  */
27
+ import type { IdHelper } from "@ingram-tech/nk-db/id";
27
28
  import { cookies, headers } from "next/headers";
28
29
  import { redirect } from "next/navigation";
29
30
  import { cache } from "react";
@@ -62,9 +63,27 @@ interface AuthLike<S extends SessionLike> {
62
63
  };
63
64
  }
64
65
 
66
+ /**
67
+ * The registry helpers for the ids a session carries. With these, every
68
+ * session read through the helpers presents `user.id` / `session.userId` as
69
+ * the user's public id and `session.activeOrganizationId` as the
70
+ * organization's, so app code compares session ids with `idColumn` reads in
71
+ * one form. Better Auth itself keeps working on raw uuids underneath.
72
+ */
73
+ export interface SessionIds {
74
+ user: IdHelper;
75
+ organization?: IdHelper;
76
+ }
77
+
65
78
  export interface AuthHelpersOptions {
66
79
  /** Where guards send unauthenticated users. Default `/login`. */
67
80
  signInPath?: string;
81
+ /**
82
+ * Encode session ids to their public form on every read (see
83
+ * {@link SessionIds}). Omit on a site that has no public id form: the
84
+ * helpers then return Better Auth's raw uuids.
85
+ */
86
+ ids?: SessionIds;
68
87
  /**
69
88
  * Cookie-name fragment that identifies the Better Auth session cookie, used
70
89
  * to detect a present-but-invalid (stale) cookie. Default `better-auth`,
@@ -73,6 +92,45 @@ export interface AuthHelpersOptions {
73
92
  sessionCookiePrefix?: string;
74
93
  }
75
94
 
95
+ const encodeIfString = (helper: IdHelper | undefined, value: unknown): unknown =>
96
+ helper && typeof value === "string" ? helper.encode(value) : value;
97
+
98
+ /**
99
+ * A session with its ids in public form: `user.id`, `session.userId` and
100
+ * `session.activeOrganizationId` (each only when present and a string). Pure —
101
+ * Better Auth's own object is left untouched, since it is what the adapter
102
+ * writes back. `encode` is idempotent, so an already-public id is unchanged.
103
+ */
104
+ export function encodeSessionIds<S extends SessionLike>(
105
+ session: S,
106
+ ids: SessionIds,
107
+ ): S {
108
+ const out: Record<string, unknown> = { ...(session as Record<string, unknown>) };
109
+ const user = session.user;
110
+ if (user && typeof user === "object" && "id" in user) {
111
+ out.user = { ...user, id: encodeIfString(ids.user, user.id) };
112
+ }
113
+ const inner = "session" in session ? session.session : undefined;
114
+ if (inner && typeof inner === "object") {
115
+ const rec = inner as Record<string, unknown>;
116
+ out.session = {
117
+ ...rec,
118
+ ...("userId" in rec
119
+ ? { userId: encodeIfString(ids.user, rec.userId) }
120
+ : {}),
121
+ ...("activeOrganizationId" in rec
122
+ ? {
123
+ activeOrganizationId: encodeIfString(
124
+ ids.organization,
125
+ rec.activeOrganizationId,
126
+ ),
127
+ }
128
+ : {}),
129
+ };
130
+ }
131
+ return out as S;
132
+ }
133
+
76
134
  export function createAuthHelpers<S extends SessionLike>(
77
135
  auth: AuthLike<S>,
78
136
  options: AuthHelpersOptions = {},
@@ -92,9 +150,12 @@ export function createAuthHelpers<S extends SessionLike>(
92
150
  * pre-mutation snapshot; re-read through `auth.api.getSession` directly if
93
151
  * that ever matters.
94
152
  */
95
- const getSession = cache(async (): Promise<S | null> =>
96
- auth.api.getSession({ headers: await headers() }),
97
- );
153
+ const getSession = cache(async (): Promise<S | null> => {
154
+ const session = await auth.api.getSession({ headers: await headers() });
155
+ return session && options.ids
156
+ ? encodeSessionIds(session, options.ids)
157
+ : session;
158
+ });
98
159
 
99
160
  /** The authenticated user, or null. */
100
161
  async function getUser(): Promise<S["user"] | null> {