rajt 0.0.29 → 0.0.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/action.ts +16 -119
- package/src/auth/auth.ts +14 -54
- package/src/auth/authnz.ts +66 -0
- package/src/auth/index.ts +2 -1
- package/src/auth/token.ts +21 -10
- package/src/context.ts +31 -0
- package/src/create-app.ts +12 -9
- package/src/http.ts +4 -7
- package/src/index.ts +3 -1
- package/src/middleware.ts +1 -1
- package/src/register.ts +5 -16
- package/src/request.ts +90 -0
- package/src/response.ts +10 -39
- package/src/routes.ts +35 -1
- package/src/utils/resolve.ts +7 -4
package/package.json
CHANGED
package/src/action.ts
CHANGED
|
@@ -3,10 +3,8 @@
|
|
|
3
3
|
import { Context, Handler, ValidationTargets } from 'hono'
|
|
4
4
|
import { z, ZodObject } from 'zod'
|
|
5
5
|
import { zValidator } from '@hono/zod-validator'
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import { HTTPException } from 'hono/http-exception'
|
|
9
|
-
import { BodyData } from 'hono/utils/body'
|
|
6
|
+
import Response from './response'
|
|
7
|
+
import cx from './context'
|
|
10
8
|
|
|
11
9
|
export type ActionType = Function | Handler | Action | (new () => Action)
|
|
12
10
|
|
|
@@ -16,10 +14,10 @@ type RuleDefinition = {
|
|
|
16
14
|
eTarget?: 'fieldErrors' | 'formErrors'
|
|
17
15
|
}
|
|
18
16
|
|
|
19
|
-
export default
|
|
20
|
-
rule<T extends keyof ValidationTargets>(target: T): { schema: (schema: ZodObject<any>) => RuleDefinition }
|
|
21
|
-
rule<T extends keyof ValidationTargets>(target: T, schema: ZodObject<any>): RuleDefinition
|
|
22
|
-
rule<T extends keyof ValidationTargets>(target: T, schema?: ZodObject<any>):
|
|
17
|
+
export default class Action {
|
|
18
|
+
static rule<T extends keyof ValidationTargets>(target: T): { schema: (schema: ZodObject<any>) => RuleDefinition }
|
|
19
|
+
static rule<T extends keyof ValidationTargets>(target: T, schema: ZodObject<any>): RuleDefinition
|
|
20
|
+
static rule<T extends keyof ValidationTargets>(target: T, schema?: ZodObject<any>):
|
|
23
21
|
| { schema: (schema: ZodObject<any>) => RuleDefinition }
|
|
24
22
|
| RuleDefinition
|
|
25
23
|
{
|
|
@@ -40,101 +38,19 @@ export default abstract class Action {
|
|
|
40
38
|
}
|
|
41
39
|
}
|
|
42
40
|
|
|
43
|
-
|
|
44
|
-
return this.cx.req.param(key)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
query() {
|
|
48
|
-
return this.cx.req.query()
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async form(cType?: string) {
|
|
52
|
-
cType ??= this.cx.req.header('Content-Type')
|
|
53
|
-
if (!cType) return {}
|
|
54
|
-
|
|
55
|
-
let formData: FormData
|
|
56
|
-
|
|
57
|
-
if (this.cx.req.bodyCache.formData) {
|
|
58
|
-
formData = await this.cx.req.bodyCache.formData
|
|
59
|
-
} else {
|
|
60
|
-
try {
|
|
61
|
-
const arrayBuffer = await this.cx.req.arrayBuffer()
|
|
62
|
-
formData = await bufferToFormData(arrayBuffer, cType)
|
|
63
|
-
this.cx.req.bodyCache.formData = formData
|
|
64
|
-
} catch (e) {
|
|
65
|
-
throw new HTTPException(400, {
|
|
66
|
-
message: 'Malformed FormData request.'
|
|
67
|
-
+ (e instanceof Error ? ` ${e.message}` : ` ${String(e)}`)
|
|
68
|
-
})
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const form: BodyData<{ all: true }> = {}
|
|
73
|
-
formData.forEach((value, key) => {
|
|
74
|
-
if (key.endsWith('[]')) {
|
|
75
|
-
;((form[key] ??= []) as unknown[]).push(value)
|
|
76
|
-
} else if (Array.isArray(form[key])) {
|
|
77
|
-
;(form[key] as unknown[]).push(value)
|
|
78
|
-
} else if (key in form) {
|
|
79
|
-
form[key] = [form[key] as string | File, value]
|
|
80
|
-
} else {
|
|
81
|
-
form[key] = value
|
|
82
|
-
}
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
return form
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async json<E>() {
|
|
89
|
-
try {
|
|
90
|
-
return await this.cx.req.json<E>()
|
|
91
|
-
} catch {
|
|
92
|
-
throw new HTTPException(400, { message: 'Malformed JSON in request body' })
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async body<E>() {
|
|
97
|
-
const cType = this.cx.req.header('Content-Type')
|
|
98
|
-
if (!cType) return {} as E
|
|
99
|
-
|
|
100
|
-
if (/^application\/([a-z-\.]+\+)?json(;\s*[a-zA-Z0-9\-]+\=([^;]+))*$/.test(cType)) {
|
|
101
|
-
return await this.json<E>()
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (
|
|
105
|
-
/^multipart\/form-data(;\s?boundary=[a-zA-Z0-9'"()+_,\-./:=?]+)?$/.test(cType)
|
|
106
|
-
&& ! /^application\/x-www-form-urlencoded(;\s*[a-zA-Z0-9\-]+\=([^;]+))*$/.test(cType)
|
|
107
|
-
) {
|
|
108
|
-
return await this.form() as E
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return {} as E
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
get response() {
|
|
115
|
-
return JsonResponse
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
get cx() {
|
|
119
|
-
return JsonResponse.cx
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
get cookie() {
|
|
123
|
-
return JsonResponse.cookie
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
validate() {
|
|
41
|
+
static validate() {
|
|
127
42
|
const rules = this.rules()
|
|
128
43
|
const h = async (c: Context) => {
|
|
129
|
-
return await this.handle(
|
|
44
|
+
return await this.handle(cx.cx)
|
|
130
45
|
}
|
|
131
46
|
if (!rules) return [h]
|
|
132
47
|
|
|
133
|
-
const rulesArray = (Array.isArray(rules) ? rules : [rules])
|
|
48
|
+
const rulesArray: Function[] = (Array.isArray(rules) ? rules : [rules])
|
|
49
|
+
// @ts-ignore
|
|
134
50
|
.map(rule => zValidator(rule.target, rule.schema, (result, c) => {
|
|
135
51
|
if (!result.success) {
|
|
136
52
|
// @ts-ignore
|
|
137
|
-
return
|
|
53
|
+
return Response.badRequest({ ...result.error.flatten()[rule.eTarget] })
|
|
138
54
|
}
|
|
139
55
|
}))
|
|
140
56
|
|
|
@@ -142,34 +58,15 @@ export default abstract class Action {
|
|
|
142
58
|
return rulesArray
|
|
143
59
|
}
|
|
144
60
|
|
|
145
|
-
run() {
|
|
61
|
+
static run() {
|
|
146
62
|
return this.validate()
|
|
147
63
|
}
|
|
148
64
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
get auth() {
|
|
152
|
-
const auth = this.cx.get('#auth')
|
|
153
|
-
return auth ? auth?.data : null
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
can(...abilities: string[]): boolean {
|
|
157
|
-
const auth = this.cx.get('#auth')
|
|
158
|
-
return auth ? auth.can(...abilities) : false
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
cant(...abilities: string[]): boolean {
|
|
162
|
-
return !this.can(...abilities)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
hasRole(...roles: string[]): boolean {
|
|
166
|
-
const auth = this.cx.get('#auth')
|
|
167
|
-
return auth ? auth.hasRole(...roles) : false
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
rules(): RuleDefinition[] | RuleDefinition | null {
|
|
65
|
+
static rules(): RuleDefinition[] | RuleDefinition | null {
|
|
171
66
|
return null
|
|
172
67
|
}
|
|
173
68
|
|
|
174
|
-
|
|
69
|
+
static async handle(c: Context): Promise<Response> {
|
|
70
|
+
return Promise.resolve(Response.raw(200, 'Action handle not implemented'))
|
|
71
|
+
}
|
|
175
72
|
}
|
package/src/auth/auth.ts
CHANGED
|
@@ -1,66 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Authnz } from './authnz'
|
|
2
|
+
import { Token } from './token'
|
|
2
3
|
|
|
3
|
-
export class
|
|
4
|
-
#
|
|
5
|
-
#roles: string[]
|
|
6
|
-
#data: T
|
|
4
|
+
export class Auth {
|
|
5
|
+
static #u: Authnz<any> | null = null
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
this.#
|
|
10
|
-
this.#roles = roles
|
|
11
|
-
this.#data = data
|
|
7
|
+
static resolve() {
|
|
8
|
+
this.#u = Authnz.fromToken(Token.fromRequest())
|
|
12
9
|
}
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return abilities.flat().every(ability => {
|
|
18
|
-
if (this.#roles.includes(ability)) return true
|
|
19
|
-
return this.#abilities.some(rule => this.#match(rule, ability))
|
|
20
|
-
})
|
|
21
|
-
}
|
|
22
|
-
cant(...abilities: string[]): boolean {
|
|
23
|
-
return !this.can(...abilities)
|
|
11
|
+
static get user() {
|
|
12
|
+
return this.#u ? this.#u?.data : null
|
|
24
13
|
}
|
|
25
14
|
|
|
26
|
-
|
|
27
|
-
return
|
|
15
|
+
static can(...abilities: string[]): boolean {
|
|
16
|
+
return this.#u ? this.#u.can(...abilities) : false
|
|
28
17
|
}
|
|
29
18
|
|
|
30
|
-
static
|
|
31
|
-
|
|
32
|
-
user = user.get()
|
|
33
|
-
const roles = [...(user?.role ? [user.role] : []), ...(user?.roles ?? [])]
|
|
34
|
-
|
|
35
|
-
const combined = [...(user?.perms ?? []), ...roles.flatMap(role => {
|
|
36
|
-
const perms = Ability.roles[role]
|
|
37
|
-
if (!perms) return []
|
|
38
|
-
return perms === '*' ? ['*'] : perms;
|
|
39
|
-
})]
|
|
40
|
-
|
|
41
|
-
const abilities = combined.includes('*') ? ['*'] : Array.from(new Set(combined))
|
|
42
|
-
|
|
43
|
-
return new Authnz(user as T, abilities, roles)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
#match(rule: string, ability: string): boolean {
|
|
47
|
-
if (rule === ability) return true
|
|
48
|
-
if (rule.endsWith('.*')) {
|
|
49
|
-
const prefix = rule.slice(0, -2)
|
|
50
|
-
return ability.startsWith(`${prefix}.`) || ability === prefix
|
|
51
|
-
}
|
|
52
|
-
return false
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
get abilities() {
|
|
56
|
-
return this.#abilities
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
get roles() {
|
|
60
|
-
return this.#roles
|
|
19
|
+
static cant(...abilities: string[]): boolean {
|
|
20
|
+
return !this.can(...abilities)
|
|
61
21
|
}
|
|
62
22
|
|
|
63
|
-
|
|
64
|
-
return this.#
|
|
23
|
+
static hasRole(...roles: string[]): boolean {
|
|
24
|
+
return this.#u ? this.#u.hasRole(...roles) : false
|
|
65
25
|
}
|
|
66
26
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Ability } from './ability'
|
|
2
|
+
|
|
3
|
+
export class Authnz<T extends object> {
|
|
4
|
+
#abilities: string[]
|
|
5
|
+
#roles: string[]
|
|
6
|
+
#data: T
|
|
7
|
+
|
|
8
|
+
constructor(data: T, abilities: string[], roles: string[]) {
|
|
9
|
+
this.#abilities = abilities
|
|
10
|
+
this.#roles = roles
|
|
11
|
+
this.#data = data
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
can(...abilities: string[]): boolean {
|
|
15
|
+
if (this.#abilities.includes('*')) return true
|
|
16
|
+
|
|
17
|
+
return abilities.flat().every(ability => {
|
|
18
|
+
if (this.#roles.includes(ability)) return true
|
|
19
|
+
return this.#abilities.some(rule => this.#match(rule, ability))
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
cant(...abilities: string[]): boolean {
|
|
23
|
+
return !this.can(...abilities)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
hasRole(...roles: string[]): boolean {
|
|
27
|
+
return roles.flat().every(role => this.#roles.includes(role))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static fromToken<T extends object>(user: any): Authnz<T> | null {
|
|
31
|
+
if (!user || user?.isInvalid()) return null
|
|
32
|
+
user = user.get()
|
|
33
|
+
const roles = [...(user?.role ? [user.role] : []), ...(user?.roles ?? [])]
|
|
34
|
+
|
|
35
|
+
const combined = [...(user?.perms ?? []), ...roles.flatMap(role => {
|
|
36
|
+
const perms = Ability.roles[role]
|
|
37
|
+
if (!perms) return []
|
|
38
|
+
return perms === '*' ? ['*'] : perms;
|
|
39
|
+
})]
|
|
40
|
+
|
|
41
|
+
const abilities = combined.includes('*') ? ['*'] : Array.from(new Set(combined))
|
|
42
|
+
|
|
43
|
+
return new Authnz(user as T, abilities, roles)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#match(rule: string, ability: string): boolean {
|
|
47
|
+
if (rule === ability) return true
|
|
48
|
+
if (rule.endsWith('.*')) {
|
|
49
|
+
const prefix = rule.slice(0, -2)
|
|
50
|
+
return ability.startsWith(`${prefix}.`) || ability === prefix
|
|
51
|
+
}
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get abilities() {
|
|
56
|
+
return this.#abilities
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get roles() {
|
|
60
|
+
return this.#roles
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get data() {
|
|
64
|
+
return this.#data
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/auth/index.ts
CHANGED
package/src/auth/token.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { Envir } from 't0n'
|
|
2
2
|
import { Token as Factory } from 'cripta'
|
|
3
|
-
import
|
|
3
|
+
import c from '../context'
|
|
4
4
|
|
|
5
5
|
export class Token {
|
|
6
6
|
static #name: string = 'Authorization'
|
|
7
7
|
static #prefix: string = 'bearer'
|
|
8
8
|
|
|
9
|
-
static fromRequest(
|
|
10
|
-
const token = this.fromHeader(
|
|
11
|
-
return token ? this.parse(
|
|
9
|
+
static fromRequest() {
|
|
10
|
+
const token = this.fromCookie() || this.fromHeader()
|
|
11
|
+
return token ? this.parse(token) : null
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
static fromHeader(
|
|
15
|
-
const header = req.header(this.#name) || req.header('HTTP_AUTHORIZATION') || req.header('REDIRECT_HTTP_AUTHORIZATION') || null
|
|
14
|
+
static fromHeader(): string | null {
|
|
15
|
+
const header = c.cx.req.header(this.#name) || c.cx.req.header('HTTP_AUTHORIZATION') || c.cx.req.header('REDIRECT_HTTP_AUTHORIZATION') || null
|
|
16
16
|
|
|
17
17
|
if (header) {
|
|
18
18
|
const position = header.toLowerCase().indexOf(this.#prefix.toLowerCase())
|
|
@@ -28,17 +28,28 @@ export class Token {
|
|
|
28
28
|
return null
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
static
|
|
32
|
-
const
|
|
31
|
+
static fromCookie(): string | null {
|
|
32
|
+
const uid = c.cx.req.header('uid')
|
|
33
|
+
|
|
34
|
+
if (uid) {
|
|
35
|
+
const auth = c.cookie.get('__auth_' + uid)
|
|
36
|
+
return auth ? auth : null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
static parse(token: string) {
|
|
43
|
+
const host = this.host(Envir.get('FLOW_SERVER') || c.cx.req.header('host') || '')
|
|
33
44
|
|
|
34
45
|
return Factory.parse(token)
|
|
35
46
|
.issuedBy(host)
|
|
36
47
|
.permittedFor(host)
|
|
37
48
|
}
|
|
38
49
|
|
|
39
|
-
static create(
|
|
50
|
+
static create(user: any, exp: number = 7200) {
|
|
40
51
|
const time = Math.floor(Date.now() / 1000)
|
|
41
|
-
const host = this.host(req.header('host') || '')
|
|
52
|
+
const host = this.host(c.cx.req.header('host') || '')
|
|
42
53
|
|
|
43
54
|
return Factory.create()
|
|
44
55
|
.issuedBy(host)
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Context } from 'hono'
|
|
2
|
+
import { getCookie, getSignedCookie, setCookie, setSignedCookie, deleteCookie } from 'hono/cookie'
|
|
3
|
+
import type { CookieOptions, CookiePrefixOptions } from 'hono/utils/cookie'
|
|
4
|
+
|
|
5
|
+
const cookieWrapper = (c: Context) => ({
|
|
6
|
+
all: () => getCookie(c),
|
|
7
|
+
allSigned: (secret: string) => getSignedCookie(c, secret),
|
|
8
|
+
get: (name: string, prefixOptions?: CookiePrefixOptions) => prefixOptions ? getCookie(c, name, prefixOptions) : getCookie(c, name),
|
|
9
|
+
getSigned: (secret: string, name: string, prefixOptions?: CookiePrefixOptions) => prefixOptions ? getSignedCookie(c, secret, name, prefixOptions) : getSignedCookie(c, secret, name),
|
|
10
|
+
set: (name: string, value: string, opt?: CookieOptions) => setCookie(c, name, value, opt),
|
|
11
|
+
setSigned: (name: string, value: string, secret: string, opt?: CookieOptions) => setSignedCookie(c, name, value, secret, opt),
|
|
12
|
+
delete: (name: string, opt?: CookieOptions) => deleteCookie(c, name, opt)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export default class CX {
|
|
16
|
+
static #c: Context
|
|
17
|
+
static #cookie: ReturnType<typeof cookieWrapper>
|
|
18
|
+
|
|
19
|
+
static setContext(c: Context) {
|
|
20
|
+
this.#c = c
|
|
21
|
+
this.#cookie = cookieWrapper(c)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static get cx(): Context {
|
|
25
|
+
return this.#c
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static get cookie() {
|
|
29
|
+
return this.#cookie
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/create-app.ts
CHANGED
|
@@ -4,13 +4,15 @@ import type { Env, Context, ErrorHandler, NotFoundHandler, Next } from 'hono'
|
|
|
4
4
|
// import type { MiddlewareHandler } from 'hono'
|
|
5
5
|
// import { createMiddleware } from 'hono/factory'
|
|
6
6
|
// import type { H, Handler, HandlerResponse } from 'hono/types'
|
|
7
|
-
import { HTTPResponseError } from 'hono/types'
|
|
8
|
-
import { Routes } from './types'
|
|
7
|
+
import type { HTTPResponseError } from 'hono/types'
|
|
8
|
+
import type { Routes } from './types'
|
|
9
9
|
import { BadRequest, Unauthorized } from './exceptions'
|
|
10
|
-
import response from './response'
|
|
11
10
|
import { resolve, resolveMiddleware } from './utils/resolve'
|
|
12
11
|
import { getMiddlewares, getHandler } from './register'
|
|
13
12
|
import env from './utils/environment'
|
|
13
|
+
import { Auth } from './auth'
|
|
14
|
+
import response from './response'
|
|
15
|
+
import cx from './context'
|
|
14
16
|
|
|
15
17
|
type InitFunction<E extends Env = Env> = (app: Hono<E>) => void
|
|
16
18
|
|
|
@@ -78,16 +80,18 @@ export const createApp = <E extends Env>(options?: ServerOptions<E>) => {
|
|
|
78
80
|
const app = options?.app ?? new Hono<E>()
|
|
79
81
|
|
|
80
82
|
app.use(async (c: Context, next: Next) => {
|
|
81
|
-
|
|
83
|
+
cx.setContext(c)
|
|
84
|
+
Auth.resolve()
|
|
82
85
|
await next()
|
|
83
86
|
})
|
|
84
87
|
getMiddlewares().forEach(mw => {
|
|
85
|
-
const h = async (c: Context, next: Next) => await resolveMiddleware(mw)(
|
|
88
|
+
const h = async (c: Context, next: Next) => await resolveMiddleware(mw)(cx.cx, next)
|
|
86
89
|
// @ts-ignore
|
|
87
90
|
mw?.path ? app.use(String(mw.path), h) : app.use(h)
|
|
88
91
|
})
|
|
89
|
-
|
|
92
|
+
// @ts-ignore
|
|
90
93
|
app.onError(options?.onError || EHandler)
|
|
94
|
+
// @ts-ignore
|
|
91
95
|
app.notFound(options?.notFound || NFHandler)
|
|
92
96
|
|
|
93
97
|
if (options?.init) options.init(app)
|
|
@@ -95,12 +99,11 @@ export const createApp = <E extends Env>(options?: ServerOptions<E>) => {
|
|
|
95
99
|
const routes = options?.routes || []
|
|
96
100
|
for (const route of routes) {
|
|
97
101
|
if (Array.isArray(route)) {
|
|
98
|
-
const handle = getHandler(route[3])
|
|
99
102
|
// @ts-ignore
|
|
100
|
-
app[route[0]](route[1], ...mw(route[2], route[3]), ...resolve(
|
|
103
|
+
app[route[0]](route[1], ...mw(route[2], route[3]), ...resolve(getHandler(route[3]), route[3]))
|
|
101
104
|
} else {
|
|
102
105
|
// @ts-ignore
|
|
103
|
-
app[route.method](route.path, ...mw(route.middlewares, route.name), ...resolve(route.handle))
|
|
106
|
+
app[route.method](route.path, ...mw(route.middlewares, route.name), ...resolve(route.handle, route.name))
|
|
104
107
|
}
|
|
105
108
|
}
|
|
106
109
|
|
package/src/http.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Context, Next } from 'hono'
|
|
2
2
|
import { MiddlewareType } from './middleware'
|
|
3
|
-
import
|
|
4
|
-
import { Ability,
|
|
3
|
+
import Response from './response'
|
|
4
|
+
import { Ability, Auth as Gate } from './auth'
|
|
5
5
|
import mergeMiddleware from './utils/merge-middleware'
|
|
6
6
|
|
|
7
7
|
function method(method: string, path = '/') {
|
|
@@ -52,15 +52,12 @@ export function Auth(...args: any[]): void | ClassDecorator {
|
|
|
52
52
|
|
|
53
53
|
function _auth(target: Function | any) {
|
|
54
54
|
mergeMiddleware(target, async (c: Context, next: Next) => {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
const auth = Authnz.fromToken(Token.fromRequest(c))
|
|
55
|
+
const auth = Gate.user()
|
|
58
56
|
const ability = Ability.fromAction(target)
|
|
59
57
|
|
|
60
58
|
if (!auth || !ability || auth.cant(ability))
|
|
61
|
-
return unauthorized
|
|
59
|
+
return Response.unauthorized()
|
|
62
60
|
|
|
63
|
-
c.set('#auth', auth)
|
|
64
61
|
await next()
|
|
65
62
|
})
|
|
66
63
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { default as Action } from './action'
|
|
2
|
+
export { Auth } from './auth/auth'
|
|
2
3
|
export { default as Middleware } from './middleware'
|
|
3
|
-
export { default as
|
|
4
|
+
export { default as Request } from './request'
|
|
5
|
+
export { default as Response } from './response'
|
|
4
6
|
export { default as Enum } from './enum'
|
package/src/middleware.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Context, MiddlewareHandler, Next } from 'hono'
|
|
2
2
|
|
|
3
3
|
export type MiddlewareType = MiddlewareHandler | Middleware | (new () => Middleware)
|
|
4
|
-
export default
|
|
4
|
+
export default class Middleware {
|
|
5
5
|
static factory?: Function
|
|
6
6
|
static opts?: object | any[]
|
|
7
7
|
static path: string = '*'
|
package/src/register.ts
CHANGED
|
@@ -1,24 +1,13 @@
|
|
|
1
|
-
export const handlers = {}
|
|
1
|
+
export const handlers: Record<string, Function> = {}
|
|
2
2
|
|
|
3
3
|
export function registerHandler(id: string, handler: any) {
|
|
4
|
-
if (
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const instance = new handler()
|
|
9
|
-
// @ts-ignore
|
|
10
|
-
handlers[id] = instance.handle.bind(instance)
|
|
11
|
-
} else if (handler.run) {
|
|
12
|
-
const instance = new handler()
|
|
13
|
-
// @ts-ignore
|
|
14
|
-
handlers[id] = instance.run.bind(instance)
|
|
15
|
-
} else {
|
|
16
|
-
console.warn(`Handler ${id} could not be registered - unsupported type`)
|
|
17
|
-
}
|
|
4
|
+
if (id in handlers)
|
|
5
|
+
console.warn(`Handler "${id}" has already been registered`)
|
|
6
|
+
|
|
7
|
+
handlers[id] = handler
|
|
18
8
|
}
|
|
19
9
|
|
|
20
10
|
export function getHandler(id: string): Function {
|
|
21
|
-
// @ts-ignore
|
|
22
11
|
const handler = handlers[id] || null
|
|
23
12
|
if (!handler) throw new Error(`Handler ${id} not registered`)
|
|
24
13
|
return handler
|
package/src/request.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { bufferToFormData } from 'hono/utils/buffer'
|
|
2
|
+
import { HTTPException } from 'hono/http-exception'
|
|
3
|
+
import type { BodyData } from 'hono/utils/body'
|
|
4
|
+
import Response from './response'
|
|
5
|
+
import c from './context'
|
|
6
|
+
|
|
7
|
+
export default class Request {
|
|
8
|
+
static param(key: string) {
|
|
9
|
+
return c.cx.req.param(key)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static query() {
|
|
13
|
+
return c.cx.req.query()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static async form(cType?: string) {
|
|
17
|
+
cType ??= c.cx.req.header('Content-Type')
|
|
18
|
+
if (!cType) return {}
|
|
19
|
+
|
|
20
|
+
let formData: FormData
|
|
21
|
+
|
|
22
|
+
if (c.cx.req.bodyCache.formData) {
|
|
23
|
+
formData = await c.cx.req.bodyCache.formData
|
|
24
|
+
} else {
|
|
25
|
+
try {
|
|
26
|
+
const arrayBuffer = await c.cx.req.arrayBuffer()
|
|
27
|
+
formData = await bufferToFormData(arrayBuffer, cType)
|
|
28
|
+
c.cx.req.bodyCache.formData = formData
|
|
29
|
+
} catch (e) {
|
|
30
|
+
throw new HTTPException(400, {
|
|
31
|
+
message: 'Malformed FormData request.'
|
|
32
|
+
+ (e instanceof Error ? ` ${e.message}` : ` ${String(e)}`)
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const form: BodyData<{ all: true }> = {}
|
|
38
|
+
formData.forEach((value, key) => {
|
|
39
|
+
if (key.endsWith('[]')) {
|
|
40
|
+
;((form[key] ??= []) as unknown[]).push(value)
|
|
41
|
+
} else if (Array.isArray(form[key])) {
|
|
42
|
+
;(form[key] as unknown[]).push(value)
|
|
43
|
+
} else if (key in form) {
|
|
44
|
+
form[key] = [form[key] as string | File, value]
|
|
45
|
+
} else {
|
|
46
|
+
form[key] = value
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return form
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static async json<E>() {
|
|
54
|
+
try {
|
|
55
|
+
return await c.cx.req.json<E>()
|
|
56
|
+
} catch {
|
|
57
|
+
throw new HTTPException(400, { message: 'Malformed JSON in request body' })
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
static async body<E>() {
|
|
62
|
+
const cType = c.cx.req.header('Content-Type')
|
|
63
|
+
if (!cType) return {} as E
|
|
64
|
+
|
|
65
|
+
if (/^application\/([a-z-\.]+\+)?json(;\s*[a-zA-Z0-9\-]+\=([^;]+))*$/.test(cType)) {
|
|
66
|
+
return await Request.json<E>()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (
|
|
70
|
+
/^multipart\/form-data(;\s?boundary=[a-zA-Z0-9'"()+_,\-./:=?]+)?$/.test(cType)
|
|
71
|
+
&& ! /^application\/x-www-form-urlencoded(;\s*[a-zA-Z0-9\-]+\=([^;]+))*$/.test(cType)
|
|
72
|
+
) {
|
|
73
|
+
return await Request.form() as E
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {} as E
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
static get response() {
|
|
80
|
+
return Response
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static get cx() {
|
|
84
|
+
return c.cx
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static get cookie() {
|
|
88
|
+
return c.cookie
|
|
89
|
+
}
|
|
90
|
+
}
|
package/src/response.ts
CHANGED
|
@@ -1,39 +1,10 @@
|
|
|
1
|
-
import { Context } from 'hono'
|
|
2
1
|
import type { ContentfulStatusCode, StatusCode } from 'hono/utils/http-status'
|
|
3
|
-
import { ErrorResponse, Errors } from './types'
|
|
4
|
-
import
|
|
5
|
-
import type { CookieOptions, CookiePrefixOptions } from 'hono/utils/cookie'
|
|
6
|
-
|
|
7
|
-
const cookieWrapper = (c: Context) => ({
|
|
8
|
-
all: () => getCookie(c),
|
|
9
|
-
allSigned: (secret: string) => getSignedCookie(c, secret),
|
|
10
|
-
get: (name: string, prefixOptions?: CookiePrefixOptions) => prefixOptions ? getCookie(c, name, prefixOptions) : getCookie(c, name),
|
|
11
|
-
getSigned: (secret: string, name: string, prefixOptions?: CookiePrefixOptions) => prefixOptions ? getSignedCookie(c, secret, name, prefixOptions) : getSignedCookie(c, secret, name),
|
|
12
|
-
set: (name: string, value: string, opt?: CookieOptions) => setCookie(c, name, value, opt),
|
|
13
|
-
setSigned: (name: string, value: string, secret: string, opt?: CookieOptions) => setSignedCookie(c, name, value, secret, opt),
|
|
14
|
-
delete: (name: string, opt?: CookieOptions) => deleteCookie(c, name, opt)
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
export default class JsonResponse {
|
|
18
|
-
static #c: Context
|
|
19
|
-
static #cookie: ReturnType<typeof cookieWrapper>
|
|
20
|
-
|
|
21
|
-
static setContext(c: Context) {
|
|
22
|
-
this.#c = c
|
|
23
|
-
this.#cookie = cookieWrapper(c)
|
|
24
|
-
return this
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
static get cx(): Context {
|
|
28
|
-
return this.#c
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
static get cookie() {
|
|
32
|
-
return this.#cookie
|
|
33
|
-
}
|
|
2
|
+
import type { ErrorResponse, Errors } from './types'
|
|
3
|
+
import c from './context'
|
|
34
4
|
|
|
5
|
+
export default class Response {
|
|
35
6
|
static raw(status?: StatusCode, body?: string) {
|
|
36
|
-
return
|
|
7
|
+
return c.cx.newResponse(body ? body : null, { status })
|
|
37
8
|
}
|
|
38
9
|
|
|
39
10
|
static ok(): Response
|
|
@@ -42,7 +13,7 @@ export default class JsonResponse {
|
|
|
42
13
|
if (data === undefined)
|
|
43
14
|
return this.raw(200)
|
|
44
15
|
|
|
45
|
-
return
|
|
16
|
+
return c.cx.json(data, 200)
|
|
46
17
|
}
|
|
47
18
|
|
|
48
19
|
static created(): Response
|
|
@@ -51,7 +22,7 @@ export default class JsonResponse {
|
|
|
51
22
|
if (data === undefined)
|
|
52
23
|
return this.raw(201)
|
|
53
24
|
|
|
54
|
-
return
|
|
25
|
+
return c.cx.json(data, 201)
|
|
55
26
|
}
|
|
56
27
|
|
|
57
28
|
static accepted(): Response
|
|
@@ -60,7 +31,7 @@ export default class JsonResponse {
|
|
|
60
31
|
if (data === undefined)
|
|
61
32
|
return this.raw(202)
|
|
62
33
|
|
|
63
|
-
return
|
|
34
|
+
return c.cx.json(data, 202)
|
|
64
35
|
}
|
|
65
36
|
|
|
66
37
|
static deleted() {
|
|
@@ -82,7 +53,7 @@ export default class JsonResponse {
|
|
|
82
53
|
if (data === undefined)
|
|
83
54
|
return this.raw(401)
|
|
84
55
|
|
|
85
|
-
return
|
|
56
|
+
return c.cx.json(data, 401)
|
|
86
57
|
}
|
|
87
58
|
|
|
88
59
|
static forbidden(): Response
|
|
@@ -91,7 +62,7 @@ export default class JsonResponse {
|
|
|
91
62
|
if (data === undefined)
|
|
92
63
|
return this.raw(403)
|
|
93
64
|
|
|
94
|
-
return
|
|
65
|
+
return c.cx.json(data, 403)
|
|
95
66
|
}
|
|
96
67
|
|
|
97
68
|
static notFound(): Response
|
|
@@ -124,6 +95,6 @@ export default class JsonResponse {
|
|
|
124
95
|
if (msg) resp.m = msg
|
|
125
96
|
if (errors) resp.e = errors
|
|
126
97
|
|
|
127
|
-
return
|
|
98
|
+
return c.cx.json(resp, status)
|
|
128
99
|
}
|
|
129
100
|
}
|
package/src/routes.ts
CHANGED
|
@@ -62,7 +62,41 @@ export async function getRoutes(
|
|
|
62
62
|
}
|
|
63
63
|
)))
|
|
64
64
|
|
|
65
|
-
return routes
|
|
65
|
+
return sortRoutes(routes)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function sortRoutes(routes: Route[]) {
|
|
69
|
+
const metas = new Map<string, { score: number; segmentsCount: number }>()
|
|
70
|
+
|
|
71
|
+
for (const route of routes)
|
|
72
|
+
metas.set(route.path, computeRouteMeta(route.path))
|
|
73
|
+
|
|
74
|
+
return routes.sort((a, b) => {
|
|
75
|
+
const metaA = metas.get(a.path)!
|
|
76
|
+
const metaB = metas.get(b.path)!
|
|
77
|
+
|
|
78
|
+
if (metaA.score === metaB.score)
|
|
79
|
+
return metaB.segmentsCount - metaA.segmentsCount
|
|
80
|
+
|
|
81
|
+
return metaB.score - metaA.score
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function computeRouteMeta(path: string) {
|
|
86
|
+
const segments = path.split('/').filter(Boolean)
|
|
87
|
+
|
|
88
|
+
let score = 0
|
|
89
|
+
for (const segment of segments) {
|
|
90
|
+
if (segment === '*') {
|
|
91
|
+
score += 0
|
|
92
|
+
} else if (segment.startsWith(':')) {
|
|
93
|
+
score += 1
|
|
94
|
+
} else {
|
|
95
|
+
score += 10
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { score, segmentsCount: segments.length }
|
|
66
100
|
}
|
|
67
101
|
|
|
68
102
|
export async function getMiddlewares(
|
package/src/utils/resolve.ts
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
import Action, { ActionType } from '../action'
|
|
2
2
|
import { MiddlewareType } from '../middleware'
|
|
3
3
|
|
|
4
|
-
export function resolve(obj: ActionType) {
|
|
4
|
+
export function resolve(obj: ActionType, id: string) {
|
|
5
5
|
if (typeof obj === 'function' && obj?.length === 2)
|
|
6
6
|
return [obj]
|
|
7
7
|
|
|
8
|
-
if (obj
|
|
8
|
+
if (obj?.run)
|
|
9
9
|
return obj.run()
|
|
10
10
|
|
|
11
|
+
if (obj?.handle)
|
|
12
|
+
return obj.handle()
|
|
13
|
+
|
|
11
14
|
const instance = new (obj as new () => Action)()
|
|
12
|
-
if (
|
|
15
|
+
if (obj?.prototype?.run)
|
|
13
16
|
return instance.run()
|
|
14
17
|
|
|
15
18
|
if (obj?.prototype?.handle)
|
|
16
19
|
return [instance.handle]
|
|
17
20
|
|
|
18
|
-
throw new Error(
|
|
21
|
+
throw new Error(`Invalid action "${id}" - unsupported type`)
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
export function resolveMiddleware(obj: MiddlewareType) {
|