duto 0.260808.0 → 0.260906.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.
Files changed (83) hide show
  1. package/LICENSE +2 -2
  2. package/README.md +1 -1
  3. package/package.json +51 -16
  4. package/src/cli.ts +42 -0
  5. package/src/enum.ts +2 -0
  6. package/src/index.d.ts +61 -0
  7. package/src/index.ts +15 -24
  8. package/src/middleware.ts +2 -0
  9. package/src/{compiler → pages/compiler}/vite-plugin.ts +1 -1
  10. package/src/{i18n → pages/i18n}/client.ts +1 -1
  11. package/src/pages/index.ts +28 -0
  12. package/src/server/adapter/aws.ts +4 -0
  13. package/src/server/adapter/workerd.ts +4 -0
  14. package/src/server/app.ts +96 -0
  15. package/src/server/auth/ability.ts +72 -0
  16. package/src/server/auth/authnz.ts +86 -0
  17. package/src/server/auth/index.ts +5 -0
  18. package/src/server/auth/token.ts +147 -0
  19. package/src/server/auth/types.ts +2 -0
  20. package/src/server/cli/commands/build.ts +41 -0
  21. package/src/server/cli/commands/deploy.ts +82 -0
  22. package/src/server/cli/commands/dev.ts +277 -0
  23. package/src/server/cli/commands/make.ts +98 -0
  24. package/src/server/cli/commands/migrate.ts +93 -0
  25. package/src/server/cli/commands/routes.ts +79 -0
  26. package/src/server/cli/index.ts +27 -0
  27. package/src/server/cli/stubs.ts +39 -0
  28. package/src/server/cli/types.ts +3 -0
  29. package/src/server/cli/utils.ts +443 -0
  30. package/src/server/config.ts +4 -0
  31. package/src/server/db/d1.d.ts +1 -0
  32. package/src/server/db/d1.ts +1 -0
  33. package/src/server/db/dynamodb.d.ts +1 -0
  34. package/src/server/db/dynamodb.ts +1 -0
  35. package/src/server/db/index.ts +1 -0
  36. package/src/server/entry.ts +19 -0
  37. package/src/server/handle.ts +30 -0
  38. package/src/server/http.ts +23 -0
  39. package/src/server/middlewares/auth.ts +40 -0
  40. package/src/server/middlewares/logger.ts +30 -0
  41. package/src/server/open-api/register.ts +29 -0
  42. package/src/server/open-api/spec.ts +107 -0
  43. package/src/server/request.ts +233 -0
  44. package/src/server/response.ts +142 -0
  45. package/src/server/routes.ts +474 -0
  46. package/src/server/types.ts +142 -0
  47. package/src/server/utils/environment.ts +17 -0
  48. package/src/server/utils.ts +46 -0
  49. package/src/server/validator.ts +70 -0
  50. package/src/utils.ts +5 -0
  51. package/src/cli/index.ts +0 -28
  52. package/src/cli/utils.ts +0 -146
  53. /package/src/{client → pages/client}/app.tsx +0 -0
  54. /package/src/{client → pages/client}/hmr.ts +0 -0
  55. /package/src/{client → pages/client}/island.ts +0 -0
  56. /package/src/{client → pages/client}/link.ts +0 -0
  57. /package/src/{client → pages/client}/router.ts +0 -0
  58. /package/src/{client → pages/client}/storage.ts +0 -0
  59. /package/src/{cli → pages}/commands/build.ts +0 -0
  60. /package/src/{cli → pages}/commands/dev-old.ts +0 -0
  61. /package/src/{cli → pages}/commands/dev.ts +0 -0
  62. /package/src/{cli → pages}/commands/preview.ts +0 -0
  63. /package/src/{compiler → pages/compiler}/aliases.ts +0 -0
  64. /package/src/{compiler → pages/compiler}/assets.ts +0 -0
  65. /package/src/{compiler → pages/compiler}/ast.ts +0 -0
  66. /package/src/{compiler → pages/compiler}/constants.ts +0 -0
  67. /package/src/{compiler → pages/compiler}/file.ts +0 -0
  68. /package/src/{compiler → pages/compiler}/index.ts +0 -0
  69. /package/src/{compiler → pages/compiler}/island.ts +0 -0
  70. /package/src/{compiler → pages/compiler}/meta.ts +0 -0
  71. /package/src/{compiler → pages/compiler}/routes.ts +0 -0
  72. /package/src/{compiler → pages/compiler}/types.ts +0 -0
  73. /package/src/{compiler → pages/compiler}/utils.ts +0 -0
  74. /package/src/{components → pages/components}/link.tsx +0 -0
  75. /package/src/{i18n → pages/i18n}/element.ts +0 -0
  76. /package/src/{i18n → pages/i18n}/server.ts +0 -0
  77. /package/src/{preact → pages/preact}/client-dev.ts +0 -0
  78. /package/src/{preact → pages/preact}/client.ts +0 -0
  79. /package/src/{preact → pages/preact}/context.ts +0 -0
  80. /package/src/{preact → pages/preact}/server.ts +0 -0
  81. /package/src/{preact → pages/preact}/signals.ts +0 -0
  82. /package/src/{preact → pages/preact}/static-html.ts +0 -0
  83. /package/src/{preact → pages/preact}/types.ts +0 -0
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025-present, ZUNQ.com
3
+ Copyright (c) 2025-present, HUB.BI
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
18
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
21
+ SOFTWARE.
package/README.md CHANGED
@@ -23,4 +23,4 @@ This framework is fully geared towards the serverless world, specifically for st
23
23
 
24
24
  ## License
25
25
 
26
- This package is licensed under the [MIT license](https://github.com/attla/duto/blob/main/LICENSE) © [Zunq](https://zunq.com)
26
+ This package is licensed under the [MIT license](https://github.com/attla/duto/blob/main/LICENSE) © [HUB](https://hub.bi)
package/package.json CHANGED
@@ -1,50 +1,84 @@
1
1
  {
2
2
  "name": "duto",
3
- "description": "Static framework for web artisans",
4
- "version": "0.260808.0",
3
+ "description": "All-in-one serverless framework for web artisans",
4
+ "version": "0.260906.0",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
7
+ "types": "./src/index.d.ts",
8
+ "bin": "./src/cli.ts",
7
9
  "files": ["src"],
8
- "bin": "./src/cli/index.ts",
9
10
  "exports": {
10
- ".": "./src/index.ts"
11
+ ".": "./src/index.ts",
12
+ "./auth": "./src/server/auth/index.ts",
13
+ "./db": "./src/db/index.ts",
14
+ "./database": "./src/db/index.ts",
15
+ "./d1": "./src/server/db/d1.ts",
16
+ "./d1/types": "./src/server/db/d1.d.ts",
17
+ "./dynamodb": "./src/server/db/dynamodb.ts",
18
+ "./dynamodb/types": "./src/server/db/dynamodb.d.ts",
19
+ "./enum": "./src/enum.ts",
20
+ "./http": "./src/server/http.ts",
21
+ "./middleware": "./src/middleware.ts",
22
+ "./types": "./src/server/types.ts",
23
+ "./types/global": "./global.d.ts",
24
+ "./env": "./src/server/utils/environment.ts",
25
+ "./length": "./src/server/utils/length.ts"
11
26
  },
12
27
  "dependencies": {
28
+ "@hono/node-server": "^1.19.11",
29
+ "@hono/standard-validator": "^0.2.2",
30
+ "@hono/zod-validator": "^0.7.6",
31
+ "@scalar/hono-api-reference": "^0.12.0",
32
+ "@minify-html/node": "^0.18.1",
13
33
  "@preact/signals": "^2.9.2",
14
34
  "@swc/core": "^1.15.21",
15
35
  "chokidar": "^3.5.2",
16
36
  "citty": "^0.2.2",
17
37
  "consola": "^3.4.2",
38
+ "cripta": "0.x",
39
+ "dotenv": "^16.5.0",
40
+ "esbuild": "^0.25.2",
18
41
  "filesize": "^11.0",
42
+ "forj": "0.x",
43
+ "hono": "^4.13.7",
44
+ "hono-openapi": "^1.3.0",
45
+ "localflare-api": "^0.5.0",
46
+ "localflare-core": "^0.5.0",
47
+ "miniflare": "^4.20260730.0",
19
48
  "pathe": "^2.0",
20
49
  "preact": "^10.29.7",
21
50
  "preact-render-to-string": "^6.6.6",
51
+ "quansync": "^0.2.11",
22
52
  "t0n": "0.x",
23
- "vite": "^8.0.0",
24
- "wouter-preact": "^3.10.0"
53
+ "wouter-preact": "^3.10.0",
54
+ "wrangler": "^4.128.0",
55
+ "zod": "^4.3.6",
56
+ "zod-openapi": "^5.4.6"
25
57
  },
26
58
  "devDependencies": {
27
- "@minify-html/node": "^0.18.1",
28
- "@preact/preset-vite": "^2.10.5",
29
- "@tailwindcss/vite": "^4.3.1",
30
- "@types/node": "^25.9.0",
31
- "bun-types": "^1.3.14",
59
+ "@cloudflare/workers-types": "^4.20260113.0",
60
+ "@types/node": "^26.4.1",
61
+ "bun-types": "^1.4.0",
32
62
  "typescript": "^5.9.3"
33
63
  },
34
- "packageManager": "bun@1.3.14",
35
- "engines": {
36
- "node": ">=18.0.0"
64
+ "resolutions": {
65
+ "@smithy/types": "^4.3.0",
66
+ "zod": "^4.3.6"
37
67
  },
68
+ "sideEffects": false,
69
+ "packageManager": "bun@1.4.0",
38
70
  "publishConfig": {
39
71
  "registry": "https://registry.npmjs.org"
40
72
  },
41
- "author": "Zunq <open-source@zunq.com>",
73
+ "author": "HUB <foss@hub.bi>",
42
74
  "license": "MIT",
43
- "homepage": "https://zunq.dev",
75
+ "homepage": "https://hub.bi/docs/duto",
44
76
  "repository": "git://github.com/attla/duto",
45
77
  "bugs": "https://github.com/attla/duto/issues",
46
78
  "keywords": [
47
79
  "SSG",
80
+ "app",
81
+ "http",
48
82
  "application",
49
83
  "framework",
50
84
  "router",
@@ -52,6 +86,7 @@
52
86
  "typescript",
53
87
  "cloudflare",
54
88
  "workers",
89
+ "workerd",
55
90
  "vercel",
56
91
  "bun"
57
92
  ]
package/src/cli.ts ADDED
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env bun
2
+
3
+ function exit(msg?: string, desc?: string) {
4
+ msg && console.error(msg)
5
+ desc && console.log(desc)
6
+ process.exit(0)
7
+ }
8
+
9
+ if (!import.meta.main)
10
+ exit('Error: Duto must be used directly as CLI command.')
11
+
12
+ if (!process?.isBun || typeof Bun === 'undefined')
13
+ exit('Error: "Bun" is not available.', ' Try install: https://bun.sh/get')
14
+
15
+ global.__st = performance.now()
16
+
17
+ import { join } from 'pathe'
18
+ import { cli } from 't0n/cli'
19
+ import { Envir } from 't0n'
20
+ import { version } from '!/package.json'
21
+ import { _root, _duto } from './utils'
22
+
23
+ let commands = {
24
+ // new:
25
+ // upgrade:
26
+ }
27
+ let pkg
28
+ try {
29
+ pkg = await import(join(_root, 'package.json'))
30
+ if (!pkg || !pkg.duto || !pkg?.duto?.type || !['pages', 'server'].includes(pkg.duto.type))
31
+ exit('Error: invalid app type.')
32
+
33
+ Envir.set('duto', pkg.duto)
34
+ commands = (await import(join(_duto, pkg.duto.type, 'cli')))?.default
35
+ } catch {}
36
+
37
+ cli({
38
+ name: 'duto',
39
+ description: 'Duto',
40
+ version,
41
+ commands,
42
+ })
package/src/enum.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { Enum } from 't0n'
2
+ export type { EnumStatic, EnumValue, EnumType } from 't0n'
package/src/index.d.ts ADDED
@@ -0,0 +1,61 @@
1
+ /// <reference types='bun-types' />
2
+ /// <reference types='node' />
3
+
4
+ // export { }
5
+
6
+ import * as Duto from './'
7
+
8
+ declare global {
9
+ // Duto
10
+ var __st: number
11
+ var __hmr: boolean
12
+
13
+ type Req = Duto.Req
14
+ type Res = Duto.Res
15
+ type $v = Duto.$v
16
+ type IRequest = Duto.IRequest
17
+ type IResponse = Duto.IResponse
18
+ type IValidator = Duto.IValidator
19
+
20
+ type Context = Duto.Context
21
+ type Next = Duto.Next
22
+
23
+ type Abilities = Duto.Abilities
24
+ type Roles = Duto.Roles
25
+
26
+ type EnumStatic = Duto.EnumStatic
27
+ type EnumValue = Duto.EnumValue
28
+ type EnumType = Duto.EnumType
29
+
30
+
31
+ // Bun:test
32
+ var test: typeof import('bun:test').test
33
+ var describe: typeof import('bun:test').describe
34
+ var it: typeof import('bun:test').it
35
+ var expect: typeof import('bun:test').expect
36
+ // Hooks
37
+ var beforeAll: typeof import('bun:test').beforeAll
38
+ var afterAll: typeof import('bun:test').afterAll
39
+ var beforeEach: typeof import('bun:test').beforeEach
40
+ var afterEach: typeof import('bun:test').afterEach
41
+ // Mocking
42
+ var mock: typeof import('bun:test').mock
43
+ var vi: typeof import('bun:test').vi
44
+ var jest: typeof import('bun:test').jest
45
+ var spyOn: typeof import('bun:test').spyOn
46
+ // Utils
47
+ var setSystemTime: typeof import('bun:test').setSystemTime
48
+ var setDefaultTimeout: typeof import('bun:test').setDefaultTimeout
49
+ // Types
50
+ var Test: typeof import('bun:test').Test
51
+ var Describe: typeof import('bun:test').Describe
52
+ var Expect: typeof import('bun:test').Expect
53
+ var Mock: typeof import('bun:test').Mock
54
+ var TestOptions: typeof import('bun:test').TestOptions
55
+ var Matchers: typeof import('bun:test').Matchers
56
+ // Namespace
57
+ namespace jest {
58
+ var fn: typeof import('bun:test').vi.fn
59
+ var mock: typeof import('bun:test').vi.mock
60
+ }
61
+ }
package/src/index.ts CHANGED
@@ -1,28 +1,19 @@
1
- export {
2
- Redirect,
3
- useLocation, useParams, useSearch, useSearchParams,
4
- matchRoute,
5
- } from 'wouter-preact'
1
+ export { default as Response } from './server/response'
2
+ export { default as Config } from './server/config'
6
3
 
7
- export {
8
- t, r, d,
9
- getLocale, setLocale,
10
- isPrefixed,
11
- } from './i18n/client'
4
+ export { Enum, Envir } from 't0n'
5
+ export type { EnumStatic, EnumValue, EnumType } from 't0n'
12
6
 
13
- export { local } from './client/storage'
7
+ export type {
8
+ IRequest, IRequest as Req, IRequest as $req,
9
+ IResponse, IResponse as Res, IResponse as $res,
10
+ IValidator, IValidator as $v,
11
+ Context, Next,
12
+ } from './server/types'
14
13
 
15
- export const navigate = <S = any>(
16
- to: string | URL,
17
- options?: { replace?: boolean; state?: S; transition?: boolean }
18
- ) => {
19
- if (window?.$duto?.sroutes?.has(to)) {
20
- location.pathname = to
21
- return
22
- }
14
+ // Auth
15
+ export { Ability } from './server/auth/ability'
16
+ export { Authnz } from './server/auth/authnz'
17
+ export { Token } from './server/auth/token'
23
18
 
24
- const n = window?.$duto?.navigate || useLocation()[1]
25
- n(to, options)
26
- }
27
-
28
- export { Link } from './components/link'
19
+ export type { Abilities, Roles } from './server/auth/types'
@@ -0,0 +1,2 @@
1
+ export { auth } from './server/middlewares/auth'
2
+ // export { cors } from './server/middlewares/cors'
@@ -9,7 +9,7 @@ import tailwind from '@tailwindcss/vite'
9
9
 
10
10
  import { EXT_RE } from './constants'
11
11
  import { setAsset, setChunk, setEntry } from './assets'
12
- import { renderPage } from './'
12
+ import { renderPage } from '.'
13
13
  import { removeExports, resolveImport } from './ast'
14
14
  import { getMetadata } from './meta'
15
15
 
@@ -1,6 +1,6 @@
1
1
  import { h, type JSX } from 'preact'
2
2
  import { local } from '#/storage'
3
- import { navigate } from '../'
3
+ import { navigate } from '..'
4
4
 
5
5
  const accepted = (lang: string | undefined, locales: string[] = [], fallback: string) => lang && locales.includes(lang) ? lang : fallback
6
6
 
@@ -0,0 +1,28 @@
1
+ export {
2
+ Redirect,
3
+ useLocation, useParams, useSearch, useSearchParams,
4
+ matchRoute,
5
+ } from 'wouter-preact'
6
+
7
+ export {
8
+ t, r, d,
9
+ getLocale, setLocale,
10
+ isPrefixed,
11
+ } from './i18n/client'
12
+
13
+ export { local } from './client/storage'
14
+
15
+ export const navigate = <S = any>(
16
+ to: string | URL,
17
+ options?: { replace?: boolean; state?: S; transition?: boolean }
18
+ ) => {
19
+ if (window?.$duto?.sroutes?.has(to)) {
20
+ location.pathname = to
21
+ return
22
+ }
23
+
24
+ const n = window?.$duto?.navigate || useLocation()[1]
25
+ n(to, options)
26
+ }
27
+
28
+ export { Link } from './components/link'
@@ -0,0 +1,4 @@
1
+ import { handle } from 'hono/aws-lambda'
2
+ import { app } from '$/entry'
3
+
4
+ export const handler = handle(app)
@@ -0,0 +1,4 @@
1
+ import { app } from '$/entry'
2
+ export default app
3
+
4
+ // export { app as default } from '$/entry'
@@ -0,0 +1,96 @@
1
+ import { Hono } from 'hono'
2
+ import { Envir } from 't0n'
3
+ import type {
4
+ Env, Context, Next,
5
+ HTTPResponseError,
6
+ ServerOptions, Route
7
+ } from './types'
8
+ import request, { GET_REQUEST } from './request'
9
+ import response from './response'
10
+ import { isDev } from './utils/environment'
11
+ import { getVerb } from './http'
12
+ import { registerMiddleware, resolveHandle, resolveMw } from './handle'
13
+
14
+ const NFHandler = () => response.notFound()
15
+ const EHandler = async (e: Error | HTTPResponseError) => {
16
+ console.error(e)
17
+
18
+ switch (true) {
19
+ // case 'status' in e && e.status === 401:
20
+ case e?.status === 401:
21
+ return response.unauthorized()
22
+
23
+ // case 'status' in e && e.status === 400: // @ts-ignore
24
+ case e?.status === 400:
25
+ return response.badRequest(null, e?.message)
26
+
27
+ default:
28
+ return response.internalError(
29
+ // @ts-ignore
30
+ isDev() // TODO: remover ou migrar para um middleware / melhorar
31
+ ? e.stack?.split('\n').map(line =>
32
+ line.replace(
33
+ /at (.+ )?\(?([^)]+)\)?/g,
34
+ (match, method, path) => {
35
+ if (!path) return match
36
+
37
+ const nodeModulesIndex = path.indexOf('node_modules')
38
+ if (nodeModulesIndex > -1)
39
+ return `${method || ''}(node_modules${path.slice(nodeModulesIndex + 'node_modules'.length)})`
40
+
41
+ const projectRoot = process.cwd()
42
+ const relativePath = path.startsWith(projectRoot) ? path.slice(projectRoot.length + 1) : path
43
+ return `${method || ''}(${relativePath})`
44
+ }
45
+ ).trim()
46
+ )
47
+ : undefined,
48
+ e.message || 'Internal Error'
49
+ )
50
+ }
51
+
52
+ // return json.internalError(
53
+ // // @ts-ignore
54
+ // isDev() ? e.stack?.split('\n at ').map() : undefined,
55
+ // e.message || 'Internal Error'
56
+ // )
57
+ // error: e.message,
58
+ // cause: e.cause || '???',
59
+ // stack: isDev (? e.stack : undefined
60
+ }
61
+
62
+ export const createApp = <E extends Env>(options?: ServerOptions<E>) => {
63
+ // const root = options?.root ?? '/'
64
+ const app = options?.app ?? new Hono<E>()
65
+
66
+ app.use(async (c: Context, next: Next) => {
67
+ await request.toContext(c)
68
+ await next()
69
+ })
70
+
71
+ const middlewares = options?.middlewares || []
72
+ for (const mw of middlewares) { // @ts-ignore
73
+ registerMiddleware(mw[2], mw[1]) // @ts-ignore
74
+ mw[0] ? app.use(String(mw[0]), resolveMw(mw[1])) : app.use(resolveMw(mw[1]))
75
+ }
76
+
77
+ // @ts-ignore
78
+ app.onError(options?.onError || EHandler)
79
+ // @ts-ignore
80
+ app.notFound(options?.notFound || NFHandler)
81
+
82
+ if (options?.init) options.init(app)
83
+
84
+ const routes = options?.routes || [] // @ts-ignore
85
+ const routeRegister = options?.routeRegister ? options.routeRegister : (_: Hono, route: Route) => {
86
+ // @ts-ignore
87
+ _[getVerb[route[0]]](route[1], ...resolveHandle(route[2], route[3], route[4]))
88
+ }
89
+
90
+ for (const route of routes)
91
+ routeRegister(app, route)
92
+
93
+ return app
94
+ }
95
+
96
+ export default createApp
@@ -0,0 +1,72 @@
1
+ import { verbAlias } from '$/http'
2
+ import type { IRequest, Routes } from '$/types'
3
+ import type { Roles, Abilities } from './types'
4
+
5
+ export class Ability {
6
+ static #roles: Roles = {}
7
+ static #abilities: Abilities = []
8
+ static #actions: Record<string, string> = {}
9
+
10
+ static empty() {
11
+ this.#roles = {}
12
+ this.#abilities = []
13
+ }
14
+
15
+ static fromRoutes(routes: Routes) {
16
+ if (!routes?.length) return
17
+
18
+ const actions: Record<string, string> = {}
19
+ const abilities = new Set()
20
+ for (const route of routes) {
21
+ const isArr = Array.isArray(route)
22
+ const name = isArr ? route[route.length-1] : route.name
23
+ if (!name || abilities.has(name)) continue
24
+
25
+ abilities.add(name)
26
+ actions[(isArr ? route[1] : route.path) +'$'+ verbAlias[isArr ? route[0] : route.method]] = name
27
+ const formatted = this.format(name)
28
+
29
+ if (formatted)
30
+ this.#abilities.push(formatted)
31
+ }
32
+
33
+ this.#actions = actions
34
+ }
35
+
36
+ static fromRequest(req: IRequest) {
37
+ const ability = this.#actions[`${req.routePath}$` + verbAlias[req.method.toLowerCase()]]
38
+ return ability ? this.format(ability) : ''
39
+ }
40
+
41
+ static fromAction(target: any): string {
42
+ return !target ? '' : this.format(typeof target === 'string' ? target : (target.name.length > 3 ? target.name : (target?.p || '')))
43
+ }
44
+
45
+ static format(path: string) {
46
+ return path === '/'
47
+ ? 'index'
48
+ : path.normalize('NFD')
49
+ .replace(/[\u0300-\u036f]/g, '')
50
+ .replace(/^\/*/, '')
51
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
52
+ .replace(/[^a-zA-Z0-9/]|[\s\-.]/g, '_')
53
+ .replace(/_+/g, '_')
54
+ .replace(/\//g, '.')
55
+ .replace(/\._/g, '.')
56
+ .replace(/^[._-]+/, '')
57
+ .replace(/[._-]+$/, '')
58
+ .toLowerCase()
59
+ }
60
+
61
+ static get abilities() {
62
+ return this.#abilities
63
+ }
64
+
65
+ static get roles() {
66
+ return this.#roles
67
+ }
68
+
69
+ static set roles(roles: Roles) {
70
+ this.#roles = roles
71
+ }
72
+ }
@@ -0,0 +1,86 @@
1
+ import { Ability } from './ability'
2
+ import type { Token } from 'cripta/token'
3
+
4
+ export class Authnz<T extends object> {
5
+ #abilities: string[]
6
+ #roles: string[]
7
+ #data: T
8
+ #token: Token
9
+
10
+ constructor(token: Token, data: T, abilities: string[], roles: string[]) {
11
+ this.#abilities = abilities
12
+ this.#roles = roles
13
+ this.#token = token
14
+ this.#data = data
15
+ }
16
+
17
+ can(...abilities: string[]): boolean {
18
+ if (this.#abilities.includes('*')) return true
19
+
20
+ return abilities.flat().every(ability => {
21
+ if (this.#roles.includes(ability)) return true
22
+ return this.#abilities.some(rule => this.#match(rule, ability))
23
+ })
24
+ }
25
+ cant(...abilities: string[]): boolean {
26
+ return !this.can(...abilities)
27
+ }
28
+
29
+ hasRole(...roles: string[]): boolean {
30
+ return roles.flat().every(role => this.#roles.includes(role))
31
+ }
32
+
33
+ // has(prop: string, value: any = null): boolean {
34
+ // return this.#token?.hasValue(prop, value) || false
35
+ // }
36
+ // hasValue(prop: string, value: any = null): boolean {
37
+ // return this.has(prop, value)
38
+ // }
39
+
40
+ static fromToken<T extends object>(token: Token): Authnz<T> | null {
41
+ if (!token || !token?.valid) return null
42
+ const user = token.body
43
+ const roles = [...(user?.role ? [user.role] : []), ...(user?.roles ?? [])]
44
+
45
+ const combined = [...(user?.perms ?? []), ...roles.flatMap(role => {
46
+ const perms = Ability.roles[role]
47
+ if (!perms) return []
48
+ return perms === '*' ? ['*'] : perms
49
+ })]
50
+
51
+ const abilities = combined.includes('*') ? ['*'] : Array.from(new Set(combined))
52
+
53
+ return new Authnz<T>(token, user, abilities, roles)
54
+ }
55
+
56
+ #match(rule: string, ability: string): boolean {
57
+ if (rule === ability) return true
58
+ if (
59
+ this.#wildcardMatch(rule, ability, '_*')
60
+ || this.#wildcardMatch(rule, ability, '-*')
61
+ || this.#wildcardMatch(rule, ability, '.*')
62
+ ) return true
63
+
64
+ return false
65
+ }
66
+
67
+ #wildcardMatch(rule: string, ability: string, suffix: string) {
68
+ return rule.endsWith(suffix)
69
+ && (ability.startsWith(rule.slice(0, -2) + suffix[0]) || ability === rule.slice(0, -2))
70
+ }
71
+
72
+ get abilities() {
73
+ return this.#abilities
74
+ }
75
+
76
+ get roles() {
77
+ return this.#roles
78
+ }
79
+
80
+ get data() {
81
+ return this.#data
82
+ }
83
+ get user() {
84
+ return this.#data
85
+ }
86
+ }
@@ -0,0 +1,5 @@
1
+ export { Ability } from './ability'
2
+ export { Authnz } from './authnz'
3
+ export { Token } from './token'
4
+
5
+ export * from './types'