fusion-framework 0.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Fusion Framework
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,56 @@
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).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm i fusion-framework
11
+ ```
12
+
13
+ From this repo:
14
+
15
+ ```bash
16
+ cd crates/fusion-node && npm install && npm run build:debug
17
+ ```
18
+
19
+ Scaffold with [Fusion Tool](https://github.com/cipherunits/fusion-tool):
20
+
21
+ ```bash
22
+ fusion init --lang typescript --name my-app
23
+ ```
24
+
25
+ ## Quick start
26
+
27
+ ```js
28
+ import { FusionBaseApi, route, status, FusionApp, getSettings, settings } from 'fusion-framework'
29
+
30
+ export const ItemModule = route('/api/[module]/{id}')(
31
+ class ItemModule extends FusionBaseApi {
32
+ get() {
33
+ return this.response({ id: this.params.id }, status.HTTP_SUCCESS)
34
+ }
35
+ },
36
+ )
37
+
38
+ const MIDDLEWARE = [] // your middleware; Fusion already adds frameworkHeaders() by default
39
+
40
+ settings.ensureLoaded()
41
+ const app = new FusionApp(getSettings())
42
+ for (const mw of MIDDLEWARE) app.use(mw)
43
+ await app.listen()
44
+ ```
45
+
46
+ ## Docs
47
+
48
+ Full guides (router, config, middleware):
49
+ **https://fusion.cipherunit.xyz/en/docs/typescript/v1**
50
+
51
+ - Site: [fusion.cipherunit.xyz](https://fusion.cipherunit.xyz/)
52
+ - GitHub: [cipherunits/fusion-framework](https://github.com/cipherunits/fusion-framework)
53
+
54
+ ## License
55
+
56
+ BSD 3-Clause
Binary file
Binary file
Binary file
Binary file
package/index.d.ts ADDED
@@ -0,0 +1,148 @@
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 header: HeaderModule
98
+ export const HTTP_METHODS: string[]
99
+
100
+ export interface HeaderModule {
101
+ [name: string]: string | ((...args: any[]) => Record<string, string>)
102
+ CONTENT_TYPE: string
103
+ CONTENT_DISPOSITION: string
104
+ LOCATION: string
105
+ AUTHORIZATION: string
106
+ APPLICATION_JSON: string
107
+ APPLICATION_OCTET_STREAM: string
108
+ APPLICATION_PDF: string
109
+ attachment(filename: string): Record<string, string>
110
+ inline(filename?: string | null): Record<string, string>
111
+ contentType(mediaType: string, charset?: string | null): Record<string, string>
112
+ location(url: string): Record<string, string>
113
+ cacheControl(value: string): Record<string, string>
114
+ download(filename: string, mediaType?: string | null): Record<string, string>
115
+ fingerprint(): Record<string, string>
116
+ }
117
+
118
+ export interface FusionSettings {
119
+ host: string
120
+ port: number
121
+ debug: boolean
122
+ env?: string
123
+ }
124
+
125
+ export interface FusionRequest {
126
+ method: string
127
+ path: string
128
+ body: string
129
+ headers: Record<string, string>
130
+ params: Record<string, string>
131
+ query: Record<string, string>
132
+ state?: Record<string, unknown>
133
+ }
134
+
135
+ export type FusionMiddleware = (
136
+ request: FusionRequest,
137
+ callNext: (request: FusionRequest) => unknown | Promise<unknown>,
138
+ ) => unknown | Promise<unknown>
139
+
140
+ export function frameworkHeaders(): FusionMiddleware
141
+
142
+ export type FusionResponse =
143
+ | string
144
+ | {
145
+ status?: number
146
+ body?: unknown
147
+ headers?: Record<string, string>
148
+ }
package/index.js ADDED
@@ -0,0 +1,747 @@
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
+ const header = Object.create(null)
67
+ if (typeof native.getHttpHeaderConstants === 'function') {
68
+ for (const entry of native.getHttpHeaderConstants()) {
69
+ header[entry.name] = entry.value
70
+ }
71
+ } else {
72
+ Object.assign(header, {
73
+ CONTENT_TYPE: 'Content-Type',
74
+ CONTENT_DISPOSITION: 'Content-Disposition',
75
+ LOCATION: 'Location',
76
+ AUTHORIZATION: 'Authorization',
77
+ APPLICATION_JSON: 'application/json',
78
+ APPLICATION_OCTET_STREAM: 'application/octet-stream',
79
+ APPLICATION_PDF: 'application/pdf',
80
+ })
81
+ }
82
+
83
+ function mergeHeaderMap(map) {
84
+ return map && typeof map === 'object' ? { ...map } : {}
85
+ }
86
+
87
+ header.attachment = (filename) =>
88
+ typeof native.headerAttachment === 'function'
89
+ ? mergeHeaderMap(native.headerAttachment(String(filename)))
90
+ : { [header.CONTENT_DISPOSITION]: `attachment; filename="${filename}"` }
91
+
92
+ header.inline = (filename) =>
93
+ typeof native.headerInline === 'function'
94
+ ? mergeHeaderMap(native.headerInline(filename == null ? null : String(filename)))
95
+ : filename
96
+ ? { [header.CONTENT_DISPOSITION]: `inline; filename="${filename}"` }
97
+ : { [header.CONTENT_DISPOSITION]: 'inline' }
98
+
99
+ header.contentType = (mediaType, charset) =>
100
+ typeof native.headerContentType === 'function'
101
+ ? mergeHeaderMap(native.headerContentType(String(mediaType), charset == null ? null : String(charset)))
102
+ : {
103
+ [header.CONTENT_TYPE]: charset
104
+ ? `${mediaType}; charset=${charset}`
105
+ : String(mediaType),
106
+ }
107
+
108
+ header.location = (url) =>
109
+ typeof native.headerLocation === 'function'
110
+ ? mergeHeaderMap(native.headerLocation(String(url)))
111
+ : { [header.LOCATION]: String(url) }
112
+
113
+ header.cacheControl = (value) =>
114
+ typeof native.headerCacheControl === 'function'
115
+ ? mergeHeaderMap(native.headerCacheControl(String(value)))
116
+ : { 'Cache-Control': String(value) }
117
+
118
+ header.download = (filename, mediaType) =>
119
+ typeof native.headerDownload === 'function'
120
+ ? mergeHeaderMap(
121
+ native.headerDownload(
122
+ String(filename),
123
+ mediaType == null ? null : String(mediaType),
124
+ ),
125
+ )
126
+ : {
127
+ ...header.contentType(mediaType || header.APPLICATION_OCTET_STREAM),
128
+ ...header.attachment(filename),
129
+ }
130
+
131
+ header.fingerprint = () =>
132
+ typeof native.getFingerprintHeaders === 'function'
133
+ ? mergeHeaderMap(native.getFingerprintHeaders())
134
+ : {
135
+ 'X-Powered-By': 'Fusion Framework',
136
+ 'X-Framework': 'Fusion',
137
+ 'X-Fusion-Version': '1.2.0',
138
+ }
139
+
140
+ function frameworkHeaders() {
141
+ const extra = header.fingerprint()
142
+ return (request, callNext) => {
143
+ const result = callNext(request)
144
+ const apply = (value) => {
145
+ if (value && typeof value.then === 'function') {
146
+ return value.then(apply)
147
+ }
148
+ if (!value || typeof value !== 'object') {
149
+ return { status: 200, body: value, headers: { ...extra } }
150
+ }
151
+ const headers = { ...extra, ...(value.headers || {}) }
152
+ return { ...value, headers }
153
+ }
154
+ return apply(result)
155
+ }
156
+ }
157
+
158
+ class FusionBaseApi {
159
+ constructor(request) {
160
+ this.request = request
161
+ }
162
+
163
+ get method() {
164
+ return String(this.request.method || '').toUpperCase()
165
+ }
166
+
167
+ get path() {
168
+ return String(this.request.path || '')
169
+ }
170
+
171
+ get body() {
172
+ return String(this.request.body || '')
173
+ }
174
+
175
+ get headers() {
176
+ return this.request.headers || {}
177
+ }
178
+
179
+ get params() {
180
+ return this.request.params || {}
181
+ }
182
+
183
+ get query() {
184
+ return this.request.query || {}
185
+ }
186
+
187
+ get state() {
188
+ return this.request.state || {}
189
+ }
190
+
191
+ response(body = '', status = 200, headers = {}) {
192
+ // Keep this helper thin: content-type inference lives in fusion-core.
193
+ const out = { status, body }
194
+ const keys = headers ? Object.keys(headers) : []
195
+ if (keys.length) out.headers = { ...headers }
196
+ return out
197
+ }
198
+ }
199
+
200
+ function apiResourceName(cls) {
201
+ const name = typeof cls === 'string' ? cls : cls.name
202
+ return native.apiResourceNameJs(name)
203
+ }
204
+
205
+ function resolveRoutePath(routePath, ApiClass) {
206
+ return native.resolveRoutePathJs(routePath, ApiClass.name)
207
+ }
208
+
209
+ function ensureState(request) {
210
+ if (!request.state || typeof request.state !== 'object') {
211
+ request.state = {}
212
+ }
213
+ return request.state
214
+ }
215
+
216
+ function isResponse(value) {
217
+ return value && typeof value === 'object' && 'status' in value
218
+ }
219
+
220
+ async function runMiddlewareChain(request, middlewares, handler) {
221
+ ensureState(request)
222
+ let index = 0
223
+
224
+ async function dispatch(i, req) {
225
+ if (i >= middlewares.length) {
226
+ return await handler(req)
227
+ }
228
+ const middleware = middlewares[i]
229
+ const callNext = (nextReq) => dispatch(i + 1, nextReq)
230
+ let result = middleware(req, callNext)
231
+ if (result && typeof result.then === 'function') {
232
+ result = await result
233
+ }
234
+ if (isResponse(result)) return result
235
+ return result
236
+ }
237
+
238
+ return dispatch(0, request)
239
+ }
240
+
241
+ function requireRoles(...rolesOrOptions) {
242
+ let roles = rolesOrOptions
243
+ let claim = 'roles'
244
+ let stateKey = 'jwt'
245
+ if (
246
+ rolesOrOptions.length === 1 &&
247
+ rolesOrOptions[0] &&
248
+ typeof rolesOrOptions[0] === 'object' &&
249
+ !Array.isArray(rolesOrOptions[0])
250
+ ) {
251
+ const opts = rolesOrOptions[0]
252
+ roles = Array.isArray(opts.roles) ? opts.roles : []
253
+ if (opts.claim) claim = opts.claim
254
+ if (opts.stateKey) stateKey = opts.stateKey
255
+ }
256
+ const allowed = new Set(roles.map(String))
257
+ return (request, callNext) => {
258
+ const payload = ensureState(request)[stateKey]
259
+ if (!payload) {
260
+ return { status: 401, body: { detail: 'Authentication required' } }
261
+ }
262
+ let userRoles = payload[claim]
263
+ if (userRoles == null) {
264
+ return { status: 403, body: { detail: `Missing '${claim}' claim` } }
265
+ }
266
+ if (typeof userRoles === 'string') userRoles = [userRoles]
267
+ if (!Array.isArray(userRoles)) {
268
+ return { status: 403, body: { detail: `Invalid '${claim}' claim` } }
269
+ }
270
+ const hasRole = userRoles.some((r) => allowed.has(String(r)))
271
+ if (!hasRole) {
272
+ return { status: 403, body: { detail: 'Insufficient permissions', required: [...allowed] } }
273
+ }
274
+ return callNext(request)
275
+ }
276
+ }
277
+
278
+ function bearerJwt(options = {}) {
279
+ const stateKey = options.stateKey || 'jwt'
280
+ const headerName = options.header || 'Authorization'
281
+ const verify = typeof options.verify === 'function' ? options.verify : null
282
+
283
+ return (request, callNext) => {
284
+ const headers = request.headers || {}
285
+ const auth =
286
+ headers[headerName] || headers[headerName.toLowerCase()] || headers[headerName.toUpperCase()]
287
+ if (!auth || !String(auth).toLowerCase().startsWith('bearer ')) {
288
+ return { status: 401, body: { detail: 'Missing bearer token' } }
289
+ }
290
+ const token = String(auth).slice(7).trim()
291
+ try {
292
+ let payload
293
+ if (verify) {
294
+ payload = verify(token)
295
+ if (!payload || typeof payload !== 'object') {
296
+ return { status: 401, body: { detail: 'Invalid token' } }
297
+ }
298
+ } else {
299
+ const parts = token.split('.')
300
+ if (parts.length !== 3) throw new Error('bad token')
301
+ const payloadB64 = parts[1] + '='.repeat((4 - (parts[1].length % 4)) % 4)
302
+ payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString('utf8'))
303
+ }
304
+ ensureState(request)[stateKey] = payload
305
+ return callNext(request)
306
+ } catch {
307
+ return { status: 401, body: { detail: 'Invalid token' } }
308
+ }
309
+ }
310
+ }
311
+
312
+ function router(routePath, options = {}) {
313
+ return function decorate(ApiClass) {
314
+ const resolvedBase = resolveRoutePath(routePath, ApiClass)
315
+
316
+ const v = (options.version ?? '').toString().trim()
317
+ const resolved =
318
+ v.length > 0 ? `${v}/${resolvedBase.replace(/^\/+/, '')}` : resolvedBase
319
+
320
+ ApiClass.__fusion_path__ = resolved
321
+ ApiClass.__fusion_path_template__ = routePath
322
+
323
+ const routeMiddleware = Array.isArray(options.middleware) ? [...options.middleware] : []
324
+ if (Array.isArray(options.roles) && options.roles.length) {
325
+ routeMiddleware.push(
326
+ requireRoles({
327
+ roles: options.roles,
328
+ claim: options.roleClaim || 'roles',
329
+ stateKey: options.roleStateKey || 'jwt',
330
+ }),
331
+ )
332
+ }
333
+
334
+ registry.push({
335
+ path: resolved,
336
+ ApiClass,
337
+ middleware: routeMiddleware,
338
+ swagger: {
339
+ tags: Array.isArray(options.tags) ? options.tags : [],
340
+ description: options.desc ?? null,
341
+ title: options.title ?? null,
342
+ deprecated: !!options.deprecated,
343
+ },
344
+ version_prefix: v,
345
+ })
346
+ return ApiClass
347
+ }
348
+ }
349
+
350
+ function configure(next = {}) {
351
+ settings.merge(next)
352
+ return getSettings()
353
+ }
354
+
355
+ function getSettings() {
356
+ settings.ensureLoaded()
357
+ return {
358
+ host: settings.host,
359
+ port: settings.port,
360
+ debug: settings.debug,
361
+ env: settings.env,
362
+ }
363
+ }
364
+
365
+ function definesMethod(ApiClass, methodName) {
366
+ let current = ApiClass
367
+ while (current && current !== Function.prototype) {
368
+ if (current === FusionBaseApi) break
369
+ if (Object.prototype.hasOwnProperty.call(current.prototype, methodName)) {
370
+ return true
371
+ }
372
+ current = Object.getPrototypeOf(current)
373
+ }
374
+ return false
375
+ }
376
+
377
+ class HTTPException extends Error {
378
+ constructor(status, detail = null, headers = {}) {
379
+ super(typeof detail === 'string' ? detail : `HTTP ${status}`)
380
+ this.status = Number(status)
381
+ this.detail = detail == null ? '' : detail
382
+ this.headers = headers || {}
383
+ }
384
+
385
+ toResponse() {
386
+ const headers = { ...this.headers }
387
+ const body = this.detail
388
+ // Keep this helper thin: content-type inference lives in fusion-core.
389
+ const out = { status: this.status, body }
390
+ const keys = headers ? Object.keys(headers) : []
391
+ if (keys.length) out.headers = headers
392
+ return out
393
+ }
394
+ }
395
+
396
+ function asObject(value) {
397
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
398
+ }
399
+
400
+ function asList(value) {
401
+ return Array.isArray(value) ? value : []
402
+ }
403
+
404
+ function truthyEnabled(value, defaultValue = true) {
405
+ if (value === undefined || value === null) return defaultValue
406
+ if (value === false || value === 0 || value === 'false' || value === '0' || value === 'off' || value === 'no') {
407
+ return false
408
+ }
409
+ if (value === true || value === 1 || value === 'true' || value === '1' || value === 'on' || value === 'yes') {
410
+ return true
411
+ }
412
+ return Boolean(value)
413
+ }
414
+
415
+ function readSwaggerSettings() {
416
+ if (!truthyEnabled(settings.get('swagger.enabled', true))) {
417
+ return { enabled: false }
418
+ }
419
+
420
+ let pathValue = settings.get('swagger.path', '/swagger')
421
+ if (pathValue === false || pathValue === null || pathValue === '' || pathValue === 'false' || pathValue === 'off') {
422
+ return { enabled: false }
423
+ }
424
+
425
+ let prefix = String(pathValue).replace(/\/+$/, '') || '/swagger'
426
+ if (!prefix.startsWith('/')) prefix = `/${prefix}`
427
+
428
+ const info = asObject(settings.get('swagger.info', {}))
429
+ for (const key of ['title', 'version', 'description', 'termsOfService', 'contact', 'license']) {
430
+ const flat = settings.get(`swagger.${key}`, undefined)
431
+ if (flat !== undefined && flat !== null && info[key] === undefined) info[key] = flat
432
+ }
433
+ if (!info.title) info.title = 'fusion-framework'
434
+ if (!info.version) info.version = '1.0.0'
435
+
436
+ const pageTitle = settings.get('swagger.title', null) || info.title || 'Fusion API Docs'
437
+
438
+ const authRaw = asObject(settings.get('swagger.auth', {}))
439
+ const schemes = asObject(authRaw.schemes)
440
+ const oauth = asObject(authRaw.oauth)
441
+ const globalSecurity = asList(authRaw.global)
442
+ let persistAuth = authRaw.persistAuthorization
443
+ if (persistAuth === undefined) persistAuth = false
444
+
445
+ const navbarRaw = asObject(settings.get('swagger.navbar', {}))
446
+ const navbar = {
447
+ enabled: truthyEnabled(navbarRaw.enabled, true),
448
+ showUrlInput: truthyEnabled(navbarRaw.showUrlInput, true),
449
+ urls: Array.isArray(navbarRaw.urls) ? navbarRaw.urls : null,
450
+ }
451
+
452
+ const ui = {
453
+ deepLinking: true,
454
+ displayOperationId: false,
455
+ defaultModelsExpandDepth: 1,
456
+ defaultModelExpandDepth: 1,
457
+ defaultModelRendering: 'example',
458
+ docExpansion: 'list',
459
+ filter: true,
460
+ tryItOutEnabled: true,
461
+ persistAuthorization: Boolean(persistAuth),
462
+ displayRequestDuration: true,
463
+ showExtensions: false,
464
+ showCommonExtensions: false,
465
+ syntaxHighlight: { activated: true, theme: 'agate' },
466
+ withCredentials: false,
467
+ validatorUrl: 'https://validator.swagger.io/validator',
468
+ ...asObject(settings.get('swagger.ui', {})),
469
+ }
470
+ if (Object.prototype.hasOwnProperty.call(authRaw, 'persistAuthorization')) {
471
+ ui.persistAuthorization = Boolean(persistAuth)
472
+ }
473
+
474
+ let servers = settings.get('swagger.servers', null)
475
+ if (!Array.isArray(servers)) servers = []
476
+
477
+ return {
478
+ enabled: true,
479
+ path: prefix,
480
+ pageTitle: String(pageTitle),
481
+ info,
482
+ servers,
483
+ auth: {
484
+ schemes,
485
+ global: globalSecurity,
486
+ oauth,
487
+ persistAuthorization: Boolean(persistAuth),
488
+ },
489
+ navbar,
490
+ ui,
491
+ }
492
+ }
493
+
494
+ function applySwaggerOpenApi(openapi, swagger) {
495
+ openapi.info = { ...asObject(openapi.info), ...swagger.info }
496
+ if (swagger.servers?.length) openapi.servers = swagger.servers
497
+ if (swagger.auth?.schemes && Object.keys(swagger.auth.schemes).length) {
498
+ openapi.components = asObject(openapi.components)
499
+ openapi.components.securitySchemes = {
500
+ ...asObject(openapi.components.securitySchemes),
501
+ ...swagger.auth.schemes,
502
+ }
503
+ }
504
+ if (swagger.auth?.global?.length) openapi.security = swagger.auth.global
505
+ return openapi
506
+ }
507
+
508
+ function swaggerUiHtml(swagger, openapiUrl) {
509
+ const uiOpts = { ...swagger.ui }
510
+ delete uiOpts.presets
511
+ delete uiOpts.plugins
512
+ delete uiOpts.layout
513
+
514
+ if (swagger.navbar?.urls?.length) {
515
+ delete uiOpts.url
516
+ uiOpts.urls = swagger.navbar.urls
517
+ } else {
518
+ uiOpts.url = openapiUrl
519
+ delete uiOpts.urls
520
+ }
521
+ uiOpts.dom_id = '#swagger-ui'
522
+
523
+ const uiJson = JSON.stringify(uiOpts).replace(/</g, '\\u003c')
524
+ const oauth = swagger.auth?.oauth && Object.keys(swagger.auth.oauth).length ? swagger.auth.oauth : null
525
+ const oauthJson = JSON.stringify(oauth).replace(/</g, '\\u003c')
526
+ const title = String(swagger.pageTitle)
527
+ .replace(/&/g, '&amp;')
528
+ .replace(/</g, '&lt;')
529
+ .replace(/>/g, '&gt;')
530
+
531
+ const navbarEnabled = !!swagger.navbar?.enabled
532
+ const showUrlInput = swagger.navbar?.showUrlInput !== false
533
+ const hideUrlCss =
534
+ navbarEnabled && !showUrlInput
535
+ ? `<style>.topbar .download-url-wrapper { display: none !important; }</style>`
536
+ : ''
537
+ const standaloneScript = navbarEnabled
538
+ ? `<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-standalone-preset.js"></script>`
539
+ : ''
540
+
541
+ return `<!doctype html>
542
+ <html>
543
+ <head>
544
+ <meta charset="utf-8" />
545
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
546
+ <title>${title}</title>
547
+ <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css" />
548
+ ${hideUrlCss}
549
+ </head>
550
+ <body>
551
+ <div id="swagger-ui"></div>
552
+ <script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
553
+ ${standaloneScript}
554
+ <script>
555
+ window.onload = function() {
556
+ var opts = ${uiJson};
557
+ opts.presets = [SwaggerUIBundle.presets.apis];
558
+ opts.plugins = [SwaggerUIBundle.plugins.DownloadUrl];
559
+ if (${navbarEnabled ? 'true' : 'false'} && typeof SwaggerUIStandalonePreset !== 'undefined') {
560
+ opts.presets.push(SwaggerUIStandalonePreset);
561
+ opts.layout = 'StandaloneLayout';
562
+ } else {
563
+ opts.layout = 'BaseLayout';
564
+ }
565
+ var ui = SwaggerUIBundle(opts);
566
+ var oauth = ${oauthJson};
567
+ if (oauth && typeof ui.initOAuth === 'function') {
568
+ ui.initOAuth(oauth);
569
+ }
570
+ window.ui = ui;
571
+ };
572
+ </script>
573
+ </body>
574
+ </html>`
575
+ }
576
+
577
+ class FusionApp {
578
+ constructor(customSettings) {
579
+ if (customSettings) settings.merge(customSettings)
580
+ this.settings = getSettings()
581
+ this.engine = new NativeApp()
582
+ this.mounted = false
583
+ // Default: advertise Fusion to clients / Wappalyzer-style detectors.
584
+ this._middleware = [frameworkHeaders()]
585
+ }
586
+
587
+ use(middleware) {
588
+ this._middleware.push(middleware)
589
+ }
590
+
591
+ mount() {
592
+ if (this.mounted) return
593
+ activeGlobalMiddleware = [...this._middleware]
594
+
595
+ for (const { path: routePath, ApiClass, middleware: routeMiddleware = [] } of registry) {
596
+ for (const methodName of HTTP_METHODS) {
597
+ if (!definesMethod(ApiClass, methodName)) continue
598
+ this.engine.route(methodName.toUpperCase(), routePath, async (request) => {
599
+ const chain = [...activeGlobalMiddleware, ...routeMiddleware]
600
+ const handler = async (req) => {
601
+ try {
602
+ const instance = new ApiClass(req)
603
+ const fn = instance[methodName]
604
+ return await Promise.resolve(fn.call(instance))
605
+ } catch (err) {
606
+ if (err instanceof HTTPException) return err.toResponse()
607
+ throw err
608
+ }
609
+ }
610
+ return runMiddlewareChain(request, chain, handler)
611
+ })
612
+ }
613
+ }
614
+
615
+ const swagger = readSwaggerSettings()
616
+ if (swagger.enabled) {
617
+ const prefix = swagger.path
618
+
619
+ const openapi = applySwaggerOpenApi(
620
+ {
621
+ openapi: '3.0.3',
622
+ info: { ...swagger.info },
623
+ paths: {},
624
+ },
625
+ swagger,
626
+ )
627
+
628
+ const parsePathParams = (pattern) => {
629
+ return String(pattern)
630
+ .split('/')
631
+ .filter((seg) => (seg.startsWith('{') && seg.endsWith('}')) || (seg.startsWith('[') && seg.endsWith(']')))
632
+ .map((seg) => seg.slice(1, -1))
633
+ }
634
+
635
+ for (const item of registry) {
636
+ const { path: p, ApiClass, swagger: routeSwagger } = item
637
+ const pathParams = parsePathParams(p)
638
+ const resolvedPath = p.startsWith('/') ? p : `/${p}`
639
+
640
+ if (!openapi.paths[resolvedPath]) openapi.paths[resolvedPath] = {}
641
+
642
+ for (const methodName of HTTP_METHODS) {
643
+ if (!definesMethod(ApiClass, methodName)) continue
644
+
645
+ const methodUpper = String(methodName).toUpperCase()
646
+ const methodLower = String(methodName).toLowerCase()
647
+
648
+ const params = pathParams.map((name) => ({
649
+ name,
650
+ in: 'path',
651
+ required: true,
652
+ schema: { type: 'string' },
653
+ }))
654
+
655
+ openapi.paths[resolvedPath][methodLower] = {
656
+ tags: routeSwagger?.tags?.length ? routeSwagger.tags : [],
657
+ summary: routeSwagger?.title ?? `${ApiClass.name}.${methodUpper}`,
658
+ description: routeSwagger?.description ?? '',
659
+ deprecated: !!routeSwagger?.deprecated,
660
+ operationId: `${ApiClass.name}_${methodLower}`,
661
+ parameters: params,
662
+ responses: { '200': { description: 'OK' } },
663
+ }
664
+ }
665
+ }
666
+
667
+ this.engine.route('GET', `${prefix}/openapi.json`, async () => openapi)
668
+ this.engine.route('GET', prefix, async () => ({
669
+ status: 200,
670
+ body: swaggerUiHtml(swagger, `${prefix}/openapi.json`),
671
+ headers: { 'content-type': 'text/html' },
672
+ }))
673
+ }
674
+
675
+ this.mounted = true
676
+ }
677
+
678
+ async listen(host, port) {
679
+ this.mount()
680
+ const snapshot = getSettings()
681
+ const h = host ?? snapshot.host
682
+ const p = port ?? snapshot.port
683
+ if (snapshot.debug) {
684
+ console.log(`fusion listening on http://${h}:${p}`)
685
+ }
686
+ await this.engine.listen(h, Number(p))
687
+ }
688
+ }
689
+
690
+ async function run(options = {}) {
691
+ const settingsModulePath =
692
+ typeof options === 'string' ? options : options && options.settingsModule
693
+ const middleware = Array.isArray(options?.middleware) ? options.middleware : []
694
+
695
+ settings.ensureLoaded([process.cwd()])
696
+ if (settingsModulePath) {
697
+ const mod = await import(pathToFileUrl(settingsModulePath))
698
+ const overlay = {}
699
+ if (mod.HOST !== undefined) overlay.host = mod.HOST
700
+ if (mod.PORT !== undefined) overlay.port = mod.PORT
701
+ if (mod.DEBUG !== undefined) overlay.debug = mod.DEBUG
702
+ if (Object.keys(overlay).length) settings.merge(overlay)
703
+ }
704
+ const app = new FusionApp()
705
+ for (const mw of middleware) app.use(mw)
706
+ await app.listen()
707
+ return app
708
+ }
709
+
710
+ function coerceParam(raw, kind = 'auto') {
711
+ return native.coerceParamJs(String(raw), kind)
712
+ }
713
+
714
+ function pathToFileUrl(filePath) {
715
+ const resolved = path.resolve(filePath)
716
+ return require('url').pathToFileURL(resolved).href
717
+ }
718
+
719
+ const route = router
720
+
721
+ module.exports = {
722
+ App: NativeApp,
723
+ Settings: NativeSettings,
724
+ FusionApp,
725
+ FusionBaseApi,
726
+ HTTPException,
727
+ router,
728
+ route,
729
+ apiResourceName,
730
+ resolveRoutePath,
731
+ configure,
732
+ getSettings,
733
+ settings,
734
+ status,
735
+ header,
736
+ HTTP_METHODS,
737
+ run,
738
+ bearerJwt,
739
+ requireRoles,
740
+ frameworkHeaders,
741
+ runMiddlewareChain,
742
+ coerceParam,
743
+ getHttpMethods: () => HTTP_METHODS,
744
+ apiResourceNameJs: native.apiResourceNameJs,
745
+ resolveRoutePathJs: native.resolveRoutePathJs,
746
+ coerceParamJs: native.coerceParamJs,
747
+ }
package/package.json CHANGED
@@ -1,10 +1,84 @@
1
1
  {
2
2
  "name": "fusion-framework",
3
- "version": "0.0.1",
4
- "description": "Test package for npm trusted publishing",
5
- "license": "MIT",
3
+ "version": "1.2.0",
4
+ "description": "Class-based HTTP framework for Node.js, powered by a shared Rust core via N-API",
5
+ "keywords": [
6
+ "http",
7
+ "framework",
8
+ "nodejs",
9
+ "http-server",
10
+ "web-framework",
11
+ "rest",
12
+ "api",
13
+ "routing",
14
+ "middleware",
15
+ "jwt",
16
+ "typescript",
17
+ "napi",
18
+ "rust",
19
+ "fusion"
20
+ ],
21
+ "homepage": "https://fusion.cipherunit.xyz/",
22
+ "bugs": {
23
+ "url": "https://github.com/cipherunits/fusion-framework/issues"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/cipherunits/fusion-framework.git",
28
+ "directory": "crates/fusion-node"
29
+ },
30
+ "author": "CipherUnits <cipherunit.dev@gmail.com>",
31
+ "license": "BSD-3-Clause",
32
+ "type": "commonjs",
33
+ "main": "./index.js",
34
+ "types": "./index.d.ts",
35
+ "exports": {
36
+ ".": {
37
+ "types": "./index.d.ts",
38
+ "require": "./index.js",
39
+ "default": "./index.js"
40
+ },
41
+ "./package.json": "./package.json"
42
+ },
6
43
  "files": [
7
- "index.js"
44
+ "index.js",
45
+ "index.d.ts",
46
+ "README.md",
47
+ "LICENSE",
48
+ "*.node"
49
+ ],
50
+ "napi": {
51
+ "name": "fusion-node",
52
+ "triples": {
53
+ "defaults": false,
54
+ "additional": [
55
+ "x86_64-unknown-linux-gnu",
56
+ "aarch64-unknown-linux-gnu",
57
+ "x86_64-pc-windows-msvc",
58
+ "aarch64-apple-darwin"
59
+ ]
60
+ }
61
+ },
62
+ "scripts": {
63
+ "artifacts": "napi artifacts",
64
+ "build": "napi build --platform --release",
65
+ "build:debug": "napi build --platform",
66
+ "prepublishOnly": "node -e \"const fs=require('fs');const ok=fs.readdirSync('.').some(f=>f.endsWith('.node'));if(!ok){console.error('Missing native .node binaries before publish');process.exit(1)}\""
67
+ },
68
+ "publishConfig": {
69
+ "access": "public",
70
+ "registry": "https://registry.npmjs.org/",
71
+ "provenance": true
72
+ },
73
+ "engines": {
74
+ "node": ">=18"
75
+ },
76
+ "os": [
77
+ "linux",
78
+ "darwin",
79
+ "win32"
8
80
  ],
9
- "main": "index.js"
10
- }
81
+ "devDependencies": {
82
+ "@napi-rs/cli": "^2.18.4"
83
+ }
84
+ }