json-rest-api 1.0.21 → 1.0.22
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/audit/00-charter.md +103 -0
- package/audit/01-scope-map.md +84 -0
- package/audit/02-coverage-matrix.csv +174 -0
- package/audit/03-invariants.md +123 -0
- package/audit/04-standards-rubric.md +56 -0
- package/audit/05-competitor-rubric.md +77 -0
- package/audit/06-findings.md +160 -0
- package/audit/07-risk-register.csv +10 -0
- package/audit/08-remediation-roadmap.md +177 -0
- package/audit/09-signoff-check.md +23 -0
- package/audit/10-final-synthesis.md +29 -0
- package/audit/evidence/code/all-files-json-rest-api.txt +168 -0
- package/audit/evidence/code/all-files-json-rest-schema.txt +12 -0
- package/audit/evidence/code/boundary-leak-checks.txt +72 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__bulk-operations-plugin.js.txt +480 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__express-plugin.js.txt +532 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__busboy-detector.js.txt +175 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__formidable-detector.js.txt +138 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__request-helpers.js.txt +222 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__file-handling-plugin.js.txt +342 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__dbTablesOperations.js.txt +304 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__fractional-positioning.js.txt +188 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__compile-schemas.js.txt +336 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__connectors-query-parser.js.txt +165 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-capabilities.js.txt +265 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-value-normalizers.js.txt +369 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__default-data-helpers.js.txt +72 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__field-utils.js.txt +139 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-constants.js.txt +45 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-field-helpers.js.txt +396 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-json-api-transformers.js.txt +162 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__payload-validators.js.txt +1314 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__schema-helpers.js.txt +488 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__scope-validations.js.txt +484 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__simplified-helpers.js.txt +857 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-cross-table-search.js.txt +565 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-json-api-transformers-querying.js.txt +485 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-pagination-helpers.js.txt +560 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-process-includes.js.txt +167 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers-base.js.txt +109 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers.js.txt +1064 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-relationship-includes.js.txt +1983 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-window-queries.js.txt +328 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__url-helpers.js.txt +119 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__storage__storage-adapter.js.txt +160 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__many-to-many-manipulations.js.txt +300 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__relationship-processor.js.txt +245 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-access.js.txt +459 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-cors-plugin.js.txt +237 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-knex-plugin.js.txt +1135 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-multihome-plugin.js.txt +314 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__compile-resource-schemas.js.txt +7 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-relationship-routes.js.txt +71 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-scope-routes.js.txt +102 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__turn-scope-init-into-vars.js.txt +49 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__validate-include-configurations.js.txt +39 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__add-route.js.txt +23 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__check-permissions.js.txt +13 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__common.js.txt +639 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete-relationship.js.txt +117 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete.js.txt +104 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__enrich-attributes.js.txt +199 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-related.js.txt +353 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-relationship.js.txt +61 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get.js.txt +184 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch-relationship.js.txt +55 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch.js.txt +191 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post-relationship.js.txt +110 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post.js.txt +168 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__put.js.txt +253 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__query.js.txt +207 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__release.js.txt +12 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin.js.txt +165 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-positioning-plugin.js.txt +668 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__socketio-plugin.js.txt +621 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__local-storage.js.txt +218 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__s3-storage.js.txt +124 -0
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__CorePlugin.js.txt +269 -0
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__Schema.js.txt +242 -0
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__index.js.txt +77 -0
- package/audit/evidence/code/critical-pass1-risk-keywords.txt +238 -0
- package/audit/evidence/code/critical-pass1-summary.csv +67 -0
- package/audit/evidence/code/critical-pass2-summary.csv +67 -0
- package/audit/evidence/code/early-silent-zones-no-test-token.txt +57 -0
- package/audit/evidence/code/error-model-signals.txt +98 -0
- package/audit/evidence/code/git-files-json-rest-api.txt +171 -0
- package/audit/evidence/code/git-files-json-rest-schema.txt +27 -0
- package/audit/evidence/code/hook-flow-signals.txt +282 -0
- package/audit/evidence/code/idproperty-signals.txt +157 -0
- package/audit/evidence/code/import-cycles-non-anyapi.txt +3 -0
- package/audit/evidence/code/import-hotspots-non-anyapi.txt +96 -0
- package/audit/evidence/code/imports-json-rest-api.txt +354 -0
- package/audit/evidence/code/imports-json-rest-schema.txt +15 -0
- package/audit/evidence/code/in-scope-files-json-rest-api.txt +160 -0
- package/audit/evidence/code/in-scope-files-json-rest-schema.txt +13 -0
- package/audit/evidence/code/non-anyapi-core-files.txt +101 -0
- package/audit/evidence/code/permission-signals.txt +193 -0
- package/audit/evidence/code/query-include-signals.txt +687 -0
- package/audit/evidence/code/transaction-signals.txt +28 -0
- package/audit/evidence/code/validation-signals.txt +650 -0
- package/audit/evidence/code/write-path-signals.txt +61 -0
- package/audit/evidence/docs/docs-claims-signals.txt +1075 -0
- package/audit/evidence/docs/schema-docs-claims-signals.txt +179 -0
- package/audit/evidence/external/corpus-freeze.md +21 -0
- package/audit/evidence/external/npm-competitor-metadata.txt +70 -0
- package/audit/evidence/external/npm-download-signals.txt +23 -0
- package/audit/evidence/external/npm-jsonapi-server.txt +8 -0
- package/audit/evidence/external/npm-modern-jsonapi-metadata.txt +38 -0
- package/audit/evidence/external/readme-fortune-signals.txt +26 -0
- package/audit/evidence/external/readme-fortune.md +165 -0
- package/audit/evidence/external/readme-json-api-serializer-signals.txt +80 -0
- package/audit/evidence/external/readme-json-api-serializer.md +690 -0
- package/audit/evidence/external/readme-jsonapi-serde-server-signals.txt +0 -0
- package/audit/evidence/external/readme-jsonapi-serde-server.md +22 -0
- package/audit/evidence/external/readme-jsonapi-serializer-signals.txt +63 -0
- package/audit/evidence/external/readme-jsonapi-serializer.md +315 -0
- package/audit/evidence/external/readme-jsonapi-server-signals.txt +19 -0
- package/audit/evidence/external/readme-jsonapi-server.md +104 -0
- package/audit/evidence/external/readme-nestjs-json-api-signals.txt +4 -0
- package/audit/evidence/external/readme-nestjs-json-api.md +94 -0
- package/audit/evidence/external/readme-yaser-json-api-nestjs-signals.txt +47 -0
- package/audit/evidence/external/readme-yaser-json-api-nestjs.md +447 -0
- package/audit/evidence/external/standard-jsonapi-snippets.txt +84 -0
- package/audit/evidence/external/standard-rfc-snippets.txt +168 -0
- package/audit/evidence/external/standards-sources.txt +6 -0
- package/audit/evidence/runtime/baseline-commits.txt +2 -0
- package/audit/evidence/runtime/db-adapter-assumptions.txt +3 -0
- package/audit/evidence/runtime/environment.txt +4 -0
- package/audit/evidence/runtime/invariant-runtime-mapping.md +38 -0
- package/audit/evidence/runtime/probe-patch-relationship-error.exitcode +1 -0
- package/audit/evidence/runtime/probe-patch-relationship-error.log +76 -0
- package/audit/evidence/runtime/probe-patch-relationship-error.mjs +67 -0
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.exitcode +1 -0
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.log +62 -0
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.mjs +47 -0
- package/audit/evidence/runtime/test-exitcodes.txt +4 -0
- package/audit/evidence/runtime/test-summary.md +107 -0
- package/audit/evidence/runtime/trace-anchors.txt +61 -0
- package/audit/evidence/tests/access-multitenancy-test-signals.txt +125 -0
- package/audit/evidence/tests/failing-tests-summary.txt +25 -0
- package/audit/evidence/tests/npm-test-json-rest-api.exitcode +1 -0
- package/audit/evidence/tests/npm-test-json-rest-api.log +5885 -0
- package/audit/evidence/tests/npm-test-json-rest-schema.exitcode +1 -0
- package/audit/evidence/tests/npm-test-json-rest-schema.log +356 -0
- package/audit/evidence/tests/query-include-test-signals.txt +948 -0
- package/audit/evidence/tests/relationship-test-signals.txt +223 -0
- package/audit/evidence/tests/skipped-tests.txt +10 -0
- package/audit/evidence/tests/test-suite-list-non-anyapi.txt +30 -0
- package/audit/evidence/tests/test-suite-list.txt +32 -0
- package/audit/subsystems/docs-drift.md +49 -0
- package/audit/subsystems/pass2-cross-review.md +131 -0
- package/audit/subsystems/plugins-access.md +51 -0
- package/audit/subsystems/plugins-bulk.md +50 -0
- package/audit/subsystems/plugins-cors.md +48 -0
- package/audit/subsystems/plugins-file.md +53 -0
- package/audit/subsystems/plugins-multihome.md +50 -0
- package/audit/subsystems/plugins-positioning.md +54 -0
- package/audit/subsystems/plugins-socketio.md +50 -0
- package/audit/subsystems/query-engine.md +61 -0
- package/audit/subsystems/relationships.md +55 -0
- package/audit/subsystems/rest-orchestration.md +66 -0
- package/audit/subsystems/schema-core.md +64 -0
- package/audit/subsystems/storage-knex.md +57 -0
- package/audit/subsystems/tests.md +48 -0
- package/audit/subsystems/transport-express.md +55 -0
- package/audit/traces/trace-read-get.md +22 -0
- package/audit/traces/trace-read-query.md +25 -0
- package/audit/traces/trace-relationship-endpoints.md +31 -0
- package/audit/traces/trace-write-delete.md +17 -0
- package/audit/traces/trace-write-patch.md +26 -0
- package/audit/traces/trace-write-post.md +22 -0
- package/audit/traces/trace-write-put.md +23 -0
- package/package.json +3 -3
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
1 /**
|
|
2
|
+
2 * Busboy File Detector for HTTP Multipart Uploads
|
|
3
|
+
3 *
|
|
4
|
+
4 * This detector handles multipart/form-data uploads using the busboy library.
|
|
5
|
+
5 * It's suitable for both HTTP and Express connectors.
|
|
6
|
+
6 *
|
|
7
|
+
7 * Features:
|
|
8
|
+
8 * - Streaming parser (memory efficient)
|
|
9
|
+
9 * - Configurable file size limits
|
|
10
|
+
10 * - Automatic field parsing
|
|
11
|
+
11 * - No temporary files by default (keeps in memory)
|
|
12
|
+
12 *
|
|
13
|
+
13 * Usage:
|
|
14
|
+
14 * ```javascript
|
|
15
|
+
15 * import { createBusboyDetector } from 'jsonrestapi/plugins/core/lib/busboy-detector.js';
|
|
16
|
+
16 *
|
|
17
|
+
17 * api.use(HttpPlugin, {
|
|
18
|
+
18 * fileParser: 'busboy',
|
|
19
|
+
19 * fileParserOptions: {
|
|
20
|
+
20 * limits: { fileSize: 10 * 1024 * 1024 } // 10MB
|
|
21
|
+
21 * }
|
|
22
|
+
22 * });
|
|
23
|
+
23 * ```
|
|
24
|
+
24 */
|
|
25
|
+
25
|
|
26
|
+
26 import { requirePackage } from 'hooked-api'
|
|
27
|
+
27 import { Readable } from 'stream'
|
|
28
|
+
28
|
|
29
|
+
29 let Busboy
|
|
30
|
+
30 try {
|
|
31
|
+
31 Busboy = (await import('busboy')).default
|
|
32
|
+
32 } catch (e) {
|
|
33
|
+
33 requirePackage('busboy', 'express/http-connector',
|
|
34
|
+
34 'Busboy is required for multipart/form-data file uploads. This is a peer dependency.')
|
|
35
|
+
35 }
|
|
36
|
+
36
|
|
37
|
+
37 /**
|
|
38
|
+
38 * Creates a busboy-based file detector
|
|
39
|
+
39 *
|
|
40
|
+
40 * @param {Object} options - Busboy configuration options
|
|
41
|
+
41 * @param {Object} options.limits - Size limits
|
|
42
|
+
42 * @param {number} options.limits.fileSize - Max file size in bytes
|
|
43
|
+
43 * @param {number} options.limits.files - Max number of files
|
|
44
|
+
44 * @param {number} options.limits.fields - Max number of fields
|
|
45
|
+
45 * @returns {Object} Detector object with detect() and parse() methods
|
|
46
|
+
46 */
|
|
47
|
+
47 export function createBusboyDetector (options = {}) {
|
|
48
|
+
48 return {
|
|
49
|
+
49 name: 'busboy-multipart',
|
|
50
|
+
50
|
|
51
|
+
51 /**
|
|
52
|
+
52 * Check if this detector can handle the request
|
|
53
|
+
53 * @param {Object} params - Request parameters
|
|
54
|
+
54 * @returns {boolean} True if this is a multipart request
|
|
55
|
+
55 */
|
|
56
|
+
56 detect: (params) => {
|
|
57
|
+
57 const req = params._httpReq || params._expressReq
|
|
58
|
+
58 if (!req || !req.headers) return false
|
|
59
|
+
59
|
|
60
|
+
60 const contentType = req.headers['content-type'] || ''
|
|
61
|
+
61 return contentType.includes('multipart/form-data')
|
|
62
|
+
62 },
|
|
63
|
+
63
|
|
64
|
+
64 /**
|
|
65
|
+
65 * Parse multipart data from the request
|
|
66
|
+
66 * @param {Object} params - Request parameters
|
|
67
|
+
67 * @returns {Promise<{fields: Object, files: Object}>} Parsed data
|
|
68
|
+
68 */
|
|
69
|
+
69 parse: async (params) => {
|
|
70
|
+
70 const req = params._httpReq || params._expressReq
|
|
71
|
+
71
|
|
72
|
+
72 return new Promise((resolve, reject) => {
|
|
73
|
+
73 const busboy = new Busboy({
|
|
74
|
+
74 headers: req.headers,
|
|
75
|
+
75 ...options
|
|
76
|
+
76 })
|
|
77
|
+
77
|
|
78
|
+
78 const fields = {}
|
|
79
|
+
79 const files = {}
|
|
80
|
+
80 const filePromises = []
|
|
81
|
+
81
|
|
82
|
+
82 // Handle fields
|
|
83
|
+
83 busboy.on('field', (fieldname, val) => {
|
|
84
|
+
84 // Handle array notation (field[] or field[0])
|
|
85
|
+
85 const arrayMatch = fieldname.match(/^(.+)\[\d*\]$/)
|
|
86
|
+
86 if (arrayMatch) {
|
|
87
|
+
87 const baseName = arrayMatch[1]
|
|
88
|
+
88 if (!fields[baseName]) {
|
|
89
|
+
89 fields[baseName] = []
|
|
90
|
+
90 }
|
|
91
|
+
91 if (Array.isArray(fields[baseName])) {
|
|
92
|
+
92 fields[baseName].push(val)
|
|
93
|
+
93 }
|
|
94
|
+
94 } else {
|
|
95
|
+
95 fields[fieldname] = val
|
|
96
|
+
96 }
|
|
97
|
+
97 })
|
|
98
|
+
98
|
|
99
|
+
99 // Handle files
|
|
100
|
+
100 busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
|
|
101
|
+
101 const chunks = []
|
|
102
|
+
102 let size = 0
|
|
103
|
+
103
|
|
104
|
+
104 const filePromise = new Promise((fileResolve, fileReject) => {
|
|
105
|
+
105 file.on('data', (chunk) => {
|
|
106
|
+
106 chunks.push(chunk)
|
|
107
|
+
107 size += chunk.length
|
|
108
|
+
108 })
|
|
109
|
+
109
|
|
110
|
+
110 file.on('limit', () => {
|
|
111
|
+
111 fileReject(new Error(`File size limit exceeded for field '${fieldname}'`))
|
|
112
|
+
112 })
|
|
113
|
+
113
|
|
114
|
+
114 file.on('end', () => {
|
|
115
|
+
115 files[fieldname] = {
|
|
116
|
+
116 filename,
|
|
117
|
+
117 mimetype,
|
|
118
|
+
118 encoding,
|
|
119
|
+
119 size,
|
|
120
|
+
120 data: Buffer.concat(chunks)
|
|
121
|
+
121 }
|
|
122
|
+
122 fileResolve()
|
|
123
|
+
123 })
|
|
124
|
+
124
|
|
125
|
+
125 file.on('error', fileReject)
|
|
126
|
+
126 })
|
|
127
|
+
127
|
|
128
|
+
128 filePromises.push(filePromise)
|
|
129
|
+
129 })
|
|
130
|
+
130
|
|
131
|
+
131 // Handle completion
|
|
132
|
+
132 busboy.on('finish', async () => {
|
|
133
|
+
133 try {
|
|
134
|
+
134 // Wait for all files to be fully read
|
|
135
|
+
135 await Promise.all(filePromises)
|
|
136
|
+
136 resolve({ fields, files })
|
|
137
|
+
137 } catch (error) {
|
|
138
|
+
138 reject(error)
|
|
139
|
+
139 }
|
|
140
|
+
140 })
|
|
141
|
+
141
|
|
142
|
+
142 // Handle errors
|
|
143
|
+
143 busboy.on('error', (error) => {
|
|
144
|
+
144 reject(error)
|
|
145
|
+
145 })
|
|
146
|
+
146
|
|
147
|
+
147 // Handle limit errors
|
|
148
|
+
148 busboy.on('partsLimit', () => {
|
|
149
|
+
149 reject(new Error('Parts limit exceeded'))
|
|
150
|
+
150 })
|
|
151
|
+
151
|
|
152
|
+
152 busboy.on('filesLimit', () => {
|
|
153
|
+
153 reject(new Error('Files limit exceeded'))
|
|
154
|
+
154 })
|
|
155
|
+
155
|
|
156
|
+
156 busboy.on('fieldsLimit', () => {
|
|
157
|
+
157 reject(new Error('Fields limit exceeded'))
|
|
158
|
+
158 })
|
|
159
|
+
159
|
|
160
|
+
160 // Pipe request to busboy
|
|
161
|
+
161 if (req.pipe) {
|
|
162
|
+
162 req.pipe(busboy)
|
|
163
|
+
163 } else if (req.on) {
|
|
164
|
+
164 // For already buffered requests
|
|
165
|
+
165 const stream = new Readable()
|
|
166
|
+
166 stream.push(req.body || req)
|
|
167
|
+
167 stream.push(null)
|
|
168
|
+
168 stream.pipe(busboy)
|
|
169
|
+
169 } else {
|
|
170
|
+
170 reject(new Error('Request object is not a stream'))
|
|
171
|
+
171 }
|
|
172
|
+
172 })
|
|
173
|
+
173 }
|
|
174
|
+
174 }
|
|
175
|
+
175 }
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
1 /**
|
|
2
|
+
2 * Formidable File Detector for HTTP Multipart Uploads
|
|
3
|
+
3 *
|
|
4
|
+
4 * This detector handles multipart/form-data uploads using the formidable library.
|
|
5
|
+
5 * Unlike busboy, formidable saves files to disk temporarily, which is better
|
|
6
|
+
6 * for large files.
|
|
7
|
+
7 *
|
|
8
|
+
8 * Features:
|
|
9
|
+
9 * - Disk-based storage (better for large files)
|
|
10
|
+
10 * - Automatic temp file cleanup
|
|
11
|
+
11 * - Progress tracking support
|
|
12
|
+
12 * - Built-in file type detection
|
|
13
|
+
13 *
|
|
14
|
+
14 * Usage:
|
|
15
|
+
15 * ```javascript
|
|
16
|
+
16 * import { createFormidableDetector } from 'jsonrestapi/plugins/core/lib/formidable-detector.js';
|
|
17
|
+
17 *
|
|
18
|
+
18 * api.use(ExpressPlugin, {
|
|
19
|
+
19 * fileParser: 'formidable',
|
|
20
|
+
20 * fileParserOptions: {
|
|
21
|
+
21 * uploadDir: './uploads/temp',
|
|
22
|
+
22 * keepExtensions: true,
|
|
23
|
+
23 * maxFileSize: 200 * 1024 * 1024 // 200MB
|
|
24
|
+
24 * }
|
|
25
|
+
25 * });
|
|
26
|
+
26 * ```
|
|
27
|
+
27 */
|
|
28
|
+
28
|
|
29
|
+
29 import { requirePackage } from 'hooked-api'
|
|
30
|
+
30 import { promises as fs } from 'fs'
|
|
31
|
+
31 import path from 'path'
|
|
32
|
+
32
|
|
33
|
+
33 let formidable
|
|
34
|
+
34 try {
|
|
35
|
+
35 formidable = (await import('formidable')).default
|
|
36
|
+
36 } catch (e) {
|
|
37
|
+
37 requirePackage('formidable', 'express/http-connector',
|
|
38
|
+
38 'Formidable is required for multipart/form-data file uploads with disk storage. This is a peer dependency.')
|
|
39
|
+
39 }
|
|
40
|
+
40
|
|
41
|
+
41 /**
|
|
42
|
+
42 * Creates a formidable-based file detector
|
|
43
|
+
43 *
|
|
44
|
+
44 * @param {Object} options - Formidable configuration options
|
|
45
|
+
45 * @param {string} options.uploadDir - Directory for temporary files
|
|
46
|
+
46 * @param {boolean} options.keepExtensions - Keep file extensions
|
|
47
|
+
47 * @param {number} options.maxFileSize - Max file size in bytes
|
|
48
|
+
48 * @param {boolean} options.multiples - Allow multiple files per field
|
|
49
|
+
49 * @returns {Object} Detector object with detect() and parse() methods
|
|
50
|
+
50 */
|
|
51
|
+
51 export function createFormidableDetector (options = {}) {
|
|
52
|
+
52 // Set default upload directory
|
|
53
|
+
53 const uploadDir = options.uploadDir || path.join(process.cwd(), 'uploads', 'temp')
|
|
54
|
+
54
|
|
55
|
+
55 return {
|
|
56
|
+
56 name: 'formidable-multipart',
|
|
57
|
+
57
|
|
58
|
+
58 /**
|
|
59
|
+
59 * Check if this detector can handle the request
|
|
60
|
+
60 * @param {Object} params - Request parameters
|
|
61
|
+
61 * @returns {boolean} True if this is a multipart request
|
|
62
|
+
62 */
|
|
63
|
+
63 detect: (params) => {
|
|
64
|
+
64 const req = params._httpReq || params._expressReq
|
|
65
|
+
65 if (!req || !req.headers) return false
|
|
66
|
+
66
|
|
67
|
+
67 const contentType = req.headers['content-type'] || ''
|
|
68
|
+
68 return contentType.includes('multipart/form-data')
|
|
69
|
+
69 },
|
|
70
|
+
70
|
|
71
|
+
71 /**
|
|
72
|
+
72 * Parse multipart data from the request
|
|
73
|
+
73 * @param {Object} params - Request parameters
|
|
74
|
+
74 * @returns {Promise<{fields: Object, files: Object}>} Parsed data
|
|
75
|
+
75 */
|
|
76
|
+
76 parse: async (params) => {
|
|
77
|
+
77 const req = params._httpReq || params._expressReq
|
|
78
|
+
78
|
|
79
|
+
79 // Ensure upload directory exists
|
|
80
|
+
80 await fs.mkdir(uploadDir, { recursive: true })
|
|
81
|
+
81
|
|
82
|
+
82 // Create form parser
|
|
83
|
+
83 const form = formidable({
|
|
84
|
+
84 uploadDir,
|
|
85
|
+
85 keepExtensions: true,
|
|
86
|
+
86 ...options
|
|
87
|
+
87 })
|
|
88
|
+
88
|
|
89
|
+
89 // Parse the request
|
|
90
|
+
90 const [fields, fileUploads] = await form.parse(req)
|
|
91
|
+
91
|
|
92
|
+
92 // Normalize formidable's file format to our standard format
|
|
93
|
+
93 const files = {}
|
|
94
|
+
94
|
|
95
|
+
95 for (const [fieldname, uploadedFiles] of Object.entries(fileUploads)) {
|
|
96
|
+
96 // Formidable returns arrays for multiple files
|
|
97
|
+
97 const fileArray = Array.isArray(uploadedFiles) ? uploadedFiles : [uploadedFiles]
|
|
98
|
+
98
|
|
99
|
+
99 // For now, just take the first file (TODO: handle multiple files per field)
|
|
100
|
+
100 const file = fileArray[0]
|
|
101
|
+
101
|
|
102
|
+
102 if (file) {
|
|
103
|
+
103 // Read file data into memory (for small files)
|
|
104
|
+
104 // For large files, you might want to keep them on disk
|
|
105
|
+
105 const data = await fs.readFile(file.filepath)
|
|
106
|
+
106
|
|
107
|
+
107 files[fieldname] = {
|
|
108
|
+
108 filename: file.originalFilename || 'unknown',
|
|
109
|
+
109 mimetype: file.mimetype || 'application/octet-stream',
|
|
110
|
+
110 size: file.size,
|
|
111
|
+
111 data,
|
|
112
|
+
112 filepath: file.filepath, // Keep for reference
|
|
113
|
+
113
|
|
114
|
+
114 // Cleanup function to remove temp file
|
|
115
|
+
115 cleanup: async () => {
|
|
116
|
+
116 try {
|
|
117
|
+
117 await fs.unlink(file.filepath)
|
|
118
|
+
118 } catch (error) {
|
|
119
|
+
119 // File might already be deleted, ignore
|
|
120
|
+
120 if (error.code !== 'ENOENT') {
|
|
121
|
+
121 throw error
|
|
122
|
+
122 }
|
|
123
|
+
123 }
|
|
124
|
+
124 }
|
|
125
|
+
125 }
|
|
126
|
+
126 }
|
|
127
|
+
127 }
|
|
128
|
+
128
|
|
129
|
+
129 // Normalize fields (formidable returns arrays for repeated fields)
|
|
130
|
+
130 const normalizedFields = {}
|
|
131
|
+
131 for (const [key, value] of Object.entries(fields)) {
|
|
132
|
+
132 normalizedFields[key] = Array.isArray(value) && value.length === 1 ? value[0] : value
|
|
133
|
+
133 }
|
|
134
|
+
134
|
|
135
|
+
135 return { fields: normalizedFields, files }
|
|
136
|
+
136 }
|
|
137
|
+
137 }
|
|
138
|
+
138 }
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
1 /**
|
|
2
|
+
2 * Helper functions for HTTP request context creation
|
|
3
|
+
3 * Used by both HTTP and Express plugins
|
|
4
|
+
4 */
|
|
5
|
+
5
|
|
6
|
+
6 /**
|
|
7
|
+
7 * Extract the real client IP address, handling proxies
|
|
8
|
+
8 * @param {Object} req - The request object (Node.js http.IncomingMessage or Express Request)
|
|
9
|
+
9 * @returns {string} The client IP address
|
|
10
|
+
10 */
|
|
11
|
+
11 export function getClientIP (req) {
|
|
12
|
+
12 // Check X-Forwarded-For header (comma-separated list, first is original client)
|
|
13
|
+
13 const xForwardedFor = req.headers['x-forwarded-for']
|
|
14
|
+
14 if (xForwardedFor) {
|
|
15
|
+
15 return xForwardedFor.split(',')[0].trim()
|
|
16
|
+
16 }
|
|
17
|
+
17
|
|
18
|
+
18 // Check X-Real-IP header (single IP)
|
|
19
|
+
19 const xRealIP = req.headers['x-real-ip']
|
|
20
|
+
20 if (xRealIP) {
|
|
21
|
+
21 return xRealIP
|
|
22
|
+
22 }
|
|
23
|
+
23
|
|
24
|
+
24 // Check Cloudflare's CF-Connecting-IP
|
|
25
|
+
25 const cfConnectingIP = req.headers['cf-connecting-ip']
|
|
26
|
+
26 if (cfConnectingIP) {
|
|
27
|
+
27 return cfConnectingIP
|
|
28
|
+
28 }
|
|
29
|
+
29
|
|
30
|
+
30 // Fall back to direct connection
|
|
31
|
+
31 return req.connection?.remoteAddress ||
|
|
32
|
+
32 req.socket?.remoteAddress ||
|
|
33
|
+
33 req.ip || // Express specific
|
|
34
|
+
34 'unknown'
|
|
35
|
+
35 }
|
|
36
|
+
36
|
|
37
|
+
37 /**
|
|
38
|
+
38 * Check if the request is using HTTPS
|
|
39
|
+
39 * @param {Object} req - The request object
|
|
40
|
+
40 * @returns {boolean} True if the request is secure
|
|
41
|
+
41 */
|
|
42
|
+
42 export function isSecure (req) {
|
|
43
|
+
43 // Express-specific property
|
|
44
|
+
44 if (req.secure !== undefined) {
|
|
45
|
+
45 return req.secure
|
|
46
|
+
46 }
|
|
47
|
+
47
|
|
48
|
+
48 // Check X-Forwarded-Proto header (used by proxies)
|
|
49
|
+
49 const xForwardedProto = req.headers['x-forwarded-proto']
|
|
50
|
+
50 if (xForwardedProto) {
|
|
51
|
+
51 return xForwardedProto === 'https'
|
|
52
|
+
52 }
|
|
53
|
+
53
|
|
54
|
+
54 // Check if connection is encrypted (Node.js)
|
|
55
|
+
55 if (req.connection?.encrypted) {
|
|
56
|
+
56 return true
|
|
57
|
+
57 }
|
|
58
|
+
58
|
|
59
|
+
59 // Check protocol directly
|
|
60
|
+
60 if (req.protocol === 'https') {
|
|
61
|
+
61 return true
|
|
62
|
+
62 }
|
|
63
|
+
63
|
|
64
|
+
64 // Default to false
|
|
65
|
+
65 return false
|
|
66
|
+
66 }
|
|
67
|
+
67
|
|
68
|
+
68 /**
|
|
69
|
+
69 * Get the hostname from the request
|
|
70
|
+
70 * @param {Object} req - The request object
|
|
71
|
+
71 * @returns {string} The hostname
|
|
72
|
+
72 */
|
|
73
|
+
73 export function getHostname (req) {
|
|
74
|
+
74 // Express-specific property
|
|
75
|
+
75 if (req.hostname) {
|
|
76
|
+
76 return req.hostname
|
|
77
|
+
77 }
|
|
78
|
+
78
|
|
79
|
+
79 // Check Host header
|
|
80
|
+
80 const hostHeader = req.headers.host
|
|
81
|
+
81 if (hostHeader) {
|
|
82
|
+
82 // Remove port if present
|
|
83
|
+
83 const colonIndex = hostHeader.indexOf(':')
|
|
84
|
+
84 return colonIndex === -1 ? hostHeader : hostHeader.substring(0, colonIndex)
|
|
85
|
+
85 }
|
|
86
|
+
86
|
|
87
|
+
87 // Fall back
|
|
88
|
+
88 return 'localhost'
|
|
89
|
+
89 }
|
|
90
|
+
90
|
|
91
|
+
91 /**
|
|
92
|
+
92 * Get the port from the request
|
|
93
|
+
93 * @param {Object} req - The request object
|
|
94
|
+
94 * @returns {number|null} The port number or null
|
|
95
|
+
95 */
|
|
96
|
+
96 export function getPort (req) {
|
|
97
|
+
97 // Check Host header for port
|
|
98
|
+
98 const hostHeader = req.headers.host
|
|
99
|
+
99 if (hostHeader) {
|
|
100
|
+
100 const colonIndex = hostHeader.indexOf(':')
|
|
101
|
+
101 if (colonIndex !== -1) {
|
|
102
|
+
102 const port = parseInt(hostHeader.substring(colonIndex + 1), 10)
|
|
103
|
+
103 if (!isNaN(port)) {
|
|
104
|
+
104 return port
|
|
105
|
+
105 }
|
|
106
|
+
106 }
|
|
107
|
+
107 }
|
|
108
|
+
108
|
|
109
|
+
109 // Check connection local port
|
|
110
|
+
110 if (req.connection?.localPort) {
|
|
111
|
+
111 return req.connection.localPort
|
|
112
|
+
112 }
|
|
113
|
+
113
|
|
114
|
+
114 if (req.socket?.localPort) {
|
|
115
|
+
115 return req.socket.localPort
|
|
116
|
+
116 }
|
|
117
|
+
117
|
|
118
|
+
118 // Default ports based on protocol
|
|
119
|
+
119 return isSecure(req) ? 443 : 80
|
|
120
|
+
120 }
|
|
121
|
+
121
|
|
122
|
+
122 /**
|
|
123
|
+
123 * Parse cookies from cookie header string
|
|
124
|
+
124 * @param {string} cookieString - The cookie header value
|
|
125
|
+
125 * @returns {Object} Parsed cookies as key-value pairs
|
|
126
|
+
126 */
|
|
127
|
+
127 export function parseCookies (cookieString) {
|
|
128
|
+
128 const cookies = {}
|
|
129
|
+
129
|
|
130
|
+
130 if (!cookieString || typeof cookieString !== 'string') {
|
|
131
|
+
131 return cookies
|
|
132
|
+
132 }
|
|
133
|
+
133
|
|
134
|
+
134 // Split by semicolons and parse each cookie
|
|
135
|
+
135 cookieString.split(';').forEach(cookie => {
|
|
136
|
+
136 const trimmedCookie = cookie.trim()
|
|
137
|
+
137 if (trimmedCookie) {
|
|
138
|
+
138 const eqIndex = trimmedCookie.indexOf('=')
|
|
139
|
+
139 if (eqIndex !== -1) {
|
|
140
|
+
140 const name = trimmedCookie.substring(0, eqIndex).trim()
|
|
141
|
+
141 const value = trimmedCookie.substring(eqIndex + 1).trim()
|
|
142
|
+
142 if (name) {
|
|
143
|
+
143 // Remove quotes if present
|
|
144
|
+
144 cookies[name] = value.replace(/^"(.*)"$/, '$1')
|
|
145
|
+
145 }
|
|
146
|
+
146 }
|
|
147
|
+
147 }
|
|
148
|
+
148 })
|
|
149
|
+
149
|
|
150
|
+
150 return cookies
|
|
151
|
+
151 }
|
|
152
|
+
152
|
|
153
|
+
153 /**
|
|
154
|
+
154 * Extract token from request based on common patterns
|
|
155
|
+
155 * @param {Object} req - The request object
|
|
156
|
+
156 * @returns {string|null} The extracted token or null
|
|
157
|
+
157 */
|
|
158
|
+
158 export function extractToken (req) {
|
|
159
|
+
159 // Check Authorization header for Bearer token
|
|
160
|
+
160 const authHeader = req.headers.authorization || req.headers.Authorization
|
|
161
|
+
161 if (authHeader && typeof authHeader === 'string') {
|
|
162
|
+
162 const bearerMatch = authHeader.match(/^Bearer\s+(.+)$/i)
|
|
163
|
+
163 if (bearerMatch) {
|
|
164
|
+
164 return bearerMatch[1]
|
|
165
|
+
165 }
|
|
166
|
+
166 }
|
|
167
|
+
167
|
|
168
|
+
168 // Check cookies for session token (Auth.js pattern)
|
|
169
|
+
169 const cookies = parseCookies(req.headers.cookie || '')
|
|
170
|
+
170 if (cookies['__Secure-authjs.session-token']) {
|
|
171
|
+
171 return cookies['__Secure-authjs.session-token']
|
|
172
|
+
172 }
|
|
173
|
+
173 if (cookies['authjs.session-token']) {
|
|
174
|
+
174 return cookies['authjs.session-token']
|
|
175
|
+
175 }
|
|
176
|
+
176
|
|
177
|
+
177 return null
|
|
178
|
+
178 }
|
|
179
|
+
179
|
|
180
|
+
180 /**
|
|
181
|
+
181 * Create a context object for the request
|
|
182
|
+
182 * @param {Object} req - The request object
|
|
183
|
+
183 * @param {Object} res - The response object
|
|
184
|
+
184 * @param {string} source - The source plugin ('http' or 'express')
|
|
185
|
+
185 * @returns {Object} The context object
|
|
186
|
+
186 */
|
|
187
|
+
187 export function createContext (req, res, source) {
|
|
188
|
+
188 const context = {
|
|
189
|
+
189 source,
|
|
190
|
+
190 auth: {
|
|
191
|
+
191 userId: null,
|
|
192
|
+
192 claims: null
|
|
193
|
+
193 },
|
|
194
|
+
194 request: {
|
|
195
|
+
195 ip: getClientIP(req),
|
|
196
|
+
196 userAgent: req.headers['user-agent'] || null,
|
|
197
|
+
197 protocol: isSecure(req) ? 'https' : 'http',
|
|
198
|
+
198 hostname: getHostname(req),
|
|
199
|
+
199 port: getPort(req),
|
|
200
|
+
200 method: req.method,
|
|
201
|
+
201 path: req.url || req.path,
|
|
202
|
+
202 headers: req.headers,
|
|
203
|
+
203 cookies: parseCookies(req.headers.cookie || ''),
|
|
204
|
+
204 token: extractToken(req)
|
|
205
|
+
205 },
|
|
206
|
+
206 raw: { req, res },
|
|
207
|
+
207 handled: false,
|
|
208
|
+
208 rejection: null
|
|
209
|
+
209 }
|
|
210
|
+
210
|
|
211
|
+
211 // Add reject function
|
|
212
|
+
212 context.reject = (status, message, details = {}) => {
|
|
213
|
+
213 context.handled = true
|
|
214
|
+
214 context.rejection = {
|
|
215
|
+
215 status,
|
|
216
|
+
216 message,
|
|
217
|
+
217 ...details
|
|
218
|
+
218 }
|
|
219
|
+
219 }
|
|
220
|
+
220
|
|
221
|
+
221 return context
|
|
222
|
+
222 }
|