@ingram-tech/nk-auth 0.13.5 → 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.5",
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",
@@ -14,6 +14,9 @@
14
14
  },
15
15
  "files": [
16
16
  "dist",
17
+ "src",
18
+ "!src/**/*.test.ts",
19
+ "!src/**/*.test.tsx",
17
20
  "migrations"
18
21
  ],
19
22
  "exports": {
@@ -65,7 +68,7 @@
65
68
  "test": "vitest run"
66
69
  },
67
70
  "dependencies": {
68
- "@ingram-tech/nk-db": "^1.6.0",
71
+ "@ingram-tech/nk-db": "^2.1.0",
69
72
  "bcrypt": "^6.0.0",
70
73
  "jose": "^6.2.9",
71
74
  "zod": "^4.4.3"
@@ -93,7 +96,7 @@
93
96
  },
94
97
  "devDependencies": {
95
98
  "@better-auth/passkey": "^1.7.0",
96
- "@ingram-tech/nk-dev": "0.11.1",
99
+ "@ingram-tech/nk-dev": "0.12.2",
97
100
  "@types/bcrypt": "^6.0.0",
98
101
  "@types/node": "^26.2.0",
99
102
  "@types/pg": "^8.23.1",
package/src/client.ts ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Browser auth re-exports, at "@ingram-tech/nk-auth/client" so the server entry
3
+ * never pulls in React. A site builds its own client to preserve full plugin
4
+ * type inference:
5
+ *
6
+ * "use client";
7
+ * import {
8
+ * createAuthClient, jwtClient, passkeyClient,
9
+ * } from "@ingram-tech/nk-auth/client";
10
+ *
11
+ * export const authClient = createAuthClient({
12
+ * baseURL: process.env.NEXT_PUBLIC_SITE_URL,
13
+ * plugins: [jwtClient(), passkeyClient()],
14
+ * });
15
+ *
16
+ * This exposes the client auth call surface:
17
+ * signUp.email / signIn.email / signIn.social / signOut / useSession,
18
+ * plus passkey.* (register / authenticate).
19
+ */
20
+ export { passkeyClient } from "@better-auth/passkey/client";
21
+ export { jwtClient } from "better-auth/client/plugins";
22
+ export { createAuthClient } from "better-auth/react";
23
+ // Password-policy primitives (pure), so a reset form validates against the same
24
+ // bounds the server enforces without a second import.
25
+ export {
26
+ CREDENTIAL_PROVIDER_ID,
27
+ DEFAULT_MAX_PASSWORD_LENGTH,
28
+ DEFAULT_MIN_PASSWORD_LENGTH,
29
+ type PasswordPolicy,
30
+ passwordSchema,
31
+ type ResetPasswordError,
32
+ type ResetPasswordErrorCode,
33
+ validateNewPassword,
34
+ } from "./password.js";
35
+ // Re-exported here too so the client can set `basePath` without a server import.
36
+ export { authBasePath } from "./paths.js";
37
+ // Headless reset-/set-password controller (React hook); the site brings the shell.
38
+ export {
39
+ type ResetPasswordClient,
40
+ type ResetPasswordStatus,
41
+ type UseResetPassword,
42
+ type UseResetPasswordOptions,
43
+ useResetPassword,
44
+ } from "./reset-password.js";
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Internal helpers shared by the server helpers and the middleware. Kept in a
3
+ * zero-`next` module so neither subpath imports the other (server pulls
4
+ * next/navigation, middleware pulls next/server — mixing them breaks the
5
+ * respective runtimes). Not a public export.
6
+ */
7
+
8
+ /**
9
+ * Request header the middleware sets to the originally-requested path so a
10
+ * server-component guard — which Next.js otherwise gives no way to learn its own
11
+ * URL — can preserve it as a `next` param when it redirects to sign-in.
12
+ */
13
+ export const NK_AUTH_PATH_HEADER = "x-nk-auth-path";
14
+
15
+ /**
16
+ * Accept only an internal, non-protocol-relative path as a post-login redirect,
17
+ * so `next` can never be turned into an open redirect to another origin.
18
+ *
19
+ * Beyond the `//` check, backslashes and ASCII controls must also be rejected:
20
+ * browsers treat `\` as `/` in http(s) URLs (so `/\evil.com` resolves to
21
+ * `https://evil.com/`) and the URL parser strips tab/newline (so an encoded
22
+ * `/\t/evil.com` collapses to `//evil.com`).
23
+ */
24
+ export function safeNextParam(value: string | null | undefined): string | null {
25
+ if (!value) return null;
26
+ if (!value.startsWith("/") || value.startsWith("//")) return null;
27
+ // oxlint-disable-next-line no-control-regex -- rejecting control chars is the point
28
+ if (/[\\\u0000-\u001f\u007f]/.test(value)) return null;
29
+ return value;
30
+ }
31
+
32
+ /**
33
+ * Build a sign-in URL: `signInPath`, plus `next` (when safe) so the user returns
34
+ * to where they were headed, plus a `stale=1` marker when a session cookie is
35
+ * present but invalid — the signal the middleware uses to clear the dead cookie.
36
+ */
37
+ export function signInUrl(
38
+ signInPath: string,
39
+ opts: { next?: string | null; stale?: boolean },
40
+ ): string {
41
+ const params = new URLSearchParams();
42
+ const next = safeNextParam(opts.next);
43
+ if (next) params.set("next", next);
44
+ if (opts.stale) params.set("stale", "1");
45
+ const qs = params.toString();
46
+ return qs ? `${signInPath}?${qs}` : signInPath;
47
+ }
package/src/id.ts ADDED
@@ -0,0 +1,12 @@
1
+ // The Ingram id codec is the standalone `id758` package, re-exported via
2
+ // `@ingram-tech/nk-db/id` — the lowest package in
3
+ // the graph — so a site can mint ids without pulling this auth slice. This
4
+ // re-export keeps `@ingram-tech/nk-auth/id` (and Better Auth's
5
+ // `advanced.database.generateId`, wired in `./options`) working unchanged. For
6
+ // the typed prefix registry (`createIdRegistry`), import from `@ingram-tech/nk-db/id`.
7
+ export {
8
+ base58Id,
9
+ fromPrefixedId,
10
+ toPrefixedId,
11
+ uuidGenerateId,
12
+ } from "@ingram-tech/nk-db/id";
package/src/index.ts ADDED
@@ -0,0 +1,36 @@
1
+ // Server entry — the Ingram Better Auth foundation. Browser re-exports live at
2
+ // "@ingram-tech/nk-auth/client" so importing the server presets never pulls in
3
+ // React. Focused subpaths (./jwt, ./organization, ./pool) let a site import
4
+ // only what it needs (e.g. avoid bcrypt when it doesn't use passwords).
5
+
6
+ export { type BackendJwtConfig, backendJwtOptions, verifyBackendJwt } from "./jwt.js";
7
+ export { base58Id, fromPrefixedId, toPrefixedId, uuidGenerateId } from "./id.js";
8
+ export { type AuthEnv, authEnv, authSecret, isConfigured } from "./keys.js";
9
+ export {
10
+ type AuthEmailKind,
11
+ type AuthEmailMessage,
12
+ type AuthEmailUser,
13
+ bcryptPassword,
14
+ makeEmailSenders,
15
+ makePasskeyOptions,
16
+ type PasskeyConfig,
17
+ passkeyOptionsForBaseUrl,
18
+ type SendEmail,
19
+ } from "./options.js";
20
+ export {
21
+ lastActiveOrganizationHooks,
22
+ lastActiveOrganizationUserField,
23
+ nkOrganizationDefaults,
24
+ } from "./organization.js";
25
+ export {
26
+ CREDENTIAL_PROVIDER_ID,
27
+ DEFAULT_MAX_PASSWORD_LENGTH,
28
+ DEFAULT_MIN_PASSWORD_LENGTH,
29
+ type PasswordPolicy,
30
+ passwordSchema,
31
+ type ResetPasswordError,
32
+ type ResetPasswordErrorCode,
33
+ validateNewPassword,
34
+ } from "./password.js";
35
+ export { authBasePath } from "./paths.js";
36
+ export { createAuthPool } from "./pool.js";
package/src/jwt.ts ADDED
@@ -0,0 +1,117 @@
1
+ import type { IdHelper } from "@ingram-tech/nk-db/id";
2
+ import type { JwtOptions } from "better-auth/plugins/jwt";
3
+ import {
4
+ createRemoteJWKSet,
5
+ errors as joseErrors,
6
+ type JWTPayload,
7
+ jwtVerify,
8
+ } from "jose";
9
+
10
+ /**
11
+ * `jwt` plugin preset for a site's OWN backend API: a short-lived token with a
12
+ * specific `audience`, typically carrying extra claims the site adds via
13
+ * `auth.api.signJWT`. Defaults to EdDSA (Better Auth's default), with the JWKS
14
+ * exposed at `/api/auth/jwks` so the backend can verify it (`verifyBackendJwt`).
15
+ */
16
+
17
+ export interface BackendJwtConfig {
18
+ /** Expected audience of the site's backend, e.g. "my-app-backend". */
19
+ audience: string;
20
+ /** Token lifetime, e.g. "15m". */
21
+ expirationTime?: string;
22
+ /**
23
+ * Don't auto-attach the JWT to the `set-auth-jwt` response header — set when
24
+ * the site mints tokens on demand via `auth.api.signJWT` instead.
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 };
33
+ }
34
+
35
+ /** `jwt` plugin options for a backend-API token (custom audience). */
36
+ export const backendJwtOptions = (config: BackendJwtConfig): JwtOptions => ({
37
+ disableSettingJwtHeader: config.disableSettingJwtHeader,
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
+ : {}),
49
+ audience: config.audience,
50
+ expirationTime: config.expirationTime,
51
+ },
52
+ });
53
+
54
+ const jwksCache = new Map<string, ReturnType<typeof createRemoteJWKSet>>();
55
+
56
+ // Forced-reload throttle (per JWKS URL). The no-matching-key path is reachable
57
+ // by anyone who can send a request with a made-up `kid`, and `.reload()`
58
+ // deliberately bypasses jose's cooldown — unthrottled, garbage tokens would
59
+ // each cost a fresh fetch against the auth origin.
60
+ const RELOAD_COOLDOWN_MS = 30_000;
61
+ const lastForcedReload = new Map<string, number>();
62
+
63
+ /**
64
+ * Verify a Better-Auth-minted backend JWT (EdDSA) against the issuer's JWKS.
65
+ * For use by the backend that consumes `backendJwtOptions` tokens. Throws on an
66
+ * invalid/expired token or audience/issuer mismatch; returns the claims.
67
+ *
68
+ * Hardened against Better Auth signing-key rotation: jose's `createRemoteJWKSet`
69
+ * refuses to refetch the JWKS for `cooldownDuration` (30s default) after any
70
+ * fetch, so a token signed with a freshly rotated key whose `kid` isn't yet in
71
+ * the cached set would fail for the *whole* cooldown window — a ~30s burst of
72
+ * auth failures on every request that verifies a token. On a no-matching-key
73
+ * miss we force a single `.reload()` (which bypasses the cooldown) and retry, so
74
+ * a rotation costs one extra fetch instead of a brief outage.
75
+ */
76
+ export const verifyBackendJwt = async (params: {
77
+ token: string;
78
+ /** The issuer's JWKS endpoint, e.g. `${BETTER_AUTH_URL}/api/auth/jwks`. */
79
+ jwksUrl: string | URL;
80
+ audience: string;
81
+ issuer: string;
82
+ }): Promise<JWTPayload> => {
83
+ const url =
84
+ typeof params.jwksUrl === "string" ? new URL(params.jwksUrl) : params.jwksUrl;
85
+ const cacheKey = url.toString();
86
+ let jwks = jwksCache.get(cacheKey);
87
+ if (!jwks) {
88
+ jwks = createRemoteJWKSet(url);
89
+ jwksCache.set(cacheKey, jwks);
90
+ }
91
+ const options = {
92
+ audience: params.audience,
93
+ issuer: params.issuer,
94
+ algorithms: ["EdDSA"],
95
+ // Tolerate a few seconds of clock skew between the minting and verifying
96
+ // hosts; jose's default of 0 fails legitimate tokens at exp/nbf boundaries.
97
+ clockTolerance: 5,
98
+ };
99
+ try {
100
+ const { payload } = await jwtVerify(params.token, jwks, options);
101
+ return payload;
102
+ } catch (err) {
103
+ if (!(err instanceof joseErrors.JWKSNoMatchingKey)) throw err;
104
+ // `kid` absent from the cached JWKS — almost always a just-rotated signing
105
+ // key. Force a refresh past the cooldown and retry exactly once, at most
106
+ // once per cooldown window so unknown-`kid` garbage can't hammer the
107
+ // issuer.
108
+ const now = Date.now();
109
+ if (now - (lastForcedReload.get(cacheKey) ?? 0) < RELOAD_COOLDOWN_MS) {
110
+ throw err;
111
+ }
112
+ lastForcedReload.set(cacheKey, now);
113
+ await jwks.reload();
114
+ const { payload } = await jwtVerify(params.token, jwks, options);
115
+ return payload;
116
+ }
117
+ };