sekisho 0.3.1 → 0.5.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 +171 -158
- package/dist/factory.cjs +1 -1
- package/dist/factory.d.ts +44 -19
- package/dist/factory.mjs +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +11 -47
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,97 +10,50 @@
|
|
|
10
10
|
|
|
11
11
|
See the [example-nextjs-app](../../packages/example-nextjs-app). This example is deployed online at [https://sekisho-demo.pages.dev](https://sekisho-demo.pages.dev).
|
|
12
12
|
|
|
13
|
-
###
|
|
13
|
+
### Login Redirects for Unauthenticated Users
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Use `NotAuthenticatedContainer` to handle login redirects in a way that works with your framework's routing constraints.
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
// app/providers.tsx
|
|
19
|
-
|
|
20
|
-
// here we create a dedicated file, as we want to make this file a client component in Next.js App Router
|
|
21
|
-
//
|
|
22
|
-
// if you don't use the Next.js App Router, you can just wrap your app's entrypoint with `SekishoProvider`
|
|
23
|
-
// directly without creating a separate file
|
|
24
|
-
|
|
25
|
-
'use client';
|
|
26
|
-
|
|
27
|
-
import { SekishoProvider } from 'sekisho';
|
|
28
|
-
import { useRouter } from 'next/navigation';
|
|
29
|
-
|
|
30
|
-
export function Providers({ children }: React.PropsWithChildren) {
|
|
31
|
-
const router = useRouter();
|
|
32
|
-
|
|
33
|
-
return (
|
|
34
|
-
<SekishoProvider
|
|
35
|
-
onNeedLogin={() => {
|
|
36
|
-
// Tell Sekisho what to do when the authentication is required.
|
|
37
|
-
router.push('/login');
|
|
38
|
-
// Sekisho can work with any router or navigation library.
|
|
39
|
-
// You can also call `navigate` from React Router's `useNavigate` here:
|
|
40
|
-
// navigate('/login');
|
|
41
|
-
}}
|
|
42
|
-
>
|
|
43
|
-
{children}
|
|
44
|
-
</SekishoProvider>
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
```
|
|
17
|
+
**Wrap your protected routes/content with `NotAuthenticatedContainer`**
|
|
48
18
|
|
|
49
19
|
```tsx
|
|
50
|
-
|
|
51
|
-
import { Providers } from './providers';
|
|
20
|
+
'use client';
|
|
52
21
|
|
|
53
|
-
|
|
54
|
-
return (
|
|
55
|
-
<html lang="en">
|
|
56
|
-
<body>
|
|
57
|
-
<Providers>{children}</Providers>
|
|
58
|
-
</body>
|
|
59
|
-
</html>
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
```
|
|
22
|
+
import { NotAuthenticatedContainer } from 'sekisho';
|
|
63
23
|
|
|
64
|
-
|
|
24
|
+
function LoginRedirect() {
|
|
25
|
+
// perform your redirect logic here, e.g. using your framework's router
|
|
26
|
+
// for example, in Next.js App Router:
|
|
27
|
+
return redirect('/login');
|
|
65
28
|
|
|
66
|
-
|
|
67
|
-
|
|
29
|
+
// in React Router:
|
|
30
|
+
const navigate = useNavigate();
|
|
31
|
+
useEffect(() => { navigate('/login'); }, [navigate]);
|
|
32
|
+
return null;
|
|
33
|
+
// or
|
|
34
|
+
return <Navigate to="/login" />;
|
|
68
35
|
|
|
69
|
-
|
|
70
|
-
return
|
|
71
|
-
<NotAuthenticatedBoundary>
|
|
72
|
-
{/* ... */}
|
|
73
|
-
</NotAuthenticatedBoundary>
|
|
74
|
-
);
|
|
36
|
+
// Wouter
|
|
37
|
+
return <Redirect to="/login" />;
|
|
75
38
|
}
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
And if you are using Next.js App Router and `error.tsx` file, due to Next.js layout, page, and error boundary heirarchy, you will also need to wrap the `error.tsx` with `NotAuthenticatedErrorWrapper`:
|
|
79
|
-
|
|
80
|
-
```tsx
|
|
81
|
-
// app/error.tsx
|
|
82
|
-
'use client';
|
|
83
39
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
export default function ErrorPage({ error, reset }) {
|
|
40
|
+
export function Protected() {
|
|
87
41
|
return (
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
42
|
+
<NotAuthenticatedContainer
|
|
43
|
+
// you can pass a React element directly as fallback
|
|
44
|
+
fallback={<LoginRedirect />}
|
|
45
|
+
// or you can pass a component that receives the error object as prop
|
|
46
|
+
fallbackComponent={LoginRedirect}
|
|
47
|
+
>
|
|
48
|
+
{/* Protected content goes here */}
|
|
49
|
+
</NotAuthenticatedContainer>
|
|
91
50
|
);
|
|
92
51
|
}
|
|
93
52
|
```
|
|
94
53
|
|
|
95
|
-
|
|
54
|
+
**Trigger a login redirect**
|
|
96
55
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
Call `needLogin()` anywhere during the React render phase.
|
|
100
|
-
|
|
101
|
-
Right now, you can't call `needLogin()` within an event handler or `useEffect`, because it won't be caught by the React error boundary mechanism. We will be implementing this in a future version.
|
|
102
|
-
|
|
103
|
-
**In a client component** (e.g. when session state is absent):
|
|
56
|
+
Call `needLogin()` anywhere during the React render phase. You can call `needLogin()` in a client component (e.g. when session state is absent):
|
|
104
57
|
|
|
105
58
|
```tsx
|
|
106
59
|
import { needLogin } from 'sekisho';
|
|
@@ -116,7 +69,7 @@ function Dashboard() {
|
|
|
116
69
|
}
|
|
117
70
|
```
|
|
118
71
|
|
|
119
|
-
|
|
72
|
+
...or you can call `needLogin()` in a [SWR](https://swr.vercel.app) middleware (e.g. when an API response carries a known auth error):
|
|
120
73
|
|
|
121
74
|
```tsx
|
|
122
75
|
import { needLogin } from 'sekisho';
|
|
@@ -131,7 +84,115 @@ export const requireAuthMiddleware: Middleware = (useSWRNext) => (key, fetcher,
|
|
|
131
84
|
};
|
|
132
85
|
```
|
|
133
86
|
|
|
134
|
-
|
|
87
|
+
> Right now, you can't call `needLogin()` within an event handler or `useEffect`, because it won't be caught by the React error boundary mechanism. We may be able to implement this in a future version.
|
|
88
|
+
|
|
89
|
+
This kinda like `<Suspense />` but for login redirects instead. And like `<Suspense />`, you can have multiple `NotAuthenticatedContainer`s nested independently — each one only catches the `needLogin()` calls within its own subtree.
|
|
90
|
+
|
|
91
|
+
**Set up with Next.js App Router**
|
|
92
|
+
|
|
93
|
+
You may use Next.js [Route Groups](https://nextjs.org/docs/app/api-reference/file-conventions/route-groups) to create protected and unprotected sections of your app:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
app/
|
|
97
|
+
├── (protected)/ ← all protected routes goes under here
|
|
98
|
+
│ ├── layout.tsx ← wrap with <NotAuthenticatedContainer> here
|
|
99
|
+
│ ├── error.tsx ← wrap with <NotAuthenticatedErrorWrapper> if you are using error.tsx file convention
|
|
100
|
+
│ └── page.tsx ← homepage, where you call needLogin() when authentication is needed
|
|
101
|
+
│
|
|
102
|
+
├── (unprotected)/ ← all unprotected routes goes under here
|
|
103
|
+
│ └── login/
|
|
104
|
+
│ └── page.tsx
|
|
105
|
+
│
|
|
106
|
+
└── layout.tsx ← your root layout with <html /> and <body />
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
// app/(protected)/layout.tsx
|
|
111
|
+
import { NotAuthenticatedContainer } from 'sekisho';
|
|
112
|
+
import { redirect } from 'next/navigation';
|
|
113
|
+
|
|
114
|
+
function LoginRedirect(): never {
|
|
115
|
+
return redirect('/login');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function ProtectedLayout({ children }: React.PropsWithChildren) {
|
|
119
|
+
return (
|
|
120
|
+
<NotAuthenticatedContainer fallback={<LoginRedirect />}>
|
|
121
|
+
{children}
|
|
122
|
+
</NotAuthenticatedContainer>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
> [!NOTE]
|
|
128
|
+
> If you are using `<NotAuthenticatedContainer />` in a `layout.tsx` file, and if you are using `error.tsx` file convention, you will need to wrap your `error.tsx` with `NotAuthenticatedErrorWrapper` due to Next.js layout, page, and error boundary hierarchy.
|
|
129
|
+
>
|
|
130
|
+
> ```tsx
|
|
131
|
+
> // app/error.tsx
|
|
132
|
+
> 'use client';
|
|
133
|
+
>
|
|
134
|
+
> import { NotAuthenticatedErrorWrapper } from 'sekisho';
|
|
135
|
+
>
|
|
136
|
+
> export default function ErrorPage({ error, reset }) {
|
|
137
|
+
> return (
|
|
138
|
+
> <NotAuthenticatedErrorWrapper error={error}>
|
|
139
|
+
> {/* Your existing error UI goes in here */}
|
|
140
|
+
> </NotAuthenticatedErrorWrapper>
|
|
141
|
+
> );
|
|
142
|
+
> }
|
|
143
|
+
> ```
|
|
144
|
+
|
|
145
|
+
**Set up with React Router**
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
import { NotAuthenticatedContainer } from 'sekisho';
|
|
149
|
+
|
|
150
|
+
const router = createBrowserRouter([
|
|
151
|
+
{
|
|
152
|
+
element: <RootLayout />,
|
|
153
|
+
children: [
|
|
154
|
+
// protected routes goes here
|
|
155
|
+
{
|
|
156
|
+
element: <Protected />,
|
|
157
|
+
children: [
|
|
158
|
+
/* protected routes goes here */
|
|
159
|
+
{ path: '/admin', element: <Dashboard /> }
|
|
160
|
+
]
|
|
161
|
+
},
|
|
162
|
+
// unprotected routes goes here
|
|
163
|
+
{ path: '/login', element: <LoginPage /> }
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
]);
|
|
167
|
+
|
|
168
|
+
function Protected() {
|
|
169
|
+
return (
|
|
170
|
+
<NotAuthenticatedContainer fallback={<Navigate to="/login" />}>
|
|
171
|
+
<Outlet />
|
|
172
|
+
</NotAuthenticatedContainer>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
> [!NOTE]
|
|
178
|
+
> If you are using React Router's `errorElement` convention, you may need to wrap your `errorElement` with `NotAuthenticatedErrorWrapper`:
|
|
179
|
+
>
|
|
180
|
+
> ```tsx
|
|
181
|
+
> {
|
|
182
|
+
> errorElement: <ErrorComponent />
|
|
183
|
+
> }
|
|
184
|
+
>
|
|
185
|
+
> function ErrorComponent() {
|
|
186
|
+
> const error = useRouteError();
|
|
187
|
+
> return (
|
|
188
|
+
> <NotAuthenticatedErrorWrapper error={error}>
|
|
189
|
+
> {/* Your existing error UI goes in here */}
|
|
190
|
+
> </NotAuthenticatedErrorWrapper>
|
|
191
|
+
> );
|
|
192
|
+
> }
|
|
193
|
+
> ```
|
|
194
|
+
|
|
195
|
+
### Restricting Access to Part of the UI
|
|
135
196
|
|
|
136
197
|
Wrap any part of the UI with `AccessRestrictedContainer` and call `accessRestricted()` inside it when the user lacks the required role or permission. Unlike `needLogin()`, which triggers a global redirect via `onNeedLogin`, `accessRestricted()` is local — `AccessRestrictedContainer` simply renders `fallback` in place of its children:
|
|
137
198
|
|
|
@@ -148,10 +209,20 @@ function AdminPanel() {
|
|
|
148
209
|
return <div>Secret admin content</div>;
|
|
149
210
|
}
|
|
150
211
|
|
|
212
|
+
function AccessRestricted() {
|
|
213
|
+
// you may render a fallback UI in place of the restricted content
|
|
214
|
+
return <p>You don't have permission to view this section.</p>
|
|
215
|
+
|
|
216
|
+
// or you may just redirect your user away
|
|
217
|
+
return redirect('/forbidden');
|
|
218
|
+
}
|
|
219
|
+
|
|
151
220
|
function Page() {
|
|
152
221
|
return (
|
|
153
222
|
<AccessRestrictedContainer
|
|
154
|
-
fallback={<
|
|
223
|
+
fallback={<AccessRestricted />}
|
|
224
|
+
// or you can pass a component that receives the error object as prop
|
|
225
|
+
fallbackComponent={AccessRestricted}
|
|
155
226
|
>
|
|
156
227
|
<AdminPanel />
|
|
157
228
|
</AccessRestrictedContainer>
|
|
@@ -163,72 +234,30 @@ This kinda like `<Suspense />` but for access control instead. And like `<Suspen
|
|
|
163
234
|
|
|
164
235
|
## Explanation
|
|
165
236
|
|
|
166
|
-
Sekisho is built on top of React's error boundaries. Both `needLogin()` and `accessRestricted()` throw a special tagged error during the React render phase, which bubbles up to the nearest matching boundary
|
|
167
|
-
|
|
168
|
-
| Function | Error thrown | Caught by | Behaviour |
|
|
169
|
-
|---|---|---|---|
|
|
170
|
-
| `needLogin()` | `NotAuthenticatedError` | `NotAuthenticatedBoundary` / `NotAuthenticatedErrorWrapper` | Calls `onNeedLogin` from `SekishoProvider` (global redirect) |
|
|
171
|
-
| `accessRestricted()` | `AccessRestrictedError` | `AccessRestrictedContainer` | Renders the `fallback` prop in place of children (local swap) |
|
|
172
|
-
|
|
173
|
-
Each boundary re-throws errors it does not own, so `AccessRestrictedContainer` never swallows an auth error, and `NotAuthenticatedBoundary` never swallows an access error. Your own error boundaries are unaffected by either.
|
|
174
|
-
|
|
175
|
-
With `NotAuthenticatedErrorWrapper` / `NotAuthenticatedBoundary` you can create protected and unprotected routes in any React app:
|
|
176
|
-
|
|
177
|
-
**Next.js App Router**
|
|
178
|
-
|
|
179
|
-
```
|
|
180
|
-
app/
|
|
181
|
-
├── (protected)/ ← all protected routes goes under here
|
|
182
|
-
│ ├── layout.tsx ← wrap children with <SekishoProvider> here
|
|
183
|
-
│ ├── error.tsx ← wrap with <NotAuthenticatedErrorWrapper> here
|
|
184
|
-
│ └── page.tsx ← homepage, where you call needLogin() when authentication is needed
|
|
185
|
-
├── (unprotected)/ ← all unprotected routes goes under here
|
|
186
|
-
│ └── login/
|
|
187
|
-
│ └── page.tsx
|
|
188
|
-
└── layout.tsx ← root layout
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
**React Router**
|
|
192
|
-
|
|
193
|
-
```tsx
|
|
194
|
-
const router = createBrowserRouter([
|
|
195
|
-
{
|
|
196
|
-
component() {
|
|
197
|
-
return (
|
|
198
|
-
<RootLayout>
|
|
199
|
-
<SekishoProvider onNeedLogin={() => navigate('/login')}>
|
|
200
|
-
<Outlet />
|
|
201
|
-
</SekishoProvider>
|
|
202
|
-
</RootLayout>
|
|
203
|
-
);
|
|
204
|
-
},
|
|
205
|
-
children: [
|
|
206
|
-
{
|
|
207
|
-
component() {
|
|
208
|
-
return (
|
|
209
|
-
<NotAuthenticatedBoundary>
|
|
210
|
-
<Outlet />
|
|
211
|
-
</NotAuthenticatedBoundary>
|
|
212
|
-
);
|
|
213
|
-
},
|
|
214
|
-
children: [
|
|
215
|
-
// protected routes goes here
|
|
216
|
-
]
|
|
217
|
-
},
|
|
218
|
-
// unprotected routes goes here
|
|
219
|
-
]
|
|
220
|
-
}
|
|
221
|
-
]);
|
|
222
|
-
```
|
|
237
|
+
Sekisho is built on top of React's error boundaries. Both `needLogin()` and `accessRestricted()` throw a special tagged error during the React render phase, which bubbles up to the nearest matching boundary. Each boundary re-throws errors it does not own, so your own error boundaries are unaffected.
|
|
223
238
|
|
|
224
|
-
## Build
|
|
239
|
+
## Build Your Own Custom Gate
|
|
225
240
|
|
|
226
241
|
Sekisho also provides a low-level abstraction `createSekisho` from `sekisho/factory` for building your own custom gate with the same underlying mechanism. Let's say you want to build a guard for new user onboarding flow, where users need to complete their profile before accessing certain parts of the app:
|
|
227
242
|
|
|
228
243
|
```tsx
|
|
229
244
|
import { createSekisho } from 'sekisho/factory';
|
|
245
|
+
import { redirect } from 'next/navigation';
|
|
246
|
+
|
|
247
|
+
const [requireOnboarding, OnboardingGate, OnboardingErrorWrapper] = createSekisho('OnboardingRequired');
|
|
230
248
|
|
|
231
|
-
|
|
249
|
+
function OnboardingRedirect(): never {
|
|
250
|
+
redirect('/onboarding');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function Protected({ children }: React.PropsWithChildren) {
|
|
254
|
+
return (
|
|
255
|
+
<OnboardingGate fallback={<OnboardingRedirect />}>
|
|
256
|
+
{/* actual content goes here */}
|
|
257
|
+
<Dashboard />
|
|
258
|
+
</OnboardingGate>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
232
261
|
|
|
233
262
|
function Dashboard() {
|
|
234
263
|
const user = useUser();
|
|
@@ -237,34 +266,18 @@ function Dashboard() {
|
|
|
237
266
|
requireOnboarding('Profile incomplete');
|
|
238
267
|
}
|
|
239
268
|
|
|
240
|
-
// actual dashboard content
|
|
241
269
|
return <div>Welcome back, {user.name}</div>;
|
|
242
270
|
}
|
|
243
|
-
|
|
244
|
-
function OnboardingGuard({ error }) {
|
|
245
|
-
redirect('/onboarding');
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Wrap your app with the boundary component
|
|
249
|
-
function Page() {
|
|
250
|
-
return (
|
|
251
|
-
<OnboardingGate
|
|
252
|
-
fallback={<OnboardingGuard />}
|
|
253
|
-
// or you can pass a component that receives the error prop
|
|
254
|
-
fallbackComponent={OnboardingGuard}
|
|
255
|
-
>
|
|
256
|
-
<Dashboard />
|
|
257
|
-
</OnboardingGate>
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
271
|
```
|
|
261
272
|
|
|
262
|
-
The `createSekisho()` factory returns a
|
|
273
|
+
The `createSekisho()` factory returns a 5-tuple so each element can be named freely on destructure:
|
|
274
|
+
`[throwFn, ContainerComponent, ErrorWrapper, isError, ErrorClass]`
|
|
263
275
|
|
|
264
276
|
- **`throwFn`** — Call this during render to trigger the guard when a condition is unmet
|
|
265
|
-
- **`
|
|
277
|
+
- **`ContainerComponent`** — Error boundary that catches errors thrown by `throwFn`. Accepts `fallback` (static UI) or `fallbackComponent` (component that receives `{ error }`). Stores the options in context for `ErrorWrapper` to reuse.
|
|
278
|
+
- **`ErrorWrapper`** — Companion for framework error boundaries (Next.js `error.tsx`, React Router `errorElement`, etc.). Reads `fallback`/`fallbackComponent` from the nearest `ContainerComponent` ancestor in context.
|
|
266
279
|
- **`isError`** — Type guard to check if an error is from this guard (useful in middleware or error handlers)
|
|
267
|
-
- **`ErrorClass`** — The error constructor
|
|
280
|
+
- **`ErrorClass`** — The error constructor
|
|
268
281
|
|
|
269
282
|
Each call to `createSekisho()` is isolated — guards never accidentally catch each other's errors, even if nested.
|
|
270
283
|
|
package/dist/factory.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("react/jsx-runtime"),r=require("foxact/create-stackless-error"),t=require("react");exports.createSekisho=function(
|
|
1
|
+
"use client";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("react/jsx-runtime"),r=require("foxact/create-stackless-error"),t=require("foxact/nullthrow"),n=require("react");exports.createSekisho=function(o){let s=new WeakSet;class u extends Error{constructor(e){super(e),this.digest="BAILOUT_TO_CLIENT_SIDE_RENDERING",this.name=o??"SekishoGuardError",s.add(this)}}function i(e){return"object"==typeof e&&null!==e&&s.has(e)}let l=n.createContext(null);function c({error:r,children:o}){let s=t.nullthrow(n.useContext(l),"<ErrorWrapper /> must be used within its corresponding container component");if(i(r)){let{fallback:t,fallbackComponent:n}=s;return n?e.jsx(n,{error:r}):t}return o}function a({children:r}){let{fallback:o,fallbackComponent:s}=t.nullthrow(n.useContext(l),"<ErrorWrapper /> must be used within its corresponding container component"),u=n.useMemo(()=>s?e.jsx(s,{error:null}):o,[o,s]);return e.jsx(n.Suspense,{fallback:u,children:r})}class h extends n.Component{constructor(e){super(e),this.state={error:null}}static getDerivedStateFromError(e){return{error:e}}render(){let r=this.state.error;if(null===r)return this.props.children;if(i(r))return e.jsx(c,{error:r});throw r}}return[function(e){throw r.createStacklessError(()=>new u(e))},function({children:r,...t}){return e.jsx(l.Provider,{value:t,children:e.jsx(h,{children:e.jsx(a,{children:r})})})},c,i,u]};
|
package/dist/factory.d.ts
CHANGED
|
@@ -3,40 +3,50 @@ interface SekishoGuardError extends Error {
|
|
|
3
3
|
readonly digest: 'BAILOUT_TO_CLIENT_SIDE_RENDERING';
|
|
4
4
|
}
|
|
5
5
|
/**
|
|
6
|
-
* Props accepted by the
|
|
6
|
+
* Props accepted by the container component returned from `createSekisho`.
|
|
7
7
|
*
|
|
8
8
|
* Exactly one of `fallback` or `fallbackComponent` must be provided:
|
|
9
9
|
*
|
|
10
10
|
* - `fallback` — a static `ReactNode` rendered in place of children when the
|
|
11
11
|
* guard fires (access-control pattern).
|
|
12
12
|
* - `fallbackComponent` — a React component that receives `{ error }` as props.
|
|
13
|
-
*
|
|
14
|
-
* navigation side-effect (auth pattern).
|
|
13
|
+
* The `error` prop will only be `null` when the error is thrown in the server
|
|
15
14
|
*/
|
|
16
|
-
type
|
|
15
|
+
type SekishoContainerProps = React.PropsWithChildren & ({
|
|
17
16
|
fallback: React.ReactNode;
|
|
18
17
|
fallbackComponent?: never;
|
|
19
18
|
} | {
|
|
20
19
|
fallback?: never;
|
|
21
20
|
fallbackComponent: React.ComponentType<{
|
|
22
|
-
error: SekishoGuardError;
|
|
21
|
+
error: SekishoGuardError | null;
|
|
23
22
|
}>;
|
|
24
23
|
});
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Props accepted by the `ErrorWrapper` component returned from `createSekisho`.
|
|
26
|
+
*
|
|
27
|
+
* Mirrors the shape of framework error boundary props (Next.js `error.tsx`,
|
|
28
|
+
* React Router `errorElement`, etc.) so the component can be used as a direct
|
|
29
|
+
* wrapper without any adapter layer.
|
|
30
|
+
*/
|
|
31
|
+
interface SekishoErrorWrapperProps extends React.PropsWithChildren {
|
|
32
|
+
error: unknown;
|
|
27
33
|
}
|
|
28
34
|
/**
|
|
29
|
-
* Creates a paired guard throw function,
|
|
30
|
-
* and error class — all isolated from every other guard in the tree.
|
|
35
|
+
* Creates a paired guard throw function, container component, error wrapper,
|
|
36
|
+
* type guard, and error class — all isolated from every other guard in the tree.
|
|
31
37
|
*
|
|
32
38
|
* Call the returned throw function anywhere in the React render phase to signal
|
|
33
|
-
* that a condition is unmet. The nearest
|
|
39
|
+
* that a condition is unmet. The nearest container component in the tree will
|
|
34
40
|
* catch it and render its `fallback` prop instead of `children`. Every other
|
|
35
41
|
* error boundary — including ones from other `createSekisho()` calls —
|
|
36
42
|
* re-throws the error unchanged.
|
|
37
43
|
*
|
|
38
|
-
*
|
|
39
|
-
* `
|
|
44
|
+
* The container component stores its `fallback`/`fallbackComponent` in context so
|
|
45
|
+
* that `ErrorWrapper` can reuse it from a framework error boundary (e.g. Next.js
|
|
46
|
+
* `error.tsx` or React Router `errorElement`) without repeating the redirect logic.
|
|
47
|
+
*
|
|
48
|
+
* Returns a 5-tuple so each element can be named freely on destructure:
|
|
49
|
+
* `[throwFn, ContainerComponent, ErrorWrapper, isError, ErrorClass]`
|
|
40
50
|
*
|
|
41
51
|
* @example
|
|
42
52
|
* // Access-control pattern — static fallback element:
|
|
@@ -47,19 +57,34 @@ interface SekishoGuardBoundaryState {
|
|
|
47
57
|
* </OnboardingGate>
|
|
48
58
|
*
|
|
49
59
|
* @example
|
|
50
|
-
* //
|
|
51
|
-
* const [requireAuth, AuthGate] = createSekisho();
|
|
60
|
+
* // Auth pattern — render-phase redirect:
|
|
61
|
+
* const [requireAuth, AuthGate, AuthErrorWrapper] = createSekisho();
|
|
62
|
+
*
|
|
63
|
+
* function LoginRedirect(): never { redirect('/login'); }
|
|
64
|
+
*
|
|
65
|
+
* // In layout:
|
|
66
|
+
* <AuthGate fallbackComponent={LoginRedirect}>{children}</AuthGate>
|
|
67
|
+
*
|
|
68
|
+
* // In Next.js error.tsx:
|
|
69
|
+
* export default function ErrorPage({ error, reset }) {
|
|
70
|
+
* return <AuthErrorWrapper error={error}>...</AuthErrorWrapper>;
|
|
71
|
+
* }
|
|
72
|
+
*
|
|
73
|
+
* // In React Router errorElement:
|
|
74
|
+
* const ErrorComponent = () => {
|
|
75
|
+
* const error = useRouteError();
|
|
76
|
+
* return <AuthErrorWrapper error={error}>...</AuthErrorWrapper>;
|
|
77
|
+
* }
|
|
52
78
|
*
|
|
53
|
-
* <
|
|
54
|
-
* <Dashboard />
|
|
55
|
-
* </AuthGate>
|
|
79
|
+
* { errorElement: <ErrorComponent /> }
|
|
56
80
|
*/
|
|
57
81
|
declare function createSekisho(errorName?: string): [
|
|
58
82
|
throwError: (message: string) => never,
|
|
59
|
-
|
|
83
|
+
ContainerComponent: (props: SekishoContainerProps) => React.ReactNode,
|
|
84
|
+
ErrorWrapper: (props: SekishoErrorWrapperProps) => React.ReactNode,
|
|
60
85
|
isError: (error: unknown) => error is SekishoGuardError,
|
|
61
86
|
ErrorClass: new (message: string) => SekishoGuardError
|
|
62
87
|
];
|
|
63
88
|
|
|
64
89
|
export { createSekisho };
|
|
65
|
-
export type {
|
|
90
|
+
export type { SekishoContainerProps, SekishoErrorWrapperProps, SekishoGuardError };
|
package/dist/factory.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";import{jsx as r}from"react/jsx-runtime";import{createStacklessError as
|
|
1
|
+
"use client";import{jsx as r}from"react/jsx-runtime";import{createStacklessError as e}from"foxact/create-stackless-error";import{nullthrow as t}from"foxact/nullthrow";import{useContext as n,createContext as o,Component as i,useMemo as s,Suspense as c}from"react";function l(l){let u=new WeakSet;class a extends Error{constructor(r){super(r),this.digest="BAILOUT_TO_CLIENT_SIDE_RENDERING",this.name=l??"SekishoGuardError",u.add(this)}}function p(r){return"object"==typeof r&&null!==r&&u.has(r)}let f=o(null);function h({error:e,children:o}){let i=t(n(f),"<ErrorWrapper /> must be used within its corresponding container component");if(p(e)){let{fallback:t,fallbackComponent:n}=i;return n?r(n,{error:e}):t}return o}function d({children:e}){let{fallback:o,fallbackComponent:i}=t(n(f),"<ErrorWrapper /> must be used within its corresponding container component"),l=s(()=>i?r(i,{error:null}):o,[o,i]);return r(c,{fallback:l,children:e})}class m extends i{constructor(r){super(r),this.state={error:null}}static getDerivedStateFromError(r){return{error:r}}render(){let e=this.state.error;if(null===e)return this.props.children;if(p(e))return r(h,{error:e});throw e}}return[function(r){throw e(()=>new a(r))},function({children:e,...t}){return r(f.Provider,{value:t,children:r(m,{children:r(d,{children:e})})})},h,p,a]}export{l as createSekisho};
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("
|
|
1
|
+
"use client";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("./factory.cjs");let[r,t,s,o,c]=e.createSekisho("NotAuthenticatedError"),[i,p,a,d,n]=e.createSekisho("AccessRestrictedError");exports.AccessRestrictedContainer=p,exports.AccessRestrictedError=n,exports.AccessRestrictedErrorWrapper=a,exports.NotAuthenticatedContainer=t,exports.NotAuthenticatedError=c,exports.NotAuthenticatedErrorWrapper=s,exports.accessRestricted=i,exports.isAccessRestrictedError=d,exports.isNeedLoginError=o,exports.needLogin=r;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,57 +1,21 @@
|
|
|
1
|
-
import * as react from 'react';
|
|
2
1
|
import * as __factory from './factory.js';
|
|
3
|
-
import {
|
|
4
|
-
export { SekishoGuardBoundaryProps, SekishoGuardBoundaryState, SekishoGuardError, createSekisho } from './factory.js';
|
|
5
|
-
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { SekishoContainerProps, SekishoErrorWrapperProps } from './factory.js';
|
|
6
3
|
|
|
7
4
|
declare const needLogin: (message: string) => never;
|
|
5
|
+
declare const NotAuthenticatedContainer: (props: SekishoContainerProps) => React.ReactNode;
|
|
6
|
+
declare const NotAuthenticatedErrorWrapper: (props: SekishoErrorWrapperProps) => React.ReactNode;
|
|
8
7
|
declare const isNeedLoginError: (error: unknown) => error is __factory.SekishoGuardError;
|
|
9
8
|
declare const NotAuthenticatedError: new (message: string) => __factory.SekishoGuardError;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
error: unknown | null | undefined;
|
|
13
|
-
}
|
|
14
|
-
/** @deprecated `SekishoErrorWrapperProps` has since been renamed to `NotAuthenticatedErrorWrapperProps` */
|
|
15
|
-
type SekishoErrorWrapperProps = NotAuthenticatedErrorWrapperProps;
|
|
16
|
-
/**
|
|
17
|
-
* The actual error handling and redirection logic for "Not Authenticated" error.
|
|
18
|
-
*
|
|
19
|
-
* Used internally by `NotAuthenticatedBoundary`. You can also use this directly in
|
|
20
|
-
* a Next.js `app/error.tsx` file for custom error handling.
|
|
21
|
-
*/
|
|
22
|
-
declare function NotAuthenticatedErrorWrapper({ error, children }: NotAuthenticatedErrorWrapperProps): react.ReactNode;
|
|
23
|
-
|
|
24
|
-
interface NotAuthenticatedBoundaryProps extends React.PropsWithChildren {
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Error boundary that catches `NotAuthenticatedError` thrown by `needLogin()`
|
|
28
|
-
* within its subtree and calls the `onNeedLogin` callback from `SekishoProvider`.
|
|
29
|
-
* All other errors are re-thrown to the next boundary up the tree.
|
|
30
|
-
*
|
|
31
|
-
* This is included inside `SekishoProvider` automatically; you only need to use
|
|
32
|
-
* it directly if you want a narrower boundary for a specific subtree.
|
|
33
|
-
*/
|
|
34
|
-
declare function NotAuthenticatedBoundary({ children }: NotAuthenticatedBoundaryProps): React.ReactNode;
|
|
35
|
-
/** @deprecated `SekishoErrorBoundaryProps` has since been renamed to `NotAuthenticatedBoundaryProps` */
|
|
36
|
-
type SekishoErrorBoundaryProps = NotAuthenticatedBoundaryProps;
|
|
37
|
-
|
|
38
|
-
interface SekishoOptions {
|
|
39
|
-
onNeedLogin: () => void;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
interface SekishoProviderProps extends React.PropsWithChildren, SekishoOptions {
|
|
43
|
-
}
|
|
44
|
-
declare function SekishoProvider({ children, ...auth }: SekishoProviderProps): react_jsx_runtime.JSX.Element;
|
|
9
|
+
type NotAuthenticatedContainerProps = SekishoContainerProps;
|
|
10
|
+
type NotAuthenticatedErrorWrapperProps = SekishoErrorWrapperProps;
|
|
45
11
|
|
|
46
12
|
declare const accessRestricted: (message: string) => never;
|
|
47
|
-
declare const AccessRestrictedContainer:
|
|
13
|
+
declare const AccessRestrictedContainer: (props: SekishoContainerProps) => React.ReactNode;
|
|
14
|
+
declare const AccessRestrictedErrorWrapper: (props: SekishoErrorWrapperProps) => React.ReactNode;
|
|
48
15
|
declare const isAccessRestrictedError: (error: unknown) => error is __factory.SekishoGuardError;
|
|
49
16
|
declare const AccessRestrictedError: new (message: string) => __factory.SekishoGuardError;
|
|
50
|
-
type AccessRestrictedContainerProps =
|
|
51
|
-
|
|
52
|
-
declare const SekishoAccessContainer: react.ComponentClass<SekishoGuardBoundaryProps, __factory.SekishoGuardBoundaryState>;
|
|
53
|
-
/** @deprecated `SekishoAccessContainerProps` has since been renamed to `AccessRestrictedContainerProps` */
|
|
54
|
-
type SekishoAccessContainerProps = AccessRestrictedContainerProps;
|
|
17
|
+
type AccessRestrictedContainerProps = SekishoContainerProps;
|
|
18
|
+
type AccessRestrictedErrorWrapperProps = SekishoErrorWrapperProps;
|
|
55
19
|
|
|
56
|
-
export { AccessRestrictedContainer, AccessRestrictedError,
|
|
57
|
-
export type { AccessRestrictedContainerProps,
|
|
20
|
+
export { AccessRestrictedContainer, AccessRestrictedError, AccessRestrictedErrorWrapper, NotAuthenticatedContainer, NotAuthenticatedError, NotAuthenticatedErrorWrapper, accessRestricted, isAccessRestrictedError, isNeedLoginError, needLogin };
|
|
21
|
+
export type { AccessRestrictedContainerProps, AccessRestrictedErrorWrapperProps, NotAuthenticatedContainerProps, NotAuthenticatedErrorWrapperProps };
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";import{
|
|
1
|
+
"use client";import{createSekisho as r}from"./factory.mjs";let[e,t,c,s,o]=r("NotAuthenticatedError"),[i,d,n,a,A]=r("AccessRestrictedError");export{d as AccessRestrictedContainer,A as AccessRestrictedError,n as AccessRestrictedErrorWrapper,t as NotAuthenticatedContainer,o as NotAuthenticatedError,c as NotAuthenticatedErrorWrapper,i as accessRestricted,a as isAccessRestrictedError,s as isNeedLoginError,e as needLogin};
|