fusion-framework 0.0.1 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # Fusion Framework (Node.js)
2
+
3
+ Class-based HTTP APIs on a shared Rust core (`fusion-core`).
4
+
5
+ Handlers use `this.params` / `this.query` / `this.body` / `this.state` (no signature param injection — that is Python-only DX).
6
+
7
+ ## Links
8
+
9
+ - **Docs:** [fusion.cipherunit.xyz](https://fusion.cipherunit.xyz/)
10
+ - **GitHub:** [cipherunits/fusion-framework](https://github.com/cipherunits/fusion-framework)
11
+ - **CLI:** [cipherunits/fusion-tool](https://github.com/cipherunits/fusion-tool)
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ cd crates/fusion-node
17
+ npm install
18
+ npm run build:debug
19
+ ```
20
+
21
+ Or after publish:
22
+
23
+ ```bash
24
+ npm i fusion-framework
25
+ ```
26
+
27
+ ## Quick start
28
+
29
+ ```js
30
+ import { FusionBaseApi, route, status, FusionApp, getSettings, settings } from 'fusion-framework'
31
+
32
+ export const ItemModule = route('/api/[module]/{id}')(
33
+ class ItemModule extends FusionBaseApi {
34
+ get() {
35
+ return this.response({ id: this.params.id }, status.HTTP_SUCCESS)
36
+ }
37
+ },
38
+ )
39
+
40
+ const MIDDLEWARE = [] // optional — framework has no defaults
41
+
42
+ settings.ensureLoaded()
43
+ const app = new FusionApp(getSettings())
44
+ for (const mw of MIDDLEWARE) app.use(mw)
45
+ await app.listen()
46
+ ```
47
+
48
+ ## Middleware
49
+
50
+ ```js
51
+ import { bearerJwt, requireRoles, route } from 'fusion-framework'
52
+
53
+ const MIDDLEWARE = [bearerJwt()] // or bearerJwt({ verify })
54
+
55
+ route('/api/admin', { roles: ['admin', 'super_admin'] })(
56
+ class AdminModule extends FusionBaseApi {
57
+ get() {
58
+ return this.response({ user: this.state.jwt?.sub })
59
+ }
60
+ },
61
+ )
62
+ ```
63
+
64
+ Sync or async: `(request, callNext) => …` / `async (request, callNext) => await callNext(request)`.
65
+
66
+ ## Status codes
67
+
68
+ ```js
69
+ import { status } from 'fusion-framework'
70
+ status.HTTP_SUCCESS // 200
71
+ status.HTTP_404_NOT_FOUND
72
+ ```
73
+
74
+ ## License
75
+
76
+ MIT
Binary file
Binary file
Binary file
package/index.d.ts ADDED
@@ -0,0 +1,127 @@
1
+ export class App {
2
+ constructor()
3
+ route(method: string, path: string, handler: (req: FusionRequest) => FusionResponse | string): void
4
+ listen(host: string, port: number): Promise<void>
5
+ }
6
+
7
+ export class Settings {
8
+ constructor()
9
+ loadJson(path?: string | null, env?: string | null, extraRoots?: string[]): void
10
+ ensureLoaded(extraRoots?: string[]): void
11
+ merge(values: Record<string, unknown>): void
12
+ get(key: string, defaultValue?: unknown): unknown
13
+ readonly host: string
14
+ readonly port: number
15
+ readonly debug: boolean
16
+ readonly env: string
17
+ }
18
+
19
+ export class FusionBaseApi {
20
+ request: FusionRequest
21
+ constructor(request: FusionRequest)
22
+ readonly method: string
23
+ readonly path: string
24
+ readonly body: string
25
+ readonly headers: Record<string, string>
26
+ readonly params: Record<string, string>
27
+ readonly query: Record<string, string>
28
+ readonly state: Record<string, unknown>
29
+ response(body?: unknown, status?: number, headers?: Record<string, string>): FusionResponse
30
+ }
31
+
32
+ export class HTTPException extends Error {
33
+ status: number
34
+ detail: unknown
35
+ headers: Record<string, string>
36
+ constructor(status: number, detail?: unknown, headers?: Record<string, string>)
37
+ toResponse(): FusionResponse
38
+ }
39
+
40
+ export class FusionApp {
41
+ constructor(settings?: Partial<FusionSettings>)
42
+ use(middleware: FusionMiddleware): void
43
+ mount(): void
44
+ listen(host?: string, port?: number): Promise<void>
45
+ }
46
+
47
+ export type RouteOptions = {
48
+ tags?: string[]
49
+ desc?: string
50
+ title?: string
51
+ version?: string
52
+ deprecated?: boolean
53
+ middleware?: FusionMiddleware[]
54
+ roles?: string[]
55
+ roleClaim?: string
56
+ roleStateKey?: string
57
+ }
58
+
59
+ export function router(path: string, options?: RouteOptions): <T>(ApiClass: T) => T
60
+ /** Alias of `router`. */
61
+ export function route(path: string, options?: RouteOptions): <T>(ApiClass: T) => T
62
+
63
+ export function bearerJwt(options?: {
64
+ stateKey?: string
65
+ header?: string
66
+ verify?: (token: string) => Record<string, unknown> | null
67
+ }): FusionMiddleware
68
+
69
+ export function requireRoles(...roles: string[]): FusionMiddleware
70
+ export function requireRoles(options: {
71
+ roles: string[]
72
+ claim?: string
73
+ stateKey?: string
74
+ }): FusionMiddleware
75
+
76
+ export function runMiddlewareChain(
77
+ request: FusionRequest,
78
+ middlewares: FusionMiddleware[],
79
+ handler: (request: FusionRequest) => unknown | Promise<unknown>,
80
+ ): Promise<unknown>
81
+
82
+ export function apiResourceName(cls: { name: string } | string): string
83
+ export function resolveRoutePath(path: string, cls: { name: string }): string
84
+ export function configure(settings: Record<string, unknown>): FusionSettings
85
+ export function getSettings(): FusionSettings
86
+ export function run(
87
+ options?: string | { settingsModule?: string; middleware?: FusionMiddleware[] },
88
+ ): Promise<FusionApp>
89
+ export function coerceParam(raw: string, kind?: string): unknown
90
+ export function getHttpMethods(): string[]
91
+ export function apiResourceNameJs(className: string): string
92
+ export function resolveRoutePathJs(template: string, className: string): string
93
+ export function coerceParamJs(raw: string, kind?: string): unknown
94
+
95
+ export const settings: Settings
96
+ export const status: Record<string, number>
97
+ export const HTTP_METHODS: string[]
98
+
99
+ export interface FusionSettings {
100
+ host: string
101
+ port: number
102
+ debug: boolean
103
+ env?: string
104
+ }
105
+
106
+ export interface FusionRequest {
107
+ method: string
108
+ path: string
109
+ body: string
110
+ headers: Record<string, string>
111
+ params: Record<string, string>
112
+ query: Record<string, string>
113
+ state?: Record<string, unknown>
114
+ }
115
+
116
+ export type FusionMiddleware = (
117
+ request: FusionRequest,
118
+ callNext: (request: FusionRequest) => unknown | Promise<unknown>,
119
+ ) => unknown | Promise<unknown>
120
+
121
+ export type FusionResponse =
122
+ | string
123
+ | {
124
+ status?: number
125
+ body?: unknown
126
+ headers?: Record<string, string>
127
+ }
package/index.js ADDED
@@ -0,0 +1,652 @@
1
+ const path = require('path')
2
+ const fs = require('fs')
3
+ const { platform, arch } = process
4
+
5
+ function napiTriple() {
6
+ const plat =
7
+ platform === 'win32' ? 'win32' : platform === 'darwin' ? 'darwin' : platform === 'linux' ? 'linux' : platform
8
+ const cpu =
9
+ arch === 'x64' ? 'x64' : arch === 'arm64' ? 'arm64' : arch === 'ia32' ? 'ia32' : arch
10
+
11
+ if (plat === 'win32' && cpu === 'x64') return 'win32-x64-msvc'
12
+ if (plat === 'darwin' && cpu === 'arm64') return 'darwin-arm64'
13
+ if (plat === 'darwin' && cpu === 'x64') return 'darwin-x64'
14
+ if (plat === 'linux' && cpu === 'x64') return 'linux-x64-gnu'
15
+ if (plat === 'linux' && cpu === 'arm64') return 'linux-arm64-gnu'
16
+ return `${plat}-${cpu}`
17
+ }
18
+
19
+ function loadNative() {
20
+ const triple = napiTriple()
21
+ const candidates = [
22
+ path.join(__dirname, `fusion-node.${triple}.node`),
23
+ path.join(__dirname, 'fusion-node.node'),
24
+ path.join(__dirname, 'fusion_node.node'),
25
+ ]
26
+ for (const candidate of candidates) {
27
+ if (fs.existsSync(candidate)) {
28
+ return require(candidate)
29
+ }
30
+ }
31
+ throw new Error(
32
+ `fusion-framework native addon not found for ${triple}. ` +
33
+ `Run \`npm run build\` in crates/fusion-node or install a published package.`,
34
+ )
35
+ }
36
+
37
+ const native = loadNative()
38
+ const NativeApp = native.App
39
+ const NativeSettings = native.Settings
40
+
41
+ const HTTP_METHODS = native.getHttpMethods()
42
+ const settings = new NativeSettings()
43
+ const registry = []
44
+ let activeGlobalMiddleware = []
45
+
46
+ const status = Object.create(null)
47
+ if (typeof native.getHttpStatusCodes === 'function') {
48
+ for (const entry of native.getHttpStatusCodes()) {
49
+ status[entry.name] = entry.code
50
+ }
51
+ } else {
52
+ // Fallback if native addon is older
53
+ Object.assign(status, {
54
+ HTTP_SUCCESS: 200,
55
+ HTTP_200_OK: 200,
56
+ HTTP_201_CREATED: 201,
57
+ HTTP_204_NO_CONTENT: 204,
58
+ HTTP_400_BAD_REQUEST: 400,
59
+ HTTP_401_UNAUTHORIZED: 401,
60
+ HTTP_403_FORBIDDEN: 403,
61
+ HTTP_404_NOT_FOUND: 404,
62
+ HTTP_500_INTERNAL_SERVER_ERROR: 500,
63
+ })
64
+ }
65
+
66
+ class FusionBaseApi {
67
+ constructor(request) {
68
+ this.request = request
69
+ }
70
+
71
+ get method() {
72
+ return String(this.request.method || '').toUpperCase()
73
+ }
74
+
75
+ get path() {
76
+ return String(this.request.path || '')
77
+ }
78
+
79
+ get body() {
80
+ return String(this.request.body || '')
81
+ }
82
+
83
+ get headers() {
84
+ return this.request.headers || {}
85
+ }
86
+
87
+ get params() {
88
+ return this.request.params || {}
89
+ }
90
+
91
+ get query() {
92
+ return this.request.query || {}
93
+ }
94
+
95
+ get state() {
96
+ return this.request.state || {}
97
+ }
98
+
99
+ response(body = '', status = 200, headers = {}) {
100
+ // Keep this helper thin: content-type inference lives in fusion-core.
101
+ const out = { status, body }
102
+ const keys = headers ? Object.keys(headers) : []
103
+ if (keys.length) out.headers = { ...headers }
104
+ return out
105
+ }
106
+ }
107
+
108
+ function apiResourceName(cls) {
109
+ const name = typeof cls === 'string' ? cls : cls.name
110
+ return native.apiResourceNameJs(name)
111
+ }
112
+
113
+ function resolveRoutePath(routePath, ApiClass) {
114
+ return native.resolveRoutePathJs(routePath, ApiClass.name)
115
+ }
116
+
117
+ function ensureState(request) {
118
+ if (!request.state || typeof request.state !== 'object') {
119
+ request.state = {}
120
+ }
121
+ return request.state
122
+ }
123
+
124
+ function isResponse(value) {
125
+ return value && typeof value === 'object' && 'status' in value
126
+ }
127
+
128
+ async function runMiddlewareChain(request, middlewares, handler) {
129
+ ensureState(request)
130
+ let index = 0
131
+
132
+ async function dispatch(i, req) {
133
+ if (i >= middlewares.length) {
134
+ return await handler(req)
135
+ }
136
+ const middleware = middlewares[i]
137
+ const callNext = (nextReq) => dispatch(i + 1, nextReq)
138
+ let result = middleware(req, callNext)
139
+ if (result && typeof result.then === 'function') {
140
+ result = await result
141
+ }
142
+ if (isResponse(result)) return result
143
+ return result
144
+ }
145
+
146
+ return dispatch(0, request)
147
+ }
148
+
149
+ function requireRoles(...rolesOrOptions) {
150
+ let roles = rolesOrOptions
151
+ let claim = 'roles'
152
+ let stateKey = 'jwt'
153
+ if (
154
+ rolesOrOptions.length === 1 &&
155
+ rolesOrOptions[0] &&
156
+ typeof rolesOrOptions[0] === 'object' &&
157
+ !Array.isArray(rolesOrOptions[0])
158
+ ) {
159
+ const opts = rolesOrOptions[0]
160
+ roles = Array.isArray(opts.roles) ? opts.roles : []
161
+ if (opts.claim) claim = opts.claim
162
+ if (opts.stateKey) stateKey = opts.stateKey
163
+ }
164
+ const allowed = new Set(roles.map(String))
165
+ return (request, callNext) => {
166
+ const payload = ensureState(request)[stateKey]
167
+ if (!payload) {
168
+ return { status: 401, body: { detail: 'Authentication required' } }
169
+ }
170
+ let userRoles = payload[claim]
171
+ if (userRoles == null) {
172
+ return { status: 403, body: { detail: `Missing '${claim}' claim` } }
173
+ }
174
+ if (typeof userRoles === 'string') userRoles = [userRoles]
175
+ if (!Array.isArray(userRoles)) {
176
+ return { status: 403, body: { detail: `Invalid '${claim}' claim` } }
177
+ }
178
+ const hasRole = userRoles.some((r) => allowed.has(String(r)))
179
+ if (!hasRole) {
180
+ return { status: 403, body: { detail: 'Insufficient permissions', required: [...allowed] } }
181
+ }
182
+ return callNext(request)
183
+ }
184
+ }
185
+
186
+ function bearerJwt(options = {}) {
187
+ const stateKey = options.stateKey || 'jwt'
188
+ const headerName = options.header || 'Authorization'
189
+ const verify = typeof options.verify === 'function' ? options.verify : null
190
+
191
+ return (request, callNext) => {
192
+ const headers = request.headers || {}
193
+ const auth =
194
+ headers[headerName] || headers[headerName.toLowerCase()] || headers[headerName.toUpperCase()]
195
+ if (!auth || !String(auth).toLowerCase().startsWith('bearer ')) {
196
+ return { status: 401, body: { detail: 'Missing bearer token' } }
197
+ }
198
+ const token = String(auth).slice(7).trim()
199
+ try {
200
+ let payload
201
+ if (verify) {
202
+ payload = verify(token)
203
+ if (!payload || typeof payload !== 'object') {
204
+ return { status: 401, body: { detail: 'Invalid token' } }
205
+ }
206
+ } else {
207
+ const parts = token.split('.')
208
+ if (parts.length !== 3) throw new Error('bad token')
209
+ const payloadB64 = parts[1] + '='.repeat((4 - (parts[1].length % 4)) % 4)
210
+ payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString('utf8'))
211
+ }
212
+ ensureState(request)[stateKey] = payload
213
+ return callNext(request)
214
+ } catch {
215
+ return { status: 401, body: { detail: 'Invalid token' } }
216
+ }
217
+ }
218
+ }
219
+
220
+ function router(routePath, options = {}) {
221
+ return function decorate(ApiClass) {
222
+ const resolvedBase = resolveRoutePath(routePath, ApiClass)
223
+
224
+ const v = (options.version ?? '').toString().trim()
225
+ const resolved =
226
+ v.length > 0 ? `${v}/${resolvedBase.replace(/^\/+/, '')}` : resolvedBase
227
+
228
+ ApiClass.__fusion_path__ = resolved
229
+ ApiClass.__fusion_path_template__ = routePath
230
+
231
+ const routeMiddleware = Array.isArray(options.middleware) ? [...options.middleware] : []
232
+ if (Array.isArray(options.roles) && options.roles.length) {
233
+ routeMiddleware.push(
234
+ requireRoles({
235
+ roles: options.roles,
236
+ claim: options.roleClaim || 'roles',
237
+ stateKey: options.roleStateKey || 'jwt',
238
+ }),
239
+ )
240
+ }
241
+
242
+ registry.push({
243
+ path: resolved,
244
+ ApiClass,
245
+ middleware: routeMiddleware,
246
+ swagger: {
247
+ tags: Array.isArray(options.tags) ? options.tags : [],
248
+ description: options.desc ?? null,
249
+ title: options.title ?? null,
250
+ deprecated: !!options.deprecated,
251
+ },
252
+ version_prefix: v,
253
+ })
254
+ return ApiClass
255
+ }
256
+ }
257
+
258
+ function configure(next = {}) {
259
+ settings.merge(next)
260
+ return getSettings()
261
+ }
262
+
263
+ function getSettings() {
264
+ settings.ensureLoaded()
265
+ return {
266
+ host: settings.host,
267
+ port: settings.port,
268
+ debug: settings.debug,
269
+ env: settings.env,
270
+ }
271
+ }
272
+
273
+ function definesMethod(ApiClass, methodName) {
274
+ let current = ApiClass
275
+ while (current && current !== Function.prototype) {
276
+ if (current === FusionBaseApi) break
277
+ if (Object.prototype.hasOwnProperty.call(current.prototype, methodName)) {
278
+ return true
279
+ }
280
+ current = Object.getPrototypeOf(current)
281
+ }
282
+ return false
283
+ }
284
+
285
+ class HTTPException extends Error {
286
+ constructor(status, detail = null, headers = {}) {
287
+ super(typeof detail === 'string' ? detail : `HTTP ${status}`)
288
+ this.status = Number(status)
289
+ this.detail = detail == null ? '' : detail
290
+ this.headers = headers || {}
291
+ }
292
+
293
+ toResponse() {
294
+ const headers = { ...this.headers }
295
+ const body = this.detail
296
+ // Keep this helper thin: content-type inference lives in fusion-core.
297
+ const out = { status: this.status, body }
298
+ const keys = headers ? Object.keys(headers) : []
299
+ if (keys.length) out.headers = headers
300
+ return out
301
+ }
302
+ }
303
+
304
+ function asObject(value) {
305
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
306
+ }
307
+
308
+ function asList(value) {
309
+ return Array.isArray(value) ? value : []
310
+ }
311
+
312
+ function truthyEnabled(value, defaultValue = true) {
313
+ if (value === undefined || value === null) return defaultValue
314
+ if (value === false || value === 0 || value === 'false' || value === '0' || value === 'off' || value === 'no') {
315
+ return false
316
+ }
317
+ if (value === true || value === 1 || value === 'true' || value === '1' || value === 'on' || value === 'yes') {
318
+ return true
319
+ }
320
+ return Boolean(value)
321
+ }
322
+
323
+ function readSwaggerSettings() {
324
+ if (!truthyEnabled(settings.get('swagger.enabled', true))) {
325
+ return { enabled: false }
326
+ }
327
+
328
+ let pathValue = settings.get('swagger.path', '/swagger')
329
+ if (pathValue === false || pathValue === null || pathValue === '' || pathValue === 'false' || pathValue === 'off') {
330
+ return { enabled: false }
331
+ }
332
+
333
+ let prefix = String(pathValue).replace(/\/+$/, '') || '/swagger'
334
+ if (!prefix.startsWith('/')) prefix = `/${prefix}`
335
+
336
+ const info = asObject(settings.get('swagger.info', {}))
337
+ for (const key of ['title', 'version', 'description', 'termsOfService', 'contact', 'license']) {
338
+ const flat = settings.get(`swagger.${key}`, undefined)
339
+ if (flat !== undefined && flat !== null && info[key] === undefined) info[key] = flat
340
+ }
341
+ if (!info.title) info.title = 'fusion-framework'
342
+ if (!info.version) info.version = '1.0.0'
343
+
344
+ const pageTitle = settings.get('swagger.title', null) || info.title || 'Fusion API Docs'
345
+
346
+ const authRaw = asObject(settings.get('swagger.auth', {}))
347
+ const schemes = asObject(authRaw.schemes)
348
+ const oauth = asObject(authRaw.oauth)
349
+ const globalSecurity = asList(authRaw.global)
350
+ let persistAuth = authRaw.persistAuthorization
351
+ if (persistAuth === undefined) persistAuth = false
352
+
353
+ const navbarRaw = asObject(settings.get('swagger.navbar', {}))
354
+ const navbar = {
355
+ enabled: truthyEnabled(navbarRaw.enabled, true),
356
+ showUrlInput: truthyEnabled(navbarRaw.showUrlInput, true),
357
+ urls: Array.isArray(navbarRaw.urls) ? navbarRaw.urls : null,
358
+ }
359
+
360
+ const ui = {
361
+ deepLinking: true,
362
+ displayOperationId: false,
363
+ defaultModelsExpandDepth: 1,
364
+ defaultModelExpandDepth: 1,
365
+ defaultModelRendering: 'example',
366
+ docExpansion: 'list',
367
+ filter: true,
368
+ tryItOutEnabled: true,
369
+ persistAuthorization: Boolean(persistAuth),
370
+ displayRequestDuration: true,
371
+ showExtensions: false,
372
+ showCommonExtensions: false,
373
+ syntaxHighlight: { activated: true, theme: 'agate' },
374
+ withCredentials: false,
375
+ validatorUrl: 'https://validator.swagger.io/validator',
376
+ ...asObject(settings.get('swagger.ui', {})),
377
+ }
378
+ if (Object.prototype.hasOwnProperty.call(authRaw, 'persistAuthorization')) {
379
+ ui.persistAuthorization = Boolean(persistAuth)
380
+ }
381
+
382
+ let servers = settings.get('swagger.servers', null)
383
+ if (!Array.isArray(servers)) servers = []
384
+
385
+ return {
386
+ enabled: true,
387
+ path: prefix,
388
+ pageTitle: String(pageTitle),
389
+ info,
390
+ servers,
391
+ auth: {
392
+ schemes,
393
+ global: globalSecurity,
394
+ oauth,
395
+ persistAuthorization: Boolean(persistAuth),
396
+ },
397
+ navbar,
398
+ ui,
399
+ }
400
+ }
401
+
402
+ function applySwaggerOpenApi(openapi, swagger) {
403
+ openapi.info = { ...asObject(openapi.info), ...swagger.info }
404
+ if (swagger.servers?.length) openapi.servers = swagger.servers
405
+ if (swagger.auth?.schemes && Object.keys(swagger.auth.schemes).length) {
406
+ openapi.components = asObject(openapi.components)
407
+ openapi.components.securitySchemes = {
408
+ ...asObject(openapi.components.securitySchemes),
409
+ ...swagger.auth.schemes,
410
+ }
411
+ }
412
+ if (swagger.auth?.global?.length) openapi.security = swagger.auth.global
413
+ return openapi
414
+ }
415
+
416
+ function swaggerUiHtml(swagger, openapiUrl) {
417
+ const uiOpts = { ...swagger.ui }
418
+ delete uiOpts.presets
419
+ delete uiOpts.plugins
420
+ delete uiOpts.layout
421
+
422
+ if (swagger.navbar?.urls?.length) {
423
+ delete uiOpts.url
424
+ uiOpts.urls = swagger.navbar.urls
425
+ } else {
426
+ uiOpts.url = openapiUrl
427
+ delete uiOpts.urls
428
+ }
429
+ uiOpts.dom_id = '#swagger-ui'
430
+
431
+ const uiJson = JSON.stringify(uiOpts).replace(/</g, '\\u003c')
432
+ const oauth = swagger.auth?.oauth && Object.keys(swagger.auth.oauth).length ? swagger.auth.oauth : null
433
+ const oauthJson = JSON.stringify(oauth).replace(/</g, '\\u003c')
434
+ const title = String(swagger.pageTitle)
435
+ .replace(/&/g, '&amp;')
436
+ .replace(/</g, '&lt;')
437
+ .replace(/>/g, '&gt;')
438
+
439
+ const navbarEnabled = !!swagger.navbar?.enabled
440
+ const showUrlInput = swagger.navbar?.showUrlInput !== false
441
+ const hideUrlCss =
442
+ navbarEnabled && !showUrlInput
443
+ ? `<style>.topbar .download-url-wrapper { display: none !important; }</style>`
444
+ : ''
445
+ const standaloneScript = navbarEnabled
446
+ ? `<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-standalone-preset.js"></script>`
447
+ : ''
448
+
449
+ return `<!doctype html>
450
+ <html>
451
+ <head>
452
+ <meta charset="utf-8" />
453
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
454
+ <title>${title}</title>
455
+ <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css" />
456
+ ${hideUrlCss}
457
+ </head>
458
+ <body>
459
+ <div id="swagger-ui"></div>
460
+ <script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
461
+ ${standaloneScript}
462
+ <script>
463
+ window.onload = function() {
464
+ var opts = ${uiJson};
465
+ opts.presets = [SwaggerUIBundle.presets.apis];
466
+ opts.plugins = [SwaggerUIBundle.plugins.DownloadUrl];
467
+ if (${navbarEnabled ? 'true' : 'false'} && typeof SwaggerUIStandalonePreset !== 'undefined') {
468
+ opts.presets.push(SwaggerUIStandalonePreset);
469
+ opts.layout = 'StandaloneLayout';
470
+ } else {
471
+ opts.layout = 'BaseLayout';
472
+ }
473
+ var ui = SwaggerUIBundle(opts);
474
+ var oauth = ${oauthJson};
475
+ if (oauth && typeof ui.initOAuth === 'function') {
476
+ ui.initOAuth(oauth);
477
+ }
478
+ window.ui = ui;
479
+ };
480
+ </script>
481
+ </body>
482
+ </html>`
483
+ }
484
+
485
+ class FusionApp {
486
+ constructor(customSettings) {
487
+ if (customSettings) settings.merge(customSettings)
488
+ this.settings = getSettings()
489
+ this.engine = new NativeApp()
490
+ this.mounted = false
491
+ this._middleware = []
492
+ }
493
+
494
+ use(middleware) {
495
+ this._middleware.push(middleware)
496
+ }
497
+
498
+ mount() {
499
+ if (this.mounted) return
500
+ activeGlobalMiddleware = [...this._middleware]
501
+
502
+ for (const { path: routePath, ApiClass, middleware: routeMiddleware = [] } of registry) {
503
+ for (const methodName of HTTP_METHODS) {
504
+ if (!definesMethod(ApiClass, methodName)) continue
505
+ this.engine.route(methodName.toUpperCase(), routePath, async (request) => {
506
+ const chain = [...activeGlobalMiddleware, ...routeMiddleware]
507
+ const handler = async (req) => {
508
+ try {
509
+ const instance = new ApiClass(req)
510
+ const fn = instance[methodName]
511
+ return await Promise.resolve(fn.call(instance))
512
+ } catch (err) {
513
+ if (err instanceof HTTPException) return err.toResponse()
514
+ throw err
515
+ }
516
+ }
517
+ return runMiddlewareChain(request, chain, handler)
518
+ })
519
+ }
520
+ }
521
+
522
+ const swagger = readSwaggerSettings()
523
+ if (swagger.enabled) {
524
+ const prefix = swagger.path
525
+
526
+ const openapi = applySwaggerOpenApi(
527
+ {
528
+ openapi: '3.0.3',
529
+ info: { ...swagger.info },
530
+ paths: {},
531
+ },
532
+ swagger,
533
+ )
534
+
535
+ const parsePathParams = (pattern) => {
536
+ return String(pattern)
537
+ .split('/')
538
+ .filter((seg) => (seg.startsWith('{') && seg.endsWith('}')) || (seg.startsWith('[') && seg.endsWith(']')))
539
+ .map((seg) => seg.slice(1, -1))
540
+ }
541
+
542
+ for (const item of registry) {
543
+ const { path: p, ApiClass, swagger: routeSwagger } = item
544
+ const pathParams = parsePathParams(p)
545
+ const resolvedPath = p.startsWith('/') ? p : `/${p}`
546
+
547
+ if (!openapi.paths[resolvedPath]) openapi.paths[resolvedPath] = {}
548
+
549
+ for (const methodName of HTTP_METHODS) {
550
+ if (!definesMethod(ApiClass, methodName)) continue
551
+
552
+ const methodUpper = String(methodName).toUpperCase()
553
+ const methodLower = String(methodName).toLowerCase()
554
+
555
+ const params = pathParams.map((name) => ({
556
+ name,
557
+ in: 'path',
558
+ required: true,
559
+ schema: { type: 'string' },
560
+ }))
561
+
562
+ openapi.paths[resolvedPath][methodLower] = {
563
+ tags: routeSwagger?.tags?.length ? routeSwagger.tags : [],
564
+ summary: routeSwagger?.title ?? `${ApiClass.name}.${methodUpper}`,
565
+ description: routeSwagger?.description ?? '',
566
+ deprecated: !!routeSwagger?.deprecated,
567
+ operationId: `${ApiClass.name}_${methodLower}`,
568
+ parameters: params,
569
+ responses: { '200': { description: 'OK' } },
570
+ }
571
+ }
572
+ }
573
+
574
+ this.engine.route('GET', `${prefix}/openapi.json`, async () => openapi)
575
+ this.engine.route('GET', prefix, async () => ({
576
+ status: 200,
577
+ body: swaggerUiHtml(swagger, `${prefix}/openapi.json`),
578
+ headers: { 'content-type': 'text/html' },
579
+ }))
580
+ }
581
+
582
+ this.mounted = true
583
+ }
584
+
585
+ async listen(host, port) {
586
+ this.mount()
587
+ const snapshot = getSettings()
588
+ const h = host ?? snapshot.host
589
+ const p = port ?? snapshot.port
590
+ if (snapshot.debug) {
591
+ console.log(`fusion listening on http://${h}:${p}`)
592
+ }
593
+ await this.engine.listen(h, Number(p))
594
+ }
595
+ }
596
+
597
+ async function run(options = {}) {
598
+ const settingsModulePath =
599
+ typeof options === 'string' ? options : options && options.settingsModule
600
+ const middleware = Array.isArray(options?.middleware) ? options.middleware : []
601
+
602
+ settings.ensureLoaded([process.cwd()])
603
+ if (settingsModulePath) {
604
+ const mod = await import(pathToFileUrl(settingsModulePath))
605
+ const overlay = {}
606
+ if (mod.HOST !== undefined) overlay.host = mod.HOST
607
+ if (mod.PORT !== undefined) overlay.port = mod.PORT
608
+ if (mod.DEBUG !== undefined) overlay.debug = mod.DEBUG
609
+ if (Object.keys(overlay).length) settings.merge(overlay)
610
+ }
611
+ const app = new FusionApp()
612
+ for (const mw of middleware) app.use(mw)
613
+ await app.listen()
614
+ return app
615
+ }
616
+
617
+ function coerceParam(raw, kind = 'auto') {
618
+ return native.coerceParamJs(String(raw), kind)
619
+ }
620
+
621
+ function pathToFileUrl(filePath) {
622
+ const resolved = path.resolve(filePath)
623
+ return require('url').pathToFileURL(resolved).href
624
+ }
625
+
626
+ const route = router
627
+
628
+ module.exports = {
629
+ App: NativeApp,
630
+ Settings: NativeSettings,
631
+ FusionApp,
632
+ FusionBaseApi,
633
+ HTTPException,
634
+ router,
635
+ route,
636
+ apiResourceName,
637
+ resolveRoutePath,
638
+ configure,
639
+ getSettings,
640
+ settings,
641
+ status,
642
+ HTTP_METHODS,
643
+ run,
644
+ bearerJwt,
645
+ requireRoles,
646
+ runMiddlewareChain,
647
+ coerceParam,
648
+ getHttpMethods: () => HTTP_METHODS,
649
+ apiResourceNameJs: native.apiResourceNameJs,
650
+ resolveRoutePathJs: native.resolveRoutePathJs,
651
+ coerceParamJs: native.coerceParamJs,
652
+ }
package/package.json CHANGED
@@ -1,10 +1,78 @@
1
1
  {
2
2
  "name": "fusion-framework",
3
- "version": "0.0.1",
4
- "description": "Test package for npm trusted publishing",
3
+ "version": "1.1.2",
4
+ "description": "Build high-performance class-based HTTP APIs in Node.js on a shared Rust core — routing, middleware, JWT auth, and TypeScript types",
5
+ "keywords": [
6
+ "http",
7
+ "framework",
8
+ "nodejs",
9
+ "node",
10
+ "rust",
11
+ "napi",
12
+ "http-server",
13
+ "web-framework",
14
+ "rest-api",
15
+ "api",
16
+ "routing",
17
+ "middleware",
18
+ "jwt",
19
+ "typescript",
20
+ "openapi",
21
+ "swagger",
22
+ "hyper",
23
+ "fusion"
24
+ ],
25
+ "homepage": "https://fusion.cipherunit.xyz/",
26
+ "bugs": {
27
+ "url": "https://github.com/cipherunits/fusion-framework/issues"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/cipherunits/fusion-framework.git",
32
+ "directory": "crates/fusion-node"
33
+ },
34
+ "author": "CipherUnits <cipherunit.dev@gmail.com>",
5
35
  "license": "MIT",
36
+ "main": "index.js",
37
+ "types": "index.d.ts",
6
38
  "files": [
7
- "index.js"
39
+ "index.js",
40
+ "index.d.ts",
41
+ "README.md",
42
+ "*.node"
8
43
  ],
9
- "main": "index.js"
44
+ "napi": {
45
+ "name": "fusion-node",
46
+ "triples": {
47
+ "defaults": true,
48
+ "additional": [
49
+ "aarch64-unknown-linux-gnu",
50
+ "aarch64-apple-darwin"
51
+ ]
52
+ }
53
+ },
54
+ "scripts": {
55
+ "artifacts": "napi artifacts",
56
+ "build": "napi build --platform --release",
57
+ "build:debug": "napi build --platform",
58
+ "prepublishOnly": "napi prepublish -t npm --skip-gh-release"
59
+ },
60
+ "publishConfig": {
61
+ "access": "public",
62
+ "registry": "https://registry.npmjs.org/",
63
+ "provenance": true
64
+ },
65
+ "engines": {
66
+ "node": ">=16"
67
+ },
68
+ "devDependencies": {
69
+ "@napi-rs/cli": "^2.18.4"
70
+ },
71
+ "optionalDependencies": {
72
+ "fusion-framework-win32-x64-msvc": "1.1.2",
73
+ "fusion-framework-darwin-x64": "1.1.2",
74
+ "fusion-framework-linux-x64-gnu": "1.1.2",
75
+ "fusion-framework-linux-arm64-gnu": "1.1.2",
76
+ "fusion-framework-darwin-arm64": "1.1.2"
77
+ }
10
78
  }