@grantry/nextjs 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +109 -2
- package/dist/cookie.d.ts +24 -0
- package/dist/cookie.d.ts.map +1 -0
- package/dist/cookie.js +24 -0
- package/dist/cookie.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +86 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware.d.ts +73 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +217 -0
- package/dist/middleware.js.map +1 -0
- package/dist/provider.d.ts +34 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +66 -0
- package/dist/provider.js.map +1 -0
- package/dist/server.d.ts +64 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +93 -0
- package/dist/server.js.map +1 -0
- package/package.json +61 -3
- package/src/cookie.ts +24 -0
- package/src/index.ts +249 -0
- package/src/middleware.ts +292 -0
- package/src/provider.tsx +73 -0
- package/src/server.ts +159 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Grantry
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,110 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @grantry/nextjs
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Grantry authentication for Next.js: the React components and hooks, `auth()` for server
|
|
4
|
+
components, route handlers and server actions, and a middleware that keeps signed-out visitors
|
|
5
|
+
off protected routes.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @grantry/nextjs
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Next.js 15 or 16 and React 18 or 19. Everything `@grantry/react` exports is
|
|
12
|
+
re-exported here, so a Next.js application does not install it separately.
|
|
13
|
+
|
|
14
|
+
## Setup
|
|
15
|
+
|
|
16
|
+
Set your publishable key (Grantry dashboard, API keys):
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
NEXT_PUBLIC_GRANTRY_PUBLISHABLE_KEY=pk_live_...
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Wrap the application in the provider:
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
// app/layout.tsx
|
|
26
|
+
import { GrantryProvider } from '@grantry/nextjs';
|
|
27
|
+
|
|
28
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
29
|
+
return (
|
|
30
|
+
<html lang="en">
|
|
31
|
+
<body>
|
|
32
|
+
<GrantryProvider publishableKey={process.env.NEXT_PUBLIC_GRANTRY_PUBLISHABLE_KEY!}>
|
|
33
|
+
{children}
|
|
34
|
+
</GrantryProvider>
|
|
35
|
+
</body>
|
|
36
|
+
</html>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Reading the visitor on the server
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { auth } from '@grantry/nextjs/server';
|
|
45
|
+
|
|
46
|
+
export default async function Page() {
|
|
47
|
+
const { userId } = await auth();
|
|
48
|
+
if (!userId) return <p>Signed out</p>;
|
|
49
|
+
return <p>Signed in as {userId}</p>;
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`auth()` verifies the `grantry_token` cookie against Grantry's published signing keys, which are
|
|
54
|
+
fetched once and cached; no request is made per render after that. It checks the signature, the
|
|
55
|
+
expiry, the issuer and that the token belongs to *your* application, read from the publishable
|
|
56
|
+
key. With no publishable key configured it throws `no_audience` rather than accept a token minted
|
|
57
|
+
for another application.
|
|
58
|
+
|
|
59
|
+
`requireAuth()` is the same but throws when nobody is signed in. For the Pages Router, or anything
|
|
60
|
+
else that cannot call `next/headers`, verify the cookie yourself with `verifyToken`, which is also
|
|
61
|
+
exported from `@grantry/nextjs/server`.
|
|
62
|
+
|
|
63
|
+
## Protecting routes
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
// proxy.ts (Next.js 16) or middleware.ts (Next.js 15)
|
|
67
|
+
import { grantryMiddleware } from '@grantry/nextjs/middleware';
|
|
68
|
+
|
|
69
|
+
export default grantryMiddleware({
|
|
70
|
+
protectedRoutes: ['/dashboard', '/account'],
|
|
71
|
+
signInUrl: '/sign-in',
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export const config = { matcher: ['/((?!_next|.*\\..*).*)'] };
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
A string protects that path and everything under it; pass a `RegExp` for anything else. Only what
|
|
78
|
+
is listed is protected. A signed-out visitor is redirected to the sign-in page with `redirect_url`
|
|
79
|
+
set to where they were going. A verified request goes on with `x-grantry-user-id` and
|
|
80
|
+
`x-grantry-session-id` set; those headers are removed from every request the middleware sees, but
|
|
81
|
+
a route outside the `matcher` never reaches it, so `auth()` is the safer read.
|
|
82
|
+
|
|
83
|
+
With no `signInUrl`, the middleware uses `GRANTRY_SIGN_IN_URL`, then
|
|
84
|
+
`NEXT_PUBLIC_GRANTRY_SIGN_IN_URL`, then the environment's hosted sign-in page, and answers `401`
|
|
85
|
+
if there is none.
|
|
86
|
+
|
|
87
|
+
## Environment variables
|
|
88
|
+
|
|
89
|
+
| Variable | Read by |
|
|
90
|
+
| --- | --- |
|
|
91
|
+
| `NEXT_PUBLIC_GRANTRY_PUBLISHABLE_KEY` | `auth()`, the middleware (and pass it to the provider) |
|
|
92
|
+
| `GRANTRY_PUBLISHABLE_KEY` | `auth()` and the middleware, before the public one |
|
|
93
|
+
| `NEXT_PUBLIC_GRANTRY_SIGN_IN_URL` | the provider and the middleware (after `GRANTRY_SIGN_IN_URL`) |
|
|
94
|
+
| `NEXT_PUBLIC_GRANTRY_SIGN_UP_URL` | the provider |
|
|
95
|
+
| `NEXT_PUBLIC_GRANTRY_SIGN_IN_FALLBACK_REDIRECT_URL`, `..._SIGN_UP_FALLBACK_REDIRECT_URL` | the provider |
|
|
96
|
+
| `NEXT_PUBLIC_GRANTRY_SIGN_IN_FORCE_REDIRECT_URL`, `..._SIGN_UP_FORCE_REDIRECT_URL` | the provider |
|
|
97
|
+
| `NEXT_PUBLIC_GRANTRY_AFTER_SIGN_OUT_URL` | the provider |
|
|
98
|
+
|
|
99
|
+
No secret key is needed or read by this package.
|
|
100
|
+
|
|
101
|
+
## About the token cookie
|
|
102
|
+
|
|
103
|
+
The browser SDK mirrors the short-lived access token into a `grantry_token` cookie on your domain
|
|
104
|
+
(`SameSite=Lax`, `Secure` on HTTPS) so the server can see who is signed in. A cookie written by
|
|
105
|
+
script cannot be `httpOnly`; the token lives about a minute and cannot mint another, and the
|
|
106
|
+
credential that can stays `httpOnly` on Grantry's domain.
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
MIT
|
package/dist/cookie.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The access-token cookie, as the middleware and `auth()` both read it.
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ ONE COPY OF THE NAME, AND A TEST HOLDS IT TO `@grantry/js`. It cannot be imported from there:
|
|
5
|
+
* this package depends on `@grantry/react` alone, and `@grantry/react`'s main entry is the browser
|
|
6
|
+
* half, which has no business in a middleware bundle. `server.ts` and `middleware.ts` each carried
|
|
7
|
+
* their own literal, one under a comment saying it was imported; `tests/cookie.test.ts` compares
|
|
8
|
+
* this one with the writer's.
|
|
9
|
+
*/
|
|
10
|
+
export declare const TOKEN_COOKIE_NAME = "grantry_token";
|
|
11
|
+
/**
|
|
12
|
+
* The token in the cookie as Next.js hands it over, or null when there is none.
|
|
13
|
+
*
|
|
14
|
+
* ⚠️ NOT DECODED A SECOND TIME. Next's cookie parser already applies `decodeURIComponent` to every
|
|
15
|
+
* value, and both readers applied it again. A JWT never contains `%`, so the second pass changed
|
|
16
|
+
* nothing for a real token — and threw `URIError` for a cookie whose decoded value held a stray
|
|
17
|
+
* `%E0`. In `auth()` that error was not a `TokenVerificationError`, so it was rethrown and every
|
|
18
|
+
* server component calling `auth()` failed to render for that visitor. Anything other than a token
|
|
19
|
+
* that is still left is reported by `verifyToken` as `malformed`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function tokenFromCookie(cookie: {
|
|
22
|
+
value: string;
|
|
23
|
+
} | undefined): string | null;
|
|
24
|
+
//# sourceMappingURL=cookie.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cookie.d.ts","sourceRoot":"","sources":["../src/cookie.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AAEjD;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAEpF"}
|
package/dist/cookie.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The access-token cookie, as the middleware and `auth()` both read it.
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ ONE COPY OF THE NAME, AND A TEST HOLDS IT TO `@grantry/js`. It cannot be imported from there:
|
|
5
|
+
* this package depends on `@grantry/react` alone, and `@grantry/react`'s main entry is the browser
|
|
6
|
+
* half, which has no business in a middleware bundle. `server.ts` and `middleware.ts` each carried
|
|
7
|
+
* their own literal, one under a comment saying it was imported; `tests/cookie.test.ts` compares
|
|
8
|
+
* this one with the writer's.
|
|
9
|
+
*/
|
|
10
|
+
export const TOKEN_COOKIE_NAME = 'grantry_token';
|
|
11
|
+
/**
|
|
12
|
+
* The token in the cookie as Next.js hands it over, or null when there is none.
|
|
13
|
+
*
|
|
14
|
+
* ⚠️ NOT DECODED A SECOND TIME. Next's cookie parser already applies `decodeURIComponent` to every
|
|
15
|
+
* value, and both readers applied it again. A JWT never contains `%`, so the second pass changed
|
|
16
|
+
* nothing for a real token — and threw `URIError` for a cookie whose decoded value held a stray
|
|
17
|
+
* `%E0`. In `auth()` that error was not a `TokenVerificationError`, so it was rethrown and every
|
|
18
|
+
* server component calling `auth()` failed to render for that visitor. Anything other than a token
|
|
19
|
+
* that is still left is reported by `verifyToken` as `malformed`.
|
|
20
|
+
*/
|
|
21
|
+
export function tokenFromCookie(cookie) {
|
|
22
|
+
return cookie?.value || null;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=cookie.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cookie.js","sourceRoot":"","sources":["../src/cookie.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,eAAe,CAAC;AAEjD;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAAC,MAAqC;IACnE,OAAO,MAAM,EAAE,KAAK,IAAI,IAAI,CAAC;AAC/B,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @grantry/nextjs — authentication for Next.js.
|
|
3
|
+
*
|
|
4
|
+
* app/layout.tsx <GrantryProvider> (client)
|
|
5
|
+
* app/page.tsx await auth() (server)
|
|
6
|
+
* middleware.ts grantryMiddleware() (edge)
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ A RE-EXPORT, NOT A SECOND IMPLEMENTATION. Everything below is the React
|
|
9
|
+
* SDK unchanged, so a Next.js application installs one dependency and imports
|
|
10
|
+
* the same names it would in any React app — and there is no second copy of a
|
|
11
|
+
* component to drift from the first. The Next-specific half is `./server` and
|
|
12
|
+
* `./middleware`, which is genuinely different code.
|
|
13
|
+
*
|
|
14
|
+
* The one exception is `GrantryProvider`, which is a four-line wrapper around
|
|
15
|
+
* the React one for the sole purpose of saying "nextjs" rather than "react" on
|
|
16
|
+
* the wire. See `./provider` for why that distinction has to exist at all.
|
|
17
|
+
*
|
|
18
|
+
* ⚠️ EVERY NAME `@grantry/react` EXPORTS IS LISTED HERE, AND A TEST HOLDS IT TO
|
|
19
|
+
* THAT. The list is by name, with no `export *`, so for months each new React
|
|
20
|
+
* export was simply absent from Next.js:
|
|
21
|
+
* `AuthCard`, `OAuthConsent`, the localization contract and the entitlement
|
|
22
|
+
* helpers all had documentation pages telling Next.js readers to import them
|
|
23
|
+
* from here. `packages/sdk-react/tests/nextjs-exports.test.ts` compares the two
|
|
24
|
+
* lists, types included, and fails on the next one.
|
|
25
|
+
*/
|
|
26
|
+
export { GrantryProvider } from './provider.js';
|
|
27
|
+
export { useAuth, useUser, useSession, useAccounts, useOAuth, useGrantry, useEntitlements, readOAuthResult, usePlans, pricingCatalogue, formatMoney, useRoutes, mergeRouteOptions, redirectTarget, requestedRedirectUrl, useSignInFlow, useSignUpFlow, entitlementsFromToken, actorFromToken, checkEntitlements, EMPTY_ENTITLEMENTS, SignedIn, SignedOut, GrantryLoading, GrantryLoaded, GrantryDegraded, GrantryFailed, useEnvironmentStatus, Protect, SignOutButton, useSignOutAction, SignIn, SignUp, CompleteAccount, UserButton, isAddingAccount, addAccountUrl, ADD_ACCOUNT_PARAM, UserAvatar, Waitlist, ResetPassword, SetupMfa, UserProfile, PricingTable, CheckoutButton, PlanDetailsButton, SubscriptionDetailsButton, ProductCard, type ProductCardProps, MenuItems, CustomAction, CustomLink, CustomPage, AuthCard, OAuthConsent, ContinueAs, ImpersonationBanner, LocalizationProvider, useTranslate, EN, AppearanceProvider, GRANTRY_STYLES, useStylesheet, variablesToStyle, THEME_COLORS, THEME_COLOR_LABELS, THEME_FONTS, THEME_LOGO_PLACEMENTS, THEME_RADII, THEME_SOCIAL_PLACEMENTS, THEME_SOCIAL_VARIANTS, brandThemeAppearance, cleanBrandTheme, themeColorScheme, DEFAULT_AVATAR_COLORS, avatarBackgroundCss, avatarInitial, avatarSeed, GrantryClient, GrantryError, TOKEN_COOKIE_NAME, useReverification, type Reverify, type ReverifyOptions, type Reverification, type ReverificationGrant, type ReverificationMethod, type GrantryProviderProps, type GrantryPreview, type SignInHostProviderProps, type ResellerHostProviderProps, type Routes, type RouteOptions, type RedirectTargets, type TokenEntitlements, type HasParams, type UseEntitlementsReturn, type SignInStatus, type SignUpStatus, type ProtectProps, type AuthCardProps, type OAuthConsentProps, type ContinueAsProps, type Localization, type LocalizationInput, type GrantryClientOptions, type SignUpParams, type SignUpResult, type BackupCodesProof, type BackupCodeEntry, type RevealedBackupCodes, type ChangeQuote, type CouponPreview, type FieldIssue, type BrandTheme, type ThemeColor, type ThemeFont, type ThemeProblem, type ThemeRadius, type AvatarStyle, type AvatarBackground, type AvatarForeground, type GrantryUser, type GrantrySession, type GrantryTrustedDevice, type GrantryBoundDevice, type GrantryBoundDevices, type GrantryAccount, type GrantryEnvironment, type GrantryErrorCode, type SignInResult, type Appearance, type ElementDescriptor, type ElementsConfig, type ElementStyle, type AppearanceVariables, type AppearanceLayout, type EnvironmentStatus, type SignOutButtonProps, type UseSignOutAction, type SignInProps, type SignUpProps, type CompleteAccountProps, type UserButtonProps, type UserProfileProps, type UseAuthReturn, type UseUserReturn, type UseSessionReturn, type UseAccountsReturn, type UseOAuthReturn, type UseSignInFlow, type UseSignUpFlow, type OAuthProviderInfo, type ConnectedAccount, type GrantryAuthorizedApp, type MfaFactor, type UserEmailAddress, type PublicPlan, type PlanPrice, type PlansResponse, type BillingSummary, type UsePlansOptions, type UsePlansReturn, type PricingCatalogue, type PricingPlan, type PricingPlanPrice, type PlanIntervalUnit, } from '@grantry/react';
|
|
28
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @grantry/nextjs — authentication for Next.js.
|
|
3
|
+
*
|
|
4
|
+
* app/layout.tsx <GrantryProvider> (client)
|
|
5
|
+
* app/page.tsx await auth() (server)
|
|
6
|
+
* middleware.ts grantryMiddleware() (edge)
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ A RE-EXPORT, NOT A SECOND IMPLEMENTATION. Everything below is the React
|
|
9
|
+
* SDK unchanged, so a Next.js application installs one dependency and imports
|
|
10
|
+
* the same names it would in any React app — and there is no second copy of a
|
|
11
|
+
* component to drift from the first. The Next-specific half is `./server` and
|
|
12
|
+
* `./middleware`, which is genuinely different code.
|
|
13
|
+
*
|
|
14
|
+
* The one exception is `GrantryProvider`, which is a four-line wrapper around
|
|
15
|
+
* the React one for the sole purpose of saying "nextjs" rather than "react" on
|
|
16
|
+
* the wire. See `./provider` for why that distinction has to exist at all.
|
|
17
|
+
*
|
|
18
|
+
* ⚠️ EVERY NAME `@grantry/react` EXPORTS IS LISTED HERE, AND A TEST HOLDS IT TO
|
|
19
|
+
* THAT. The list is by name, with no `export *`, so for months each new React
|
|
20
|
+
* export was simply absent from Next.js:
|
|
21
|
+
* `AuthCard`, `OAuthConsent`, the localization contract and the entitlement
|
|
22
|
+
* helpers all had documentation pages telling Next.js readers to import them
|
|
23
|
+
* from here. `packages/sdk-react/tests/nextjs-exports.test.ts` compares the two
|
|
24
|
+
* lists, types included, and fails on the next one.
|
|
25
|
+
*/
|
|
26
|
+
export { GrantryProvider } from './provider.js';
|
|
27
|
+
export {
|
|
28
|
+
// Hooks
|
|
29
|
+
useAuth, useUser, useSession, useAccounts, useOAuth, useGrantry, useEntitlements, readOAuthResult,
|
|
30
|
+
// The public plans, for pricing cards of your own
|
|
31
|
+
usePlans, pricingCatalogue, formatMoney,
|
|
32
|
+
// Where the application's own pages are: the provider's route options and their helpers.
|
|
33
|
+
useRoutes, mergeRouteOptions, redirectTarget, requestedRedirectUrl,
|
|
34
|
+
// Headless flows
|
|
35
|
+
useSignInFlow, useSignUpFlow,
|
|
36
|
+
// Entitlements, read straight off an access token
|
|
37
|
+
entitlementsFromToken, actorFromToken, checkEntitlements, EMPTY_ENTITLEMENTS,
|
|
38
|
+
// Control
|
|
39
|
+
SignedIn, SignedOut, GrantryLoading, GrantryLoaded,
|
|
40
|
+
/*
|
|
41
|
+
* ⚠️ THE CONNECTION PAIR, WHICH IS NOT THE SESSION PAIR. See the React
|
|
42
|
+
* package's `control.tsx` for why they are separate rather than four states
|
|
43
|
+
* of one machine the way Clerk's are.
|
|
44
|
+
*/
|
|
45
|
+
GrantryDegraded, GrantryFailed, useEnvironmentStatus, Protect,
|
|
46
|
+
// Unstyled actions
|
|
47
|
+
SignOutButton, useSignOutAction,
|
|
48
|
+
// Drop-in components. Cut from a release and back; see the React package's
|
|
49
|
+
// index for what changed on the way back in (the default look, not the code).
|
|
50
|
+
SignIn, SignUp,
|
|
51
|
+
/*
|
|
52
|
+
* ⚠️ RE-EXPORTED BY NAME, SO A NEW COMPONENT IS UNREACHABLE FROM NEXT.JS
|
|
53
|
+
* UNTIL IT IS LISTED HERE. `<SignIn />` draws this itself when the account
|
|
54
|
+
* owes something, but an application that redirects somewhere other than its
|
|
55
|
+
* sign-in route has to render it there.
|
|
56
|
+
*/
|
|
57
|
+
CompleteAccount, UserButton,
|
|
58
|
+
/*
|
|
59
|
+
* ⚠️ NOT A COMPONENT, AND IT HAS TO BE HERE ANYWAY. `<UserButton />` sends
|
|
60
|
+
* "Add account" to the sign-in page with a marker on the link, and a Next.js
|
|
61
|
+
* application cannot let that visitor past its own gate without this. The
|
|
62
|
+
* list above is by name, so an unlisted helper is the same as one that does
|
|
63
|
+
* not exist -- and the failure is a redirect loop in somebody else's app.
|
|
64
|
+
*/
|
|
65
|
+
isAddingAccount, addAccountUrl, ADD_ACCOUNT_PARAM,
|
|
66
|
+
/*
|
|
67
|
+
* ⚠️ FOUR COMPONENTS THAT WERE EXPORTED FROM REACT AND UNREACHABLE FROM HERE,
|
|
68
|
+
* which is the same as not existing for a Next.js application — this list is
|
|
69
|
+
* by name and there is no deep-import escape hatch.
|
|
70
|
+
*/
|
|
71
|
+
UserAvatar, Waitlist, ResetPassword, SetupMfa, UserProfile, PricingTable, CheckoutButton, PlanDetailsButton, SubscriptionDetailsButton, ProductCard, MenuItems, CustomAction, CustomLink, CustomPage,
|
|
72
|
+
// The card itself, the consent card, "Continue as" and the support-visit banner.
|
|
73
|
+
AuthCard, OAuthConsent, ContinueAs, ImpersonationBanner,
|
|
74
|
+
// Localization
|
|
75
|
+
LocalizationProvider, useTranslate, EN,
|
|
76
|
+
// Appearance
|
|
77
|
+
AppearanceProvider, GRANTRY_STYLES, useStylesheet, variablesToStyle,
|
|
78
|
+
// The brand theme: what Hosted sign-in → Theme stores, the hosted screens' look
|
|
79
|
+
THEME_COLORS, THEME_COLOR_LABELS, THEME_FONTS, THEME_LOGO_PLACEMENTS, THEME_RADII, THEME_SOCIAL_PLACEMENTS, THEME_SOCIAL_VARIANTS, brandThemeAppearance, cleanBrandTheme, themeColorScheme,
|
|
80
|
+
// Avatars, drawn as the components draw them
|
|
81
|
+
DEFAULT_AVATAR_COLORS, avatarBackgroundCss, avatarInitial, avatarSeed,
|
|
82
|
+
// The client
|
|
83
|
+
GrantryClient, GrantryError, TOKEN_COOKIE_NAME,
|
|
84
|
+
// "Verify it's you" (0330): the dialog before a sensitive action, and what it resolves with.
|
|
85
|
+
useReverification, } from '@grantry/react';
|
|
86
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7C,OAAO;AACL,QAAQ;AACR,OAAO,EACP,OAAO,EACP,UAAU,EACV,WAAW,EACX,QAAQ,EACR,UAAU,EACV,eAAe,EACf,eAAe;AACf,kDAAkD;AAClD,QAAQ,EACR,gBAAgB,EAChB,WAAW;AAEX,yFAAyF;AACzF,SAAS,EACT,iBAAiB,EACjB,cAAc,EACd,oBAAoB;AAEpB,iBAAiB;AACjB,aAAa,EACb,aAAa;AAEb,kDAAkD;AAClD,qBAAqB,EACrB,cAAc,EACd,iBAAiB,EACjB,kBAAkB;AAElB,UAAU;AACV,QAAQ,EACR,SAAS,EACT,cAAc,EACd,aAAa;AACb;;;;GAIG;AACH,eAAe,EACf,aAAa,EACb,oBAAoB,EACpB,OAAO;AAEP,mBAAmB;AACnB,aAAa,EACb,gBAAgB;AAEhB,2EAA2E;AAC3E,8EAA8E;AAC9E,MAAM,EACN,MAAM;AACN;;;;;GAKG;AACH,eAAe,EACf,UAAU;AACV;;;;;;GAMG;AACH,eAAe,EACf,aAAa,EACb,iBAAiB;AACjB;;;;GAIG;AACH,UAAU,EACV,QAAQ,EACR,aAAa,EACb,QAAQ,EACR,WAAW,EACX,YAAY,EACZ,cAAc,EACd,iBAAiB,EACjB,yBAAyB,EACzB,WAAW,EAEX,SAAS,EACT,YAAY,EACZ,UAAU,EACV,UAAU;AACV,iFAAiF;AACjF,QAAQ,EACR,YAAY,EACZ,UAAU,EACV,mBAAmB;AAEnB,eAAe;AACf,oBAAoB,EACpB,YAAY,EACZ,EAAE;AAEF,aAAa;AACb,kBAAkB,EAClB,cAAc,EACd,aAAa,EACb,gBAAgB;AAEhB,gFAAgF;AAChF,YAAY,EACZ,kBAAkB,EAClB,WAAW,EACX,qBAAqB,EACrB,WAAW,EACX,uBAAuB,EACvB,qBAAqB,EACrB,oBAAoB,EACpB,eAAe,EACf,gBAAgB;AAEhB,6CAA6C;AAC7C,qBAAqB,EACrB,mBAAmB,EACnB,aAAa,EACb,UAAU;AAEV,aAAa;AACb,aAAa,EACb,YAAY,EACZ,iBAAiB;AAEjB,6FAA6F;AAC7F,iBAAiB,GAuFlB,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route protection in Next.js middleware — `@grantry/nextjs/middleware`.
|
|
3
|
+
*
|
|
4
|
+
* export default grantryMiddleware({
|
|
5
|
+
* protectedRoutes: ['/dashboard', '/account'],
|
|
6
|
+
* signInUrl: '/sign-in',
|
|
7
|
+
* });
|
|
8
|
+
*
|
|
9
|
+
* export const config = { matcher: ['/((?!_next|.*\\..*).*)'] };
|
|
10
|
+
*
|
|
11
|
+
* Middleware runs before rendering, so an unauthenticated visitor is redirected
|
|
12
|
+
* without the protected page ever being built — which is the difference between
|
|
13
|
+
* protecting a route and merely hiding it. No request is let through signed out,
|
|
14
|
+
* a support visit's included; see `visitTicket`.
|
|
15
|
+
*/
|
|
16
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
17
|
+
export interface GrantryMiddlewareOptions {
|
|
18
|
+
/**
|
|
19
|
+
* Path patterns requiring a signed-in visitor.
|
|
20
|
+
*
|
|
21
|
+
* A path prefix as a string — `'/dashboard'` protects it and everything under
|
|
22
|
+
* it — or a `RegExp` for anything else. `'/dashboard(.*)'`, the spelling other
|
|
23
|
+
* libraries use, means the same prefix; any other string that looks like a
|
|
24
|
+
* pattern is refused when the middleware is made (`prefixOf`).
|
|
25
|
+
* Anything not listed is public — an allowlist would silently protect new
|
|
26
|
+
* routes, which sounds safer but produces a site where adding a public page
|
|
27
|
+
* quietly breaks it.
|
|
28
|
+
*/
|
|
29
|
+
protectedRoutes?: Array<string | RegExp>;
|
|
30
|
+
/**
|
|
31
|
+
* Where to send unauthenticated visitors.
|
|
32
|
+
*
|
|
33
|
+
* Unset: `GRANTRY_SIGN_IN_URL`, then `NEXT_PUBLIC_GRANTRY_SIGN_IN_URL` — the
|
|
34
|
+
* variable `<GrantryProvider>` reads, so the two agree — then the hosted
|
|
35
|
+
* sign-in screen on the environment's sign-in URL, which is looked up with the
|
|
36
|
+
* publishable key (one request, remembered for five minutes). With none of
|
|
37
|
+
* those the visitor is answered 401 (docs/paths-and-domains.md §6).
|
|
38
|
+
* ⚠️ It defaulted to `/sign-in`, a page Grantry could only guess the
|
|
39
|
+
* application had.
|
|
40
|
+
*/
|
|
41
|
+
signInUrl?: string;
|
|
42
|
+
/** Override for self-hosted deployments and local development. */
|
|
43
|
+
apiUrl?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Your publishable key, which names the application these tokens must belong
|
|
46
|
+
* to, and finds the hosted sign-in screen when no sign-in page is named. Read
|
|
47
|
+
* from `GRANTRY_PUBLISHABLE_KEY`, then `NEXT_PUBLIC_GRANTRY_PUBLISHABLE_KEY`,
|
|
48
|
+
* when omitted. ⚠️ With no key and no `environmentId`, a request carrying a
|
|
49
|
+
* token makes the middleware THROW (`no_audience`) rather than redirect — no
|
|
50
|
+
* token is ever admitted without knowing the application, and a sign-in page
|
|
51
|
+
* cannot fix a server that does not know which one it is. See
|
|
52
|
+
* `@grantry/react/server`.
|
|
53
|
+
*/
|
|
54
|
+
publishableKey?: string;
|
|
55
|
+
/** The environment id, for a deployment that configures it directly. */
|
|
56
|
+
environmentId?: string;
|
|
57
|
+
/** Require a verified email address, not merely a session. */
|
|
58
|
+
requireVerifiedEmail?: boolean;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A string route pattern, as the path prefix it protects.
|
|
62
|
+
*
|
|
63
|
+
* ⚠️ `'/dashboard(.*)'` PROTECTED NOTHING UNTIL 1 OCTOBER 2026, AND THE DOCS SAID TO WRITE IT. A
|
|
64
|
+
* string was compared as a literal prefix, so the pattern matched only a path that began with the
|
|
65
|
+
* characters `/dashboard(.*)` — none — and the middleware let every request through, signed out.
|
|
66
|
+
* The quickstart, the middleware page and this file's own example all used it (the docs audit). It
|
|
67
|
+
* is the spelling Clerk's route matcher takes, so somebody arriving from there writes it too: it is
|
|
68
|
+
* read as the prefix it means, and anything else that looks like a pattern is refused rather than
|
|
69
|
+
* silently matched against nothing.
|
|
70
|
+
*/
|
|
71
|
+
export declare function prefixOf(pattern: string): string;
|
|
72
|
+
export declare function grantryMiddleware(options?: GrantryMiddlewareOptions): (request: NextRequest) => Promise<NextResponse>;
|
|
73
|
+
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAsB7D,MAAM,WAAW,wBAAwB;IACvC;;;;;;;;;;OAUG;IACH,eAAe,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;IACzC;;;;;;;;;;OAUG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;;;OASG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,wEAAwE;IACxE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,8DAA8D;IAC9D,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAUhD;AAuED,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,wBAA6B,IAiCrC,SAAS,WAAW,KAAG,OAAO,CAAC,YAAY,CAAC,CAqF9E"}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route protection in Next.js middleware — `@grantry/nextjs/middleware`.
|
|
3
|
+
*
|
|
4
|
+
* export default grantryMiddleware({
|
|
5
|
+
* protectedRoutes: ['/dashboard', '/account'],
|
|
6
|
+
* signInUrl: '/sign-in',
|
|
7
|
+
* });
|
|
8
|
+
*
|
|
9
|
+
* export const config = { matcher: ['/((?!_next|.*\\..*).*)'] };
|
|
10
|
+
*
|
|
11
|
+
* Middleware runs before rendering, so an unauthenticated visitor is redirected
|
|
12
|
+
* without the protected page ever being built — which is the difference between
|
|
13
|
+
* protecting a route and merely hiding it. No request is let through signed out,
|
|
14
|
+
* a support visit's included; see `visitTicket`.
|
|
15
|
+
*/
|
|
16
|
+
import { NextResponse } from 'next/server';
|
|
17
|
+
import { REDIRECT_URL_PARAMETER, TICKET_PARAMETER, environmentSignInUrl, hostedSignInPage, serverRouteOptions, verifyToken, } from '@grantry/react/server';
|
|
18
|
+
import { TOKEN_COOKIE_NAME, tokenFromCookie } from './cookie.js';
|
|
19
|
+
/**
|
|
20
|
+
* The headers a verified request goes on with, for the application's pages to read.
|
|
21
|
+
*
|
|
22
|
+
* ⚠️ REMOVED FROM EVERY REQUEST THIS MIDDLEWARE SEES, NOT ONLY THE ONES IT VERIFIES. They were
|
|
23
|
+
* replaced on a protected route and passed through untouched everywhere else, so a visitor who sent
|
|
24
|
+
* `x-grantry-user-id: <anybody>` to a public page had it arrive as though this middleware had set
|
|
25
|
+
* it — and the docs tell a page it can trust these names. A page outside the middleware's `matcher`
|
|
26
|
+
* is still beyond reach, which is why `auth()`, which verifies the cookie itself, is the safer read.
|
|
27
|
+
*/
|
|
28
|
+
const FORWARDED_HEADERS = ['x-grantry-user-id', 'x-grantry-session-id'];
|
|
29
|
+
/**
|
|
30
|
+
* A string route pattern, as the path prefix it protects.
|
|
31
|
+
*
|
|
32
|
+
* ⚠️ `'/dashboard(.*)'` PROTECTED NOTHING UNTIL 1 OCTOBER 2026, AND THE DOCS SAID TO WRITE IT. A
|
|
33
|
+
* string was compared as a literal prefix, so the pattern matched only a path that began with the
|
|
34
|
+
* characters `/dashboard(.*)` — none — and the middleware let every request through, signed out.
|
|
35
|
+
* The quickstart, the middleware page and this file's own example all used it (the docs audit). It
|
|
36
|
+
* is the spelling Clerk's route matcher takes, so somebody arriving from there writes it too: it is
|
|
37
|
+
* read as the prefix it means, and anything else that looks like a pattern is refused rather than
|
|
38
|
+
* silently matched against nothing.
|
|
39
|
+
*/
|
|
40
|
+
export function prefixOf(pattern) {
|
|
41
|
+
const prefix = pattern.endsWith('(.*)') ? pattern.slice(0, -'(.*)'.length) : pattern;
|
|
42
|
+
if (/[()*?+[\]{}|^$\\]/u.test(prefix)) {
|
|
43
|
+
throw new Error(`Grantry: the route pattern "${pattern}" is not a path prefix. ` +
|
|
44
|
+
'Use a prefix such as "/dashboard", or pass a RegExp.');
|
|
45
|
+
}
|
|
46
|
+
const trimmed = prefix.replace(/\/+$/u, '');
|
|
47
|
+
return trimmed === '' ? '/' : trimmed;
|
|
48
|
+
}
|
|
49
|
+
function matchesPath(pathname, patterns) {
|
|
50
|
+
return patterns.some((pattern) => {
|
|
51
|
+
if (typeof pattern === 'string') {
|
|
52
|
+
// A prefix, anchored at a path boundary so `/dashboard` does not also
|
|
53
|
+
// protect `/dashboards-public`.
|
|
54
|
+
return pattern === '/' || pathname === pattern || pathname.startsWith(`${pattern}/`);
|
|
55
|
+
}
|
|
56
|
+
/*
|
|
57
|
+
* ⚠️ `lastIndex` RESET FIRST. A RegExp with the `g` or `y` flag remembers where its last match
|
|
58
|
+
* ended and starts the next `test` there, so `/^\/admin/g` matched one request, missed the
|
|
59
|
+
* next — letting it through signed out — and matched the one after.
|
|
60
|
+
*/
|
|
61
|
+
pattern.lastIndex = 0;
|
|
62
|
+
return pattern.test(pathname);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Whether a path is protected, as written and as decoded.
|
|
67
|
+
*
|
|
68
|
+
* ⚠️ BOTH SPELLINGS, BECAUSE THE ROUTER AND THIS CHECK MAY NOT AGREE ON ONE. `nextUrl.pathname`
|
|
69
|
+
* keeps percent-encoding (`/%64ashboard`), and a router that decodes before it matches a page
|
|
70
|
+
* would build `/dashboard` for an address the prefix never saw. Matching the decoded path too
|
|
71
|
+
* costs nothing on an ordinary request and only ever protects more.
|
|
72
|
+
*/
|
|
73
|
+
function matches(pathname, patterns) {
|
|
74
|
+
if (matchesPath(pathname, patterns))
|
|
75
|
+
return true;
|
|
76
|
+
let decoded;
|
|
77
|
+
try {
|
|
78
|
+
decoded = decodeURIComponent(pathname);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return decoded !== pathname && matchesPath(decoded, patterns);
|
|
84
|
+
}
|
|
85
|
+
/** The request as the application will see it, with no forwarded header the visitor wrote. */
|
|
86
|
+
function withoutForwardedHeaders(request) {
|
|
87
|
+
const headers = new Headers(request.headers);
|
|
88
|
+
for (const name of FORWARDED_HEADERS)
|
|
89
|
+
headers.delete(name);
|
|
90
|
+
return headers;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* A support visit's first request: a navigation carrying `?__grantry_ticket=`.
|
|
94
|
+
*
|
|
95
|
+
* ⚠️ CARRIED TO SIGN-IN, NEVER LET THROUGH. A view-as link opens the Home URL
|
|
96
|
+
* with its ticket (0242), and `@grantry/js` spends it on whatever page loads. A
|
|
97
|
+
* plain redirect to sign-in dropped the query string, so a protected Home URL
|
|
98
|
+
* turned every visit into a sign-in form for an operator with no password to
|
|
99
|
+
* type — and the first fix, letting the request through signed out, was a way
|
|
100
|
+
* past this middleware for anybody: the ticket is not checked here, so adding
|
|
101
|
+
* the parameter to any protected address rendered that page to a signed-out
|
|
102
|
+
* visitor. A page that trusted this middleware to keep its data from signed-out
|
|
103
|
+
* visitors would have served it.
|
|
104
|
+
*
|
|
105
|
+
* So the ticket rides the redirect instead. The sign-in page is public, the SDK
|
|
106
|
+
* spends the ticket there before anything else runs, and `<SignIn />` moves a
|
|
107
|
+
* visit on to `redirect_url` — the address the link pointed at, without the
|
|
108
|
+
* ticket. The protected page is only ever built for a signed-in request.
|
|
109
|
+
*
|
|
110
|
+
* Only GET and HEAD: a form post or a server action is never a navigation from a
|
|
111
|
+
* link, and carries nothing anywhere.
|
|
112
|
+
*/
|
|
113
|
+
function visitTicket(request) {
|
|
114
|
+
if (request.method !== 'GET' && request.method !== 'HEAD')
|
|
115
|
+
return null;
|
|
116
|
+
return request.nextUrl.searchParams.get(TICKET_PARAMETER) || null;
|
|
117
|
+
}
|
|
118
|
+
export function grantryMiddleware(options = {}) {
|
|
119
|
+
const { protectedRoutes: patterns = [], apiUrl, publishableKey, environmentId, requireVerifiedEmail = false, } = options;
|
|
120
|
+
// Read once, here, so a pattern that cannot work fails the deploy and not a visitor.
|
|
121
|
+
const protectedRoutes = patterns.map((pattern) => typeof pattern === 'string' ? prefixOf(pattern) : pattern);
|
|
122
|
+
/**
|
|
123
|
+
* Where sign-in is: the option, the server's variables, the hosted sign-in screen.
|
|
124
|
+
*
|
|
125
|
+
* ⚠️ THE ONE SDK THAT DOES NOT DEFAULT TO REDIRECT SIGN-IN, because it cannot.
|
|
126
|
+
* A redirect sign-in keeps its verifier in the browser and spends its code
|
|
127
|
+
* there (docs/hosted-sign-in.md §6), and the only page this middleware knows
|
|
128
|
+
* is the protected one it is turning the visitor away from. Name a sign-in
|
|
129
|
+
* page — `<SignIn mode="redirect" />` makes one a single line.
|
|
130
|
+
*/
|
|
131
|
+
async function signInPage() {
|
|
132
|
+
const configured = options.signInUrl ?? serverRouteOptions().signInUrl;
|
|
133
|
+
if (configured)
|
|
134
|
+
return configured;
|
|
135
|
+
const signInUrl = await environmentSignInUrl({
|
|
136
|
+
...(publishableKey ? { publishableKey } : {}),
|
|
137
|
+
...(apiUrl ? { apiUrl } : {}),
|
|
138
|
+
});
|
|
139
|
+
return hostedSignInPage(signInUrl, 'sign-in');
|
|
140
|
+
}
|
|
141
|
+
return async function middleware(request) {
|
|
142
|
+
if (!matches(request.nextUrl.pathname, protectedRoutes)) {
|
|
143
|
+
// A public page: through, with nothing that claims to be this middleware's word.
|
|
144
|
+
return FORWARDED_HEADERS.some((name) => request.headers.has(name))
|
|
145
|
+
? NextResponse.next({ request: { headers: withoutForwardedHeaders(request) } })
|
|
146
|
+
: NextResponse.next();
|
|
147
|
+
}
|
|
148
|
+
const token = tokenFromCookie(request.cookies.get(TOKEN_COOKIE_NAME));
|
|
149
|
+
const turnAway = async () => {
|
|
150
|
+
const signIn = await signInPage();
|
|
151
|
+
if (!signIn) {
|
|
152
|
+
return new NextResponse('Sign-in is required, and this application has no sign-in page.', {
|
|
153
|
+
status: 401,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
const target = new URL(signIn, request.url);
|
|
157
|
+
const here = new URL(request.url);
|
|
158
|
+
// A support visit: the sign-in page spends the ticket, and the visit goes on to
|
|
159
|
+
// where the link pointed, without it. See `visitTicket`.
|
|
160
|
+
const ticket = visitTicket(request);
|
|
161
|
+
if (ticket) {
|
|
162
|
+
here.searchParams.delete(TICKET_PARAMETER);
|
|
163
|
+
target.searchParams.set(TICKET_PARAMETER, ticket);
|
|
164
|
+
}
|
|
165
|
+
/*
|
|
166
|
+
* So the app can return the visitor where they were headed rather than
|
|
167
|
+
* dumping them on a home page after signing in. ⚠️ A PATH WHEN SIGN-IN IS
|
|
168
|
+
* ON THIS ORIGIN, THE WHOLE ADDRESS WHEN IT IS NOT: on the hosted sign-in
|
|
169
|
+
* screen a path would name a page of the sign-in URL's. The query
|
|
170
|
+
* string goes too: `/dashboard?tab=billing` came back as `/dashboard`.
|
|
171
|
+
* The sign-in page follows it only on its own origin (`requestedRedirectUrl`).
|
|
172
|
+
*/
|
|
173
|
+
target.searchParams.set(REDIRECT_URL_PARAMETER, target.origin === here.origin ? `${here.pathname}${here.search}` : here.href);
|
|
174
|
+
const response = NextResponse.redirect(target);
|
|
175
|
+
// The ticket is in this address; the page it leads to has no need to be told where it came from.
|
|
176
|
+
if (ticket)
|
|
177
|
+
response.headers.set('Referrer-Policy', 'no-referrer');
|
|
178
|
+
return response;
|
|
179
|
+
};
|
|
180
|
+
if (!token)
|
|
181
|
+
return turnAway();
|
|
182
|
+
try {
|
|
183
|
+
const verified = await verifyToken(token, {
|
|
184
|
+
...(apiUrl ? { apiUrl } : {}),
|
|
185
|
+
...(publishableKey ? { publishableKey } : {}),
|
|
186
|
+
...(environmentId ? { environmentId } : {}),
|
|
187
|
+
});
|
|
188
|
+
if (requireVerifiedEmail && !verified.emailVerified) {
|
|
189
|
+
return turnAway();
|
|
190
|
+
}
|
|
191
|
+
// Passed to the application so a server component can read the user
|
|
192
|
+
// without verifying a second time. Set on the *outgoing* request headers,
|
|
193
|
+
// which the visitor cannot forge — anything they send under these names
|
|
194
|
+
// is replaced here (and removed on public pages; see `FORWARDED_HEADERS`).
|
|
195
|
+
const headers = withoutForwardedHeaders(request);
|
|
196
|
+
headers.set('x-grantry-user-id', verified.userId);
|
|
197
|
+
headers.set('x-grantry-session-id', verified.sessionId);
|
|
198
|
+
return NextResponse.next({ request: { headers } });
|
|
199
|
+
}
|
|
200
|
+
catch (cause) {
|
|
201
|
+
/*
|
|
202
|
+
* ⚠️ A MISSING AUDIENCE IS THE ONE FAILURE THAT MUST NOT REDIRECT. Every
|
|
203
|
+
* other reason means this request cannot be trusted, and the sign-in page
|
|
204
|
+
* is the honest answer; `no_audience` means the application never said
|
|
205
|
+
* which Grantry environment it belongs to, and sending the visitor to
|
|
206
|
+
* sign in would loop them forever against a check that can never pass.
|
|
207
|
+
*/
|
|
208
|
+
if (cause instanceof Error &&
|
|
209
|
+
cause.name === 'TokenVerificationError' &&
|
|
210
|
+
cause.reason === 'no_audience') {
|
|
211
|
+
throw cause;
|
|
212
|
+
}
|
|
213
|
+
return turnAway();
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"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"}
|