sekisho 0.1.1 → 0.2.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/README.md +40 -5
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +77 -13
- package/dist/index.mjs +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<h1 align="center">⛩️ sekisho</h1>
|
|
2
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>
|
|
3
|
+
<p align="center">Authentication and Access Control for any React app ([online demo](https://sekisho-demo.pages.dev/login))</p>
|
|
4
4
|
|
|
5
5
|
----
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
### Full example
|
|
10
10
|
|
|
11
|
-
See the [example-nextjs-app](../../packages/example-nextjs-app).
|
|
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
|
### Simple setup
|
|
14
14
|
|
|
@@ -131,13 +131,48 @@ export const requireAuthMiddleware: Middleware = (useSWRNext) => (key, fetcher,
|
|
|
131
131
|
};
|
|
132
132
|
```
|
|
133
133
|
|
|
134
|
+
### Restricting access
|
|
135
|
+
|
|
136
|
+
Wrap any part of the UI with `SekishoAccessContainer` 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 — `SekishoAccessContainer` simply renders `fallback` in place of its children:
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
import { accessRestricted, SekishoAccessContainer } from 'sekisho';
|
|
140
|
+
|
|
141
|
+
function AdminPanel() {
|
|
142
|
+
const { role } = useCurrentUser();
|
|
143
|
+
|
|
144
|
+
if (role !== 'admin') {
|
|
145
|
+
accessRestricted('Admin only');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return <div>Secret admin content</div>;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function Page() {
|
|
152
|
+
return (
|
|
153
|
+
<SekishoAccessContainer
|
|
154
|
+
fallback={<p>You don't have permission to view this section.</p>}
|
|
155
|
+
>
|
|
156
|
+
<AdminPanel />
|
|
157
|
+
</SekishoAccessContainer>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
This kinda like `<Suspense />` but for access control instead. And like `<Suspense />`, you can have multiple `SekishoAccessContainer`s nested independently — each one only catches the `accessRestricted()` calls within its own subtree.
|
|
163
|
+
|
|
134
164
|
## Explanation
|
|
135
165
|
|
|
136
|
-
Sekisho is built on top of React's error boundaries.
|
|
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` | `SekishoErrorBoundary` / `SekishoErrorWrapper` | Calls `onNeedLogin` from `SekishoProvider` (global redirect) |
|
|
171
|
+
| `accessRestricted()` | `AccessRestrictedError` | `SekishoAccessContainer` | Renders the `fallback` prop in place of children (local swap) |
|
|
137
172
|
|
|
138
|
-
|
|
173
|
+
Each boundary re-throws errors it does not own, so `SekishoAccessContainer` never swallows an auth error, and `SekishoErrorBoundary` never swallows an access error. Your own error boundaries are unaffected by either.
|
|
139
174
|
|
|
140
|
-
|
|
175
|
+
With `SekishoErrorWrapper` / `SekishoErrorBoundary` you can create protected and unprotected routes in any React app:
|
|
141
176
|
|
|
142
177
|
**Next.js App Router**
|
|
143
178
|
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("foxact/use-isomorphic-layout-effect"),
|
|
1
|
+
"use client";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("foxact/create-stackless-error"),r=require("foxact/use-isomorphic-layout-effect"),t=require("foxact/nullthrow"),s=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");class n extends Error{constructor(...e){super(...e),this.name="NotAuthenticatedError",this.notAuthenticated=!0,this.$sekishoError=!0,this.digest="BAILOUT_TO_CLIENT_SIDE_RENDERING"}}function c(e){return!!e&&"object"==typeof e&&"notAuthenticated"in e&&!0===e.notAuthenticated&&"$sekishoError"in e&&!0===e.$sekishoError}let u=s.createContext(null);function a({error:e,children:i}){let{onNeedLogin:n}=t.nullthrow(s.useContext(u),"useSekishoOptions must be used within a SekishoOptionsProvider"),h=c(e),d=o.useStableHandler(n);return(r.useLayoutEffect(()=>{h&&d()},[h,d]),h)?null:i}class h extends s.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(a,{error:this.state.needLoginErrorObject,children:this.props.children}):this.props.children}}class d extends Error{constructor(...e){super(...e),this.name="AccessRestrictedError",this.accessRestricted=!0,this.$sekishoError=!0,this.digest="BAILOUT_TO_CLIENT_SIDE_RENDERING"}}function l(e){return!!e&&"object"==typeof e&&"accessRestricted"in e&&!0===e.accessRestricted&&"$sekishoError"in e&&!0===e.$sekishoError}class p extends s.Component{constructor(e){super(e),this.state={restricted:!1}}static getDerivedStateFromError(e){if(l(e))return{restricted:!0};throw e}render(){return this.state.restricted?this.props.fallback:this.props.children}}exports.AccessRestrictedError=d,exports.NotAuthenticatedError=n,exports.SekishoAccessContainer=p,exports.SekishoErrorBoundary=h,exports.SekishoErrorWrapper=a,exports.SekishoProvider=function({children:e,...r}){return i.jsx(u.Provider,{value:r,children:i.jsx(h,{children:e})})},exports.accessRestricted=function(r){throw e.createStacklessError(()=>new d(r))},exports.isAccessRestrictedError=l,exports.isNeedLoginError=c,exports.needLogin=function(r){throw e.createStacklessError(()=>new n(r))};
|
package/dist/index.d.ts
CHANGED
|
@@ -2,25 +2,39 @@ import * as react from 'react';
|
|
|
2
2
|
import { Component } from 'react';
|
|
3
3
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
4
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
5
|
declare class NotAuthenticatedError extends Error {
|
|
14
6
|
readonly name = "NotAuthenticatedError";
|
|
15
7
|
readonly notAuthenticated = true;
|
|
16
8
|
readonly $sekishoError = true;
|
|
9
|
+
/**
|
|
10
|
+
* Normally, when React encounters an error during rendering in the server,
|
|
11
|
+
* it will find the nearest Suspense boundary and render its fallback (on
|
|
12
|
+
* the client it would find the nearest error boundary instead).
|
|
13
|
+
*
|
|
14
|
+
* For various purposes, Next.js also uses this mechanism to bail out of
|
|
15
|
+
* server-side rendering to do client-side rendering only. Thus Next.js
|
|
16
|
+
* create this magic digest 'BAILOUT_TO_CLIENT_SIDE_RENDERING' that does
|
|
17
|
+
* nothing but allow every error report (console, reportError, etc.) to
|
|
18
|
+
* skip this error
|
|
19
|
+
*
|
|
20
|
+
* Next.js doesn't handle this error in any special way. It is React itself
|
|
21
|
+
* who contains the "special handling".
|
|
22
|
+
*
|
|
23
|
+
* So we can safely re-use this digest to make sure that the error is not
|
|
24
|
+
* reported by Next.js
|
|
25
|
+
*/
|
|
26
|
+
readonly digest = "BAILOUT_TO_CLIENT_SIDE_RENDERING";
|
|
17
27
|
}
|
|
18
28
|
declare function isNeedLoginError(error: unknown): error is NotAuthenticatedError;
|
|
19
29
|
/**
|
|
20
|
-
*
|
|
30
|
+
* Throw a `NotAuthenticatedError` from anywhere in the React render phase.
|
|
31
|
+
*
|
|
32
|
+
* The error is caught by the nearest `SekishoErrorWrapper` or
|
|
33
|
+
* `SekishoErrorBoundary`, which then calls the `onNeedLogin` callback supplied
|
|
34
|
+
* to `SekishoProvider`.
|
|
21
35
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
36
|
+
* Common call sites: ky afterResponse hooks (HTTP 401), SWR middleware, or
|
|
37
|
+
* directly inside a component when session state is absent.
|
|
24
38
|
*/
|
|
25
39
|
declare function needLogin(message: string): never;
|
|
26
40
|
|
|
@@ -54,5 +68,55 @@ interface SekishoProviderProps extends React.PropsWithChildren, SekishoOptions {
|
|
|
54
68
|
}
|
|
55
69
|
declare function SekishoProvider({ children, ...auth }: SekishoProviderProps): react_jsx_runtime.JSX.Element;
|
|
56
70
|
|
|
57
|
-
|
|
58
|
-
|
|
71
|
+
declare class AccessRestrictedError extends Error {
|
|
72
|
+
readonly name = "AccessRestrictedError";
|
|
73
|
+
readonly accessRestricted = true;
|
|
74
|
+
readonly $sekishoError = true;
|
|
75
|
+
/**
|
|
76
|
+
* Normally, when React encounters an error during rendering in the server,
|
|
77
|
+
* it will find the nearest Suspense boundary and render its fallback (on
|
|
78
|
+
* the client it would find the nearest error boundary instead).
|
|
79
|
+
*
|
|
80
|
+
* For various purposes, Next.js also uses this mechanism to bail out of
|
|
81
|
+
* server-side rendering to do client-side rendering only. Thus Next.js
|
|
82
|
+
* create this magic digest 'BAILOUT_TO_CLIENT_SIDE_RENDERING' that does
|
|
83
|
+
* nothing but allow every error report (console, reportError, etc.) to
|
|
84
|
+
* skip this error
|
|
85
|
+
*
|
|
86
|
+
* Next.js doesn't handle this error in any special way. It is React itself
|
|
87
|
+
* who contains the "special handling".
|
|
88
|
+
*
|
|
89
|
+
* So we can safely re-use this digest to make sure that the error is not
|
|
90
|
+
* reported by Next.js
|
|
91
|
+
*/
|
|
92
|
+
readonly digest = "BAILOUT_TO_CLIENT_SIDE_RENDERING";
|
|
93
|
+
}
|
|
94
|
+
declare function isAccessRestrictedError(error: unknown): error is AccessRestrictedError;
|
|
95
|
+
/**
|
|
96
|
+
* Throw an `AccessRestrictedError` from anywhere in the React render phase.
|
|
97
|
+
*
|
|
98
|
+
* The error is caught by the nearest `SekishoAccessContainer`, which renders
|
|
99
|
+
* its `fallback` prop instead of its children. Any other error boundary in the
|
|
100
|
+
* tree (including `SekishoErrorBoundary`) re-throws it unchanged.
|
|
101
|
+
*/
|
|
102
|
+
declare function accessRestricted(message: string): never;
|
|
103
|
+
|
|
104
|
+
interface SekishoAccessContainerProps extends React.PropsWithChildren {
|
|
105
|
+
fallback: React.ReactNode;
|
|
106
|
+
}
|
|
107
|
+
interface State {
|
|
108
|
+
restricted: boolean;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Error boundary that catches `AccessRestrictedError` thrown by
|
|
112
|
+
* `accessRestricted()` within its subtree and renders `fallback` in place of
|
|
113
|
+
* `children`. All other errors are re-thrown to the next boundary up the tree.
|
|
114
|
+
*/
|
|
115
|
+
declare class SekishoAccessContainer extends Component<SekishoAccessContainerProps, State> {
|
|
116
|
+
constructor(props: SekishoAccessContainerProps);
|
|
117
|
+
static getDerivedStateFromError(this: void, error: unknown): State;
|
|
118
|
+
render(): React.ReactNode;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export { AccessRestrictedError, NotAuthenticatedError, SekishoAccessContainer, SekishoErrorBoundary, SekishoErrorWrapper, SekishoProvider, accessRestricted, isAccessRestrictedError, isNeedLoginError, needLogin };
|
|
122
|
+
export type { SekishoAccessContainerProps, SekishoErrorBoundaryProps, SekishoErrorWrapperProps, SekishoOptions, SekishoProviderProps };
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";import{
|
|
1
|
+
"use client";import{createStacklessError as r}from"foxact/create-stackless-error";import{useLayoutEffect as e}from"foxact/use-isomorphic-layout-effect";import{nullthrow as t}from"foxact/nullthrow";import{createContext as o,useContext as s,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 c}from"react/jsx-runtime";class h extends Error{constructor(...r){super(...r),this.name="NotAuthenticatedError",this.notAuthenticated=!0,this.$sekishoError=!0,this.digest="BAILOUT_TO_CLIENT_SIDE_RENDERING"}}function d(r){return!!r&&"object"==typeof r&&"notAuthenticated"in r&&!0===r.notAuthenticated&&"$sekishoError"in r&&!0===r.$sekishoError}function a(e){throw r(()=>new h(e))}let u=o(null);function l({error:r,children:o}){let{onNeedLogin:i}=t(s(u),"useSekishoOptions must be used within a SekishoOptionsProvider"),c=d(r),h=n(i);return(e(()=>{c&&h()},[c,h]),c)?null:o}class E extends i{constructor(r){super(r),this.state={needLoginErrorObject:null}}static getDerivedStateFromError(r){if(d(r))return{needLoginErrorObject:r};throw r}render(){return this.state.needLoginErrorObject?c(l,{error:this.state.needLoginErrorObject,children:this.props.children}):this.props.children}}function p({children:r,...e}){return c(u.Provider,{value:e,children:c(E,{children:r})})}class f extends Error{constructor(...r){super(...r),this.name="AccessRestrictedError",this.accessRestricted=!0,this.$sekishoError=!0,this.digest="BAILOUT_TO_CLIENT_SIDE_RENDERING"}}function m(r){return!!r&&"object"==typeof r&&"accessRestricted"in r&&!0===r.accessRestricted&&"$sekishoError"in r&&!0===r.$sekishoError}function k(e){throw r(()=>new f(e))}class w extends i{constructor(r){super(r),this.state={restricted:!1}}static getDerivedStateFromError(r){if(m(r))return{restricted:!0};throw r}render(){return this.state.restricted?this.props.fallback:this.props.children}}export{f as AccessRestrictedError,h as NotAuthenticatedError,w as SekishoAccessContainer,E as SekishoErrorBoundary,l as SekishoErrorWrapper,p as SekishoProvider,k as accessRestricted,m as isAccessRestrictedError,d as isNeedLoginError,a as needLogin};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sekisho",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Authentication and Access Control for any React app",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,10 +26,10 @@
|
|
|
26
26
|
"author": "Sukka <https://skk.moe>",
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"foxact": "^0.3.
|
|
29
|
+
"foxact": "^0.3.1"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@swc/core": "^1.15.
|
|
32
|
+
"@swc/core": "^1.15.32",
|
|
33
33
|
"@types/react": "^19.2.14",
|
|
34
34
|
"bunchee": "^6.10.0"
|
|
35
35
|
},
|