@invect/rbac 0.0.1 → 0.0.3

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.
Files changed (35) hide show
  1. package/README.md +39 -77
  2. package/dist/backend/index.cjs +72 -40
  3. package/dist/backend/index.cjs.map +1 -1
  4. package/dist/backend/index.d.cts +49 -0
  5. package/dist/backend/index.d.cts.map +1 -0
  6. package/dist/backend/index.d.mts +49 -0
  7. package/dist/backend/index.d.mts.map +1 -0
  8. package/dist/backend/index.d.ts +1 -1
  9. package/dist/backend/index.d.ts.map +1 -1
  10. package/dist/backend/index.mjs +72 -40
  11. package/dist/backend/index.mjs.map +1 -1
  12. package/dist/backend/plugin.d.ts +12 -14
  13. package/dist/backend/plugin.d.ts.map +1 -1
  14. package/dist/frontend/components/TeamsPage.d.ts.map +1 -1
  15. package/dist/frontend/components/access-control/ScopeDetailPanel.d.ts.map +1 -1
  16. package/dist/frontend/components/access-control/index.d.ts +0 -1
  17. package/dist/frontend/components/access-control/index.d.ts.map +1 -1
  18. package/dist/frontend/index.cjs +61 -61
  19. package/dist/frontend/index.cjs.map +1 -1
  20. package/dist/frontend/index.d.cts +227 -0
  21. package/dist/frontend/index.d.cts.map +1 -0
  22. package/dist/frontend/index.d.mts +227 -0
  23. package/dist/frontend/index.d.mts.map +1 -0
  24. package/dist/frontend/index.d.ts +2 -2
  25. package/dist/frontend/index.d.ts.map +1 -1
  26. package/dist/frontend/index.mjs +7 -7
  27. package/dist/frontend/index.mjs.map +1 -1
  28. package/dist/frontend/types.d.ts +1 -1
  29. package/dist/shared/types.d.cts +2 -0
  30. package/dist/shared/types.d.mts +2 -0
  31. package/dist/types-D4DI2gyU.d.cts +175 -0
  32. package/dist/types-D4DI2gyU.d.cts.map +1 -0
  33. package/dist/types-DxJoguYy.d.mts +175 -0
  34. package/dist/types-DxJoguYy.d.mts.map +1 -0
  35. package/package.json +44 -46
package/README.md CHANGED
@@ -1,103 +1,65 @@
1
- # @invect/rbac
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="../../../.github/assets/logo-light.svg">
4
+ <img alt="Invect" src="../../../.github/assets/logo-dark.svg" width="50">
5
+ </picture>
6
+ </p>
2
7
 
3
- RBAC (Role-Based Access Control) plugin for Invect — adds flow sharing, permission management UI, and access control panels.
8
+ <h1 align="center">@invect/rbac</h1>
4
9
 
5
- ## Requirements
10
+ <p align="center">
11
+ Role-based access control plugin for Invect.
12
+ <br />
13
+ <a href="https://invect.dev/docs/plugins"><strong>Docs</strong></a>
14
+ </p>
6
15
 
7
- This plugin **requires** the `@invect/user-auth` plugin to be loaded for session resolution. RBAC handles *authorization* on top of the authentication layer that `@invect/user-auth` provides.
16
+ ---
8
17
 
9
- ## Installation
18
+ Adds flow-level permissions, sharing UI, and access control enforcement to Invect. Requires [`@invect/user-auth`](../auth) for session resolution.
19
+
20
+ ## Install
10
21
 
11
22
  ```bash
12
23
  pnpm add @invect/rbac
13
24
  ```
14
25
 
15
- ## Usage
16
-
17
- ### Backend
26
+ ## Backend
18
27
 
19
- ```typescript
20
- import { betterAuthPlugin } from '@invect/user-auth';
28
+ ```ts
29
+ import { authentication } from '@invect/user-auth';
21
30
  import { rbacPlugin } from '@invect/rbac';
22
31
 
23
- app.use('/invect', createInvectRouter({
24
- databaseUrl: 'sqlite://...',
25
- auth: {
26
- enabled: true,
27
- useFlowAccessTable: true,
28
- },
32
+ const invectRouter = await createInvectRouter({
33
+ database: { type: 'sqlite', connectionString: 'file:./dev.db' },
34
+ encryptionKey: process.env.INVECT_ENCRYPTION_KEY!,
29
35
  plugins: [
30
- betterAuthPlugin({ auth }), // Auth MUST be registered first
31
- rbacPlugin({
32
- useFlowAccessTable: true,
33
- }),
36
+ authentication({ globalAdmins: [{ email: 'admin@example.com', pw: 'secret' }] }), // Must come first
37
+ rbacPlugin(),
34
38
  ],
35
- }));
39
+ });
40
+
41
+ app.use('/invect', invectRouter);
36
42
  ```
37
43
 
38
- ### Frontend
44
+ ## Frontend
39
45
 
40
46
  ```tsx
41
- import { Invect } from '@invect/frontend';
47
+ import { Invect } from '@invect/ui';
42
48
  import { rbacFrontendPlugin } from '@invect/rbac/ui';
43
49
 
44
- function App() {
45
- return (
46
- <Invect
47
- apiBaseUrl="http://localhost:3000/invect"
48
- plugins={[rbacFrontendPlugin]} // When plugin system is wired
49
- />
50
- );
51
- }
52
- ```
53
-
54
- ### Using components directly (before plugin system is wired)
55
-
56
- ```tsx
57
- import { RbacProvider, useRbac, ShareFlowModal, FlowAccessPanel } from '@invect/rbac/ui';
50
+ <Invect apiBaseUrl="http://localhost:3000/invect" plugins={[rbacFrontendPlugin]} />;
58
51
  ```
59
52
 
60
- ## What's Included
61
-
62
- ### Backend Plugin (`@invect/rbac`)
63
- - Plugin endpoints for flow access management (namespaced under `/rbac/`)
64
- - UI manifest endpoint (`GET /rbac/ui-manifest`)
65
- - Authorization hooks for flow-level ACL enforcement
66
- - Auth dependency checking (warns if `@invect/user-auth` is missing)
67
-
68
- ### Frontend Plugin (`@invect/rbac/ui`)
69
- - **RbacProvider** — Context provider that fetches and caches user identity/permissions
70
- - **useRbac()** — Hook for checking permissions in components
71
- - **ShareButton** — Flow header action that opens the share modal
72
- - **ShareFlowModal** — Modal for granting/revoking flow access
73
- - **FlowAccessPanel** — Flow editor panel tab showing access records
74
- - **AccessControlPage** — Admin page for viewing roles and permissions
75
- - **UserMenuSection** — Sidebar component showing current user info
53
+ The plugin contributes sidebar items, an access management page, a flow-level access panel tab, and a share button in the flow editor header.
76
54
 
77
- ### Shared Types (`@invect/rbac/types`)
78
- - `FlowAccessRecord`, `GrantFlowAccessRequest`, `FlowAccessPermission`
79
- - `AuthMeResponse`, `RolePermissionEntry`
80
- - Plugin UI manifest types
55
+ ## Exports
81
56
 
82
- ## Package Exports
57
+ | Entry Point | Content |
58
+ | -------------------- | ------------------------------------------------------------------------------------------- |
59
+ | `@invect/rbac` | Backend plugin (Node.js) |
60
+ | `@invect/rbac/ui` | Frontend plugin — `rbacFrontendPlugin`, `RbacProvider`, `ShareFlowModal`, `FlowAccessPanel` |
61
+ | `@invect/rbac/types` | Shared types — `FlowAccessRecord`, `FlowAccessPermission`, etc. |
83
62
 
84
- | Entry Point | Import | Content |
85
- |-------------|--------|---------|
86
- | `@invect/rbac` | `import { rbacPlugin } from '@invect/rbac'` | Backend plugin (Node.js) |
87
- | `@invect/rbac/ui` | `import { rbacFrontendPlugin } from '@invect/rbac/ui'` | Frontend plugin (Browser) |
88
- | `@invect/rbac/types` | `import type { FlowAccessRecord } from '@invect/rbac/types'` | Shared types |
63
+ ## License
89
64
 
90
- ## Architecture
91
-
92
- ```
93
- @invect/user-auth (authentication)
94
-
95
- │ provides InvectIdentity via session resolution
96
-
97
-
98
- @invect/rbac (authorization)
99
- ├── backend: hooks + endpoints
100
- │ └── delegates to core's FlowAccessService + AuthorizationService
101
- └── frontend: provider + components
102
- └── fetches /auth/me, renders access management UI
103
- ```
65
+ [MIT](../../../LICENSE)
@@ -9,8 +9,8 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
9
9
  * ```ts
10
10
  * import { resolveTeamIds } from '@invect/rbac/backend';
11
11
  *
12
- * betterAuthPlugin({
13
- * auth,
12
+ * auth({
13
+ * auth: betterAuthInstance,
14
14
  * mapUser: async (user, session) => ({
15
15
  * id: user.id,
16
16
  * name: user.name ?? undefined,
@@ -198,6 +198,58 @@ async function listAllDirectFlowAccess(db, flowId) {
198
198
  const now = Date.now();
199
199
  return rows.map(normalizeFlowAccessRecord).filter((record) => !record.expiresAt || new Date(record.expiresAt).getTime() > now);
200
200
  }
201
+ async function grantDirectFlowAccess(db, input) {
202
+ const now = (/* @__PURE__ */ new Date()).toISOString();
203
+ const existingRows = await db.query("SELECT id FROM flow_access WHERE flow_id = ? AND user_id IS ? AND team_id IS ?", [
204
+ input.flowId,
205
+ input.userId ?? null,
206
+ input.teamId ?? null
207
+ ]);
208
+ if (existingRows.length > 0) {
209
+ const existingId = String(existingRows[0].id);
210
+ await db.execute("UPDATE flow_access SET permission = ?, granted_by = ?, granted_at = ?, expires_at = ? WHERE id = ?", [
211
+ input.permission,
212
+ input.grantedBy ?? null,
213
+ now,
214
+ input.expiresAt ?? null,
215
+ existingId
216
+ ]);
217
+ return {
218
+ id: existingId,
219
+ flowId: input.flowId,
220
+ userId: input.userId ?? null,
221
+ teamId: input.teamId ?? null,
222
+ permission: input.permission,
223
+ grantedBy: input.grantedBy ?? null,
224
+ grantedAt: now,
225
+ expiresAt: input.expiresAt ?? null
226
+ };
227
+ }
228
+ const id = crypto.randomUUID();
229
+ await db.execute("INSERT INTO flow_access (id, flow_id, user_id, team_id, permission, granted_by, granted_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
230
+ id,
231
+ input.flowId,
232
+ input.userId ?? null,
233
+ input.teamId ?? null,
234
+ input.permission,
235
+ input.grantedBy ?? null,
236
+ now,
237
+ input.expiresAt ?? null
238
+ ]);
239
+ return {
240
+ id,
241
+ flowId: input.flowId,
242
+ userId: input.userId ?? null,
243
+ teamId: input.teamId ?? null,
244
+ permission: input.permission,
245
+ grantedBy: input.grantedBy ?? null,
246
+ grantedAt: now,
247
+ expiresAt: input.expiresAt ?? null
248
+ };
249
+ }
250
+ async function revokeDirectFlowAccess(db, accessId) {
251
+ await db.execute("DELETE FROM flow_access WHERE id = ?", [accessId]);
252
+ }
201
253
  async function listAllEffectiveFlowAccessForPreview(db, flowId, overrideScopeId) {
202
254
  const direct = await listAllDirectFlowAccess(db, flowId);
203
255
  const scopeId = overrideScopeId === void 0 ? await getFlowScopeId(db, flowId) : overrideScopeId;
@@ -280,8 +332,17 @@ async function resolveAccessChangeNames(db, entries) {
280
332
  source: entry.source
281
333
  }));
282
334
  }
283
- function rbacPlugin(options = {}) {
284
- const { useFlowAccessTable = true, adminPermission = "flow:read", enableTeams = true } = options;
335
+ function rbac(options = {}) {
336
+ const { frontend, ...backendOptions } = options;
337
+ return {
338
+ id: "rbac",
339
+ name: "Role-Based Access Control",
340
+ backend: _rbacBackendPlugin(backendOptions),
341
+ frontend
342
+ };
343
+ }
344
+ function _rbacBackendPlugin(options = {}) {
345
+ const { adminPermission = "flow:read", enableTeams = true } = options;
285
346
  const teamsSchema = enableTeams ? {
286
347
  flows: { fields: { scope_id: {
287
348
  type: "string",
@@ -445,10 +506,10 @@ function rbacPlugin(options = {}) {
445
506
  "rbac_scope_access"
446
507
  ] : []
447
508
  ],
448
- setupInstructions: "The RBAC plugin requires better-auth tables (user, session). Make sure @invect/user-auth is configured, then run `npx invect generate` followed by `npx drizzle-kit push`.",
509
+ setupInstructions: "The RBAC plugin requires user-auth tables (user, session). Make sure @invect/user-auth is configured, then run `npx invect-cli generate` followed by `npx drizzle-kit push`.",
449
510
  init: async (ctx) => {
450
- if (!ctx.hasPlugin("better-auth")) ctx.logger.warn("RBAC plugin requires the @invect/user-auth plugin. RBAC will work with reduced functionality (no session resolution). Make sure betterAuthPlugin() is registered before rbacPlugin().");
451
- ctx.logger.info("RBAC plugin initialized", { useFlowAccessTable });
511
+ if (!ctx.hasPlugin("user-auth")) ctx.logger.warn("RBAC plugin requires the @invect/user-auth plugin. RBAC will work with reduced functionality (no session resolution). Make sure auth() is registered before rbac().");
512
+ ctx.logger.info("RBAC plugin initialized");
452
513
  },
453
514
  endpoints: [
454
515
  {
@@ -511,13 +572,6 @@ function rbacPlugin(options = {}) {
511
572
  status: 400,
512
573
  body: { error: "Missing flowId parameter" }
513
574
  };
514
- if (!ctx.core.isFlowAccessTableEnabled()) return {
515
- status: 501,
516
- body: {
517
- error: "Not Implemented",
518
- message: "Flow access table not enabled. Set auth.useFlowAccessTable: true in config."
519
- }
520
- };
521
575
  if (!ctx.identity) return {
522
576
  status: 401,
523
577
  body: {
@@ -534,7 +588,7 @@ function rbacPlugin(options = {}) {
534
588
  };
535
589
  return {
536
590
  status: 200,
537
- body: { access: await ctx.core.listFlowAccess(flowId) }
591
+ body: { access: await listAllDirectFlowAccess(ctx.database, flowId) }
538
592
  };
539
593
  }
540
594
  },
@@ -548,13 +602,6 @@ function rbacPlugin(options = {}) {
548
602
  status: 400,
549
603
  body: { error: "Missing flowId parameter" }
550
604
  };
551
- if (!ctx.core.isFlowAccessTableEnabled()) return {
552
- status: 501,
553
- body: {
554
- error: "Not Implemented",
555
- message: "Flow access table not enabled. Set auth.useFlowAccessTable: true in config."
556
- }
557
- };
558
605
  const { userId, teamId, permission, expiresAt } = ctx.body;
559
606
  if (!userId && !teamId) return {
560
607
  status: 400,
@@ -585,7 +632,7 @@ function rbacPlugin(options = {}) {
585
632
  };
586
633
  return {
587
634
  status: 201,
588
- body: await ctx.core.grantFlowAccess({
635
+ body: await grantDirectFlowAccess(ctx.database, {
589
636
  flowId,
590
637
  userId,
591
638
  teamId,
@@ -606,13 +653,6 @@ function rbacPlugin(options = {}) {
606
653
  status: 400,
607
654
  body: { error: "Missing flowId or accessId parameter" }
608
655
  };
609
- if (!ctx.core.isFlowAccessTableEnabled()) return {
610
- status: 501,
611
- body: {
612
- error: "Not Implemented",
613
- message: "Flow access table not enabled. Set auth.useFlowAccessTable: true in config."
614
- }
615
- };
616
656
  if (!ctx.identity) return {
617
657
  status: 401,
618
658
  body: {
@@ -627,7 +667,7 @@ function rbacPlugin(options = {}) {
627
667
  message: "Owner access is required to manage sharing"
628
668
  }
629
669
  };
630
- await ctx.core.revokeFlowAccess(accessId);
670
+ await revokeDirectFlowAccess(ctx.database, accessId);
631
671
  return {
632
672
  status: 204,
633
673
  body: null
@@ -639,13 +679,6 @@ function rbacPlugin(options = {}) {
639
679
  path: "/rbac/flows/accessible",
640
680
  isPublic: false,
641
681
  handler: async (ctx) => {
642
- if (!ctx.core.isFlowAccessTableEnabled()) return {
643
- status: 501,
644
- body: {
645
- error: "Not Implemented",
646
- message: "Flow access table not enabled. Set auth.useFlowAccessTable: true in config."
647
- }
648
- };
649
682
  const identity = ctx.identity;
650
683
  if (!identity) return {
651
684
  status: 401,
@@ -1324,7 +1357,6 @@ function rbacPlugin(options = {}) {
1324
1357
  } catch {}
1325
1358
  } : void 0,
1326
1359
  onAuthorize: async (context) => {
1327
- if (!useFlowAccessTable) return;
1328
1360
  const { identity, resource, action } = context;
1329
1361
  if (!identity || !resource?.id) return;
1330
1362
  if (!FLOW_RESOURCE_TYPES.has(resource.type)) return;
@@ -1359,7 +1391,7 @@ function rbacPlugin(options = {}) {
1359
1391
  };
1360
1392
  }
1361
1393
  //#endregion
1362
- exports.rbacPlugin = rbacPlugin;
1394
+ exports.rbac = rbac;
1363
1395
  exports.resolveTeamIds = resolveTeamIds;
1364
1396
 
1365
1397
  //# sourceMappingURL=index.cjs.map