@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 +54 -44
- 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
|
@@ -1,32 +1,28 @@
|
|
|
1
1
|
# @ingram-tech/nk-auth
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
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
|
|
15
|
-
| `backendJwtOptions` / `verifyBackendJwt` (`./jwt`) | a JWT for
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
29
|
-
>
|
|
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
|
|
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
|
|
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
|
|
123
|
-
|
|
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
|
|
126
|
-
> baseline into
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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`)
|
|
236
|
-
`body` / `ctaLabel` / `preview` overrides
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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 (
|
|
276
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
469
|
-
|
|
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;
|
|
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.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.
|
|
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.
|
|
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> {
|