@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.
- package/README.md +39 -77
- package/dist/backend/index.cjs +72 -40
- package/dist/backend/index.cjs.map +1 -1
- package/dist/backend/index.d.cts +49 -0
- package/dist/backend/index.d.cts.map +1 -0
- package/dist/backend/index.d.mts +49 -0
- package/dist/backend/index.d.mts.map +1 -0
- package/dist/backend/index.d.ts +1 -1
- package/dist/backend/index.d.ts.map +1 -1
- package/dist/backend/index.mjs +72 -40
- package/dist/backend/index.mjs.map +1 -1
- package/dist/backend/plugin.d.ts +12 -14
- package/dist/backend/plugin.d.ts.map +1 -1
- package/dist/frontend/components/TeamsPage.d.ts.map +1 -1
- package/dist/frontend/components/access-control/ScopeDetailPanel.d.ts.map +1 -1
- package/dist/frontend/components/access-control/index.d.ts +0 -1
- package/dist/frontend/components/access-control/index.d.ts.map +1 -1
- package/dist/frontend/index.cjs +61 -61
- package/dist/frontend/index.cjs.map +1 -1
- package/dist/frontend/index.d.cts +227 -0
- package/dist/frontend/index.d.cts.map +1 -0
- package/dist/frontend/index.d.mts +227 -0
- package/dist/frontend/index.d.mts.map +1 -0
- package/dist/frontend/index.d.ts +2 -2
- package/dist/frontend/index.d.ts.map +1 -1
- package/dist/frontend/index.mjs +7 -7
- package/dist/frontend/index.mjs.map +1 -1
- package/dist/frontend/types.d.ts +1 -1
- package/dist/shared/types.d.cts +2 -0
- package/dist/shared/types.d.mts +2 -0
- package/dist/types-D4DI2gyU.d.cts +175 -0
- package/dist/types-D4DI2gyU.d.cts.map +1 -0
- package/dist/types-DxJoguYy.d.mts +175 -0
- package/dist/types-DxJoguYy.d.mts.map +1 -0
- package/package.json +44 -46
package/README.md
CHANGED
|
@@ -1,103 +1,65 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
8
|
+
<h1 align="center">@invect/rbac</h1>
|
|
4
9
|
|
|
5
|
-
|
|
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
|
-
|
|
16
|
+
---
|
|
8
17
|
|
|
9
|
-
|
|
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
|
-
##
|
|
16
|
-
|
|
17
|
-
### Backend
|
|
26
|
+
## Backend
|
|
18
27
|
|
|
19
|
-
```
|
|
20
|
-
import {
|
|
28
|
+
```ts
|
|
29
|
+
import { authentication } from '@invect/user-auth';
|
|
21
30
|
import { rbacPlugin } from '@invect/rbac';
|
|
22
31
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
+
## Frontend
|
|
39
45
|
|
|
40
46
|
```tsx
|
|
41
|
-
import { Invect } from '@invect/
|
|
47
|
+
import { Invect } from '@invect/ui';
|
|
42
48
|
import { rbacFrontendPlugin } from '@invect/rbac/ui';
|
|
43
49
|
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
- `FlowAccessRecord`, `GrantFlowAccessRequest`, `FlowAccessPermission`
|
|
79
|
-
- `AuthMeResponse`, `RolePermissionEntry`
|
|
80
|
-
- Plugin UI manifest types
|
|
55
|
+
## Exports
|
|
81
56
|
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
package/dist/backend/index.cjs
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
284
|
-
const {
|
|
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
|
|
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("
|
|
451
|
-
ctx.logger.info("RBAC plugin initialized"
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
1394
|
+
exports.rbac = rbac;
|
|
1363
1395
|
exports.resolveTeamIds = resolveTeamIds;
|
|
1364
1396
|
|
|
1365
1397
|
//# sourceMappingURL=index.cjs.map
|