@poly-x/next 0.1.1 → 0.2.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 +127 -23
- package/dist/index.cjs +2 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +2 -1
- package/dist/proxy.cjs +24 -4
- package/dist/proxy.d.cts +2 -1
- package/dist/proxy.d.mts +2 -1
- package/dist/proxy.mjs +23 -5
- package/dist/server.cjs +978 -23
- package/dist/server.d.cts +270 -3
- package/dist/server.d.mts +270 -3
- package/dist/server.mjs +959 -13
- package/dist/session-cookie-AEnY4PVW.d.cts +21 -0
- package/dist/session-cookie-AEnY4PVW.d.mts +21 -0
- package/dist/session-cookie-BQD10B3D.cjs +34 -0
- package/dist/session-cookie-C23WspJZ.mjs +23 -0
- package/dist/styles.css +151 -0
- package/package.json +3 -3
- package/dist/server-session-Df9HXl_5.cjs +0 -254
- package/dist/server-session-pY-nzh5x.mjs +0 -207
package/README.md
CHANGED
|
@@ -4,57 +4,161 @@ PolyX authentication for the Next.js App Router. Everything in `@poly-x/react`,
|
|
|
4
4
|
plus a server side: tokens live **AES-GCM-encrypted in an httpOnly cookie on your
|
|
5
5
|
own domain** — browser JavaScript never sees them.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Adopt it in three files
|
|
8
|
+
|
|
9
|
+
Install, set two environment values, add three files. None of them contains
|
|
10
|
+
security logic.
|
|
8
11
|
|
|
9
12
|
```bash
|
|
10
13
|
npm install @poly-x/next
|
|
11
14
|
```
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
```bash
|
|
17
|
+
NEXT_PUBLIC_POLYX_PUBLISHABLE_KEY=pk_test_… # your app's publishable key
|
|
18
|
+
POLYX_SESSION_SECRET=… # encrypts the session cookie
|
|
19
|
+
```
|
|
15
20
|
|
|
16
|
-
|
|
21
|
+
**1. Mount the routes.** One declaration mounts every action:
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
```ts
|
|
24
|
+
// app/api/polyx/[...polyx]/route.ts
|
|
25
|
+
import { createPolyXHandler } from "@poly-x/next/server";
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
import { PolyXProvider } from "@poly-x/next";
|
|
27
|
+
export const { GET, POST } = createPolyXHandler();
|
|
23
28
|
```
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
**2. Gate your routes** (Next 16 `proxy.ts`; `middleware.ts` on ≤15):
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
// proxy.ts
|
|
34
|
+
import { proxy } from "@poly-x/next/proxy";
|
|
35
|
+
|
|
36
|
+
export default proxy({ protectedRoutes: ["/dashboard"] });
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**3. Declare each backend you call.** Because the browser holds no token, calls to
|
|
40
|
+
your own API are proxied server-side — you declare the upstream, the SDK attaches
|
|
41
|
+
the credential, refreshes it on expiry and retries once:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
// app/api/billing/proxy/[...path]/route.ts
|
|
45
|
+
import { createDataProxy } from "@poly-x/next/server";
|
|
46
|
+
|
|
47
|
+
export const { GET, POST, PUT, PATCH, DELETE } = createDataProxy({
|
|
48
|
+
upstream: process.env.BILLING_URL!,
|
|
49
|
+
label: "Billing",
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Point your existing client at `/api/billing/proxy` instead of the upstream. No
|
|
54
|
+
call sites change beyond the base URL.
|
|
55
|
+
|
|
56
|
+
## Reading the session
|
|
26
57
|
|
|
27
|
-
|
|
28
|
-
Components can't set cookies):
|
|
58
|
+
**Server** — `auth()` is read-only, so it is safe in a Server Component:
|
|
29
59
|
|
|
30
60
|
```ts
|
|
31
61
|
import { auth } from "@poly-x/next/server";
|
|
32
62
|
|
|
33
|
-
export
|
|
63
|
+
export async function currentUserId(): Promise<string | null> {
|
|
34
64
|
const { isSignedIn, userId } = await auth();
|
|
35
|
-
|
|
65
|
+
return isSignedIn ? userId : null;
|
|
36
66
|
}
|
|
37
67
|
```
|
|
38
68
|
|
|
39
|
-
|
|
40
|
-
|
|
69
|
+
For the full profile from the platform, use `getSessionContext()`. It distinguishes
|
|
70
|
+
"the directory is briefly unavailable" from "signed out", so a transient outage
|
|
71
|
+
never signs your users out:
|
|
41
72
|
|
|
42
73
|
```ts
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
export
|
|
74
|
+
import { getSessionContext } from "@poly-x/next/server";
|
|
75
|
+
|
|
76
|
+
export async function loadProfile() {
|
|
77
|
+
const ctx = await getSessionContext();
|
|
78
|
+
if (ctx.isSignedIn && ctx.profileStatus === "unavailable") {
|
|
79
|
+
// still signed in — the profile just could not be loaded right now
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
return ctx.user;
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Client** — the hooks work with no provider under this custody model:
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
"use client";
|
|
90
|
+
import { useAuth, useUser } from "@poly-x/next";
|
|
91
|
+
|
|
92
|
+
export function Profile() {
|
|
93
|
+
const { isLoaded, isSignedIn, signOut, refresh } = useAuth();
|
|
94
|
+
const user = useUser();
|
|
95
|
+
if (!isLoaded) return null;
|
|
96
|
+
if (!isSignedIn) return <a href="/sign-in">Sign in</a>;
|
|
97
|
+
return (
|
|
98
|
+
<div>
|
|
99
|
+
<span>{user?.email}</span>
|
|
100
|
+
<button onClick={() => void refresh()}>Re-check permissions</button>
|
|
101
|
+
<button onClick={() => void signOut({ redirectTo: "/sign-in" })}>Sign out</button>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
46
105
|
```
|
|
47
106
|
|
|
48
|
-
|
|
107
|
+
`signOut()` performs the full-page navigation this custody model requires — a
|
|
108
|
+
client-side redirect would leave the httpOnly cookie in place and your route gate
|
|
109
|
+
would bounce the user straight back in. Do not hand-roll it.
|
|
110
|
+
|
|
111
|
+
`refresh()` re-reads authorization and resolves `true` only when the claims
|
|
112
|
+
actually changed; pair it with `useLiveAuthz` to re-guard a tab without a reload.
|
|
113
|
+
|
|
114
|
+
`consumeAuthEvent()` returns `"signed-in" | "signed-out" | null` once after the
|
|
115
|
+
navigation, for a toast or an analytics call that would otherwise be destroyed
|
|
116
|
+
when the page unloads.
|
|
117
|
+
|
|
118
|
+
## Styles
|
|
119
|
+
|
|
120
|
+
If you render `<SignIn>` or `<UserButton>`, import the stylesheet **before** your
|
|
121
|
+
Tailwind entry. Cascade-layer order is fixed at first declaration, so importing it
|
|
122
|
+
later silently loses to your utilities:
|
|
49
123
|
|
|
50
124
|
```ts
|
|
51
|
-
|
|
52
|
-
|
|
125
|
+
// app/layout.tsx
|
|
126
|
+
import "@poly-x/next/styles.css";
|
|
127
|
+
import "./globals.css";
|
|
53
128
|
```
|
|
54
129
|
|
|
130
|
+
## When something goes wrong
|
|
131
|
+
|
|
132
|
+
| Symptom | Cause |
|
|
133
|
+
|---|---|
|
|
134
|
+
| `401` from a proxied call, no upstream request made | No session, or the cookie would not unseal (rotated `POLYX_SESSION_SECRET`). |
|
|
135
|
+
| `401` from a proxied call to a path you did not declare | Only declared upstreams receive a credential. |
|
|
136
|
+
| `{ status: "configuration_error", code: "REDIRECT_URI_NOT_ALLOWED" }` | The redirect URI the SDK sent is not registered. Behind a proxy set `POLYX_PUBLIC_ORIGIN`, or enable `POLYX_TRUST_FORWARDED_ORIGIN` — `request.url` is the bind address. The server log names the URI actually used. |
|
|
137
|
+
| `{ status: "session_too_large" }` | The sealed session exceeds the ~4096-byte browser cookie limit. Usually a tenant configured to embed full permissions in the JWT. |
|
|
138
|
+
| Sign-in returns 200 but the user is never signed in | Upgrade to ≥ 0.1.1 — earlier versions wrote an oversized cookie the browser silently discarded. |
|
|
139
|
+
|
|
140
|
+
## Profile self-service
|
|
141
|
+
|
|
142
|
+
`createPolyXHandler()` also mounts `/api/polyx/profile`, which backs
|
|
143
|
+
`<UserProfile>` — nothing extra to wire. It reads and updates **only** the
|
|
144
|
+
signed-in person's own profile: the identity comes from the sealed session, never
|
|
145
|
+
from the request, so it cannot be aimed at anyone else.
|
|
146
|
+
|
|
147
|
+
The submitted body is rebuilt from an allow-list rather than forwarded. The
|
|
148
|
+
platform's update applies whatever recognised fields it receives — including
|
|
149
|
+
`email`, `password` and `role` — so relaying a browser's multipart verbatim would
|
|
150
|
+
turn a profile form into a way to change a credential or grant a role. Only the
|
|
151
|
+
six profile fields ever leave the server.
|
|
152
|
+
|
|
55
153
|
## Exports
|
|
56
154
|
|
|
57
|
-
`@poly-x/next`
|
|
58
|
-
`
|
|
155
|
+
- `@poly-x/next` — the client surface (re-exports `@poly-x/react`): `PolyXProvider`,
|
|
156
|
+
`useAuth`, `useUser`, `useSession`, `useLiveAuthz`, `consumeAuthEvent`, `<SignIn>`,
|
|
157
|
+
`<UserButton>`, `<UserProfile>`, `<Protected>`.
|
|
158
|
+
- `@poly-x/next/server` — `createPolyXHandler`, `createDataProxy`, `auth`,
|
|
159
|
+
`getSessionContext`, `refreshSession`, `createAuthHandlers`, `ServerSession`,
|
|
160
|
+
`sealSession` / `openSession`, `SESSION_COOKIE_MAX_BYTES`, `resolvePublicOrigin`.
|
|
161
|
+
- `@poly-x/next/proxy` — `proxy`, `DEFAULT_SESSION_COOKIE`, `readSessionCookie`
|
|
162
|
+
(edge-safe: no server-only imports).
|
|
59
163
|
|
|
60
164
|
MIT licensed.
|
package/dist/index.cjs
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
//#region src/index.ts
|
|
4
4
|
const PACKAGE_NAME = "@poly-x/next";
|
|
5
|
-
|
|
5
|
+
/** The published version of this package, injected at build time. */
|
|
6
|
+
const version = "0.2.0";
|
|
6
7
|
//#endregion
|
|
7
8
|
exports.PACKAGE_NAME = PACKAGE_NAME;
|
|
8
9
|
exports.version = version;
|
package/dist/index.d.cts
CHANGED
|
@@ -2,6 +2,7 @@ export * from "@poly-x/react";
|
|
|
2
2
|
|
|
3
3
|
//#region src/index.d.ts
|
|
4
4
|
declare const PACKAGE_NAME = "@poly-x/next";
|
|
5
|
-
|
|
5
|
+
/** The published version of this package, injected at build time. */
|
|
6
|
+
declare const version: string;
|
|
6
7
|
//#endregion
|
|
7
8
|
export { PACKAGE_NAME, version };
|
package/dist/index.d.mts
CHANGED
|
@@ -2,6 +2,7 @@ export * from "@poly-x/react";
|
|
|
2
2
|
|
|
3
3
|
//#region src/index.d.ts
|
|
4
4
|
declare const PACKAGE_NAME = "@poly-x/next";
|
|
5
|
-
|
|
5
|
+
/** The published version of this package, injected at build time. */
|
|
6
|
+
declare const version: string;
|
|
6
7
|
//#endregion
|
|
7
8
|
export { PACKAGE_NAME, version };
|
package/dist/index.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export * from "@poly-x/react";
|
|
3
3
|
//#region src/index.ts
|
|
4
4
|
const PACKAGE_NAME = "@poly-x/next";
|
|
5
|
-
|
|
5
|
+
/** The published version of this package, injected at build time. */
|
|
6
|
+
const version = "0.2.0";
|
|
6
7
|
//#endregion
|
|
7
8
|
export { PACKAGE_NAME, version };
|
package/dist/proxy.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
require("./
|
|
2
|
+
const require_session_cookie = require("./session-cookie-BQD10B3D.cjs");
|
|
3
3
|
let next_server = require("next/server");
|
|
4
4
|
//#region src/proxy.ts
|
|
5
5
|
/**
|
|
@@ -8,17 +8,37 @@ let next_server = require("next/server");
|
|
|
8
8
|
* gates on the session cookie's *presence* (a fast edge check) and redirects
|
|
9
9
|
* signed-out users to sign-in; `auth()` does the real decrypt server-side.
|
|
10
10
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Make a matcher that gives the same answer every time it is called.
|
|
13
|
+
*
|
|
14
|
+
* `RegExp.prototype.test` advances `lastIndex` when the pattern carries `g` or `y`, so testing the
|
|
15
|
+
* SAME regex object repeatedly alternates true/false/true. This middleware holds the consumer's
|
|
16
|
+
* patterns for the lifetime of the process and tests them once per request — so a consumer writing
|
|
17
|
+
* `/^\/admin/g`, an easy habit, would have their protected routes gated on every OTHER request.
|
|
18
|
+
* That fails **open**, intermittently, and is close to undebuggable from the outside.
|
|
19
|
+
*
|
|
20
|
+
* The stateful flags are stripped into a fresh regex here, once at construction: no per-request
|
|
21
|
+
* allocation, and the consumer's own object is left untouched (they may be using it elsewhere, and
|
|
22
|
+
* resetting its `lastIndex` behind their back would be its own surprise).
|
|
23
|
+
*/
|
|
24
|
+
function toMatcher(route) {
|
|
25
|
+
if (typeof route === "string") return (pathname) => pathname.startsWith(route);
|
|
26
|
+
if (!route.global && !route.sticky) return (pathname) => route.test(pathname);
|
|
27
|
+
const stateless = new RegExp(route.source, route.flags.replace(/[gy]/g, ""));
|
|
28
|
+
return (pathname) => stateless.test(pathname);
|
|
13
29
|
}
|
|
14
30
|
function proxy(config) {
|
|
15
31
|
const cookieName = config.cookieName ?? "polyx_session";
|
|
16
32
|
const signInPath = config.signInPath ?? "/api/polyx/signin";
|
|
33
|
+
const matchers = config.protectedRoutes.map(toMatcher);
|
|
34
|
+
const isProtected = (pathname) => matchers.some((match) => match(pathname));
|
|
17
35
|
return (request) => {
|
|
18
|
-
if (!isProtected(request.nextUrl.pathname
|
|
36
|
+
if (!isProtected(request.nextUrl.pathname)) return next_server.NextResponse.next();
|
|
19
37
|
if (request.cookies.get(cookieName)?.value) return next_server.NextResponse.next();
|
|
20
38
|
return next_server.NextResponse.redirect(new URL(signInPath, request.url));
|
|
21
39
|
};
|
|
22
40
|
}
|
|
23
41
|
//#endregion
|
|
42
|
+
exports.DEFAULT_SESSION_COOKIE = require_session_cookie.DEFAULT_SESSION_COOKIE;
|
|
24
43
|
exports.proxy = proxy;
|
|
44
|
+
exports.readSessionCookie = require_session_cookie.readSessionCookie;
|
package/dist/proxy.d.cts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { n as readSessionCookie, t as DEFAULT_SESSION_COOKIE } from "./session-cookie-AEnY4PVW.cjs";
|
|
1
2
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
3
|
|
|
3
4
|
//#region src/proxy.d.ts
|
|
@@ -10,4 +11,4 @@ interface ProxyConfig {
|
|
|
10
11
|
}
|
|
11
12
|
declare function proxy(config: ProxyConfig): (request: NextRequest) => NextResponse;
|
|
12
13
|
//#endregion
|
|
13
|
-
export { ProxyConfig, proxy };
|
|
14
|
+
export { DEFAULT_SESSION_COOKIE, ProxyConfig, proxy, readSessionCookie };
|
package/dist/proxy.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { n as readSessionCookie, t as DEFAULT_SESSION_COOKIE } from "./session-cookie-AEnY4PVW.mjs";
|
|
1
2
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
3
|
|
|
3
4
|
//#region src/proxy.d.ts
|
|
@@ -10,4 +11,4 @@ interface ProxyConfig {
|
|
|
10
11
|
}
|
|
11
12
|
declare function proxy(config: ProxyConfig): (request: NextRequest) => NextResponse;
|
|
12
13
|
//#endregion
|
|
13
|
-
export { ProxyConfig, proxy };
|
|
14
|
+
export { DEFAULT_SESSION_COOKIE, ProxyConfig, proxy, readSessionCookie };
|
package/dist/proxy.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import "./
|
|
1
|
+
import { n as readSessionCookie, t as DEFAULT_SESSION_COOKIE } from "./session-cookie-C23WspJZ.mjs";
|
|
2
2
|
import { NextResponse } from "next/server";
|
|
3
3
|
//#region src/proxy.ts
|
|
4
4
|
/**
|
|
@@ -7,17 +7,35 @@ import { NextResponse } from "next/server";
|
|
|
7
7
|
* gates on the session cookie's *presence* (a fast edge check) and redirects
|
|
8
8
|
* signed-out users to sign-in; `auth()` does the real decrypt server-side.
|
|
9
9
|
*/
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Make a matcher that gives the same answer every time it is called.
|
|
12
|
+
*
|
|
13
|
+
* `RegExp.prototype.test` advances `lastIndex` when the pattern carries `g` or `y`, so testing the
|
|
14
|
+
* SAME regex object repeatedly alternates true/false/true. This middleware holds the consumer's
|
|
15
|
+
* patterns for the lifetime of the process and tests them once per request — so a consumer writing
|
|
16
|
+
* `/^\/admin/g`, an easy habit, would have their protected routes gated on every OTHER request.
|
|
17
|
+
* That fails **open**, intermittently, and is close to undebuggable from the outside.
|
|
18
|
+
*
|
|
19
|
+
* The stateful flags are stripped into a fresh regex here, once at construction: no per-request
|
|
20
|
+
* allocation, and the consumer's own object is left untouched (they may be using it elsewhere, and
|
|
21
|
+
* resetting its `lastIndex` behind their back would be its own surprise).
|
|
22
|
+
*/
|
|
23
|
+
function toMatcher(route) {
|
|
24
|
+
if (typeof route === "string") return (pathname) => pathname.startsWith(route);
|
|
25
|
+
if (!route.global && !route.sticky) return (pathname) => route.test(pathname);
|
|
26
|
+
const stateless = new RegExp(route.source, route.flags.replace(/[gy]/g, ""));
|
|
27
|
+
return (pathname) => stateless.test(pathname);
|
|
12
28
|
}
|
|
13
29
|
function proxy(config) {
|
|
14
30
|
const cookieName = config.cookieName ?? "polyx_session";
|
|
15
31
|
const signInPath = config.signInPath ?? "/api/polyx/signin";
|
|
32
|
+
const matchers = config.protectedRoutes.map(toMatcher);
|
|
33
|
+
const isProtected = (pathname) => matchers.some((match) => match(pathname));
|
|
16
34
|
return (request) => {
|
|
17
|
-
if (!isProtected(request.nextUrl.pathname
|
|
35
|
+
if (!isProtected(request.nextUrl.pathname)) return NextResponse.next();
|
|
18
36
|
if (request.cookies.get(cookieName)?.value) return NextResponse.next();
|
|
19
37
|
return NextResponse.redirect(new URL(signInPath, request.url));
|
|
20
38
|
};
|
|
21
39
|
}
|
|
22
40
|
//#endregion
|
|
23
|
-
export { proxy };
|
|
41
|
+
export { DEFAULT_SESSION_COOKIE, proxy, readSessionCookie };
|