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 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
+ };
@@ -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
+ };
@@ -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
+ }