@pylonsync/next 0.3.95 → 0.3.96
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/package.json +3 -3
- package/src/proxy.ts +85 -2
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.3.
|
|
6
|
+
"version": "0.3.96",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"description": "Next.js helpers for Pylon — cookie-based auth gate, server-side session helpers, and reusable client hooks.",
|
|
9
9
|
"exports": {
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"check": "tsc -p tsconfig.json --noEmit"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@pylonsync/sdk": "0.3.
|
|
22
|
-
"@pylonsync/react": "0.3.
|
|
21
|
+
"@pylonsync/sdk": "0.3.96",
|
|
22
|
+
"@pylonsync/react": "0.3.96"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
25
|
"next": ">=16.0.0",
|
package/src/proxy.ts
CHANGED
|
@@ -18,8 +18,23 @@ export type CreatePylonProxyOptions = {
|
|
|
18
18
|
*/
|
|
19
19
|
loginUrl?: string;
|
|
20
20
|
/**
|
|
21
|
-
* Routes the proxy applies to.
|
|
22
|
-
*
|
|
21
|
+
* Routes the proxy applies to. Defaults to `["/dashboard/:path*"]`.
|
|
22
|
+
*
|
|
23
|
+
* The returned `config.matcher` is what Next.js statically extracts
|
|
24
|
+
* at build time to decide which requests run through this proxy.
|
|
25
|
+
* Pylon ALSO uses this list at runtime: if the consumer's outer
|
|
26
|
+
* `proxy.ts` exports a broader `config.matcher` than what was passed
|
|
27
|
+
* here (e.g. to also handle `/login` in their own logic), Pylon's
|
|
28
|
+
* proxy function falls through with `NextResponse.next()` for any
|
|
29
|
+
* request whose path doesn't match this option — so a consumer
|
|
30
|
+
* stacking proxies doesn't accidentally trigger Pylon's
|
|
31
|
+
* redirect-to-login on routes Pylon was never supposed to gate.
|
|
32
|
+
*
|
|
33
|
+
* Before 0.3.96 this option was only used to populate the returned
|
|
34
|
+
* `config.matcher`; the runtime function ignored it. Consumers with
|
|
35
|
+
* broader outer matchers hit ERR_TOO_MANY_REDIRECTS when an
|
|
36
|
+
* unauthenticated user landed on /login (Pylon redirected /login →
|
|
37
|
+
* /login?next=/login → /login → …).
|
|
23
38
|
*/
|
|
24
39
|
matcher?: string[];
|
|
25
40
|
};
|
|
@@ -38,6 +53,13 @@ export type CreatePylonProxyOptions = {
|
|
|
38
53
|
* export { proxy, config };
|
|
39
54
|
* ```
|
|
40
55
|
*
|
|
56
|
+
* If you wrap Pylon's proxy in your own logic (handling e.g. signed-in
|
|
57
|
+
* users hitting `/login`), pass `matcher` so Pylon only gates the
|
|
58
|
+
* paths you actually want gated. Your outer `config.matcher` can be
|
|
59
|
+
* broader; Pylon's runtime function checks each request's path
|
|
60
|
+
* against the option it was constructed with and falls through
|
|
61
|
+
* (`NextResponse.next()`) for anything outside it.
|
|
62
|
+
*
|
|
41
63
|
* The proxy only checks cookie *presence* — a forged value will fail the
|
|
42
64
|
* server-side `/api/auth/me` revalidation in your layout. Its job is to
|
|
43
65
|
* short-circuit the obvious "no session at all" case before any page
|
|
@@ -48,8 +70,30 @@ export function createPylonProxy(opts: CreatePylonProxyOptions = {}) {
|
|
|
48
70
|
opts.cookieName ?? process.env.PYLON_COOKIE_NAME ?? "pylon_session";
|
|
49
71
|
const loginUrl = opts.loginUrl ?? "/login";
|
|
50
72
|
const matcher = opts.matcher ?? ["/dashboard/:path*"];
|
|
73
|
+
const matchers = matcher.map(compilePathMatcher);
|
|
51
74
|
|
|
52
75
|
function proxy(request: NextRequest) {
|
|
76
|
+
const pathname = request.nextUrl.pathname;
|
|
77
|
+
|
|
78
|
+
// Self-loop guard. Without this, a no-session GET to `/login`
|
|
79
|
+
// itself (when the consumer's outer config.matcher includes
|
|
80
|
+
// `/login` — common when they want to handle "signed-in user
|
|
81
|
+
// hits /login" themselves) gets redirected to
|
|
82
|
+
// `/login?next=/login`, the browser follows, same proxy runs,
|
|
83
|
+
// infinite redirect → ERR_TOO_MANY_REDIRECTS in the browser.
|
|
84
|
+
// Surfaced 2026-05-15 by a pylon-cloud-style consumer with a
|
|
85
|
+
// composed proxy.
|
|
86
|
+
if (pathname === loginUrl) return NextResponse.next();
|
|
87
|
+
|
|
88
|
+
// Runtime matcher gate. The `matcher` option is now load-bearing
|
|
89
|
+
// (not just documentation for `config.matcher`) — requests
|
|
90
|
+
// outside it fall through. Lets consumers compose Pylon's
|
|
91
|
+
// proxy under a broader outer matcher without accidentally
|
|
92
|
+
// gating routes Pylon was never told about.
|
|
93
|
+
if (matchers.length > 0 && !matchers.some((m) => m(pathname))) {
|
|
94
|
+
return NextResponse.next();
|
|
95
|
+
}
|
|
96
|
+
|
|
53
97
|
const session = request.cookies.get(cookieName);
|
|
54
98
|
if (session) return NextResponse.next();
|
|
55
99
|
|
|
@@ -63,3 +107,42 @@ export function createPylonProxy(opts: CreatePylonProxyOptions = {}) {
|
|
|
63
107
|
|
|
64
108
|
return { proxy, config: { matcher } };
|
|
65
109
|
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Compile a Next.js-style matcher pattern into a predicate that
|
|
113
|
+
* accepts a pathname and returns true on match. Covers the subset
|
|
114
|
+
* Pylon consumers actually use:
|
|
115
|
+
*
|
|
116
|
+
* - exact: "/login" → only "/login"
|
|
117
|
+
* - segment: "/api/:slug" → "/api/x", not "/api/x/y"
|
|
118
|
+
* - rest (greedy): "/dashboard/:path*" → "/dashboard" and any subpath
|
|
119
|
+
* - rest (>=1): "/api/:rest+" → "/api/x", "/api/x/y", NOT "/api"
|
|
120
|
+
*
|
|
121
|
+
* Anything more exotic (regex modifiers, optional groups, multiple
|
|
122
|
+
* params per segment) we punt on — Pylon's docs only show the
|
|
123
|
+
* patterns above, and a stricter parser would silently mis-match
|
|
124
|
+
* fancier strings instead of failing loudly.
|
|
125
|
+
*/
|
|
126
|
+
function compilePathMatcher(pattern: string): (pathname: string) => boolean {
|
|
127
|
+
// Split into segments and translate each. Leading `/` is implicit.
|
|
128
|
+
const segs = pattern.split("/").filter(Boolean);
|
|
129
|
+
const parts: string[] = ["^"];
|
|
130
|
+
for (const seg of segs) {
|
|
131
|
+
if (seg === ":path*" || /^:\w+\*$/.test(seg)) {
|
|
132
|
+
// Zero-or-more trailing segments. Optional with leading slash.
|
|
133
|
+
parts.push("(?:/.*)?");
|
|
134
|
+
} else if (/^:\w+\+$/.test(seg)) {
|
|
135
|
+
// One-or-more trailing segments.
|
|
136
|
+
parts.push("/.+");
|
|
137
|
+
} else if (/^:\w+$/.test(seg)) {
|
|
138
|
+
// Single segment.
|
|
139
|
+
parts.push("/[^/]+");
|
|
140
|
+
} else {
|
|
141
|
+
// Literal — escape regex metacharacters.
|
|
142
|
+
parts.push("/" + seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
parts.push("/?$");
|
|
146
|
+
const re = new RegExp(parts.join(""));
|
|
147
|
+
return (pathname) => re.test(pathname);
|
|
148
|
+
}
|