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 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. 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.
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
- This way, you can centralize your authentication logic and keep it separate from your UI components.
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
- Also, with the `SekishoErrorWrapper` / `SekishoErrorBoundary`, you can create protected routes and unprotected routes more easily with any React apps:
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"),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};
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
- * This is a shortcut function to throw a "Not Authenticated" error.
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
- * 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
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
- export { NotAuthenticatedError, SekishoErrorBoundary, SekishoErrorWrapper, SekishoProvider, isNeedLoginError, needLogin };
58
- export type { SekishoErrorBoundaryProps, SekishoErrorWrapperProps, SekishoOptions, SekishoProviderProps };
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 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};
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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sekisho",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Authentication and Access Control for any React app",
5
5
  "repository": {
6
6
  "type": "git",