@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 +19 -3
- package/dist/jwt.d.ts +9 -0
- package/dist/jwt.d.ts.map +1 -1
- package/dist/jwt.js +9 -0
- package/dist/jwt.js.map +1 -1
- package/dist/server.d.ts +51 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +36 -27
- package/dist/server.js.map +1 -1
- package/package.json +3 -3
- package/src/jwt.ts +17 -0
- package/src/server.ts +64 -3
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;
|
|
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":"
|
|
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>;
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"
|
|
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 () =>
|
|
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();
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"
|
|
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.
|
|
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.
|
|
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.
|
|
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> {
|