sekisho 0.0.0 → 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 +204 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.mjs +1 -0
- package/package.json +41 -9
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sukka
|
|
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
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
<h1 align="center">⛩️ sekisho</h1>
|
|
2
|
+
<p align="center"><sup>(関所, <em>historical checkpoint for travel and security</em> in Japanese)</sup></p>
|
|
3
|
+
<p align="center">Authentication and Access Control for any React app</p>
|
|
4
|
+
|
|
5
|
+
----
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
### Full example
|
|
10
|
+
|
|
11
|
+
See the [example-nextjs-app](../../packages/example-nextjs-app).
|
|
12
|
+
|
|
13
|
+
### Simple setup
|
|
14
|
+
|
|
15
|
+
Wrap your app with `SekishoProvider` with options:
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
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
|
+
```
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
// app/layout.tsx
|
|
51
|
+
import { Providers } from './providers';
|
|
52
|
+
|
|
53
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
54
|
+
return (
|
|
55
|
+
<html lang="en">
|
|
56
|
+
<body>
|
|
57
|
+
<Providers>{children}</Providers>
|
|
58
|
+
</body>
|
|
59
|
+
</html>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
By default, `SekishoProvider` already includes `SekishoErrorBoundary` that will catch any `NotAuthenticatedError` thrown by `needLogin()` in the subtree. But if you have special error handling needs in certain parts of your app, you can also always import `SekishoErrorBoundary` directly to wrap those parts:
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
import { SekishoErrorBoundary } from 'sekisho';
|
|
68
|
+
|
|
69
|
+
function SomePartOfApp() {
|
|
70
|
+
return (
|
|
71
|
+
<SekishoErrorBoundary>
|
|
72
|
+
{/* ... */}
|
|
73
|
+
</SekishoErrorBoundary>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
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 `SekishoErrorWrapper`:
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
// app/error.tsx
|
|
82
|
+
'use client';
|
|
83
|
+
|
|
84
|
+
import { SekishoErrorWrapper } from 'sekisho';
|
|
85
|
+
|
|
86
|
+
export default function ErrorPage({ error, reset }) {
|
|
87
|
+
return (
|
|
88
|
+
<SekishoErrorWrapper error={error}>
|
|
89
|
+
{/* Your existing error UI goes in here */}
|
|
90
|
+
</SekishoErrorWrapper>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
> `SekishoErrorWrapper` is actually used by `SekishoErrorBoundary` internally, containing all the core logic.
|
|
96
|
+
|
|
97
|
+
### Triggering a login redirect
|
|
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):
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
import { needLogin } from 'sekisho';
|
|
107
|
+
|
|
108
|
+
function Dashboard() {
|
|
109
|
+
const session = useAuthSession();
|
|
110
|
+
|
|
111
|
+
if (!session) {
|
|
112
|
+
needLogin('No active session');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return <div>Welcome, {session.username}</div>;
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**In a [SWR](https://swr.vercel.app) middleware** (e.g. when an API response carries a known auth error):
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
import { needLogin } from 'sekisho';
|
|
123
|
+
import type { Middleware } from 'swr';
|
|
124
|
+
|
|
125
|
+
export const requireAuthMiddleware: Middleware = (useSWRNext) => (key, fetcher, config) => {
|
|
126
|
+
const swr = useSWRNext(key, fetcher, config);
|
|
127
|
+
if (swr.error && isApiAuthError(swr.error)) {
|
|
128
|
+
needLogin(swr.error.message);
|
|
129
|
+
}
|
|
130
|
+
return swr;
|
|
131
|
+
};
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Explanation
|
|
135
|
+
|
|
136
|
+
Sekisho is built on top of React's error boundaries. When you call `needLogin()` within the React render phase, it throws a special `NotAuthenticatedError`. React will then bubble the error up to the nearest React error boundary, and that's when Sekisho's error boundary went into action: it checks if the error is a `NotAuthenticatedError`, and if so, it calls the `onNeedLogin` callback you provided to `SekishoProvider`, and re-throws the error if it is not.
|
|
137
|
+
|
|
138
|
+
This way, you can centralize your authentication logic and keep it separate from your UI components.
|
|
139
|
+
|
|
140
|
+
Also, with the `SekishoErrorWrapper` / `SekishoErrorBoundary`, you can create protected routes and unprotected routes more easily with any React apps:
|
|
141
|
+
|
|
142
|
+
**Next.js App Router**
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
app/
|
|
146
|
+
├── (protected)/ ← all protected routes goes under here
|
|
147
|
+
│ ├── layout.tsx ← wrap children with <SekishoProvider> here
|
|
148
|
+
│ ├── error.tsx ← wrap with <SekishoErrorWrapper> here
|
|
149
|
+
│ └── page.tsx ← homepage, where you call needLogin() when authentication is needed
|
|
150
|
+
├── (unprotected)/ ← all unprotected routes goes under here
|
|
151
|
+
│ └── login/
|
|
152
|
+
│ └── page.tsx
|
|
153
|
+
└── layout.tsx ← root layout
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**React Router**
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
const router = createBrowserRouter([
|
|
160
|
+
{
|
|
161
|
+
component() {
|
|
162
|
+
return (
|
|
163
|
+
<RootLayout>
|
|
164
|
+
<SekishoProvider onNeedLogin={() => navigate('/login')}>
|
|
165
|
+
<Outlet />
|
|
166
|
+
</SekishoProvider>
|
|
167
|
+
</RootLayout>
|
|
168
|
+
);
|
|
169
|
+
},
|
|
170
|
+
children: [
|
|
171
|
+
{
|
|
172
|
+
component() {
|
|
173
|
+
return (
|
|
174
|
+
<SekishoErrorBoundary>
|
|
175
|
+
<Outlet />
|
|
176
|
+
</SekishoErrorBoundary>
|
|
177
|
+
);
|
|
178
|
+
},
|
|
179
|
+
children: [
|
|
180
|
+
// protected routes goes here
|
|
181
|
+
]
|
|
182
|
+
},
|
|
183
|
+
// unprotected routes goes here
|
|
184
|
+
]
|
|
185
|
+
}
|
|
186
|
+
]);
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## License
|
|
190
|
+
|
|
191
|
+
[MIT](LICENSE)
|
|
192
|
+
|
|
193
|
+
----
|
|
194
|
+
|
|
195
|
+
**sekisho** © [Sukka](https://github.com/SukkaW), Released under the [MIT](./LICENSE) License.
|
|
196
|
+
Authored and maintained by Sukka with help from contributors ([list](https://github.com/SukkaW/sekisho/graphs/contributors)).
|
|
197
|
+
|
|
198
|
+
> [Personal Website](https://skk.moe) · [Blog](https://blog.skk.moe) · GitHub [@SukkaW](https://github.com/SukkaW) · Telegram Channel [@SukkaChannel](https://t.me/SukkaChannel) · Mastodon [@sukka@acg.mn](https://acg.mn/@sukka) · Twitter [@isukkaw](https://twitter.com/isukkaw) · BlueSky [@skk.moe](https://bsky.app/profile/skk.moe)
|
|
199
|
+
|
|
200
|
+
<p align="center">
|
|
201
|
+
<a href="https://github.com/sponsors/SukkaW/">
|
|
202
|
+
<img src="https://sponsor.cdn.skk.moe/sponsors.svg"/>
|
|
203
|
+
</a>
|
|
204
|
+
</p>
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("foxact/use-isomorphic-layout-effect"),r=require("foxact/nullthrow"),t=require("react"),o=require("foxact/use-stable-handler-only-when-you-know-what-you-are-doing-or-you-will-be-fired"),i=require("react/jsx-runtime");let n=Object.getOwnPropertyDescriptor(Error,"stackTraceLimit"),s=n?.writable&&"number"==typeof n.value;class u extends Error{constructor(...e){super(...e),this.name="NotAuthenticatedError",this.notAuthenticated=!0,this.$sekishoError=!0}}function c(e){return!!e&&"object"==typeof e&&"notAuthenticated"in e&&!0===e.notAuthenticated&&"$sekishoError"in e&&!0===e.$sekishoError}let a=t.createContext(null);function l({error:i,children:n}){let{onNeedLogin:s}=r.nullthrow(t.useContext(a),"useSekishoOptions must be used within a SekishoOptionsProvider"),u=c(i),h=o.useStableHandler(s);return(e.useLayoutEffect(()=>{u&&h()},[u,h]),u)?null:n}class h extends t.Component{constructor(e){super(e),this.state={needLoginErrorObject:null}}static getDerivedStateFromError(e){if(c(e))return{needLoginErrorObject:e};throw e}render(){return this.state.needLoginErrorObject?i.jsx(l,{error:this.state.needLoginErrorObject,children:this.props.children}):this.props.children}}exports.NotAuthenticatedError=u,exports.SekishoErrorBoundary=h,exports.SekishoErrorWrapper=l,exports.SekishoProvider=function({children:e,...r}){return i.jsx(a.Provider,{value:r,children:i.jsx(h,{children:e})})},exports.isNeedLoginError=c,exports.needLogin=function(e){let r=Error.stackTraceLimit;s&&(Error.stackTraceLimit=0);let t=new u(e);throw s&&(Error.stackTraceLimit=r),t};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { Component } from 'react';
|
|
3
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
declare global {
|
|
6
|
+
interface ErrorConstructor {
|
|
7
|
+
/**
|
|
8
|
+
* The `Error.stackTraceLimit` property is v8 only, add this to the type with nullish
|
|
9
|
+
*/
|
|
10
|
+
stackTraceLimit?: number;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
declare class NotAuthenticatedError extends Error {
|
|
14
|
+
readonly name = "NotAuthenticatedError";
|
|
15
|
+
readonly notAuthenticated = true;
|
|
16
|
+
readonly $sekishoError = true;
|
|
17
|
+
}
|
|
18
|
+
declare function isNeedLoginError(error: unknown): error is NotAuthenticatedError;
|
|
19
|
+
/**
|
|
20
|
+
* This is a shortcut function to throw a "Not Authenticated" error.
|
|
21
|
+
*
|
|
22
|
+
* This can be used in ky's interceptor hooks, when HTTP 401 is detected, we can simply call this
|
|
23
|
+
* This can also be used in SWR middleware, when API response JSON contains a known "not authenticated" error, we can call this
|
|
24
|
+
*/
|
|
25
|
+
declare function needLogin(message: string): never;
|
|
26
|
+
|
|
27
|
+
interface SekishoErrorWrapperProps extends React.PropsWithChildren {
|
|
28
|
+
error: unknown | null | undefined;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The actual error handling and redirection logic for "Not Authenticated" error
|
|
32
|
+
*
|
|
33
|
+
* This is used internally by SekishoErrorBoundary. And if you are using Next.js App Router,
|
|
34
|
+
* you can use this directly in the "error.tsx" file.
|
|
35
|
+
*/
|
|
36
|
+
declare function SekishoErrorWrapper({ error, children }: SekishoErrorWrapperProps): react.ReactNode;
|
|
37
|
+
|
|
38
|
+
interface SekishoErrorBoundaryProps extends React.PropsWithChildren {
|
|
39
|
+
}
|
|
40
|
+
interface SekishoErrorBoundaryState {
|
|
41
|
+
needLoginErrorObject: unknown | null | undefined;
|
|
42
|
+
}
|
|
43
|
+
declare class SekishoErrorBoundary extends Component<SekishoErrorBoundaryProps, SekishoErrorBoundaryState> {
|
|
44
|
+
constructor(props: SekishoErrorBoundaryProps);
|
|
45
|
+
static getDerivedStateFromError(this: void, error: unknown): SekishoErrorBoundaryState;
|
|
46
|
+
render(): React.ReactNode;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface SekishoOptions {
|
|
50
|
+
onNeedLogin: () => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface SekishoProviderProps extends React.PropsWithChildren, SekishoOptions {
|
|
54
|
+
}
|
|
55
|
+
declare function SekishoProvider({ children, ...auth }: SekishoProviderProps): react_jsx_runtime.JSX.Element;
|
|
56
|
+
|
|
57
|
+
export { NotAuthenticatedError, SekishoErrorBoundary, SekishoErrorWrapper, SekishoProvider, isNeedLoginError, needLogin };
|
|
58
|
+
export type { SekishoErrorBoundaryProps, SekishoErrorWrapperProps, SekishoOptions, SekishoProviderProps };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";import{useLayoutEffect as r}from"foxact/use-isomorphic-layout-effect";import{nullthrow as e}from"foxact/nullthrow";import{createContext as t,useContext as o,Component as i}from"react";import{useStableHandler as n}from"foxact/use-stable-handler-only-when-you-know-what-you-are-doing-or-you-will-be-fired";import{jsx as s}from"react/jsx-runtime";let c=Object.getOwnPropertyDescriptor(Error,"stackTraceLimit"),a=c?.writable&&"number"==typeof c.value;class u extends Error{constructor(...r){super(...r),this.name="NotAuthenticatedError",this.notAuthenticated=!0,this.$sekishoError=!0}}function h(r){return!!r&&"object"==typeof r&&"notAuthenticated"in r&&!0===r.notAuthenticated&&"$sekishoError"in r&&!0===r.$sekishoError}function l(r){let e=Error.stackTraceLimit;a&&(Error.stackTraceLimit=0);let t=new u(r);throw a&&(Error.stackTraceLimit=e),t}let d=t(null);function p({error:t,children:i}){let{onNeedLogin:s}=e(o(d),"useSekishoOptions must be used within a SekishoOptionsProvider"),c=h(t),a=n(s);return(r(()=>{c&&a()},[c,a]),c)?null:i}class m extends i{constructor(r){super(r),this.state={needLoginErrorObject:null}}static getDerivedStateFromError(r){if(h(r))return{needLoginErrorObject:r};throw r}render(){return this.state.needLoginErrorObject?s(p,{error:this.state.needLoginErrorObject,children:this.props.children}):this.props.children}}function f({children:r,...e}){return s(d.Provider,{value:e,children:s(m,{children:r})})}export{u as NotAuthenticatedError,m as SekishoErrorBoundary,p as SekishoErrorWrapper,f as SekishoProvider,h as isNeedLoginError,l as needLogin};
|
package/package.json
CHANGED
|
@@ -1,11 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sekisho",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "",
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Authentication and Access Control for any React app",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/SukkaW/sekisho",
|
|
8
|
+
"directory": "packages/sekisho"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/index.cjs",
|
|
11
|
+
"module": "./dist/index.mjs",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"files": [
|
|
14
|
+
"README.md",
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.mjs",
|
|
21
|
+
"require": "./dist/index.cjs",
|
|
22
|
+
"default": "./dist/index.cjs"
|
|
23
|
+
},
|
|
24
|
+
"./package.json": "./package.json"
|
|
25
|
+
},
|
|
26
|
+
"author": "Sukka <https://skk.moe>",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"foxact": "^0.3.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@swc/core": "^1.15.30",
|
|
33
|
+
"@types/react": "^19.2.14",
|
|
34
|
+
"bunchee": "^6.10.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"react": ">=17.0.0"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "bunchee --minify --no-sourcemap",
|
|
41
|
+
"lint": "eslint --format=sukka ."
|
|
42
|
+
}
|
|
43
|
+
}
|