@ingram-tech/nk-auth 0.13.6 → 0.14.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
@@ -12,7 +12,7 @@ exactly one Better Auth copy in the app.
12
12
  | Export (subpath) | For |
13
13
  | --- | --- |
14
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`) |
15
+ | `backendJwtOptions` / `verifyBackendJwt` (`./jwt`) | a JWT for the site's own backend API (custom `audience`; `ids` mints `sub` as the public user id) |
16
16
  | `nkOrganizationDefaults`, `lastActiveOrganizationHooks`, `lastActiveOrganizationUserField` (`./organization`) | org-plugin defaults + active-org restore/persist |
17
17
  | `createAuthPool` (`./pool`) | **deprecated** — alias of `createPool` from [`@ingram-tech/nk-db`](../nk-db); inject the site's shared pool instead |
18
18
  | `makeEmailSenders`, `makePasskeyOptions`, `passkeyOptionsForBaseUrl`, `uuidGenerateId` (`./`) | email hooks, passkeys (`passkeyOptionsForBaseUrl` derives `rpID`/`origin` from a single base URL), UUID ids |
@@ -268,7 +268,9 @@ Data access lives in [`@ingram-tech/nk-db`](../nk-db), not here. Query over the
268
268
  direct `pg` connection and wrap reads/writes in `withRls` / `withRlsTransaction`,
269
269
  which set `request.jwt.claims` + `SET LOCAL ROLE` per transaction from the Better
270
270
  Auth session — so `auth.uid()` policies fire unchanged, with no JWT minting and
271
- no REST proxy. See its README for the pattern.
271
+ no REST proxy. See its README for the pattern. Pass `user.id` as it comes from
272
+ the helpers: a public `usr_…` is decoded to the uuid before it reaches the
273
+ claims GUC, so a policy's `auth.uid()` still compares to a `uuid` column.
272
274
 
273
275
  ## 4. Client
274
276
 
@@ -312,9 +314,23 @@ export const {
312
314
  redirectIfAuthenticated,
313
315
  getLinkedProviders, // providerIds linked to the current user
314
316
  hasCredentialAccount, // does the current user have an email/password login?
315
- } = createAuthHelpers(auth);
317
+ } = createAuthHelpers(auth, {
318
+ // Session ids in public form (`usr_…`, `org_…`), the same form `idColumn`
319
+ // reads return, so `row.organization_id === session.activeOrganizationId`
320
+ // holds. Omit on a site with no public id form.
321
+ ids: { user: ids.user, organization: ids.organization },
322
+ });
316
323
  ```
317
324
 
325
+ With `ids`, every read through the helpers presents `user.id`,
326
+ `session.userId` and `session.activeOrganizationId` as public ids; Better Auth
327
+ itself keeps working on raw uuids underneath, and anything read directly from
328
+ `auth.api.*` is raw — the same rule as raw SQL vs `idColumn`. Pass the same
329
+ helper to `backendJwtOptions({ audience, ids: { user } })` and the backend
330
+ JWT's `sub` (and payload `id`) is the public id too. RLS needs nothing: nk-db's
331
+ `withRls*` decode a prefixed `sub` before it reaches `request.jwt.claims`, so
332
+ `auth.uid()` policies hold either way.
333
+
318
334
  ```tsx
319
335
  // app/dashboard/page.tsx — gate a protected page (validated, DB-backed).
320
336
  import { requireUser } from "@/lib/auth/session";
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.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",
@@ -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.0",
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.12.2",
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> {