@donkeylabs/cli 1.1.16 → 1.1.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@donkeylabs/cli",
3
- "version": "1.1.16",
3
+ "version": "1.1.17",
4
4
  "type": "module",
5
5
  "description": "CLI for @donkeylabs/server - project scaffolding and code generation",
6
6
  "main": "./src/index.ts",
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Permissions Helper for Svelte UI
3
+ *
4
+ * Provides reactive permission checking for UI locking.
5
+ *
6
+ * Usage in +page.server.ts:
7
+ * ```ts
8
+ * export const load = async ({ locals }) => {
9
+ * const api = createApi({ locals });
10
+ * const permissions = await api.permissions.context({ tenantId });
11
+ * return { permissions };
12
+ * };
13
+ * ```
14
+ *
15
+ * Usage in +page.svelte:
16
+ * ```svelte
17
+ * <script>
18
+ * import { createPermissions } from "$lib/permissions";
19
+ * let { data } = $props();
20
+ * const can = createPermissions(data.permissions);
21
+ * </script>
22
+ *
23
+ * {#if can.has("documents.create")}
24
+ * <Button>Create Document</Button>
25
+ * {/if}
26
+ * ```
27
+ */
28
+
29
+ import { createApi } from "./api";
30
+
31
+ export interface PermissionContext {
32
+ tenantId: string;
33
+ roles: Array<{ id: string; name: string }>;
34
+ permissions: string[];
35
+ }
36
+
37
+ export interface PermissionsHelper {
38
+ /** Check if user has a static permission */
39
+ has: (permission: string) => boolean;
40
+
41
+ /** Check if user has all of the specified permissions */
42
+ hasAll: (...permissions: string[]) => boolean;
43
+
44
+ /** Check if user has any of the specified permissions */
45
+ hasAny: (...permissions: string[]) => boolean;
46
+
47
+ /** Check if user has a specific role */
48
+ hasRole: (roleName: string) => boolean;
49
+
50
+ /** Get all permissions */
51
+ all: () => string[];
52
+
53
+ /** Get all roles */
54
+ roles: () => Array<{ id: string; name: string }>;
55
+
56
+ /** Get tenant ID */
57
+ tenantId: () => string;
58
+
59
+ /** Check resource access (async - makes API call) */
60
+ canAccess: (
61
+ resourceType: string,
62
+ resourceId: string,
63
+ action: "create" | "read" | "write" | "delete" | "admin",
64
+ ownerId?: string
65
+ ) => Promise<boolean>;
66
+
67
+ /** Batch check resource access (async - makes single API call) */
68
+ canAccessMany: (
69
+ checks: Array<{
70
+ resourceType: string;
71
+ resourceId: string;
72
+ action: "create" | "read" | "write" | "delete" | "admin";
73
+ ownerId?: string;
74
+ }>
75
+ ) => Promise<Record<string, boolean>>;
76
+ }
77
+
78
+ /**
79
+ * Create a permissions helper from context
80
+ */
81
+ export function createPermissions(context: PermissionContext): PermissionsHelper {
82
+ const permissionSet = new Set(context.permissions);
83
+ const api = createApi();
84
+
85
+ return {
86
+ has(permission: string): boolean {
87
+ // Exact match
88
+ if (permissionSet.has(permission)) return true;
89
+
90
+ // Wildcard match
91
+ if (permissionSet.has("*")) return true;
92
+
93
+ // Resource wildcard (e.g., "documents.*")
94
+ const [resource] = permission.split(".");
95
+ if (permissionSet.has(`${resource}.*`)) return true;
96
+
97
+ return false;
98
+ },
99
+
100
+ hasAll(...permissions: string[]): boolean {
101
+ return permissions.every((p) => this.has(p));
102
+ },
103
+
104
+ hasAny(...permissions: string[]): boolean {
105
+ return permissions.some((p) => this.has(p));
106
+ },
107
+
108
+ hasRole(roleName: string): boolean {
109
+ return context.roles.some(
110
+ (r) => r.name.toLowerCase() === roleName.toLowerCase()
111
+ );
112
+ },
113
+
114
+ all(): string[] {
115
+ return context.permissions;
116
+ },
117
+
118
+ roles(): Array<{ id: string; name: string }> {
119
+ return context.roles;
120
+ },
121
+
122
+ tenantId(): string {
123
+ return context.tenantId;
124
+ },
125
+
126
+ async canAccess(
127
+ resourceType: string,
128
+ resourceId: string,
129
+ action: "create" | "read" | "write" | "delete" | "admin",
130
+ ownerId?: string
131
+ ): Promise<boolean> {
132
+ const result = await api.permissions.canAccess({
133
+ tenantId: context.tenantId,
134
+ checks: [{ resourceType, resourceId, action, ownerId }],
135
+ });
136
+ return result[`${resourceType}:${resourceId}:${action}`] ?? false;
137
+ },
138
+
139
+ async canAccessMany(
140
+ checks: Array<{
141
+ resourceType: string;
142
+ resourceId: string;
143
+ action: "create" | "read" | "write" | "delete" | "admin";
144
+ ownerId?: string;
145
+ }>
146
+ ): Promise<Record<string, boolean>> {
147
+ return api.permissions.canAccess({
148
+ tenantId: context.tenantId,
149
+ checks,
150
+ });
151
+ },
152
+ };
153
+ }
154
+
155
+ /**
156
+ * Svelte component helper - use in templates
157
+ *
158
+ * Usage:
159
+ * ```svelte
160
+ * <script>
161
+ * import { Can } from "$lib/permissions";
162
+ * let { data } = $props();
163
+ * </script>
164
+ *
165
+ * <Can permission="documents.create" context={data.permissions}>
166
+ * <Button>Create</Button>
167
+ * </Can>
168
+ * ```
169
+ */
170
+ export function canCheck(
171
+ context: PermissionContext,
172
+ permission: string
173
+ ): boolean {
174
+ const helper = createPermissions(context);
175
+ return helper.has(permission);
176
+ }
177
+
178
+ /**
179
+ * Type helper for defining permissions in your app
180
+ *
181
+ * Usage:
182
+ * ```ts
183
+ * const PERMISSIONS = definePermissions({
184
+ * documents: ["create", "read", "write", "delete", "admin"],
185
+ * users: ["invite", "remove", "manage"],
186
+ * billing: ["view", "manage"],
187
+ * } as const);
188
+ *
189
+ * // Type: "documents.create" | "documents.read" | ...
190
+ * type Permission = typeof PERMISSIONS[number];
191
+ * ```
192
+ */
193
+ export function definePermissions<
194
+ T extends Record<string, readonly string[]>
195
+ >(permissions: T): Array<`${Extract<keyof T, string>}.${T[keyof T][number]}`> {
196
+ const result: string[] = [];
197
+ for (const [resource, actions] of Object.entries(permissions)) {
198
+ for (const action of actions) {
199
+ result.push(`${resource}.${action}`);
200
+ }
201
+ }
202
+ return result as any;
203
+ }
@@ -7,9 +7,12 @@ import { demoPlugin } from "./plugins/demo";
7
7
  import { workflowDemoPlugin } from "./plugins/workflow-demo";
8
8
  import { authPlugin } from "./plugins/auth";
9
9
  import { emailPlugin } from "./plugins/email";
10
+ import { permissionsPlugin } from "./plugins/permissions";
10
11
  import demoRoutes from "./routes/demo";
11
12
  import { exampleRouter } from "./routes/example";
12
13
  import { authRouter } from "./routes/auth";
14
+ import { permissionsRouter } from "./routes/permissions";
15
+ import { tenantsRouter } from "./routes/tenants";
13
16
 
14
17
  // Simple in-memory database
15
18
  const db = new Kysely<{}>({
@@ -67,11 +70,53 @@ server.registerPlugin(emailPlugin({
67
70
  baseUrl: process.env.PUBLIC_BASE_URL || "http://localhost:5173",
68
71
  }));
69
72
 
73
+ // =============================================================================
74
+ // PERMISSIONS PLUGIN - Multi-tenant RBAC
75
+ // =============================================================================
76
+ // Define your app's permissions here for type-safe checking.
77
+ // Client can use: api.permissions.check({ permissions: ["documents.create"] })
78
+ //
79
+ server.registerPlugin(permissionsPlugin({
80
+ permissions: {
81
+ // Documents
82
+ documents: ["create", "read", "write", "delete", "admin"],
83
+ // Members & Roles
84
+ members: ["invite", "remove", "list"],
85
+ roles: ["create", "assign", "manage"],
86
+ // Billing (example)
87
+ billing: ["view", "manage"],
88
+ } as const,
89
+ defaultRoles: [
90
+ {
91
+ name: "Admin",
92
+ permissions: ["*"], // Full access
93
+ isDefault: false,
94
+ },
95
+ {
96
+ name: "Member",
97
+ permissions: [
98
+ "documents.create",
99
+ "documents.read",
100
+ "documents.write",
101
+ "members.list",
102
+ ],
103
+ isDefault: true, // Auto-assigned to new members
104
+ },
105
+ {
106
+ name: "Viewer",
107
+ permissions: ["documents.read", "members.list"],
108
+ isDefault: false,
109
+ },
110
+ ],
111
+ }));
112
+
70
113
  server.registerPlugin(demoPlugin);
71
114
  server.registerPlugin(workflowDemoPlugin);
72
115
 
73
116
  // Register routes
74
117
  server.use(authRouter);
118
+ server.use(permissionsRouter);
119
+ server.use(tenantsRouter);
75
120
  server.use(demoRoutes);
76
121
  server.use(exampleRouter);
77
122