@voltro/cli 0.11.4 → 0.13.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/CHANGELOG.md +356 -0
- package/dist/{apiBuild-DHHuiwXF.js → apiBuild-B6_ehbem.js} +9 -8
- package/dist/apiBuild-Bzpb383i.js +2 -0
- package/dist/appModuleLoader-C9r9mxZt.js +19 -0
- package/dist/bin.js +21 -10
- package/dist/cliArgs-qdZSElM3.js +28 -0
- package/dist/{commands-BwbMqR-1.js → commands-DYOuGlEC.js} +2903 -2647
- package/dist/dbCommand-ADD4mrTI.js +2 -0
- package/dist/dbCommand-DIOJQwsq.js +876 -0
- package/dist/{dev-Cvojvb_J.js → dev-Fb9BmavX.js} +1697 -2411
- package/dist/dev-xdijqojc.js +3 -0
- package/dist/frameworkTableAssembly-CYlrafoK.js +600 -0
- package/dist/frameworkTableAssembly-XvKO7wQt.js +2 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +3 -3
- package/dist/inspect-DcZ04OME.js +2 -0
- package/dist/inspect-Dwx0_tUj.js +921 -0
- package/dist/inspectMetrics-DOiiII34.js +3408 -0
- package/dist/manifestBuild-D1MzJAiQ.js +2 -0
- package/dist/manifestBuild-P9yuCY2d.js +39 -0
- package/dist/seedRunner-D6eu-u5U.js +229 -0
- package/dist/serveCommand-CqYxzBZR.js +1113 -0
- package/dist/serveEntry.js +4 -4
- package/dist/{start-DyJE6w1R.js → start-CjG_0xhR.js} +346 -343
- package/dist/startEntry.js +2 -2
- package/dist/startup.js +1 -1
- package/dist/{startupRunner-DhlX9nqd.js → startupRunner-DPGFchOa.js} +19 -33
- package/package.json +17 -17
- package/templates/AGENTS.md +1 -0
- package/templates/agent-docs/_index.md +1 -0
- package/templates/agent-docs/authentication.md +36 -0
- package/templates/agent-docs/cli.md +254 -14
- package/templates/agent-docs/configuration.md +48 -0
- package/templates/agent-docs/data.md +39 -0
- package/templates/agent-docs/database/migrations.md +32 -0
- package/templates/agent-docs/database/transactions.md +31 -0
- package/templates/agent-docs/scheduling.md +35 -1
- package/templates/agent-docs/security.md +48 -0
- package/templates/agent-docs/templates/apibackends.md +90 -17
- package/templates/agent-docs/testing.md +24 -0
- package/templates/agent-docs/whats-new.md +81 -0
- package/templates/apps/api-ai/package.json +7 -7
- package/templates/apps/api-auth/package.json +8 -8
- package/templates/apps/api-backend/package.json +7 -7
- package/templates/apps/api-backend-deactivation/package.json +7 -7
- package/templates/apps/api-backend-deactivation/tests/users.deactivate.test.ts +1 -1
- package/templates/apps/api-backend-mail/package.json +8 -8
- package/templates/apps/api-backend-mariadb/package.json +9 -9
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-data-advanced/package.json +8 -8
- package/templates/apps/api-data-advanced/tests/queries.test.ts +6 -1
- package/templates/apps/api-durable/package.json +8 -8
- package/templates/apps/api-feature-flags/package.json +9 -9
- package/templates/apps/api-governance/app.config.ts +0 -1
- package/templates/apps/api-governance/package.json +8 -8
- package/templates/apps/api-kv/package.json +8 -8
- package/templates/apps/api-kv/tests/sync.test.ts +2 -2
- package/templates/apps/api-moderation/package.json +8 -8
- package/templates/apps/api-observability/package.json +8 -8
- package/templates/apps/api-ratelimit/package.json +8 -8
- package/templates/apps/api-rbac/README.md +53 -14
- package/templates/apps/api-rbac/app.config.ts +24 -26
- package/templates/apps/api-rbac/authz.ts +76 -0
- package/templates/apps/api-rbac/database/schema.ts +23 -6
- package/templates/apps/api-rbac/mutations/notes.create.mutation.server.ts +7 -5
- package/templates/apps/api-rbac/mutations/notes.create.mutation.ts +15 -8
- package/templates/apps/api-rbac/mutations/notes.delete.mutation.server.ts +54 -0
- package/templates/apps/api-rbac/mutations/notes.delete.mutation.ts +43 -0
- package/templates/apps/api-rbac/mutations/teams.rename.mutation.server.ts +19 -0
- package/templates/apps/api-rbac/mutations/teams.rename.mutation.ts +37 -0
- package/templates/apps/api-rbac/package.json +8 -8
- package/templates/apps/api-rbac/queries/notes.list.query.server.ts +12 -0
- package/templates/apps/api-rbac/queries/notes.list.query.ts +26 -0
- package/templates/apps/api-rbac/template.json +2 -2
- package/templates/apps/api-rbac/tests/authz.test.ts +233 -0
- package/templates/apps/api-rest/package.json +7 -7
- package/templates/apps/api-saas/package.json +11 -11
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-versioning/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +9 -8
- package/templates/apps/changelog/package.json +6 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-app/package.json +8 -8
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-docs/package.json +7 -7
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/package.json +7 -7
- package/templates/apps/frontend-spa/package.json +7 -7
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-static-blog/package.json +6 -6
- package/dist/apiBuild-D9MtlD02.js +0 -2
- package/dist/dev-BsBsIv93.js +0 -2
- package/dist/inspectMetrics-D4EJ8LTi.js +0 -4255
- package/dist/serveCommand-ARqSs73o.js +0 -1117
- package/templates/apps/api-rbac/tests/notes.create.test.ts +0 -80
|
@@ -12,18 +12,18 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@effect/platform": "^0.96.1",
|
|
14
14
|
"@effect/rpc": "^0.75.1",
|
|
15
|
-
"@voltro/cli": "0.
|
|
16
|
-
"@voltro/database": "0.
|
|
17
|
-
"@voltro/env": "0.
|
|
18
|
-
"@voltro/plugin-multitenancy": "0.
|
|
19
|
-
"@voltro/plugin-storage": "0.
|
|
20
|
-
"@voltro/protocol": "0.
|
|
21
|
-
"@voltro/runtime": "0.
|
|
22
|
-
"@voltro/sql-mysql": "0.
|
|
15
|
+
"@voltro/cli": "0.13.0",
|
|
16
|
+
"@voltro/database": "0.13.0",
|
|
17
|
+
"@voltro/env": "0.13.0",
|
|
18
|
+
"@voltro/plugin-multitenancy": "0.13.0",
|
|
19
|
+
"@voltro/plugin-storage": "0.13.0",
|
|
20
|
+
"@voltro/protocol": "0.13.0",
|
|
21
|
+
"@voltro/runtime": "0.13.0",
|
|
22
|
+
"@voltro/sql-mysql": "0.13.0",
|
|
23
23
|
"effect": "^3.21.2"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@voltro/testing": "0.
|
|
26
|
+
"@voltro/testing": "0.13.0",
|
|
27
27
|
"typescript": "^5.7.0",
|
|
28
28
|
"vitest": "^3.0.0"
|
|
29
29
|
}
|
|
@@ -10,17 +10,17 @@
|
|
|
10
10
|
"test": "voltro test"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@voltro/cli": "0.
|
|
14
|
-
"@voltro/database": "0.
|
|
15
|
-
"@voltro/env": "0.
|
|
16
|
-
"@voltro/plugin-multitenancy": "0.
|
|
17
|
-
"@voltro/plugin-storage": "0.
|
|
18
|
-
"@voltro/protocol": "0.
|
|
19
|
-
"@voltro/runtime": "0.
|
|
13
|
+
"@voltro/cli": "0.13.0",
|
|
14
|
+
"@voltro/database": "0.13.0",
|
|
15
|
+
"@voltro/env": "0.13.0",
|
|
16
|
+
"@voltro/plugin-multitenancy": "0.13.0",
|
|
17
|
+
"@voltro/plugin-storage": "0.13.0",
|
|
18
|
+
"@voltro/protocol": "0.13.0",
|
|
19
|
+
"@voltro/runtime": "0.13.0",
|
|
20
20
|
"effect": "^3.21.2"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@voltro/testing": "0.
|
|
23
|
+
"@voltro/testing": "0.13.0",
|
|
24
24
|
"typescript": "^5.7.0",
|
|
25
25
|
"vitest": "^3.0.0"
|
|
26
26
|
}
|
|
@@ -11,17 +11,17 @@
|
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@effect/platform": "^0.96.1",
|
|
13
13
|
"@effect/rpc": "^0.75.1",
|
|
14
|
-
"@voltro/cli": "0.
|
|
15
|
-
"@voltro/database": "0.
|
|
16
|
-
"@voltro/env": "0.
|
|
17
|
-
"@voltro/plugin-governance": "0.
|
|
18
|
-
"@voltro/plugin-multitenancy": "0.
|
|
19
|
-
"@voltro/protocol": "0.
|
|
20
|
-
"@voltro/runtime": "0.
|
|
14
|
+
"@voltro/cli": "0.13.0",
|
|
15
|
+
"@voltro/database": "0.13.0",
|
|
16
|
+
"@voltro/env": "0.13.0",
|
|
17
|
+
"@voltro/plugin-governance": "0.13.0",
|
|
18
|
+
"@voltro/plugin-multitenancy": "0.13.0",
|
|
19
|
+
"@voltro/protocol": "0.13.0",
|
|
20
|
+
"@voltro/runtime": "0.13.0",
|
|
21
21
|
"effect": "^3.21.2"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@voltro/testing": "0.
|
|
24
|
+
"@voltro/testing": "0.13.0",
|
|
25
25
|
"typescript": "^5.7.0",
|
|
26
26
|
"vitest": "^3.0.0"
|
|
27
27
|
}
|
|
@@ -71,7 +71,12 @@ describe('authors.withBooks', () => {
|
|
|
71
71
|
const rows = await ctx.store.query(authorsWithBooksExecute({}, ctx).descriptor)
|
|
72
72
|
|
|
73
73
|
expect(rows).toHaveLength(1)
|
|
74
|
-
|
|
74
|
+
// Narrow BEFORE describing the shape: under `noUncheckedIndexedAccess`
|
|
75
|
+
// `rows[0]` is `Row | undefined`, and a cast that spans that gap is the one
|
|
76
|
+
// TypeScript refuses outright ("neither type sufficiently overlaps").
|
|
77
|
+
const first = rows[0]
|
|
78
|
+
if (first === undefined) throw new Error('expected one author row')
|
|
79
|
+
const ada = first as unknown as { id: string; name: string; books: ReadonlyArray<{ title: string }> }
|
|
75
80
|
expect(ada.name).toBe('Ada')
|
|
76
81
|
expect(ada.books.map((b) => b.title).sort()).toEqual(['Analytical Engine', 'Notes'])
|
|
77
82
|
})
|
|
@@ -11,17 +11,17 @@
|
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@effect/platform": "^0.96.1",
|
|
13
13
|
"@effect/rpc": "^0.75.1",
|
|
14
|
-
"@voltro/cli": "0.
|
|
15
|
-
"@voltro/database": "0.
|
|
16
|
-
"@voltro/env": "0.
|
|
17
|
-
"@voltro/plugin-multitenancy": "0.
|
|
18
|
-
"@voltro/protocol": "0.
|
|
19
|
-
"@voltro/runtime": "0.
|
|
20
|
-
"@voltro/workflow": "0.
|
|
14
|
+
"@voltro/cli": "0.13.0",
|
|
15
|
+
"@voltro/database": "0.13.0",
|
|
16
|
+
"@voltro/env": "0.13.0",
|
|
17
|
+
"@voltro/plugin-multitenancy": "0.13.0",
|
|
18
|
+
"@voltro/protocol": "0.13.0",
|
|
19
|
+
"@voltro/runtime": "0.13.0",
|
|
20
|
+
"@voltro/workflow": "0.13.0",
|
|
21
21
|
"effect": "^3.21.2"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@voltro/testing": "0.
|
|
24
|
+
"@voltro/testing": "0.13.0",
|
|
25
25
|
"typescript": "^5.7.0",
|
|
26
26
|
"vitest": "^3.0.0"
|
|
27
27
|
}
|
|
@@ -12,18 +12,18 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@effect/platform": "^0.96.1",
|
|
14
14
|
"@effect/rpc": "^0.75.1",
|
|
15
|
-
"@voltro/cli": "0.
|
|
16
|
-
"@voltro/database": "0.
|
|
17
|
-
"@voltro/env": "0.
|
|
18
|
-
"@voltro/plugin-flags": "0.
|
|
19
|
-
"@voltro/plugin-multitenancy": "0.
|
|
20
|
-
"@voltro/protocol": "0.
|
|
21
|
-
"@voltro/runtime": "0.
|
|
22
|
-
"@voltro/sql-postgres": "0.
|
|
15
|
+
"@voltro/cli": "0.13.0",
|
|
16
|
+
"@voltro/database": "0.13.0",
|
|
17
|
+
"@voltro/env": "0.13.0",
|
|
18
|
+
"@voltro/plugin-flags": "0.13.0",
|
|
19
|
+
"@voltro/plugin-multitenancy": "0.13.0",
|
|
20
|
+
"@voltro/protocol": "0.13.0",
|
|
21
|
+
"@voltro/runtime": "0.13.0",
|
|
22
|
+
"@voltro/sql-postgres": "0.13.0",
|
|
23
23
|
"effect": "^3.21.2"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@voltro/testing": "0.
|
|
26
|
+
"@voltro/testing": "0.13.0",
|
|
27
27
|
"typescript": "^5.7.0",
|
|
28
28
|
"vitest": "^3.0.0"
|
|
29
29
|
}
|
|
@@ -12,17 +12,17 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@effect/platform": "^0.96.1",
|
|
14
14
|
"@effect/rpc": "^0.75.1",
|
|
15
|
-
"@voltro/cli": "0.
|
|
16
|
-
"@voltro/database": "0.
|
|
17
|
-
"@voltro/env": "0.
|
|
18
|
-
"@voltro/plugin-audit": "0.
|
|
19
|
-
"@voltro/plugin-governance": "0.
|
|
20
|
-
"@voltro/protocol": "0.
|
|
21
|
-
"@voltro/runtime": "0.
|
|
15
|
+
"@voltro/cli": "0.13.0",
|
|
16
|
+
"@voltro/database": "0.13.0",
|
|
17
|
+
"@voltro/env": "0.13.0",
|
|
18
|
+
"@voltro/plugin-audit": "0.13.0",
|
|
19
|
+
"@voltro/plugin-governance": "0.13.0",
|
|
20
|
+
"@voltro/protocol": "0.13.0",
|
|
21
|
+
"@voltro/runtime": "0.13.0",
|
|
22
22
|
"effect": "^3.21.2"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@voltro/testing": "0.
|
|
25
|
+
"@voltro/testing": "0.13.0",
|
|
26
26
|
"typescript": "^5.7.0",
|
|
27
27
|
"vitest": "^3.0.0"
|
|
28
28
|
}
|
|
@@ -11,17 +11,17 @@
|
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@effect/platform": "^0.96.1",
|
|
13
13
|
"@effect/rpc": "^0.75.1",
|
|
14
|
-
"@voltro/cli": "0.
|
|
15
|
-
"@voltro/database": "0.
|
|
16
|
-
"@voltro/env": "0.
|
|
17
|
-
"@voltro/kv": "0.
|
|
18
|
-
"@voltro/plugin-multitenancy": "0.
|
|
19
|
-
"@voltro/protocol": "0.
|
|
20
|
-
"@voltro/runtime": "0.
|
|
14
|
+
"@voltro/cli": "0.13.0",
|
|
15
|
+
"@voltro/database": "0.13.0",
|
|
16
|
+
"@voltro/env": "0.13.0",
|
|
17
|
+
"@voltro/kv": "0.13.0",
|
|
18
|
+
"@voltro/plugin-multitenancy": "0.13.0",
|
|
19
|
+
"@voltro/protocol": "0.13.0",
|
|
20
|
+
"@voltro/runtime": "0.13.0",
|
|
21
21
|
"effect": "^3.21.2"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@voltro/testing": "0.
|
|
24
|
+
"@voltro/testing": "0.13.0",
|
|
25
25
|
"typescript": "^5.7.0",
|
|
26
26
|
"vitest": "^3.0.0"
|
|
27
27
|
}
|
|
@@ -38,7 +38,7 @@ describe('sync.pull (durable cursor + idempotency markers)', () => {
|
|
|
38
38
|
const rows = await ctx.store.query(database.syncedEvents.descriptor)
|
|
39
39
|
expect(rows).toHaveLength(3)
|
|
40
40
|
expect(rows.every((r) => r['tenantId'] === 'acme')).toBe(true)
|
|
41
|
-
expect((rows.map((r) => r['externalId']) as
|
|
41
|
+
expect((rows.map((r) => r['externalId']) as string[]).sort()).toEqual([
|
|
42
42
|
'ext-1',
|
|
43
43
|
'ext-2',
|
|
44
44
|
'ext-3',
|
|
@@ -63,7 +63,7 @@ describe('sync.pull (durable cursor + idempotency markers)', () => {
|
|
|
63
63
|
|
|
64
64
|
const rows = await ctx.store.query(database.syncedEvents.descriptor)
|
|
65
65
|
expect(rows).toHaveLength(4) // 2 + 2, no duplicates
|
|
66
|
-
expect((rows.map((r) => r['sequence']) as
|
|
66
|
+
expect((rows.map((r) => r['sequence']) as number[]).sort((a, b) => a - b)).toEqual([
|
|
67
67
|
1, 2, 3, 4,
|
|
68
68
|
])
|
|
69
69
|
})
|
|
@@ -12,17 +12,17 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@effect/platform": "^0.96.1",
|
|
14
14
|
"@effect/rpc": "^0.75.1",
|
|
15
|
-
"@voltro/cli": "0.
|
|
16
|
-
"@voltro/database": "0.
|
|
17
|
-
"@voltro/env": "0.
|
|
18
|
-
"@voltro/plugin-moderation": "0.
|
|
19
|
-
"@voltro/plugin-multitenancy": "0.
|
|
20
|
-
"@voltro/protocol": "0.
|
|
21
|
-
"@voltro/runtime": "0.
|
|
15
|
+
"@voltro/cli": "0.13.0",
|
|
16
|
+
"@voltro/database": "0.13.0",
|
|
17
|
+
"@voltro/env": "0.13.0",
|
|
18
|
+
"@voltro/plugin-moderation": "0.13.0",
|
|
19
|
+
"@voltro/plugin-multitenancy": "0.13.0",
|
|
20
|
+
"@voltro/protocol": "0.13.0",
|
|
21
|
+
"@voltro/runtime": "0.13.0",
|
|
22
22
|
"effect": "^3.21.2"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@voltro/testing": "0.
|
|
25
|
+
"@voltro/testing": "0.13.0",
|
|
26
26
|
"typescript": "^5.7.0",
|
|
27
27
|
"vitest": "^3.0.0"
|
|
28
28
|
}
|
|
@@ -12,17 +12,17 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@effect/platform": "^0.96.1",
|
|
14
14
|
"@effect/rpc": "^0.75.1",
|
|
15
|
-
"@voltro/cli": "0.
|
|
16
|
-
"@voltro/database": "0.
|
|
17
|
-
"@voltro/plugin-multitenancy": "0.
|
|
18
|
-
"@voltro/plugin-prometheus": "0.
|
|
19
|
-
"@voltro/plugin-sentry": "0.
|
|
20
|
-
"@voltro/protocol": "0.
|
|
21
|
-
"@voltro/runtime": "0.
|
|
15
|
+
"@voltro/cli": "0.13.0",
|
|
16
|
+
"@voltro/database": "0.13.0",
|
|
17
|
+
"@voltro/plugin-multitenancy": "0.13.0",
|
|
18
|
+
"@voltro/plugin-prometheus": "0.13.0",
|
|
19
|
+
"@voltro/plugin-sentry": "0.13.0",
|
|
20
|
+
"@voltro/protocol": "0.13.0",
|
|
21
|
+
"@voltro/runtime": "0.13.0",
|
|
22
22
|
"effect": "^3.21.2"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@voltro/testing": "0.
|
|
25
|
+
"@voltro/testing": "0.13.0",
|
|
26
26
|
"typescript": "^5.7.0",
|
|
27
27
|
"vitest": "^3.0.0"
|
|
28
28
|
}
|
|
@@ -12,17 +12,17 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@effect/platform": "^0.96.1",
|
|
14
14
|
"@effect/rpc": "^0.75.1",
|
|
15
|
-
"@voltro/cli": "0.
|
|
16
|
-
"@voltro/database": "0.
|
|
17
|
-
"@voltro/env": "0.
|
|
18
|
-
"@voltro/plugin-multitenancy": "0.
|
|
19
|
-
"@voltro/plugin-ratelimit": "0.
|
|
20
|
-
"@voltro/protocol": "0.
|
|
21
|
-
"@voltro/runtime": "0.
|
|
15
|
+
"@voltro/cli": "0.13.0",
|
|
16
|
+
"@voltro/database": "0.13.0",
|
|
17
|
+
"@voltro/env": "0.13.0",
|
|
18
|
+
"@voltro/plugin-multitenancy": "0.13.0",
|
|
19
|
+
"@voltro/plugin-ratelimit": "0.13.0",
|
|
20
|
+
"@voltro/protocol": "0.13.0",
|
|
21
|
+
"@voltro/runtime": "0.13.0",
|
|
22
22
|
"effect": "^3.21.2"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@voltro/testing": "0.
|
|
25
|
+
"@voltro/testing": "0.13.0",
|
|
26
26
|
"typescript": "^5.7.0",
|
|
27
27
|
"vitest": "^3.0.0"
|
|
28
28
|
}
|
|
@@ -13,19 +13,39 @@ pnpm --filter @{{projectName}}/{{appName}} dev
|
|
|
13
13
|
|
|
14
14
|
## What this shows
|
|
15
15
|
|
|
16
|
-
- **Roles → scopes, as config** — `
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- **
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
- **
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
16
|
+
- **Roles → scopes, as config** — `authz.ts` declares the role map; an rpc
|
|
17
|
+
interceptor resolves each caller's roles to scopes and publishes them to the
|
|
18
|
+
framework's effective-scope seam. That map is also the app's **declared scope
|
|
19
|
+
vocabulary**: `voltro check` fails on a guard requiring a scope no role grants.
|
|
20
|
+
- **Declarative guards** (`notes.create`, `notes.list`) — `guards: [{ scope }]`
|
|
21
|
+
on the descriptor. Enforced before the executor and before the transaction,
|
|
22
|
+
and the only form `voltro check` can inspect. **Prefer this.**
|
|
23
|
+
- **Resource-scoped guards** (`teams.rename`) — `guards: [{ scope, resource }]`
|
|
24
|
+
plus `resolveResourceRoles`, so "owner" means owner *of that team*.
|
|
25
|
+
- **In-handler guards** (`notes.delete`) — `can()` / `permission()` for the
|
|
26
|
+
decision a descriptor cannot make, because it depends on the loaded row.
|
|
27
|
+
- **Where roles come from** — the shipped `resolveRoles` is a DEMO mapping the
|
|
28
|
+
request's **tenant** to a role, so you can try each with an `x-tenant` header.
|
|
29
|
+
Production reads the caller's real roles (`subject.metadata.roles`, or a DB
|
|
30
|
+
lookup).
|
|
31
|
+
|
|
32
|
+
## Which guard form?
|
|
33
|
+
|
|
34
|
+
Put in `guards:` everything decidable from the **subject** and the **input**.
|
|
35
|
+
Reach for `permission()` / `can()` only for what needs loaded data.
|
|
36
|
+
|
|
37
|
+
The difference is not style. A declarative guard lands in the capability
|
|
38
|
+
manifest, so `voltro check` catches a scope no role grants — a bug that is
|
|
39
|
+
otherwise invisible, because the procedure simply becomes uncallable by
|
|
40
|
+
everyone, forever, with no error anywhere. An in-handler call is invisible to
|
|
41
|
+
that check; a typo there is a branch that is silently never taken.
|
|
42
|
+
|
|
43
|
+
## Failure postures
|
|
44
|
+
|
|
45
|
+
| Resolver | On throw / reject |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `resolveRoles` / `resolvePermissions` | logs, degrades to the subject's **own** scopes — never grants |
|
|
48
|
+
| `resolveResourceRoles` | **denies** (fail-closed) |
|
|
29
49
|
|
|
30
50
|
## Try it
|
|
31
51
|
|
|
@@ -41,6 +61,25 @@ curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json
|
|
|
41
61
|
# → { ok:false, error:{ _tag:"ScopeError", required:"notes:write" } }
|
|
42
62
|
```
|
|
43
63
|
|
|
64
|
+
```bash
|
|
65
|
+
voltro check --offline # no running app needed — usable as a CI gate
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Tests
|
|
69
|
+
|
|
70
|
+
`tests/authz.test.ts` drives the **real** plugin — `makeTestContext({ plugins })`
|
|
71
|
+
composes the actual interceptor chain and `invoke` enforces the descriptor's
|
|
72
|
+
guards with the same function the serve pipeline calls.
|
|
73
|
+
|
|
74
|
+
Do not hand-set `subject.scopes` to the value you expect. That tests the guard
|
|
75
|
+
while assuming the resolution that produces it, and the resolution is the half
|
|
76
|
+
that actually breaks (a renamed role, a throwing resolver, a scope no role
|
|
77
|
+
grants) — all of which pass a pre-stamped test.
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pnpm --filter @{{projectName}}/{{appName}} test
|
|
81
|
+
```
|
|
82
|
+
|
|
44
83
|
## On the web side
|
|
45
84
|
|
|
46
85
|
```tsx
|
|
@@ -54,7 +93,7 @@ const canWrite = useCan('notes:write')
|
|
|
54
93
|
The UI gate ships in `@voltro/client`, not in the plugin: scopes are a framework
|
|
55
94
|
concept, so gating a button must not require an rbac dependency.
|
|
56
95
|
|
|
57
|
-
`useCan` is a UI affordance only — the server
|
|
96
|
+
`useCan` is a UI affordance only — the server-side guard is the real
|
|
58
97
|
enforcement. Never trust the client.
|
|
59
98
|
|
|
60
99
|
## Persisted roles
|
|
@@ -1,26 +1,31 @@
|
|
|
1
1
|
// RBAC backend for the {{projectName}} project.
|
|
2
2
|
//
|
|
3
3
|
// `rbacPlugin` compiles a role→scope map; an rpc interceptor resolves each
|
|
4
|
-
// caller's ROLES to SCOPES and
|
|
5
|
-
//
|
|
6
|
-
// `Forbidden`) just work. The `admin:full` scope is a blanket bypass.
|
|
4
|
+
// caller's ROLES to SCOPES and publishes them to the framework's effective-
|
|
5
|
+
// scope seam. From there BOTH authorization forms see them:
|
|
7
6
|
//
|
|
8
|
-
//
|
|
7
|
+
// • declarative `guards:` on a descriptor — enforced by the framework in the
|
|
8
|
+
// dispatch spine, before the executor and (for a mutation) before the
|
|
9
|
+
// transaction opens. PREFER THIS. It is the only form `voltro check` can
|
|
10
|
+
// inspect, so a guard requiring a scope no role grants is caught
|
|
11
|
+
// statically instead of becoming a permanently uncallable procedure.
|
|
12
|
+
// • in-handler `permission()` / `can()` — for authz that needs LOADED data a
|
|
13
|
+
// descriptor guard cannot see (see `notes.delete`).
|
|
14
|
+
//
|
|
15
|
+
// The `admin:full` scope is a blanket bypass for both.
|
|
16
|
+
//
|
|
17
|
+
// The role map itself lives in `authz.ts` so the tests can build the real
|
|
18
|
+
// plugin from the real roles.
|
|
19
|
+
//
|
|
20
|
+
// Config-only — no external infra.
|
|
9
21
|
import { defineEnv, envVar } from '@voltro/env'
|
|
10
22
|
import { rbacPlugin } from '@voltro/plugin-rbac'
|
|
23
|
+
import { demoRolesForTenant, roles, rolesOnTeam } from './authz'
|
|
11
24
|
|
|
12
25
|
export const env = defineEnv({
|
|
13
26
|
LOG_LEVEL: envVar.enum(['debug', 'info', 'warn', 'error'], { access: 'public', default: 'info' }),
|
|
14
27
|
})
|
|
15
28
|
|
|
16
|
-
// role → the scopes it grants. Scopes are `domain:action`; `admin:full` is the
|
|
17
|
-
// framework's blanket bypass (passes every permission check).
|
|
18
|
-
const roles = {
|
|
19
|
-
viewer: ['notes:read'],
|
|
20
|
-
editor: ['notes:read', 'notes:write'],
|
|
21
|
-
admin: ['admin:full'],
|
|
22
|
-
}
|
|
23
|
-
|
|
24
29
|
export default {
|
|
25
30
|
type: 'api' as const,
|
|
26
31
|
name: '{{capProjectName}}{{capAppName}}',
|
|
@@ -28,20 +33,13 @@ export default {
|
|
|
28
33
|
plugins: [
|
|
29
34
|
rbacPlugin({
|
|
30
35
|
roles,
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
const demo: Record<string, ReadonlyArray<string>> = {
|
|
39
|
-
acme: ['admin'],
|
|
40
|
-
editors: ['editor'],
|
|
41
|
-
readers: ['viewer'],
|
|
42
|
-
}
|
|
43
|
-
return demo[subject.tenantId ?? ''] ?? ['viewer']
|
|
44
|
-
},
|
|
36
|
+
resolveRoles: (subject) => demoRolesForTenant(subject.tenantId),
|
|
37
|
+
// PER-RESOURCE roles — what makes `guards: [{ scope, resource }]` mean
|
|
38
|
+
// "owner OF THIS TEAM" instead of "owner globally". Consulted only for
|
|
39
|
+
// the gap: a globally-held scope (or `admin:full`) passes without ever
|
|
40
|
+
// calling this. Fail-CLOSED — if it throws or rejects the guard DENIES,
|
|
41
|
+
// it does not fall back to the global answer.
|
|
42
|
+
resolveResourceRoles: (subject, resource) => rolesOnTeam(subject.id, resource),
|
|
45
43
|
}),
|
|
46
44
|
],
|
|
47
45
|
env,
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// The app's authorization vocabulary, in ONE place.
|
|
2
|
+
//
|
|
3
|
+
// Split out of `app.config.ts` so the tests can build the real plugin from the
|
|
4
|
+
// real role map. A test that redeclares the roles it is testing proves only
|
|
5
|
+
// that its own copy is self-consistent — the copy drifts, the test stays
|
|
6
|
+
// green, and the drift is exactly the thing worth catching.
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* role → the scopes it grants. Scopes are `domain:action`; `'*'` expands to
|
|
10
|
+
* the framework's blanket bypass (`admin:full`), which passes every check.
|
|
11
|
+
*
|
|
12
|
+
* This map is ALSO the app's declared scope vocabulary: `rbacPlugin` publishes
|
|
13
|
+
* the union of it, and `voltro check` errors on any descriptor guard requiring
|
|
14
|
+
* a scope that appears nowhere here. So: add a scope to a guard → add it to a
|
|
15
|
+
* role, or the build tells you the procedure is uncallable.
|
|
16
|
+
*
|
|
17
|
+
* (`notes:purge`, used by `notes.delete` via `can()`, is deliberately absent —
|
|
18
|
+
* only the wildcard `admin` role reaches it. See that handler for why that is
|
|
19
|
+
* a lesson rather than an oversight.)
|
|
20
|
+
*/
|
|
21
|
+
export const roles = {
|
|
22
|
+
viewer: ['notes:read'],
|
|
23
|
+
editor: ['notes:read', 'notes:write'],
|
|
24
|
+
owner: ['notes:read', 'notes:write', 'notes:delete', 'teams:rename'],
|
|
25
|
+
admin: ['*'],
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* DEMO membership table — which roles a caller holds ON A SPECIFIC team.
|
|
30
|
+
* PRODUCTION does a DB read (a `memberships` lookup keyed by `subject.id` +
|
|
31
|
+
* the resource id); this literal keeps the template infra-free.
|
|
32
|
+
*/
|
|
33
|
+
export const teamMemberships: Record<string, Record<string, ReadonlyArray<string>>> = {
|
|
34
|
+
// subjectId → teamId → the roles held on THAT team
|
|
35
|
+
u_alice: { team_core: ['owner'], team_marketing: ['viewer'] },
|
|
36
|
+
u_bob: { team_marketing: ['owner'] },
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Roles the caller holds on ONE resource. `Subject.id` is `string | null`
|
|
41
|
+
* (an anonymous caller has no id), and an anonymous caller is a member of
|
|
42
|
+
* nothing — so the null case returns `[]` rather than being coerced into a
|
|
43
|
+
* lookup key that could collide.
|
|
44
|
+
*/
|
|
45
|
+
export const rolesOnTeam = (
|
|
46
|
+
subjectId: string | null | undefined,
|
|
47
|
+
teamId: string,
|
|
48
|
+
): ReadonlyArray<string> =>
|
|
49
|
+
subjectId == null ? [] : teamMemberships[subjectId]?.[teamId] ?? []
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* DEMO role resolver — assigns a role by the request's TENANT so every role is
|
|
53
|
+
* reachable with an `x-tenant` header and zero auth setup.
|
|
54
|
+
*
|
|
55
|
+
* PRODUCTION reads the caller's REAL roles: the default resolver reads
|
|
56
|
+
* `subject.metadata.roles` (set by your auth strategy), or do a DB lookup here
|
|
57
|
+
* (`subject.id` / `subject.tenantId` → roles). Delete this and omit
|
|
58
|
+
* `resolveRoles` entirely to fall back to the metadata default.
|
|
59
|
+
*
|
|
60
|
+
* Know the failure posture whatever you put here: if this throws or rejects,
|
|
61
|
+
* rbac logs and degrades to the subject's OWN scopes. It never grants on
|
|
62
|
+
* failure — but it also does not deny a caller whose api key already carried
|
|
63
|
+
* valid scopes.
|
|
64
|
+
*/
|
|
65
|
+
export const demoRolesForTenant = (
|
|
66
|
+
// `Subject.tenantId` is `string | null` — an anonymous caller has no tenant.
|
|
67
|
+
tenantId: string | null | undefined,
|
|
68
|
+
): ReadonlyArray<string> => {
|
|
69
|
+
const demo: Record<string, ReadonlyArray<string>> = {
|
|
70
|
+
acme: ['admin'],
|
|
71
|
+
owners: ['owner'],
|
|
72
|
+
editors: ['editor'],
|
|
73
|
+
readers: ['viewer'],
|
|
74
|
+
}
|
|
75
|
+
return demo[tenantId ?? ''] ?? ['viewer']
|
|
76
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
// Schema for the {{projectName}} RBAC backend — a
|
|
2
|
-
//
|
|
1
|
+
// Schema for the {{projectName}} RBAC backend — a `notes` domain guarded by
|
|
2
|
+
// GLOBAL scopes, plus a `teams` domain guarded PER RESOURCE. `actors` +
|
|
3
|
+
// `tenants` are the framework core tables.
|
|
3
4
|
|
|
4
5
|
import {
|
|
6
|
+
boolean,
|
|
5
7
|
databaseHandle,
|
|
6
8
|
id,
|
|
7
9
|
table,
|
|
@@ -25,13 +27,28 @@ export const tenants = table('tenants', {
|
|
|
25
27
|
})
|
|
26
28
|
|
|
27
29
|
export const notes = table('notes', {
|
|
28
|
-
id:
|
|
29
|
-
title:
|
|
30
|
-
body:
|
|
30
|
+
id: id({ prefix: 'note' }),
|
|
31
|
+
title: text(),
|
|
32
|
+
body: text(),
|
|
33
|
+
// `notes.delete` demonstrates authz that a descriptor guard CANNOT express:
|
|
34
|
+
// whether a note may be hard-deleted depends on this column, which is only
|
|
35
|
+
// known after the row is loaded.
|
|
36
|
+
archived: boolean().default(false),
|
|
37
|
+
})
|
|
38
|
+
.with(tenant())
|
|
39
|
+
.reactive()
|
|
40
|
+
|
|
41
|
+
// Guarded per-resource rather than globally: holding `teams:rename` on
|
|
42
|
+
// `team_core` must not let you rename `team_marketing`. The grant comes from
|
|
43
|
+
// `resolveResourceRoles` in app.config.ts.
|
|
44
|
+
export const teams = table('teams', {
|
|
45
|
+
id: id({ prefix: 'team' }),
|
|
46
|
+
name: text(),
|
|
31
47
|
})
|
|
32
48
|
.with(tenant())
|
|
33
49
|
.reactive()
|
|
34
50
|
|
|
35
51
|
export type Note = InferRow<typeof notes>
|
|
52
|
+
export type Team = InferRow<typeof teams>
|
|
36
53
|
|
|
37
|
-
export const database = databaseHandle({ actors, tenants, notes })
|
|
54
|
+
export const database = databaseHandle({ actors, tenants, notes, teams })
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
// No authz in here — deliberately. The descriptor's `guards: [{ scope:
|
|
2
|
+
// 'notes:write' }]` already refused an unscoped caller before this executor
|
|
3
|
+
// was reached. Re-checking the same scope by hand would be a second copy of
|
|
4
|
+
// the rule that can drift from the first, and the one `voltro check` reads is
|
|
5
|
+
// the descriptor's.
|
|
6
|
+
|
|
1
7
|
import { EffectStore } from '@voltro/runtime'
|
|
2
8
|
import type { AppContext } from '@voltro/runtime'
|
|
3
|
-
import { permission } from '@voltro/plugin-rbac'
|
|
4
9
|
import { Effect } from 'effect'
|
|
5
10
|
|
|
6
|
-
const execute = (input: { title: string; body: string },
|
|
11
|
+
const execute = (input: { title: string; body: string }, _ctx: AppContext) =>
|
|
7
12
|
Effect.gen(function* () {
|
|
8
|
-
// RBAC guard — passes for editor/admin, fails typed ScopeError for viewer.
|
|
9
|
-
yield* permission(ctx, 'notes:write')
|
|
10
|
-
|
|
11
13
|
const store = yield* EffectStore
|
|
12
14
|
// tenant() auto-fills tenantId from the subject — no need to pass it.
|
|
13
15
|
const row = yield* store.insert('notes', { title: input.title, body: input.body })
|