sekisho 0.1.1 → 0.2.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 +40 -5
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +41 -13
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
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
|
|
1
|
+
"use client";Object.defineProperty(exports,"__esModule",{value:!0});var r=require("foxact/use-isomorphic-layout-effect"),e=require("foxact/nullthrow"),t=require("react"),s=require("foxact/use-stable-handler-only-when-you-know-what-you-are-doing-or-you-will-be-fired"),o=require("react/jsx-runtime");let i=Object.getOwnPropertyDescriptor(Error,"stackTraceLimit"),n=i?.writable&&"number"==typeof i.value;function c(r){let e=Error.stackTraceLimit;n&&(Error.stackTraceLimit=0);let t=r();return n&&(Error.stackTraceLimit=e),t}class u extends Error{constructor(...r){super(...r),this.name="NotAuthenticatedError",this.notAuthenticated=!0,this.$sekishoError=!0}}function a(r){return!!r&&"object"==typeof r&&"notAuthenticated"in r&&!0===r.notAuthenticated&&"$sekishoError"in r&&!0===r.$sekishoError}let h=t.createContext(null);function d({error:o,children:i}){let{onNeedLogin:n}=e.nullthrow(t.useContext(h),"useSekishoOptions must be used within a SekishoOptionsProvider"),c=a(o),u=s.useStableHandler(n);return(r.useLayoutEffect(()=>{c&&u()},[c,u]),c)?null:i}class l extends t.Component{constructor(r){super(r),this.state={needLoginErrorObject:null}}static getDerivedStateFromError(r){if(a(r))return{needLoginErrorObject:r};throw r}render(){return this.state.needLoginErrorObject?o.jsx(d,{error:this.state.needLoginErrorObject,children:this.props.children}):this.props.children}}class p extends Error{constructor(...r){super(...r),this.name="AccessRestrictedError",this.accessRestricted=!0,this.$sekishoError=!0}}function E(r){return!!r&&"object"==typeof r&&"accessRestricted"in r&&!0===r.accessRestricted&&"$sekishoError"in r&&!0===r.$sekishoError}class x extends t.Component{constructor(r){super(r),this.state={restricted:!1}}static getDerivedStateFromError(r){if(E(r))return{restricted:!0};throw r}render(){return this.state.restricted?this.props.fallback:this.props.children}}exports.AccessRestrictedError=p,exports.NotAuthenticatedError=u,exports.SekishoAccessContainer=x,exports.SekishoErrorBoundary=l,exports.SekishoErrorWrapper=d,exports.SekishoProvider=function({children:r,...e}){return o.jsx(h.Provider,{value:e,children:o.jsx(l,{children:r})})},exports.accessRestricted=function(r){throw c(()=>new p(r))},exports.isAccessRestrictedError=E,exports.isNeedLoginError=a,exports.needLogin=function(r){throw c(()=>new u(r))};
|
package/dist/index.d.ts
CHANGED
|
@@ -2,14 +2,6 @@ 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;
|
|
@@ -17,10 +9,14 @@ declare class NotAuthenticatedError extends Error {
|
|
|
17
9
|
}
|
|
18
10
|
declare function isNeedLoginError(error: unknown): error is NotAuthenticatedError;
|
|
19
11
|
/**
|
|
20
|
-
*
|
|
12
|
+
* Throw a `NotAuthenticatedError` from anywhere in the React render phase.
|
|
13
|
+
*
|
|
14
|
+
* The error is caught by the nearest `SekishoErrorWrapper` or
|
|
15
|
+
* `SekishoErrorBoundary`, which then calls the `onNeedLogin` callback supplied
|
|
16
|
+
* to `SekishoProvider`.
|
|
21
17
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
18
|
+
* Common call sites: ky afterResponse hooks (HTTP 401), SWR middleware, or
|
|
19
|
+
* directly inside a component when session state is absent.
|
|
24
20
|
*/
|
|
25
21
|
declare function needLogin(message: string): never;
|
|
26
22
|
|
|
@@ -54,5 +50,37 @@ interface SekishoProviderProps extends React.PropsWithChildren, SekishoOptions {
|
|
|
54
50
|
}
|
|
55
51
|
declare function SekishoProvider({ children, ...auth }: SekishoProviderProps): react_jsx_runtime.JSX.Element;
|
|
56
52
|
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
declare class AccessRestrictedError extends Error {
|
|
54
|
+
readonly name = "AccessRestrictedError";
|
|
55
|
+
readonly accessRestricted = true;
|
|
56
|
+
readonly $sekishoError = true;
|
|
57
|
+
}
|
|
58
|
+
declare function isAccessRestrictedError(error: unknown): error is AccessRestrictedError;
|
|
59
|
+
/**
|
|
60
|
+
* Throw an `AccessRestrictedError` from anywhere in the React render phase.
|
|
61
|
+
*
|
|
62
|
+
* The error is caught by the nearest `SekishoAccessContainer`, which renders
|
|
63
|
+
* its `fallback` prop instead of its children. Any other error boundary in the
|
|
64
|
+
* tree (including `SekishoErrorBoundary`) re-throws it unchanged.
|
|
65
|
+
*/
|
|
66
|
+
declare function accessRestricted(message: string): never;
|
|
67
|
+
|
|
68
|
+
interface SekishoAccessContainerProps extends React.PropsWithChildren {
|
|
69
|
+
fallback: React.ReactNode;
|
|
70
|
+
}
|
|
71
|
+
interface State {
|
|
72
|
+
restricted: boolean;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Error boundary that catches `AccessRestrictedError` thrown by
|
|
76
|
+
* `accessRestricted()` within its subtree and renders `fallback` in place of
|
|
77
|
+
* `children`. All other errors are re-thrown to the next boundary up the tree.
|
|
78
|
+
*/
|
|
79
|
+
declare class SekishoAccessContainer extends Component<SekishoAccessContainerProps, State> {
|
|
80
|
+
constructor(props: SekishoAccessContainerProps);
|
|
81
|
+
static getDerivedStateFromError(this: void, error: unknown): State;
|
|
82
|
+
render(): React.ReactNode;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export { AccessRestrictedError, NotAuthenticatedError, SekishoAccessContainer, SekishoErrorBoundary, SekishoErrorWrapper, SekishoProvider, accessRestricted, isAccessRestrictedError, isNeedLoginError, needLogin };
|
|
86
|
+
export type { SekishoAccessContainerProps, SekishoErrorBoundaryProps, SekishoErrorWrapperProps, SekishoOptions, SekishoProviderProps };
|
package/dist/index.mjs
CHANGED
|
@@ -1 +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
|
|
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 s}from"react";import{useStableHandler as i}from"foxact/use-stable-handler-only-when-you-know-what-you-are-doing-or-you-will-be-fired";import{jsx as n}from"react/jsx-runtime";let c=Object.getOwnPropertyDescriptor(Error,"stackTraceLimit"),a=c?.writable&&"number"==typeof c.value;function h(r){let e=Error.stackTraceLimit;a&&(Error.stackTraceLimit=0);let t=r();return a&&(Error.stackTraceLimit=e),t}class u extends Error{constructor(...r){super(...r),this.name="NotAuthenticatedError",this.notAuthenticated=!0,this.$sekishoError=!0}}function d(r){return!!r&&"object"==typeof r&&"notAuthenticated"in r&&!0===r.notAuthenticated&&"$sekishoError"in r&&!0===r.$sekishoError}function l(r){throw h(()=>new u(r))}let p=t(null);function E({error:t,children:s}){let{onNeedLogin:n}=e(o(p),"useSekishoOptions must be used within a SekishoOptionsProvider"),c=d(t),a=i(n);return(r(()=>{c&&a()},[c,a]),c)?null:s}class f extends s{constructor(r){super(r),this.state={needLoginErrorObject:null}}static getDerivedStateFromError(r){if(d(r))return{needLoginErrorObject:r};throw r}render(){return this.state.needLoginErrorObject?n(E,{error:this.state.needLoginErrorObject,children:this.props.children}):this.props.children}}function m({children:r,...e}){return n(p.Provider,{value:e,children:n(f,{children:r})})}class k extends Error{constructor(...r){super(...r),this.name="AccessRestrictedError",this.accessRestricted=!0,this.$sekishoError=!0}}function w(r){return!!r&&"object"==typeof r&&"accessRestricted"in r&&!0===r.accessRestricted&&"$sekishoError"in r&&!0===r.$sekishoError}function b(r){throw h(()=>new k(r))}class g extends s{constructor(r){super(r),this.state={restricted:!1}}static getDerivedStateFromError(r){if(w(r))return{restricted:!0};throw r}render(){return this.state.restricted?this.props.fallback:this.props.children}}export{k as AccessRestrictedError,u as NotAuthenticatedError,g as SekishoAccessContainer,f as SekishoErrorBoundary,E as SekishoErrorWrapper,m as SekishoProvider,b as accessRestricted,w as isAccessRestrictedError,d as isNeedLoginError,l as needLogin};
|