Introduce Casbin Middleware (#676)
* feat: impl casbin middleware * fix: defaultCheckPermission logic * fix: divide authorizer * chore: testing and docs * fixed: hono version * fix: export and module resolution * fix: typo * docs: fix model * fix: conflict * fix: versionpull/736/head
parent
69b3cfe726
commit
b7e740f930
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@hono/casbin': major
|
||||
---
|
||||
|
||||
Initial release
|
|
@ -0,0 +1,25 @@
|
|||
name: ci-cabin
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'packages/cabin/**'
|
||||
pull_request:
|
||||
branches: ['*']
|
||||
paths:
|
||||
- 'packages/cabin/**'
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./packages/cabin
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn build
|
||||
- run: yarn test
|
|
@ -35,6 +35,7 @@
|
|||
"build:react-compat": "yarn workspace @hono/react-compat build",
|
||||
"build:effect-validator": "yarn workspace @hono/effect-validator build",
|
||||
"build:conform-validator": "yarn workspace @hono/conform-validator build",
|
||||
"build:casbin": "yarn workspace @hono/casbin build",
|
||||
"build": "run-p 'build:*'",
|
||||
"lint": "eslint 'packages/**/*.{ts,tsx}'",
|
||||
"lint:fix": "eslint --fix 'packages/**/*.{ts,tsx}'",
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
# Casbin Middleware for Hono
|
||||
|
||||
This is a third-party [Casbin](https://casbin.org) middleware for [Hono](https://github.com/honojs/hono).
|
||||
|
||||
This middleware can be used to enforce authorization policies defined using Casbin in your Hono routes.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm i hono @hono/casbin casbin
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Before using the middleware, you must set up your Casbin model and policy files.
|
||||
|
||||
For details on how to write authorization policies and other information, please refer to the [Casbin documentation](https://casbin.org/).
|
||||
|
||||
### Example model.conf
|
||||
|
||||
```conf
|
||||
[request_definition]
|
||||
r = sub, obj, act
|
||||
|
||||
[policy_definition]
|
||||
p = sub, obj, act
|
||||
|
||||
[policy_effect]
|
||||
e = some(where (p.eft == allow))
|
||||
|
||||
[matchers]
|
||||
m = r.sub == p.sub && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*")
|
||||
```
|
||||
|
||||
### Example policy.csv
|
||||
|
||||
```csv
|
||||
p, alice, /dataset1/*, *
|
||||
p, bob, /dataset1/*, GET
|
||||
```
|
||||
|
||||
## Usage with Basic HTTP Authentication
|
||||
|
||||
You can perform authorization control after Basic authentication by combining it with `basicAuthorizer`.
|
||||
(The client needs to send `Authentication: Basic {Base64Encoded(username:password)}`.)
|
||||
|
||||
Let's look at an example.
|
||||
Use the `model` and `policy` files from the [Configuration](#configuration) section.
|
||||
You can implement a scenario where `alice` and `bob` have different permissions. Alice has access to all methods on `/dataset1/test`, while Bob has access only to the `GET` method.
|
||||
|
||||
```ts
|
||||
import { Hono } from 'hono'
|
||||
import { basicAuth } from 'hono/basic-auth'
|
||||
import { newEnforcer } from 'casbin'
|
||||
import { casbin } from '@hono/casbin'
|
||||
import { basicAuthorizer } from '@hono/casbin/helper'
|
||||
|
||||
const app = new Hono()
|
||||
app.use('*',
|
||||
basicAuth(
|
||||
{
|
||||
username: 'alice', // alice has full access to /dataset1/test
|
||||
password: 'password',
|
||||
},
|
||||
{
|
||||
username: 'bob', // bob cannot post to /dataset1/test
|
||||
password: 'password',
|
||||
}
|
||||
),
|
||||
casbin({
|
||||
newEnforcer: newEnforcer('examples/model.conf', 'examples/policy.csv'),
|
||||
authorizer: basicAuthorizer
|
||||
})
|
||||
)
|
||||
app.get('/dataset1/test', (c) => c.text('dataset1 test')) // alice and bob can access /dataset1/test
|
||||
app.post('/dataset1/test', (c) => c.text('dataset1 test')) // Only alice can access /dataset1/test
|
||||
```
|
||||
|
||||
## Usage with JWT Authentication
|
||||
|
||||
By using `jwtAuthorizer`, you can perform authorization control after JWT authentication.
|
||||
By default, `jwtAuthorizer` uses the `sub` in the JWT payload as the username.
|
||||
|
||||
```ts
|
||||
import { Hono } from 'hono'
|
||||
import { jwt } from 'hono/jwt'
|
||||
import { newEnforcer } from 'casbin'
|
||||
import { casbin } from '@hono/casbin'
|
||||
import { jwtAuthorizer } from '@hono/casbin/helper'
|
||||
|
||||
const app = new Hono()
|
||||
app.use('*',
|
||||
jwt({
|
||||
secret: 'it-is-very-secret',
|
||||
}),
|
||||
casbin({
|
||||
newEnforcer: newEnforcer('examples/model.conf', 'examples/policy.csv'),
|
||||
authorizer: jwtAuthorizer
|
||||
})
|
||||
)
|
||||
app.get('/dataset1/test', (c) => c.text('dataset1 test')) // alice and bob can access /dataset1/test
|
||||
app.post('/dataset1/test', (c) => c.text('dataset1 test')) // Only alice can access /dataset1/test
|
||||
```
|
||||
|
||||
Of course, you can use claims other than the `sub` claim.
|
||||
Specify the `key` as a user-friendly name and the `value` as the JWT claim name. The `Payload` key used for evaluation in the enforcer will be the `value`.
|
||||
|
||||
```ts
|
||||
const claimMapping = {
|
||||
username: 'username',
|
||||
}
|
||||
// ...
|
||||
casbin({
|
||||
newEnforcer: newEnforcer('examples/model.conf', 'examples/policy.csv'),
|
||||
authorizer: (c, e) => jwtAuthorizer(c, e, claimMapping)
|
||||
})
|
||||
```
|
||||
|
||||
## Usage with Customized Authorizer
|
||||
|
||||
You can also use a customized authorizer function to handle the authorization logic.
|
||||
|
||||
```ts
|
||||
import { Hono } from 'hono'
|
||||
import { newEnforcer } from 'casbin'
|
||||
import { casbin } from '@hono/casbin'
|
||||
|
||||
const app = new Hono()
|
||||
app.use('*', casbin({
|
||||
newEnforcer: newEnforcer('path-to-your-model.conf', 'path-to-your-policy.csv'),
|
||||
authorizer: async (c, enforcer) => {
|
||||
const { user, path, method } = c
|
||||
return await enforcer.enforce(user, path, method)
|
||||
}
|
||||
}))
|
||||
```
|
||||
|
||||
## Author
|
||||
|
||||
sugar-cat https://github.com/sugar-cat7
|
|
@ -0,0 +1,14 @@
|
|||
[request_definition]
|
||||
r = sub, obj, act
|
||||
|
||||
[policy_definition]
|
||||
p = sub, obj, act
|
||||
|
||||
[role_definition]
|
||||
g = _, _
|
||||
|
||||
[policy_effect]
|
||||
e = some(where (p.eft == allow))
|
||||
|
||||
[matchers]
|
||||
m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*")
|
|
@ -0,0 +1,5 @@
|
|||
p, dataset1_admin, /dataset1/*, *
|
||||
p, dataset2_admin, /dataset2/*, *
|
||||
|
||||
g, alice, dataset1_admin
|
||||
g, bob, dataset2_admin
|
|
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"name": "@hono/casbin",
|
||||
"version": "0.0.0",
|
||||
"description": "Casbin middleware for Hono",
|
||||
"type": "module",
|
||||
"main": "dist/index.cjs",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/index.d.cts",
|
||||
"default": "./dist/index.cjs"
|
||||
}
|
||||
},
|
||||
"./helper": {
|
||||
"import": {
|
||||
"types": "./dist/helper/index.d.ts",
|
||||
"default": "./dist/helper/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/helper/index.d.cts",
|
||||
"default": "./dist/helper/index.cjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "vitest --run",
|
||||
"build": "tsup ./src/index.ts ./src/helper/index.ts --format esm,cjs --dts",
|
||||
"publint": "publint",
|
||||
"release": "yarn build && yarn test && yarn publint && yarn publish"
|
||||
},
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
"registry": "https://registry.npmjs.org",
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/honojs/middleware.git"
|
||||
},
|
||||
"homepage": "https://github.com/honojs/middleware",
|
||||
"peerDependencies": {
|
||||
"casbin": ">=5.30.0",
|
||||
"hono": ">=4.5.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"casbin": "^5.30.0",
|
||||
"hono": "^4.5.11",
|
||||
"tsup": "^8.1.0",
|
||||
"typescript": "^5.5.3",
|
||||
"vitest": "^2.0.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import type { Enforcer } from 'casbin'
|
||||
import type { Context } from 'hono'
|
||||
import { auth } from 'hono/utils/basic-auth'
|
||||
|
||||
const getUserName = (c: Context): string => {
|
||||
const requestUser = auth(c.req.raw)
|
||||
if (!requestUser) {
|
||||
return ''
|
||||
}
|
||||
return requestUser.username
|
||||
}
|
||||
|
||||
export const basicAuthorizer = async (c: Context, enforcer: Enforcer): Promise<boolean> => {
|
||||
const { path, method } = c.req
|
||||
const user = getUserName(c)
|
||||
return enforcer.enforce(user, path, method)
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from './jwt'
|
||||
export * from './basic-auth'
|
|
@ -0,0 +1,36 @@
|
|||
import { decode } from 'hono/jwt'
|
||||
import type { Enforcer } from 'casbin'
|
||||
import type { Context } from 'hono'
|
||||
import type { JWTPayload } from 'hono/utils/jwt/types'
|
||||
|
||||
export const jwtAuthorizer = async (
|
||||
c: Context,
|
||||
enforcer: Enforcer,
|
||||
claimMapping: Record<string, string> = { userID: 'sub' }
|
||||
): Promise<boolean> => {
|
||||
// Note: if use hono/jwt, the payload is stored in c.get('jwtPayload')
|
||||
// https://github.com/honojs/hono/blob/8ba02273e829318d7f8797267f52229e531b8bd5/src/middleware/jwt/jwt.ts#L136
|
||||
let payload: JWTPayload = c.get('jwtPayload')
|
||||
|
||||
if (!payload) {
|
||||
const credentials = c.req.header('Authorization')
|
||||
if (!credentials) return false
|
||||
|
||||
const parts = credentials.split(/\s+/)
|
||||
if (parts.length !== 2 || parts[0] !== 'Bearer') return false
|
||||
|
||||
const token = parts[1]
|
||||
|
||||
try {
|
||||
const decoded = decode(token)
|
||||
payload = decoded.payload
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const args = Object.values(claimMapping).map((key) => payload[key])
|
||||
|
||||
const { path, method } = c.req
|
||||
return await enforcer.enforce(...args, path, method)
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { Enforcer } from 'casbin'
|
||||
import { type Context, MiddlewareHandler } from 'hono'
|
||||
|
||||
interface CasbinOptions {
|
||||
newEnforcer: Promise<Enforcer>
|
||||
authorizer: (c: Context, enforcer: Enforcer) => Promise<boolean>
|
||||
}
|
||||
|
||||
export const casbin = (opt: CasbinOptions): MiddlewareHandler => {
|
||||
return async (c, next) => {
|
||||
const enforcer = await opt.newEnforcer
|
||||
if (!(enforcer instanceof Enforcer)) {
|
||||
return c.json({ error: 'Invalid enforcer' }, 500)
|
||||
}
|
||||
|
||||
const isAllowed = await opt.authorizer(c, enforcer)
|
||||
if (!isAllowed) {
|
||||
return c.json({ error: 'Forbidden' }, 403)
|
||||
}
|
||||
await next()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,738 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import { Hono } from 'hono'
|
||||
import { newEnforcer } from 'casbin'
|
||||
import { casbin } from '../src'
|
||||
import { basicAuthorizer, jwtAuthorizer } from '../src/helper'
|
||||
import { jwt, sign } from 'hono/jwt'
|
||||
import { basicAuth } from 'hono/basic-auth'
|
||||
|
||||
describe('Casbin Middleware Tests', () => {
|
||||
describe('BasicAuthorizer', () => {
|
||||
const app = new Hono()
|
||||
const enforcer = newEnforcer('examples/model.conf', 'examples/policy.csv')
|
||||
app.use('*', casbin({ newEnforcer: enforcer, authorizer: basicAuthorizer }))
|
||||
app.get('/dataset1/test', (c) => c.text('dataset1 test'))
|
||||
app.post('/dataset1/test', (c) => c.text('dataset1 test'))
|
||||
app.put('/dataset1/test', (c) => c.text('dataset1 test'))
|
||||
app.delete('/dataset1/test', (c) => c.text('dataset1 test'))
|
||||
|
||||
it('test[Success]: p, alice, /dataset1/*, GET', async () => {
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from('alice:password').toString('base64')}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
|
||||
it('test[Success]: p, alice, /dataset1/*, POST', async () => {
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from('alice:password').toString('base64')}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
|
||||
it('test[Success]: p, alice, /dataset1/*, PUT', async () => {
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from('alice:password').toString('base64')}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
|
||||
it('test[Success]: p, alice, /dataset1/*, DELETE', async () => {
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from('alice:password').toString('base64')}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
|
||||
it('test[Not Exist User]: p, cathy, /dataset1/*, - GET 403', async () => {
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from('cathy:password').toString('base64')}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('test[Not Exist User]: p, cathy, /dataset1/*, - POST 403', async () => {
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from('cathy:password').toString('base64')}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('test[Not Exist User]: p, cathy, /dataset1/*, - PUT 403', async () => {
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from('cathy:password').toString('base64')}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('test[Not Exist User]: p, cathy, /dataset1/*, - DELETE 403', async () => {
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from('cathy:password').toString('base64')}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('test[Insufficient Permissions]: p, bob, /dataset1/*, - GET 403', async () => {
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from('bob:password').toString('base64')}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('test[Insufficient Permissions]: p, bob, /dataset1/*, - POST 403', async () => {
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from('bob:password').toString('base64')}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('test[Insufficient Permissions]: p, bob, /dataset1/*, - PUT 403', async () => {
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from('bob:password').toString('base64')}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('test[Insufficient Permissions]: p, bob, /dataset1/*, - DELETE 403', async () => {
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from('bob:password').toString('base64')}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
})
|
||||
|
||||
describe('BasicAuthorizer with hono/basic-auth', () => {
|
||||
const app = new Hono()
|
||||
const enforcer = newEnforcer('examples/model.conf', 'examples/policy.csv')
|
||||
app.use(
|
||||
'*',
|
||||
basicAuth(
|
||||
{
|
||||
username: 'alice',
|
||||
password: 'password',
|
||||
},
|
||||
{
|
||||
username: 'bob',
|
||||
password: 'password',
|
||||
}
|
||||
),
|
||||
casbin({ newEnforcer: enforcer, authorizer: basicAuthorizer })
|
||||
)
|
||||
app.get('/dataset1/test', (c) => c.text('dataset1 test'))
|
||||
app.post('/dataset1/test', (c) => c.text('dataset1 test'))
|
||||
app.put('/dataset1/test', (c) => c.text('dataset1 test'))
|
||||
app.delete('/dataset1/test', (c) => c.text('dataset1 test'))
|
||||
|
||||
it('test[Success]: p, alice, /dataset1/*, GET', async () => {
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from('alice:password').toString('base64')}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
|
||||
it('test[Success]: p, alice, /dataset1/*, POST', async () => {
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from('alice:password').toString('base64')}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
|
||||
it('test[Success]: p, alice, /dataset1/*, PUT', async () => {
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from('alice:password').toString('base64')}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
|
||||
it('test[Success]: p, alice, /dataset1/*, DELETE', async () => {
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from('alice:password').toString('base64')}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
|
||||
it('test[Insufficient Permissions]: p, bob, /dataset1/*, - GET 403', async () => {
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from('bob:password').toString('base64')}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('test[Insufficient Permissions]: p, bob, /dataset1/*, - POST 403', async () => {
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from('bob:password').toString('base64')}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('test[Insufficient Permissions]: p, bob, /dataset1/*, - PUT 403', async () => {
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from('bob:password').toString('base64')}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('test[Insufficient Permissions]: p, bob, /dataset1/*, - DELETE 403', async () => {
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from('bob:password').toString('base64')}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
})
|
||||
|
||||
describe('JWTAuthorizer', () => {
|
||||
const app = new Hono()
|
||||
const enforcer = newEnforcer('examples/model.conf', 'examples/policy.csv')
|
||||
|
||||
app.use('*', casbin({ newEnforcer: enforcer, authorizer: jwtAuthorizer }))
|
||||
app.get('/dataset1/test', (c) => c.text('dataset1 test'))
|
||||
app.post('/dataset1/test', (c) => c.text('dataset1 test'))
|
||||
app.put('/dataset1/test', (c) => c.text('dataset1 test'))
|
||||
app.delete('/dataset1/test', (c) => c.text('dataset1 test'))
|
||||
|
||||
it('test[Success]: p, alice, /dataset1/*, GET 200', async () => {
|
||||
const token = await sign({ sub: 'alice' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
|
||||
it('test[Success]: p, alice, /dataset1/*, POST 200', async () => {
|
||||
const token = await sign({ sub: 'alice' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
|
||||
it('test[Success]: p, alice, /dataset1/*, PUT 200', async () => {
|
||||
const token = await sign({ sub: 'alice' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
|
||||
it('test[Success]: p, alice, /dataset1/*, DELETE 200', async () => {
|
||||
const token = await sign({ sub: 'alice' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
|
||||
it('test[Not Exist User]: p & g, dataset1_admin, /dataset1/*, * - GET 403', async () => {
|
||||
const token = await sign({ sub: 'cathy' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('test[Not Exist User]: p & g, dataset1_admin, /dataset1/*, * - POST 403', async () => {
|
||||
const token = await sign({ sub: 'cathy' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('test[Not Exist User]: p & g, dataset1_admin, /dataset1/*, * - PUT 403', async () => {
|
||||
const token = await sign({ sub: 'cathy' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('test[Not Exist User]: p & g, dataset1_admin, /dataset1/*, * - DELETE 403', async () => {
|
||||
const token = await sign({ sub: 'cathy' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('test[Insufficient Permissions]: p, bob, /dataset1/test - GET 403', async () => {
|
||||
const token = await sign({ sub: 'bob' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('test[Insufficient Permissions]: p, bob, /dataset1/test - POST 403', async () => {
|
||||
const token = await sign({ sub: 'bob' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('test[Insufficient Permissions]: p, bob, /dataset1/test - PUT 403', async () => {
|
||||
const token = await sign({ sub: 'bob' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('test[Insufficient Permissions]: p, bob, /dataset1/test - DELETE 403', async () => {
|
||||
const token = await sign({ sub: 'bob' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
})
|
||||
|
||||
describe('JWTAuthorizer With Custom Claim', () => {
|
||||
const app = new Hono()
|
||||
const enforcer = newEnforcer('examples/model.conf', 'examples/policy.csv')
|
||||
const customClaimMapping = { userID: 'custom_id' }
|
||||
app.use(
|
||||
'*',
|
||||
casbin({
|
||||
newEnforcer: enforcer,
|
||||
authorizer: (c, e) => jwtAuthorizer(c, e, customClaimMapping),
|
||||
})
|
||||
)
|
||||
app.get('/dataset1/test', (c) => c.text('dataset1 test'))
|
||||
app.post('/dataset1/test', (c) => c.text('dataset1 test'))
|
||||
app.put('/dataset1/test', (c) => c.text('dataset1 test'))
|
||||
app.delete('/dataset1/test', (c) => c.text('dataset1 test'))
|
||||
|
||||
it('test[Success]: p, alice, /dataset1/*, GET 200', async () => {
|
||||
const token = await sign({ custom_id: 'alice' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
|
||||
it('test[Success]: p, alice, /dataset1/*, POST 200', async () => {
|
||||
const token = await sign({ custom_id: 'alice' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
|
||||
it('test[Success]: p, alice, /dataset1/*, PUT 200', async () => {
|
||||
const token = await sign({ custom_id: 'alice' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
|
||||
it('test[Success]: p, alice, /dataset1/*, DELETE 200', async () => {
|
||||
const token = await sign({ custom_id: 'alice' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
|
||||
it('test[Not Exist User]: p & g, dataset1_admin, /dataset1/*, * - GET 403', async () => {
|
||||
const token = await sign({ custom_id: 'cathy' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('test[Not Exist User]: p & g, dataset1_admin, /dataset1/*, * - POST 403', async () => {
|
||||
const token = await sign({ custom_id: 'cathy' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('test[Not Exist User]: p & g, dataset1_admin, /dataset1/*, * - PUT 403', async () => {
|
||||
const token = await sign({ custom_id: 'cathy' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('test[Not Exist User]: p & g, dataset1_admin, /dataset1/*, * - DELETE 403', async () => {
|
||||
const token = await sign({ custom_id: 'cathy' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('test[Insufficient Permissions]: p, bob, /dataset1/test - GET 403', async () => {
|
||||
const token = await sign({ custom_id: 'bob' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('test[Insufficient Permissions]: p, bob, /dataset1/test - POST 403', async () => {
|
||||
const token = await sign({ custom_id: 'bob' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('test[Insufficient Permissions]: p, bob, /dataset1/test - PUT 403', async () => {
|
||||
const token = await sign({ custom_id: 'bob' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('test[Insufficient Permissions]: p, bob, /dataset1/test - DELETE 403', async () => {
|
||||
const token = await sign({ custom_id: 'bob' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
})
|
||||
|
||||
describe('JWTAuthorizer With hono/jwt', () => {
|
||||
const app = new Hono()
|
||||
const enforcer = newEnforcer('examples/model.conf', 'examples/policy.csv')
|
||||
app.use(
|
||||
'*',
|
||||
jwt({
|
||||
secret: 'secret',
|
||||
}),
|
||||
casbin({ newEnforcer: enforcer, authorizer: jwtAuthorizer })
|
||||
)
|
||||
app.get('/dataset1/test', (c) => c.text('dataset1 test'))
|
||||
app.post('/dataset1/test', (c) => c.text('dataset1 test'))
|
||||
app.put('/dataset1/test', (c) => c.text('dataset1 test'))
|
||||
app.delete('/dataset1/test', (c) => c.text('dataset1 test'))
|
||||
|
||||
it('test[Success]: p, alice, /dataset1/*, GET 200', async () => {
|
||||
const token = await sign({ sub: 'alice' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
|
||||
it('test[Success]: p, alice, /dataset1/*, POST 200', async () => {
|
||||
const token = await sign({ sub: 'alice' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
|
||||
it('test[Success]: p, alice, /dataset1/*, PUT 200', async () => {
|
||||
const token = await sign({ sub: 'alice' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
|
||||
it('test[Success]: p, alice, /dataset1/*, DELETE 200', async () => {
|
||||
const token = await sign({ sub: 'alice' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
|
||||
it('test[Not Exist User]: p & g, dataset1_admin, /dataset1/*, * - GET 403', async () => {
|
||||
const token = await sign({ sub: 'cathy' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('test[Not Exist User]: p & g, dataset1_admin, /dataset1/*, * - POST 403', async () => {
|
||||
const token = await sign({ sub: 'cathy' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('test[Not Exist User]: p & g, dataset1_admin, /dataset1/*, * - PUT 403', async () => {
|
||||
const token = await sign({ sub: 'cathy' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('test[Not Exist User]: p & g, dataset1_admin, /dataset1/*, * - DELETE 403', async () => {
|
||||
const token = await sign({ sub: 'cathy' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('test[Insufficient Permissions]: p, bob, /dataset1/test - GET 403', async () => {
|
||||
const token = await sign({ sub: 'bob' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('test[Insufficient Permissions]: p, bob, /dataset1/test - POST 403', async () => {
|
||||
const token = await sign({ sub: 'bob' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('test[Insufficient Permissions]: p, bob, /dataset1/test - PUT 403', async () => {
|
||||
const token = await sign({ sub: 'bob' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('test[Insufficient Permissions]: p, bob, /dataset1/test - DELETE 403', async () => {
|
||||
const token = await sign({ sub: 'bob' }, 'secret')
|
||||
const req = new Request('http://localhost/dataset1/test', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const res = await app.fetch(req)
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"exactOptionalPropertyTypes": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/// <reference types="vitest" />
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
},
|
||||
})
|
99
yarn.lock
99
yarn.lock
|
@ -2278,6 +2278,21 @@ __metadata:
|
|||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@hono/casbin@workspace:packages/casbin":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@hono/casbin@workspace:packages/casbin"
|
||||
dependencies:
|
||||
casbin: "npm:^5.30.0"
|
||||
hono: "npm:^4.5.11"
|
||||
tsup: "npm:^8.1.0"
|
||||
typescript: "npm:^5.5.3"
|
||||
vitest: "npm:^2.0.1"
|
||||
peerDependencies:
|
||||
casbin: ">=5.30.0"
|
||||
hono: ">=4.5.11"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@hono/clerk-auth@workspace:packages/clerk-auth":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@hono/clerk-auth@workspace:packages/clerk-auth"
|
||||
|
@ -6303,6 +6318,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"await-lock@npm:^2.0.1":
|
||||
version: 2.2.2
|
||||
resolution: "await-lock@npm:2.2.2"
|
||||
checksum: bedf00dad44c6325a655bf3bd523ab9e1ce41023da6a8c379990c76ac1d942ac7e5301627ab84ba37917ab5247506ba429b7f6e4bf77074093f255571b9ad5ee
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"babel-jest@npm:^28.1.3":
|
||||
version: 28.1.3
|
||||
resolution: "babel-jest@npm:28.1.3"
|
||||
|
@ -6689,6 +6711,16 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"buffer@npm:^6.0.3":
|
||||
version: 6.0.3
|
||||
resolution: "buffer@npm:6.0.3"
|
||||
dependencies:
|
||||
base64-js: "npm:^1.3.1"
|
||||
ieee754: "npm:^1.2.1"
|
||||
checksum: 2a905fbbcde73cc5d8bd18d1caa23715d5f83a5935867c2329f0ac06104204ba7947be098fe1317fbd8830e26090ff8e764f08cd14fefc977bb248c3487bcbd0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"builtins@npm:^1.0.3":
|
||||
version: 1.0.3
|
||||
resolution: "builtins@npm:1.0.3"
|
||||
|
@ -6903,6 +6935,19 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"casbin@npm:^5.30.0":
|
||||
version: 5.30.0
|
||||
resolution: "casbin@npm:5.30.0"
|
||||
dependencies:
|
||||
await-lock: "npm:^2.0.1"
|
||||
buffer: "npm:^6.0.3"
|
||||
csv-parse: "npm:^5.3.5"
|
||||
expression-eval: "npm:^5.0.0"
|
||||
minimatch: "npm:^7.4.2"
|
||||
checksum: e3fee7b9b94f4cb6ae9b550fda80f64c46998828fb1c57b3516f6d66e1c98c6e928fa21e5c48af9d831d2dc066a24199d324d4f0866990d244ce3652ae8af509
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"catharsis@npm:^0.9.0":
|
||||
version: 0.9.0
|
||||
resolution: "catharsis@npm:0.9.0"
|
||||
|
@ -7745,6 +7790,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"csv-parse@npm:^5.3.5":
|
||||
version: 5.5.6
|
||||
resolution: "csv-parse@npm:5.5.6"
|
||||
checksum: b4f6e9b885e4488829356455157bd009f3fed4119c5fbaadab1a879e85f0a9a1b62cd01e6de68ff77a50f820a6261722bce1b799da1ace2e2126e0b7c8d86760
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"csv-stringify@npm:^5.6.5":
|
||||
version: 5.6.5
|
||||
resolution: "csv-stringify@npm:5.6.5"
|
||||
|
@ -9735,6 +9787,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"expression-eval@npm:^5.0.0":
|
||||
version: 5.0.1
|
||||
resolution: "expression-eval@npm:5.0.1"
|
||||
dependencies:
|
||||
jsep: "npm:^0.3.0"
|
||||
checksum: 74f9e1e54e50b3c924a71bcddf1550c51f15e24646f2b6cb8c45c7dd3731eb3f0e1e9a6dbf895549ddc445fe66909c373779ea6bf7f6f1e90d6bcef1590543ff
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"extend@npm:^3.0.0, extend@npm:^3.0.2":
|
||||
version: 3.0.2
|
||||
resolution: "extend@npm:3.0.2"
|
||||
|
@ -11050,9 +11111,16 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"hono@npm:^4.5.1":
|
||||
version: 4.5.1
|
||||
resolution: "hono@npm:4.5.1"
|
||||
checksum: 71228cefd3808b4bb42c10de23d67f135678eb0ab7fdfd1728d934ec2ee1241be9b1260f279b81acd2a6a57346d06d047a2650ff9bddd48fda6581751a690d80
|
||||
version: 4.5.3
|
||||
resolution: "hono@npm:4.5.3"
|
||||
checksum: 360fec2ea66b85d688dd9ce50eb6cdf94f6a2b8508e0b37b688fda3a94e7438cc4c143c0282b7aea41f7be8fa69f0b6d0039931fc8e1526c11ae09303dccce30
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hono@npm:^4.5.11":
|
||||
version: 4.5.11
|
||||
resolution: "hono@npm:4.5.11"
|
||||
checksum: 839c90273b17ed3797d34a19d12dc577d6f98590a4261bf87488880db13137c0c84a165e7810dc50af2ce4680c8fc915c0bb65673bf5fc54c6d951f18454b2c5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -11218,7 +11286,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ieee754@npm:^1.1.13":
|
||||
"ieee754@npm:^1.1.13, ieee754@npm:^1.2.1":
|
||||
version: 1.2.1
|
||||
resolution: "ieee754@npm:1.2.1"
|
||||
checksum: b0782ef5e0935b9f12883a2e2aa37baa75da6e66ce6515c168697b42160807d9330de9a32ec1ed73149aea02e0d822e572bca6f1e22bdcbd2149e13b050b17bb
|
||||
|
@ -13118,6 +13186,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jsep@npm:^0.3.0":
|
||||
version: 0.3.5
|
||||
resolution: "jsep@npm:0.3.5"
|
||||
checksum: fb5def7a4ba1cee41d144ebdd0d477785dc84b6bc1fed6cf5169f106de980dbe363bf99cb36a450435d7fd952d22b1d76e1609aeb5c7e7cbbbdb6d15fad03614
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jsesc@npm:^2.5.1":
|
||||
version: 2.5.2
|
||||
resolution: "jsesc@npm:2.5.2"
|
||||
|
@ -14843,9 +14918,19 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minimatch@npm:^9.0.0, minimatch@npm:^9.0.3, minimatch@npm:^9.0.4, minimatch@npm:^9.0.5":
|
||||
version: 9.0.5
|
||||
resolution: "minimatch@npm:9.0.5"
|
||||
"minimatch@npm:^7.4.2":
|
||||
version: 7.4.6
|
||||
resolution: "minimatch@npm:7.4.6"
|
||||
dependencies:
|
||||
brace-expansion: "npm:^2.0.1"
|
||||
checksum: e587bf3d90542555a3d58aca94c549b72d58b0a66545dd00eef808d0d66e5d9a163d3084da7f874e83ca8cc47e91c670e6c6f6593a3e7bb27fcc0e6512e87c67
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minimatch@npm:^9.0.1":
|
||||
version: 9.0.3
|
||||
resolution: "minimatch@npm:9.0.3"
|
||||
|
||||
dependencies:
|
||||
brace-expansion: "npm:^2.0.1"
|
||||
checksum: de96cf5e35bdf0eab3e2c853522f98ffbe9a36c37797778d2665231ec1f20a9447a7e567cb640901f89e4daaa95ae5d70c65a9e8aa2bb0019b6facbc3c0575ed
|
||||
|
|
Loading…
Reference in New Issue