gatehouse 0.1.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 +321 -0
- package/dist/adapters/authjs.d.ts +32 -0
- package/dist/adapters/authjs.js +14 -0
- package/dist/adapters/clerk.d.ts +27 -0
- package/dist/adapters/clerk.js +15 -0
- package/dist/adapters/supabase.d.ts +35 -0
- package/dist/adapters/supabase.js +27 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +66 -0
- package/dist/next.d.ts +103 -0
- package/dist/next.js +105 -0
- package/dist/react.d.ts +100 -0
- package/dist/react.js +108 -0
- package/dist/types-1JY9ADLk.d.ts +41 -0
- package/package.json +73 -0
package/README.md
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# Gatehouse
|
|
2
|
+
|
|
3
|
+
Drop-in RBAC for Next.js. Define roles once, protect everything.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install gatehouse
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
### 1. Define your roles (one file, once)
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
// lib/gatehouse.ts
|
|
15
|
+
import { createGatehouse } from "gatehouse";
|
|
16
|
+
|
|
17
|
+
export const gh = createGatehouse({
|
|
18
|
+
roles: {
|
|
19
|
+
owner: ["*"],
|
|
20
|
+
admin: ["project:*", "member:invite", "member:remove"],
|
|
21
|
+
member: ["project:read", "project:create", "task:*"],
|
|
22
|
+
viewer: ["project:read", "task:read"],
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Roles are hierarchical — first is highest. Wildcards work: `project:*` matches `project:read`, `project:create`, etc. `*` matches everything.
|
|
28
|
+
|
|
29
|
+
### 2. Protect your UI
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
import { Gate } from "gatehouse/react";
|
|
33
|
+
|
|
34
|
+
<Gate allow="project:create">
|
|
35
|
+
<CreateButton />
|
|
36
|
+
</Gate>
|
|
37
|
+
|
|
38
|
+
<Gate role="admin" fallback={<span>Admin only</span>}>
|
|
39
|
+
<AdminPanel />
|
|
40
|
+
</Gate>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 3. Protect your API routes
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
// lib/gate.ts
|
|
47
|
+
import { createServerGate } from "gatehouse/next";
|
|
48
|
+
import { gh } from "./gatehouse";
|
|
49
|
+
import { auth } from "./auth";
|
|
50
|
+
|
|
51
|
+
export const gate = createServerGate({
|
|
52
|
+
gatehouse: gh,
|
|
53
|
+
resolve: async () => {
|
|
54
|
+
const session = await auth();
|
|
55
|
+
if (!session) return null;
|
|
56
|
+
return { role: session.user.role };
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
// app/api/projects/route.ts
|
|
63
|
+
import { withGate } from "gatehouse/next";
|
|
64
|
+
import { gate } from "@/lib/gate";
|
|
65
|
+
|
|
66
|
+
export const POST = withGate(async () => {
|
|
67
|
+
await gate("project:create");
|
|
68
|
+
return Response.json({ ok: true });
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
That's it. Three files, working RBAC.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Auth Provider Adapters
|
|
77
|
+
|
|
78
|
+
### Clerk
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { createServerGate } from "gatehouse/next";
|
|
82
|
+
import { clerkResolver } from "gatehouse/adapters/clerk";
|
|
83
|
+
import { gh } from "./gatehouse";
|
|
84
|
+
|
|
85
|
+
export const gate = createServerGate({
|
|
86
|
+
gatehouse: gh,
|
|
87
|
+
resolve: clerkResolver(), // reads from publicMetadata.role
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Supabase
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { createServerGate } from "gatehouse/next";
|
|
95
|
+
import { supabaseResolver } from "gatehouse/adapters/supabase";
|
|
96
|
+
import { gh } from "./gatehouse";
|
|
97
|
+
import { createClient } from "@/lib/supabase/server";
|
|
98
|
+
|
|
99
|
+
export const gate = createServerGate({
|
|
100
|
+
gatehouse: gh,
|
|
101
|
+
resolve: supabaseResolver({ createClient }),
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Auth.js (NextAuth)
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { createServerGate } from "gatehouse/next";
|
|
109
|
+
import { authjsResolver } from "gatehouse/adapters/authjs";
|
|
110
|
+
import { gh } from "./gatehouse";
|
|
111
|
+
import { auth } from "./auth";
|
|
112
|
+
|
|
113
|
+
export const gate = createServerGate({
|
|
114
|
+
gatehouse: gh,
|
|
115
|
+
resolve: authjsResolver({ auth }),
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## React Components & Hooks
|
|
122
|
+
|
|
123
|
+
### `<GatehouseProvider>`
|
|
124
|
+
|
|
125
|
+
Wrap your app to provide RBAC context:
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
// app/layout.tsx
|
|
129
|
+
import { GatehouseProvider } from "gatehouse/react";
|
|
130
|
+
import { gh } from "@/lib/gatehouse";
|
|
131
|
+
|
|
132
|
+
export default function Layout({ children }: { children: React.ReactNode }) {
|
|
133
|
+
return (
|
|
134
|
+
<GatehouseProvider
|
|
135
|
+
gatehouse={gh}
|
|
136
|
+
resolve={async () => {
|
|
137
|
+
const res = await fetch("/api/me");
|
|
138
|
+
if (!res.ok) return null;
|
|
139
|
+
return res.json(); // { role: "admin" }
|
|
140
|
+
}}
|
|
141
|
+
>
|
|
142
|
+
{children}
|
|
143
|
+
</GatehouseProvider>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### `<Gate>`
|
|
149
|
+
|
|
150
|
+
Declarative permission gate:
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
import { Gate } from "gatehouse/react";
|
|
154
|
+
|
|
155
|
+
// Single permission
|
|
156
|
+
<Gate allow="project:create">
|
|
157
|
+
<CreateButton />
|
|
158
|
+
</Gate>
|
|
159
|
+
|
|
160
|
+
// Role check
|
|
161
|
+
<Gate role="admin">
|
|
162
|
+
<AdminPanel />
|
|
163
|
+
</Gate>
|
|
164
|
+
|
|
165
|
+
// Multiple permissions (all required)
|
|
166
|
+
<Gate allOf={["project:edit", "project:delete"]}>
|
|
167
|
+
<DangerZone />
|
|
168
|
+
</Gate>
|
|
169
|
+
|
|
170
|
+
// Multiple permissions (any sufficient)
|
|
171
|
+
<Gate anyOf={["project:edit", "project:create"]}>
|
|
172
|
+
<EditMenu />
|
|
173
|
+
</Gate>
|
|
174
|
+
|
|
175
|
+
// With fallback and loading state
|
|
176
|
+
<Gate allow="billing:manage" fallback={<UpgradePrompt />} loading={<Skeleton />}>
|
|
177
|
+
<BillingDashboard />
|
|
178
|
+
</Gate>
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Hooks
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
import { useGate, useRole, usePermissions, useGatehouse } from "gatehouse/react";
|
|
185
|
+
|
|
186
|
+
function MyComponent() {
|
|
187
|
+
const canCreate = useGate("project:create");
|
|
188
|
+
const role = useRole(); // "admin" | null
|
|
189
|
+
const perms = usePermissions(); // ["project:*", "member:invite", ...]
|
|
190
|
+
const { gatehouse, subject, loading } = useGatehouse();
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Next.js Middleware
|
|
197
|
+
|
|
198
|
+
Protect routes at the edge:
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
// middleware.ts
|
|
202
|
+
import { createMiddleware } from "gatehouse/next";
|
|
203
|
+
|
|
204
|
+
export default createMiddleware({
|
|
205
|
+
protected: ["/dashboard/:path*", "/api/projects/:path*"],
|
|
206
|
+
isAuthenticated: (req) => !!req.cookies.get("session"),
|
|
207
|
+
loginUrl: "/login", // default
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
export const config = {
|
|
211
|
+
matcher: ["/dashboard/:path*", "/api/projects/:path*"],
|
|
212
|
+
};
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Server-Side Gate API
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
import { gate } from "@/lib/gate";
|
|
221
|
+
|
|
222
|
+
// Require authentication (throws 401 if not logged in)
|
|
223
|
+
const subject = await gate();
|
|
224
|
+
|
|
225
|
+
// Require specific permission (throws 403 if denied)
|
|
226
|
+
const subject = await gate("project:create");
|
|
227
|
+
|
|
228
|
+
// Require all permissions
|
|
229
|
+
const subject = await gate.all(["project:edit", "project:delete"]);
|
|
230
|
+
|
|
231
|
+
// Require any permission
|
|
232
|
+
const subject = await gate.any(["project:edit", "project:create"]);
|
|
233
|
+
|
|
234
|
+
// Require minimum role
|
|
235
|
+
const subject = await gate.role("admin");
|
|
236
|
+
|
|
237
|
+
// Soft check (returns null instead of throwing)
|
|
238
|
+
const subject = await gate.check("project:create");
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Core API (Framework-Agnostic)
|
|
244
|
+
|
|
245
|
+
Use Gatehouse without React or Next.js:
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
import { createGatehouse } from "gatehouse";
|
|
249
|
+
|
|
250
|
+
const gh = createGatehouse({
|
|
251
|
+
roles: {
|
|
252
|
+
owner: ["*"],
|
|
253
|
+
admin: ["project:*", "member:invite"],
|
|
254
|
+
member: ["project:read", "task:*"],
|
|
255
|
+
viewer: ["project:read", "task:read"],
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
gh.can("admin", "project:create"); // true (matches project:*)
|
|
260
|
+
gh.can("viewer", "project:create"); // false
|
|
261
|
+
gh.canAll("member", ["task:read", "task:create"]); // true
|
|
262
|
+
gh.canAny("viewer", ["task:create", "project:read"]); // true
|
|
263
|
+
gh.isAtLeast("admin", "member"); // true
|
|
264
|
+
gh.isAtLeast("viewer", "admin"); // false
|
|
265
|
+
gh.permissionsFor("admin"); // ["project:*", "member:invite"]
|
|
266
|
+
gh.roles; // ["owner", "admin", "member", "viewer"]
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Wildcard Permissions
|
|
272
|
+
|
|
273
|
+
```
|
|
274
|
+
"*" → matches everything
|
|
275
|
+
"project:*" → matches project:read, project:create, project:delete, etc.
|
|
276
|
+
"project:read" → exact match only
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Role Hierarchy
|
|
282
|
+
|
|
283
|
+
Roles are ordered by definition — first role is highest rank:
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
const gh = createGatehouse({
|
|
287
|
+
roles: {
|
|
288
|
+
owner: ["*"], // rank 0 (highest)
|
|
289
|
+
admin: ["project:*"], // rank 1
|
|
290
|
+
member: ["task:*"], // rank 2
|
|
291
|
+
viewer: ["task:read"], // rank 3 (lowest)
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
gh.isAtLeast("owner", "admin"); // true — owner outranks admin
|
|
296
|
+
gh.isAtLeast("viewer", "member"); // false — viewer is below member
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## TypeScript
|
|
302
|
+
|
|
303
|
+
Full type inference from your config:
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
const gh = createGatehouse({
|
|
307
|
+
roles: {
|
|
308
|
+
owner: ["*"],
|
|
309
|
+
admin: ["project:*"],
|
|
310
|
+
viewer: ["project:read"],
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// gh.can() only accepts roles you defined
|
|
315
|
+
gh.can("owner", "anything"); // OK
|
|
316
|
+
gh.can("superadmin", "x"); // Type error: "superadmin" is not a valid role
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## License
|
|
320
|
+
|
|
321
|
+
MIT
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { c as GatehouseSubject } from '../types-1JY9ADLk.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Auth.js (NextAuth) adapter for Gatehouse.
|
|
5
|
+
*
|
|
6
|
+
* Reads role from `session.user.role` (requires extending the session callback).
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* // lib/gate.ts
|
|
10
|
+
* import { createServerGate } from "gatehouse/next";
|
|
11
|
+
* import { authjsResolver } from "gatehouse/adapters/authjs";
|
|
12
|
+
* import { gh } from "./gatehouse";
|
|
13
|
+
* import { auth } from "./auth"; // your Auth.js auth() export
|
|
14
|
+
*
|
|
15
|
+
* export const gate = createServerGate({
|
|
16
|
+
* gatehouse: gh,
|
|
17
|
+
* resolve: authjsResolver({ auth }),
|
|
18
|
+
* });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
declare function authjsResolver(options: {
|
|
22
|
+
/** The Auth.js `auth()` function. */
|
|
23
|
+
auth: () => Promise<{
|
|
24
|
+
user?: {
|
|
25
|
+
role?: string;
|
|
26
|
+
};
|
|
27
|
+
} | null>;
|
|
28
|
+
/** Default role for authenticated users. Default: "viewer" */
|
|
29
|
+
defaultRole?: string;
|
|
30
|
+
}): () => Promise<GatehouseSubject | null>;
|
|
31
|
+
|
|
32
|
+
export { authjsResolver };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// src/adapters/authjs.ts
|
|
2
|
+
function authjsResolver(options) {
|
|
3
|
+
const defaultRole = options.defaultRole ?? "viewer";
|
|
4
|
+
return async () => {
|
|
5
|
+
const session = await options.auth();
|
|
6
|
+
if (!session?.user) return null;
|
|
7
|
+
return {
|
|
8
|
+
role: session.user.role ?? defaultRole
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
authjsResolver
|
|
14
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { c as GatehouseSubject } from '../types-1JY9ADLk.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Clerk adapter for Gatehouse.
|
|
5
|
+
*
|
|
6
|
+
* Reads role from Clerk's `publicMetadata.role` (the standard pattern).
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* // lib/gate.ts
|
|
10
|
+
* import { createServerGate } from "gatehouse/next";
|
|
11
|
+
* import { clerkResolver } from "gatehouse/adapters/clerk";
|
|
12
|
+
* import { gh } from "./gatehouse";
|
|
13
|
+
*
|
|
14
|
+
* export const gate = createServerGate({
|
|
15
|
+
* gatehouse: gh,
|
|
16
|
+
* resolve: clerkResolver(),
|
|
17
|
+
* });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
declare function clerkResolver(options?: {
|
|
21
|
+
/** Custom metadata key for the role. Default: "role" */
|
|
22
|
+
roleKey?: string;
|
|
23
|
+
/** Default role for authenticated users with no role set. Default: "viewer" */
|
|
24
|
+
defaultRole?: string;
|
|
25
|
+
}): () => Promise<GatehouseSubject | null>;
|
|
26
|
+
|
|
27
|
+
export { clerkResolver };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// src/adapters/clerk.ts
|
|
2
|
+
function clerkResolver(options) {
|
|
3
|
+
const roleKey = options?.roleKey ?? "role";
|
|
4
|
+
const defaultRole = options?.defaultRole ?? "viewer";
|
|
5
|
+
return async () => {
|
|
6
|
+
const { currentUser } = await import("@clerk/nextjs/server");
|
|
7
|
+
const user = await currentUser();
|
|
8
|
+
if (!user) return null;
|
|
9
|
+
const role = user.publicMetadata?.[roleKey];
|
|
10
|
+
return { role: role ?? defaultRole };
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
clerkResolver
|
|
15
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { c as GatehouseSubject } from '../types-1JY9ADLk.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Supabase adapter for Gatehouse.
|
|
5
|
+
*
|
|
6
|
+
* Reads role from `app_metadata.role` or a custom profiles table.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* // lib/gate.ts
|
|
10
|
+
* import { createServerGate } from "gatehouse/next";
|
|
11
|
+
* import { supabaseResolver } from "gatehouse/adapters/supabase";
|
|
12
|
+
* import { gh } from "./gatehouse";
|
|
13
|
+
* import { createClient } from "@/lib/supabase/server";
|
|
14
|
+
*
|
|
15
|
+
* export const gate = createServerGate({
|
|
16
|
+
* gatehouse: gh,
|
|
17
|
+
* resolve: supabaseResolver({ createClient }),
|
|
18
|
+
* });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
declare function supabaseResolver(options: {
|
|
22
|
+
/** Function that creates a Supabase server client. */
|
|
23
|
+
createClient: () => any;
|
|
24
|
+
/** Where to read the role from. Default: "app_metadata" */
|
|
25
|
+
source?: "app_metadata" | "user_metadata" | {
|
|
26
|
+
table: string;
|
|
27
|
+
column?: string;
|
|
28
|
+
};
|
|
29
|
+
/** Metadata key for the role. Default: "role" */
|
|
30
|
+
roleKey?: string;
|
|
31
|
+
/** Default role for authenticated users. Default: "viewer" */
|
|
32
|
+
defaultRole?: string;
|
|
33
|
+
}): () => Promise<GatehouseSubject | null>;
|
|
34
|
+
|
|
35
|
+
export { supabaseResolver };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/adapters/supabase.ts
|
|
2
|
+
function supabaseResolver(options) {
|
|
3
|
+
const roleKey = options.roleKey ?? "role";
|
|
4
|
+
const defaultRole = options.defaultRole ?? "viewer";
|
|
5
|
+
return async () => {
|
|
6
|
+
const supabase = options.createClient();
|
|
7
|
+
const {
|
|
8
|
+
data: { user }
|
|
9
|
+
} = await supabase.auth.getUser();
|
|
10
|
+
if (!user) return null;
|
|
11
|
+
let role;
|
|
12
|
+
const source = options.source ?? "app_metadata";
|
|
13
|
+
if (source === "app_metadata") {
|
|
14
|
+
role = user.app_metadata?.[roleKey];
|
|
15
|
+
} else if (source === "user_metadata") {
|
|
16
|
+
role = user.user_metadata?.[roleKey];
|
|
17
|
+
} else {
|
|
18
|
+
const column = source.column ?? "role";
|
|
19
|
+
const { data } = await supabase.from(source.table).select(column).eq("user_id", user.id).single();
|
|
20
|
+
role = data?.[column];
|
|
21
|
+
}
|
|
22
|
+
return { role: role ?? defaultRole };
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export {
|
|
26
|
+
supabaseResolver
|
|
27
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { R as RoleDefinitions, G as GatehouseConfig, a as Gatehouse, P as PermissionPattern } from './types-1JY9ADLk.js';
|
|
2
|
+
export { E as ExtractPermissions, b as ExtractRoles, c as GatehouseSubject } from './types-1JY9ADLk.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create a Gatehouse instance.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* const gh = createGatehouse({
|
|
9
|
+
* roles: {
|
|
10
|
+
* owner: ["*"],
|
|
11
|
+
* admin: ["project:*", "member:invite", "member:remove"],
|
|
12
|
+
* member: ["project:read", "project:create", "task:*"],
|
|
13
|
+
* viewer: ["project:read", "task:read"],
|
|
14
|
+
* },
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Roles are hierarchical — first role is highest. An `owner` is "at least" an `admin`.
|
|
19
|
+
*/
|
|
20
|
+
declare function createGatehouse<T extends RoleDefinitions>(config: GatehouseConfig<T>): Gatehouse<T>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if a permission pattern matches a concrete permission.
|
|
24
|
+
*
|
|
25
|
+
* Patterns:
|
|
26
|
+
* "*" → matches everything
|
|
27
|
+
* "project:*" → matches "project:read", "project:create", etc.
|
|
28
|
+
* "project:read" → exact match only
|
|
29
|
+
*/
|
|
30
|
+
declare function matchesPermission(pattern: PermissionPattern, permission: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Check if any pattern in a list matches the given permission.
|
|
33
|
+
*/
|
|
34
|
+
declare function hasPermission(patterns: readonly PermissionPattern[], permission: string): boolean;
|
|
35
|
+
|
|
36
|
+
export { Gatehouse, GatehouseConfig, PermissionPattern, RoleDefinitions, createGatehouse, hasPermission, matchesPermission };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// src/core/permissions.ts
|
|
2
|
+
function matchesPermission(pattern, permission) {
|
|
3
|
+
if (pattern === "*") return true;
|
|
4
|
+
if (pattern === permission) return true;
|
|
5
|
+
if (pattern.endsWith(":*")) {
|
|
6
|
+
const prefix = pattern.slice(0, -1);
|
|
7
|
+
return permission.startsWith(prefix);
|
|
8
|
+
}
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
function hasPermission(patterns, permission) {
|
|
12
|
+
return patterns.some((p) => matchesPermission(p, permission));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/core/gatehouse.ts
|
|
16
|
+
function createGatehouse(config) {
|
|
17
|
+
const roleNames = Object.keys(config.roles);
|
|
18
|
+
const roleRank = /* @__PURE__ */ new Map();
|
|
19
|
+
roleNames.forEach((name, i) => roleRank.set(name, i));
|
|
20
|
+
function resolveSubject(input) {
|
|
21
|
+
if (typeof input === "string") return { role: input };
|
|
22
|
+
return input;
|
|
23
|
+
}
|
|
24
|
+
function getPatterns(subject) {
|
|
25
|
+
const rolePatterns = config.roles[subject.role] ?? [];
|
|
26
|
+
if (!subject.permissions?.length) return rolePatterns;
|
|
27
|
+
return [...rolePatterns, ...subject.permissions];
|
|
28
|
+
}
|
|
29
|
+
function can(roleOrSubject, permission) {
|
|
30
|
+
const subject = resolveSubject(roleOrSubject);
|
|
31
|
+
return hasPermission(getPatterns(subject), permission);
|
|
32
|
+
}
|
|
33
|
+
function canAll(roleOrSubject, permissions) {
|
|
34
|
+
const subject = resolveSubject(roleOrSubject);
|
|
35
|
+
const patterns = getPatterns(subject);
|
|
36
|
+
return permissions.every((p) => hasPermission(patterns, p));
|
|
37
|
+
}
|
|
38
|
+
function canAny(roleOrSubject, permissions) {
|
|
39
|
+
const subject = resolveSubject(roleOrSubject);
|
|
40
|
+
const patterns = getPatterns(subject);
|
|
41
|
+
return permissions.some((p) => hasPermission(patterns, p));
|
|
42
|
+
}
|
|
43
|
+
function isAtLeast(roleA, roleB) {
|
|
44
|
+
const rankA = roleRank.get(roleA);
|
|
45
|
+
const rankB = roleRank.get(roleB);
|
|
46
|
+
if (rankA === void 0 || rankB === void 0) return false;
|
|
47
|
+
return rankA <= rankB;
|
|
48
|
+
}
|
|
49
|
+
function permissionsFor(role) {
|
|
50
|
+
return [...config.roles[role] ?? []];
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
can,
|
|
54
|
+
canAll,
|
|
55
|
+
canAny,
|
|
56
|
+
isAtLeast,
|
|
57
|
+
permissionsFor,
|
|
58
|
+
roles: roleNames,
|
|
59
|
+
config: config.roles
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export {
|
|
63
|
+
createGatehouse,
|
|
64
|
+
hasPermission,
|
|
65
|
+
matchesPermission
|
|
66
|
+
};
|
package/dist/next.d.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { R as RoleDefinitions, a as Gatehouse, c as GatehouseSubject, b as ExtractRoles } from './types-1JY9ADLk.js';
|
|
2
|
+
import { NextRequest, NextResponse } from 'next/server.js';
|
|
3
|
+
|
|
4
|
+
/** Options for creating a server-side gate. */
|
|
5
|
+
interface CreateServerGateOptions<T extends RoleDefinitions> {
|
|
6
|
+
gatehouse: Gatehouse<T>;
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the current user from the request context.
|
|
9
|
+
* Return null if unauthenticated.
|
|
10
|
+
*/
|
|
11
|
+
resolve: () => Promise<GatehouseSubject<ExtractRoles<T>> | null>;
|
|
12
|
+
}
|
|
13
|
+
declare class GatehouseError extends Error {
|
|
14
|
+
status: number;
|
|
15
|
+
constructor(message: string, status: number);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create a server-side gate for Next.js API routes and Server Components.
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* // lib/gate.ts
|
|
22
|
+
* import { createServerGate } from "gatehouse/next";
|
|
23
|
+
* import { gh } from "./gatehouse";
|
|
24
|
+
* import { auth } from "./auth";
|
|
25
|
+
*
|
|
26
|
+
* export const gate = createServerGate({
|
|
27
|
+
* gatehouse: gh,
|
|
28
|
+
* resolve: async () => {
|
|
29
|
+
* const session = await auth();
|
|
30
|
+
* if (!session) return null;
|
|
31
|
+
* return { role: session.user.role };
|
|
32
|
+
* },
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* Then in API routes:
|
|
37
|
+
* ```ts
|
|
38
|
+
* export async function POST() {
|
|
39
|
+
* const subject = await gate("project:create");
|
|
40
|
+
* // subject is typed — guaranteed to have permission
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
declare function createServerGate<T extends RoleDefinitions>(options: CreateServerGateOptions<T>): {
|
|
45
|
+
(permission: string): Promise<GatehouseSubject<ExtractRoles<T>>>;
|
|
46
|
+
(): Promise<GatehouseSubject<ExtractRoles<T>>>;
|
|
47
|
+
all(permissions: string[]): Promise<GatehouseSubject<ExtractRoles<T>>>;
|
|
48
|
+
any(permissions: string[]): Promise<GatehouseSubject<ExtractRoles<T>>>;
|
|
49
|
+
role(role: ExtractRoles<T>): Promise<GatehouseSubject<ExtractRoles<T>>>;
|
|
50
|
+
check(permission?: string): Promise<GatehouseSubject<ExtractRoles<T>> | null>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
interface GatehouseMiddlewareConfig {
|
|
54
|
+
/**
|
|
55
|
+
* Routes that require authentication. Supports Next.js matcher patterns.
|
|
56
|
+
* e.g. ["/dashboard/:path*", "/api/projects/:path*"]
|
|
57
|
+
*/
|
|
58
|
+
protected: string[];
|
|
59
|
+
/** Where to redirect unauthenticated users. Default: "/login" */
|
|
60
|
+
loginUrl?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Check if the request is authenticated.
|
|
63
|
+
* Return true if authenticated, false otherwise.
|
|
64
|
+
*/
|
|
65
|
+
isAuthenticated: (request: NextRequest) => boolean | Promise<boolean>;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Create a Next.js middleware that protects routes.
|
|
69
|
+
*
|
|
70
|
+
* ```ts
|
|
71
|
+
* // middleware.ts
|
|
72
|
+
* import { createMiddleware } from "gatehouse/next";
|
|
73
|
+
*
|
|
74
|
+
* export default createMiddleware({
|
|
75
|
+
* protected: ["/dashboard/:path*", "/api/projects/:path*"],
|
|
76
|
+
* isAuthenticated: (req) => !!req.cookies.get("session"),
|
|
77
|
+
* });
|
|
78
|
+
*
|
|
79
|
+
* export const config = {
|
|
80
|
+
* matcher: ["/dashboard/:path*", "/api/projects/:path*"],
|
|
81
|
+
* };
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
declare function createMiddleware(options: GatehouseMiddlewareConfig): (request: NextRequest) => Promise<NextResponse<unknown>>;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Wrap a Next.js route handler to automatically catch GatehouseErrors
|
|
88
|
+
* and return proper HTTP responses.
|
|
89
|
+
*
|
|
90
|
+
* ```ts
|
|
91
|
+
* import { withGate } from "gatehouse/next";
|
|
92
|
+
* import { gate } from "@/lib/gate";
|
|
93
|
+
*
|
|
94
|
+
* export const POST = withGate(async () => {
|
|
95
|
+
* const subject = await gate("project:create");
|
|
96
|
+
* // ... create project
|
|
97
|
+
* return NextResponse.json({ ok: true });
|
|
98
|
+
* });
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
declare function withGate(handler: (request: Request) => Promise<Response>): (request: Request) => Promise<Response>;
|
|
102
|
+
|
|
103
|
+
export { type CreateServerGateOptions, GatehouseError, type GatehouseMiddlewareConfig, createMiddleware, createServerGate, withGate };
|
package/dist/next.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// src/next/server.ts
|
|
2
|
+
var GatehouseError = class extends Error {
|
|
3
|
+
constructor(message, status) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.status = status;
|
|
6
|
+
this.name = "GatehouseError";
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
function createServerGate(options) {
|
|
10
|
+
async function gate(permission) {
|
|
11
|
+
const subject = await options.resolve();
|
|
12
|
+
if (!subject) {
|
|
13
|
+
throw new GatehouseError("Unauthorized", 401);
|
|
14
|
+
}
|
|
15
|
+
if (permission && !options.gatehouse.can(subject, permission)) {
|
|
16
|
+
throw new GatehouseError("Forbidden", 403);
|
|
17
|
+
}
|
|
18
|
+
return subject;
|
|
19
|
+
}
|
|
20
|
+
gate.all = async function gateAll(permissions) {
|
|
21
|
+
const subject = await options.resolve();
|
|
22
|
+
if (!subject) throw new GatehouseError("Unauthorized", 401);
|
|
23
|
+
if (!options.gatehouse.canAll(subject, permissions)) {
|
|
24
|
+
throw new GatehouseError("Forbidden", 403);
|
|
25
|
+
}
|
|
26
|
+
return subject;
|
|
27
|
+
};
|
|
28
|
+
gate.any = async function gateAny(permissions) {
|
|
29
|
+
const subject = await options.resolve();
|
|
30
|
+
if (!subject) throw new GatehouseError("Unauthorized", 401);
|
|
31
|
+
if (!options.gatehouse.canAny(subject, permissions)) {
|
|
32
|
+
throw new GatehouseError("Forbidden", 403);
|
|
33
|
+
}
|
|
34
|
+
return subject;
|
|
35
|
+
};
|
|
36
|
+
gate.role = async function gateRole(role) {
|
|
37
|
+
const subject = await options.resolve();
|
|
38
|
+
if (!subject) throw new GatehouseError("Unauthorized", 401);
|
|
39
|
+
if (!options.gatehouse.isAtLeast(subject.role, role)) {
|
|
40
|
+
throw new GatehouseError("Forbidden", 403);
|
|
41
|
+
}
|
|
42
|
+
return subject;
|
|
43
|
+
};
|
|
44
|
+
gate.check = async function gateCheck(permission) {
|
|
45
|
+
const subject = await options.resolve();
|
|
46
|
+
if (!subject) return null;
|
|
47
|
+
if (permission && !options.gatehouse.can(subject, permission)) return null;
|
|
48
|
+
return subject;
|
|
49
|
+
};
|
|
50
|
+
return gate;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/next/middleware.ts
|
|
54
|
+
import { NextResponse } from "next/server.js";
|
|
55
|
+
function createMiddleware(options) {
|
|
56
|
+
const loginUrl = options.loginUrl ?? "/login";
|
|
57
|
+
return async function middleware(request) {
|
|
58
|
+
const isProtected = options.protected.some((pattern) => {
|
|
59
|
+
const regex = patternToRegex(pattern);
|
|
60
|
+
return regex.test(request.nextUrl.pathname);
|
|
61
|
+
});
|
|
62
|
+
if (!isProtected) {
|
|
63
|
+
return NextResponse.next();
|
|
64
|
+
}
|
|
65
|
+
const authenticated = await options.isAuthenticated(request);
|
|
66
|
+
if (!authenticated) {
|
|
67
|
+
if (request.nextUrl.pathname.startsWith("/api/")) {
|
|
68
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
69
|
+
}
|
|
70
|
+
const url = request.nextUrl.clone();
|
|
71
|
+
url.pathname = loginUrl;
|
|
72
|
+
url.searchParams.set("from", request.nextUrl.pathname);
|
|
73
|
+
return NextResponse.redirect(url);
|
|
74
|
+
}
|
|
75
|
+
return NextResponse.next();
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function patternToRegex(pattern) {
|
|
79
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:path\\\*/g, ".*").replace(/:\\w+/g, "[^/]+");
|
|
80
|
+
return new RegExp(`^${escaped}$`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/next/catch.ts
|
|
84
|
+
import { NextResponse as NextResponse2 } from "next/server.js";
|
|
85
|
+
function withGate(handler) {
|
|
86
|
+
return async function wrappedHandler(request) {
|
|
87
|
+
try {
|
|
88
|
+
return await handler(request);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (error instanceof GatehouseError) {
|
|
91
|
+
return NextResponse2.json(
|
|
92
|
+
{ error: error.message },
|
|
93
|
+
{ status: error.status }
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export {
|
|
101
|
+
GatehouseError,
|
|
102
|
+
createMiddleware,
|
|
103
|
+
createServerGate,
|
|
104
|
+
withGate
|
|
105
|
+
};
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { R as RoleDefinitions, a as Gatehouse, c as GatehouseSubject } from './types-1JY9ADLk.js';
|
|
4
|
+
|
|
5
|
+
interface GatehouseContextValue<T extends RoleDefinitions = RoleDefinitions> {
|
|
6
|
+
gatehouse: Gatehouse<T>;
|
|
7
|
+
subject: GatehouseSubject | null;
|
|
8
|
+
loading: boolean;
|
|
9
|
+
}
|
|
10
|
+
interface GatehouseProviderProps<T extends RoleDefinitions> {
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
gatehouse: Gatehouse<T>;
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the current user's role. Called once on mount.
|
|
15
|
+
* Return `null` if user is not authenticated.
|
|
16
|
+
*/
|
|
17
|
+
resolve: () => Promise<GatehouseSubject | null> | GatehouseSubject | null;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Provide RBAC context to your app.
|
|
21
|
+
*
|
|
22
|
+
* ```tsx
|
|
23
|
+
* <GatehouseProvider gatehouse={gh} resolve={() => ({ role: "admin" })}>
|
|
24
|
+
* {children}
|
|
25
|
+
* </GatehouseProvider>
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
declare function GatehouseProvider<T extends RoleDefinitions>({ children, gatehouse, resolve, }: GatehouseProviderProps<T>): react_jsx_runtime.JSX.Element;
|
|
29
|
+
|
|
30
|
+
interface GateProps {
|
|
31
|
+
children: ReactNode;
|
|
32
|
+
/**
|
|
33
|
+
* Permission required. e.g. "project:create"
|
|
34
|
+
* Use `allow` for single permission, `allOf` for all, `anyOf` for any.
|
|
35
|
+
*/
|
|
36
|
+
allow?: string;
|
|
37
|
+
/** Require ALL of these permissions. */
|
|
38
|
+
allOf?: string[];
|
|
39
|
+
/** Require ANY of these permissions. */
|
|
40
|
+
anyOf?: string[];
|
|
41
|
+
/** Require this role or higher. */
|
|
42
|
+
role?: string;
|
|
43
|
+
/** Shown when permission is denied. */
|
|
44
|
+
fallback?: ReactNode;
|
|
45
|
+
/** Shown while resolve() is loading. */
|
|
46
|
+
loading?: ReactNode;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Declarative permission gate.
|
|
50
|
+
*
|
|
51
|
+
* ```tsx
|
|
52
|
+
* <Gate allow="project:create">
|
|
53
|
+
* <CreateButton />
|
|
54
|
+
* </Gate>
|
|
55
|
+
*
|
|
56
|
+
* <Gate role="admin" fallback={<p>Admin only</p>}>
|
|
57
|
+
* <AdminPanel />
|
|
58
|
+
* </Gate>
|
|
59
|
+
*
|
|
60
|
+
* <Gate anyOf={["project:edit", "project:delete"]}>
|
|
61
|
+
* <EditMenu />
|
|
62
|
+
* </Gate>
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
declare function Gate({ children, allow, allOf, anyOf, role, fallback, loading: loadingFallback, }: GateProps): react_jsx_runtime.JSX.Element;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check a single permission.
|
|
69
|
+
*
|
|
70
|
+
* ```ts
|
|
71
|
+
* const canCreate = useGate("project:create");
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
declare function useGate(permission: string): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Get the current user's role.
|
|
77
|
+
*
|
|
78
|
+
* ```ts
|
|
79
|
+
* const role = useRole(); // "admin" | null
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
declare function useRole(): string | null;
|
|
83
|
+
/**
|
|
84
|
+
* Get all permissions for the current user's role.
|
|
85
|
+
*
|
|
86
|
+
* ```ts
|
|
87
|
+
* const perms = usePermissions(); // ["project:read", "project:create", ...]
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
declare function usePermissions(): string[];
|
|
91
|
+
/**
|
|
92
|
+
* Full access to the Gatehouse instance and current subject.
|
|
93
|
+
*
|
|
94
|
+
* ```ts
|
|
95
|
+
* const { gatehouse, subject, loading } = useGatehouse();
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
declare function useGatehouse(): GatehouseContextValue<RoleDefinitions>;
|
|
99
|
+
|
|
100
|
+
export { Gate, type GateProps, GatehouseProvider, type GatehouseProviderProps, useGate, useGatehouse, usePermissions, useRole };
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// src/react/context.tsx
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
useContext,
|
|
5
|
+
useState,
|
|
6
|
+
useEffect
|
|
7
|
+
} from "react";
|
|
8
|
+
import { jsx } from "react/jsx-runtime";
|
|
9
|
+
var GatehouseContext = createContext(null);
|
|
10
|
+
function GatehouseProvider({
|
|
11
|
+
children,
|
|
12
|
+
gatehouse,
|
|
13
|
+
resolve
|
|
14
|
+
}) {
|
|
15
|
+
const [subject, setSubject] = useState(null);
|
|
16
|
+
const [loading, setLoading] = useState(true);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
let cancelled = false;
|
|
19
|
+
Promise.resolve(resolve()).then((result) => {
|
|
20
|
+
if (!cancelled) {
|
|
21
|
+
setSubject(result);
|
|
22
|
+
setLoading(false);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
return () => {
|
|
26
|
+
cancelled = true;
|
|
27
|
+
};
|
|
28
|
+
}, [resolve]);
|
|
29
|
+
return /* @__PURE__ */ jsx(
|
|
30
|
+
GatehouseContext.Provider,
|
|
31
|
+
{
|
|
32
|
+
value: {
|
|
33
|
+
gatehouse,
|
|
34
|
+
subject,
|
|
35
|
+
loading
|
|
36
|
+
},
|
|
37
|
+
children
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
function useGatehouseContext() {
|
|
42
|
+
const ctx = useContext(GatehouseContext);
|
|
43
|
+
if (!ctx) {
|
|
44
|
+
throw new Error("useGatehouseContext must be used within <GatehouseProvider>");
|
|
45
|
+
}
|
|
46
|
+
return ctx;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/react/gate.tsx
|
|
50
|
+
import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
|
|
51
|
+
function Gate({
|
|
52
|
+
children,
|
|
53
|
+
allow,
|
|
54
|
+
allOf,
|
|
55
|
+
anyOf,
|
|
56
|
+
role,
|
|
57
|
+
fallback = null,
|
|
58
|
+
loading: loadingFallback = null
|
|
59
|
+
}) {
|
|
60
|
+
const { gatehouse, subject, loading } = useGatehouseContext();
|
|
61
|
+
if (loading) return /* @__PURE__ */ jsx2(Fragment, { children: loadingFallback });
|
|
62
|
+
if (!subject) return /* @__PURE__ */ jsx2(Fragment, { children: fallback });
|
|
63
|
+
if (role && !gatehouse.isAtLeast(subject.role, role)) {
|
|
64
|
+
return /* @__PURE__ */ jsx2(Fragment, { children: fallback });
|
|
65
|
+
}
|
|
66
|
+
if (allow && !gatehouse.can(subject, allow)) {
|
|
67
|
+
return /* @__PURE__ */ jsx2(Fragment, { children: fallback });
|
|
68
|
+
}
|
|
69
|
+
if (allOf && !gatehouse.canAll(subject, allOf)) {
|
|
70
|
+
return /* @__PURE__ */ jsx2(Fragment, { children: fallback });
|
|
71
|
+
}
|
|
72
|
+
if (anyOf && !gatehouse.canAny(subject, anyOf)) {
|
|
73
|
+
return /* @__PURE__ */ jsx2(Fragment, { children: fallback });
|
|
74
|
+
}
|
|
75
|
+
return /* @__PURE__ */ jsx2(Fragment, { children });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/react/hooks.ts
|
|
79
|
+
import { useMemo } from "react";
|
|
80
|
+
function useGate(permission) {
|
|
81
|
+
const { gatehouse, subject } = useGatehouseContext();
|
|
82
|
+
return useMemo(
|
|
83
|
+
() => subject ? gatehouse.can(subject, permission) : false,
|
|
84
|
+
[gatehouse, subject, permission]
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
function useRole() {
|
|
88
|
+
const { subject } = useGatehouseContext();
|
|
89
|
+
return subject?.role ?? null;
|
|
90
|
+
}
|
|
91
|
+
function usePermissions() {
|
|
92
|
+
const { gatehouse, subject } = useGatehouseContext();
|
|
93
|
+
return useMemo(
|
|
94
|
+
() => subject ? gatehouse.permissionsFor(subject.role) : [],
|
|
95
|
+
[gatehouse, subject]
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
function useGatehouse() {
|
|
99
|
+
return useGatehouseContext();
|
|
100
|
+
}
|
|
101
|
+
export {
|
|
102
|
+
Gate,
|
|
103
|
+
GatehouseProvider,
|
|
104
|
+
useGate,
|
|
105
|
+
useGatehouse,
|
|
106
|
+
usePermissions,
|
|
107
|
+
useRole
|
|
108
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** A permission string like "project:read" or "project:*" or "*" */
|
|
2
|
+
type PermissionPattern = string;
|
|
3
|
+
/** Role-to-permissions mapping. Keys are role names, values are permission arrays. */
|
|
4
|
+
type RoleDefinitions = Record<string, readonly PermissionPattern[]>;
|
|
5
|
+
/**
|
|
6
|
+
* Extract all concrete permission strings from a role definitions object.
|
|
7
|
+
* Excludes wildcards — those are matching patterns, not concrete permissions.
|
|
8
|
+
*/
|
|
9
|
+
type ExtractPermissions<T extends RoleDefinitions> = T[keyof T][number] extends infer P ? P extends `${string}*` ? never : P extends string ? P : never : never;
|
|
10
|
+
/** Extract role names from a role definitions object. */
|
|
11
|
+
type ExtractRoles<T extends RoleDefinitions> = keyof T & string;
|
|
12
|
+
/** The resolved identity for permission checks. */
|
|
13
|
+
interface GatehouseSubject<R extends string = string> {
|
|
14
|
+
role: R;
|
|
15
|
+
/** Optional extra permissions beyond what the role grants. */
|
|
16
|
+
permissions?: string[];
|
|
17
|
+
}
|
|
18
|
+
/** Configuration for createGatehouse(). */
|
|
19
|
+
interface GatehouseConfig<T extends RoleDefinitions> {
|
|
20
|
+
/** Role definitions. First role is highest rank. Order defines hierarchy. */
|
|
21
|
+
roles: T;
|
|
22
|
+
}
|
|
23
|
+
/** The Gatehouse instance returned by createGatehouse(). */
|
|
24
|
+
interface Gatehouse<T extends RoleDefinitions> {
|
|
25
|
+
/** Check if a role (or subject) has a specific permission. */
|
|
26
|
+
can: (roleOrSubject: ExtractRoles<T> | GatehouseSubject<ExtractRoles<T>>, permission: string) => boolean;
|
|
27
|
+
/** Check if a role has ALL listed permissions. */
|
|
28
|
+
canAll: (roleOrSubject: ExtractRoles<T> | GatehouseSubject<ExtractRoles<T>>, permissions: string[]) => boolean;
|
|
29
|
+
/** Check if a role has ANY of the listed permissions. */
|
|
30
|
+
canAny: (roleOrSubject: ExtractRoles<T> | GatehouseSubject<ExtractRoles<T>>, permissions: string[]) => boolean;
|
|
31
|
+
/** Check if roleA is at least as high as roleB in the hierarchy. */
|
|
32
|
+
isAtLeast: (roleA: ExtractRoles<T>, roleB: ExtractRoles<T>) => boolean;
|
|
33
|
+
/** Get all concrete permissions for a role. */
|
|
34
|
+
permissionsFor: (role: ExtractRoles<T>) => string[];
|
|
35
|
+
/** Ordered role names from highest to lowest. */
|
|
36
|
+
roles: ExtractRoles<T>[];
|
|
37
|
+
/** The raw role definitions. */
|
|
38
|
+
config: T;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type { ExtractPermissions as E, GatehouseConfig as G, PermissionPattern as P, RoleDefinitions as R, Gatehouse as a, ExtractRoles as b, GatehouseSubject as c };
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gatehouse",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Drop-in RBAC for Next.js. 5 lines to working permissions.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./react": {
|
|
12
|
+
"types": "./dist/react.d.ts",
|
|
13
|
+
"import": "./dist/react.js"
|
|
14
|
+
},
|
|
15
|
+
"./next": {
|
|
16
|
+
"types": "./dist/next.d.ts",
|
|
17
|
+
"import": "./dist/next.js"
|
|
18
|
+
},
|
|
19
|
+
"./adapters/clerk": {
|
|
20
|
+
"types": "./dist/adapters/clerk.d.ts",
|
|
21
|
+
"import": "./dist/adapters/clerk.js"
|
|
22
|
+
},
|
|
23
|
+
"./adapters/supabase": {
|
|
24
|
+
"types": "./dist/adapters/supabase.d.ts",
|
|
25
|
+
"import": "./dist/adapters/supabase.js"
|
|
26
|
+
},
|
|
27
|
+
"./adapters/authjs": {
|
|
28
|
+
"types": "./dist/adapters/authjs.d.ts",
|
|
29
|
+
"import": "./dist/adapters/authjs.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup",
|
|
37
|
+
"dev": "tsup --watch",
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"lint": "eslint src/",
|
|
40
|
+
"prepublishOnly": "npm run build"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"react": ">=18",
|
|
44
|
+
"next": ">=14"
|
|
45
|
+
},
|
|
46
|
+
"peerDependenciesMeta": {
|
|
47
|
+
"react": { "optional": true },
|
|
48
|
+
"next": { "optional": true }
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/react": "^19.0.0",
|
|
52
|
+
"@types/node": "^22.0.0",
|
|
53
|
+
"next": "^15.0.0",
|
|
54
|
+
"react": "^19.0.0",
|
|
55
|
+
"tsup": "^8.0.0",
|
|
56
|
+
"typescript": "^5.7.0"
|
|
57
|
+
},
|
|
58
|
+
"keywords": [
|
|
59
|
+
"rbac",
|
|
60
|
+
"permissions",
|
|
61
|
+
"authorization",
|
|
62
|
+
"nextjs",
|
|
63
|
+
"react",
|
|
64
|
+
"access-control",
|
|
65
|
+
"roles",
|
|
66
|
+
"gate"
|
|
67
|
+
],
|
|
68
|
+
"license": "MIT",
|
|
69
|
+
"repository": {
|
|
70
|
+
"type": "git",
|
|
71
|
+
"url": "https://github.com/gatehouse-rbac/gatehouse"
|
|
72
|
+
}
|
|
73
|
+
}
|