json-rest-api 1.0.20 → 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 +5 -5
package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-access.js.txt
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
1 import { RestApiResourceError } from '../../lib/rest-api-errors.js'
|
|
2
|
+
2
|
|
3
|
+
3 export const AccessPlugin = {
|
|
4
|
+
4 name: 'rest-api-access',
|
|
5
|
+
5 dependencies: ['rest-api'],
|
|
6
|
+
6
|
|
7
|
+
7 async install ({ addHook, helpers, log, pluginOptions = {}, scopes }) {
|
|
8
|
+
8 const ownershipOptions = pluginOptions.ownership || pluginOptions.autoOwnership || {}
|
|
9
|
+
9
|
|
10
|
+
10 const config = {
|
|
11
|
+
11 ownership: {
|
|
12
|
+
12 enabled: ownershipOptions.enabled !== false,
|
|
13
|
+
13 field: ownershipOptions.field || 'user_id',
|
|
14
|
+
14 userResource: ownershipOptions.userResource || 'users',
|
|
15
|
+
15 excludeResources: ownershipOptions.excludeResources || [],
|
|
16
|
+
16 filterByOwner: ownershipOptions.filterByOwner !== false,
|
|
17
|
+
17 requireOwnership: ownershipOptions.requireOwnership || false
|
|
18
|
+
18 }
|
|
19
|
+
19 }
|
|
20
|
+
20
|
|
21
|
+
21 const state = {
|
|
22
|
+
22 authCheckers: new Map()
|
|
23
|
+
23 }
|
|
24
|
+
24
|
|
25
|
+
25 const ownershipField = () => config.ownership.field
|
|
26
|
+
26
|
|
27
|
+
27 // Minimal helper: treat ownerField false/null as ownership disabled (alias)
|
|
28
|
+
28 const isOwnershipDisabled = (scopeOptions = {}) => (
|
|
29
|
+
29 scopeOptions?.ownership === false ||
|
|
30
|
+
30 scopeOptions?.ownerField === false ||
|
|
31
|
+
31 scopeOptions?.ownerField === null
|
|
32
|
+
32 )
|
|
33
|
+
33
|
|
34
|
+
34 const evaluateOwnership = ({ record, field, schemaInfo, userId }) => {
|
|
35
|
+
35 if (!record) return 'unknown'
|
|
36
|
+
36
|
|
37
|
+
37 const idProperty = schemaInfo?.idProperty || 'id'
|
|
38
|
+
38 const schemaStructure = schemaInfo?.schemaStructure || {}
|
|
39
|
+
39 const schemaRelationships = schemaInfo?.schemaRelationships || {}
|
|
40
|
+
40
|
|
41
|
+
41 const matchesUser = (value) => value !== undefined && value !== null && String(value) === userId
|
|
42
|
+
42
|
|
43
|
+
43 if (field === idProperty) {
|
|
44
|
+
44 if (record.id === undefined || record.id === null) {
|
|
45
|
+
45 return 'unknown'
|
|
46
|
+
46 }
|
|
47
|
+
47 return matchesUser(record.id) ? 'match' : 'mismatch'
|
|
48
|
+
48 }
|
|
49
|
+
49
|
|
50
|
+
50 const fieldSchema = schemaStructure[field]
|
|
51
|
+
51
|
|
52
|
+
52 if (record.type && record.attributes && fieldSchema && !fieldSchema.belongsTo) {
|
|
53
|
+
53 const attributeValue = record.attributes[field]
|
|
54
|
+
54 if (attributeValue !== undefined && attributeValue !== null) {
|
|
55
|
+
55 return matchesUser(attributeValue) ? 'match' : 'mismatch'
|
|
56
|
+
56 }
|
|
57
|
+
57 }
|
|
58
|
+
58
|
|
59
|
+
59 let relationshipName
|
|
60
|
+
60 if (fieldSchema?.belongsTo) {
|
|
61
|
+
61 relationshipName = fieldSchema.as || field
|
|
62
|
+
62 } else if (schemaRelationships[field]) {
|
|
63
|
+
63 relationshipName = field
|
|
64
|
+
64 }
|
|
65
|
+
65
|
|
66
|
+
66 if (!relationshipName) {
|
|
67
|
+
67 return 'unknown'
|
|
68
|
+
68 }
|
|
69
|
+
69
|
|
70
|
+
70 const relationship = record.relationships?.[relationshipName]
|
|
71
|
+
71 const relData = relationship?.data
|
|
72
|
+
72 if (!relData) {
|
|
73
|
+
73 return 'unknown'
|
|
74
|
+
74 }
|
|
75
|
+
75
|
|
76
|
+
76 if (Array.isArray(relData)) {
|
|
77
|
+
77 return relData.some((item) => matchesUser(item?.id)) ? 'match' : 'mismatch'
|
|
78
|
+
78 }
|
|
79
|
+
79
|
|
80
|
+
80 if (relData?.id === undefined || relData?.id === null) {
|
|
81
|
+
81 return 'unknown'
|
|
82
|
+
82 }
|
|
83
|
+
83
|
|
84
|
+
84 return matchesUser(relData.id) ? 'match' : 'mismatch'
|
|
85
|
+
85 }
|
|
86
|
+
86
|
|
87
|
+
87 const resolveStorageAdapter = (scopeName, hookContext = {}) => {
|
|
88
|
+
88 if (!scopeName) return null
|
|
89
|
+
89
|
|
90
|
+
90 if (hookContext.storageAdapter) return hookContext.storageAdapter
|
|
91
|
+
91 if (hookContext.knexQuery?.storageAdapter) return hookContext.knexQuery.storageAdapter
|
|
92
|
+
92
|
|
93
|
+
93 if (helpers.getStorageAdapter) {
|
|
94
|
+
94 return helpers.getStorageAdapter(scopeName)
|
|
95
|
+
95 }
|
|
96
|
+
96 return null
|
|
97
|
+
97 }
|
|
98
|
+
98
|
|
99
|
+
99 const translateColumnReference = ({ field, tableName, scopeName, hookContext }) => {
|
|
100
|
+
100 const storageAdapter = resolveStorageAdapter(scopeName, hookContext)
|
|
101
|
+
101 const translated = storageAdapter?.translateColumn?.(field) ?? field
|
|
102
|
+
102 if (tableName) {
|
|
103
|
+
103 return `${tableName}.${translated}`
|
|
104
|
+
104 }
|
|
105
|
+
105 return translated
|
|
106
|
+
106 }
|
|
107
|
+
107
|
|
108
|
+
108 const translateFilterValue = ({ field, value, scopeName, hookContext }) => {
|
|
109
|
+
109 const storageAdapter = resolveStorageAdapter(scopeName, hookContext)
|
|
110
|
+
110 if (!storageAdapter?.translateFilterValue) return value
|
|
111
|
+
111 return storageAdapter.translateFilterValue(field, value)
|
|
112
|
+
112 }
|
|
113
|
+
113
|
|
114
|
+
114 function registerBuiltinCheckers () {
|
|
115
|
+
115 state.authCheckers.set('public', () => true)
|
|
116
|
+
116
|
|
117
|
+
117 state.authCheckers.set('authenticated', (context) => {
|
|
118
|
+
118 if (context.auth?.system === true) return true
|
|
119
|
+
119 return !!(context.auth?.userId || context.auth?.providerId)
|
|
120
|
+
120 })
|
|
121
|
+
121
|
|
122
|
+
122 state.authCheckers.set('owns', (context, { existingRecord, scopeVars }) => {
|
|
123
|
+
123 if (context.auth?.system === true) return true
|
|
124
|
+
124 if (!context.auth?.userId) return false
|
|
125
|
+
125 if (context.method === 'post') return true
|
|
126
|
+
126
|
|
127
|
+
127 // Minimal record is always prepared upstream (dataGetMinimal for reads, request payload snapshot for POST).
|
|
128
|
+
128 const record = existingRecord
|
|
129
|
+
129 if (!record) return true
|
|
130
|
+
130
|
|
131
|
+
131 const userId = String(context.auth.userId)
|
|
132
|
+
132 const field = scopeVars?.ownershipField || ownershipField()
|
|
133
|
+
133 const schemaInfo = scopeVars?.schemaInfo || {}
|
|
134
|
+
134
|
|
135
|
+
135 const ownershipStatus = evaluateOwnership({
|
|
136
|
+
136 record,
|
|
137
|
+
137 field,
|
|
138
|
+
138 schemaInfo,
|
|
139
|
+
139 userId
|
|
140
|
+
140 })
|
|
141
|
+
141
|
|
142
|
+
142 return ownershipStatus === 'match'
|
|
143
|
+
143 })
|
|
144
|
+
144 }
|
|
145
|
+
145
|
|
146
|
+
146 registerBuiltinCheckers()
|
|
147
|
+
147
|
|
148
|
+
148 addHook('scope:added', 'rest-auth-process-rules', {}, ({ context, scopes }) => {
|
|
149
|
+
149 const { scopeName } = context
|
|
150
|
+
150 const scope = scopes[scopeName]
|
|
151
|
+
151
|
|
152
|
+
152 const auth = context.scopeOptions?.auth
|
|
153
|
+
153 if (!auth) return
|
|
154
|
+
154
|
|
155
|
+
155 scope.vars.authRules = auth
|
|
156
|
+
156 scope.vars.ownershipField = context.scopeOptions?.ownershipField
|
|
157
|
+
157
|
|
158
|
+
158 log?.info?.(`Auth rules registered for ${scopeName}`, {
|
|
159
|
+
159 query: auth.query,
|
|
160
|
+
160 get: auth.get,
|
|
161
|
+
161 post: auth.post,
|
|
162
|
+
162 patch: auth.patch,
|
|
163
|
+
163 delete: auth.delete
|
|
164
|
+
164 })
|
|
165
|
+
165 })
|
|
166
|
+
166
|
|
167
|
+
167 addHook('schema:enrich', 'rest-auth-auto-ownership-field', {}, ({ context }) => {
|
|
168
|
+
168 if (!config.ownership.enabled) return
|
|
169
|
+
169 const { fields, scopeName, scopeOptions = {} } = context
|
|
170
|
+
170 if (config.ownership.excludeResources.includes(scopeName)) return
|
|
171
|
+
171 if (isOwnershipDisabled(scopeOptions)) return
|
|
172
|
+
172
|
|
173
|
+
173 const field = ownershipField()
|
|
174
|
+
174 const relationshipName = fields[field]?.as || fields[field]?.belongsTo || field
|
|
175
|
+
175
|
|
176
|
+
176 if (fields[field]) {
|
|
177
|
+
177 if (!fields[field].belongsTo) {
|
|
178
|
+
178 fields[field].belongsTo = config.ownership.userResource
|
|
179
|
+
179 }
|
|
180
|
+
180 if (!fields[field].as) {
|
|
181
|
+
181 fields[field].as = relationshipName
|
|
182
|
+
182 }
|
|
183
|
+
183 return
|
|
184
|
+
184 }
|
|
185
|
+
185
|
|
186
|
+
186 fields[field] = {
|
|
187
|
+
187 type: 'id',
|
|
188
|
+
188 belongsTo: config.ownership.userResource,
|
|
189
|
+
189 ...(relationshipName ? { as: relationshipName } : {}),
|
|
190
|
+
190 nullable: true,
|
|
191
|
+
191 indexed: true,
|
|
192
|
+
192 description: 'Automatically managed ownership field'
|
|
193
|
+
193 }
|
|
194
|
+
194 })
|
|
195
|
+
195
|
|
196
|
+
196 function setOwnerOnInput ({ context, scopeName, scopes, scopeOptions }) {
|
|
197
|
+
197 if (!config.ownership.enabled) return
|
|
198
|
+
198 if (config.ownership.excludeResources.includes(scopeName)) return
|
|
199
|
+
199 if (!context.auth?.userId) {
|
|
200
|
+
200 if (config.ownership.requireOwnership) {
|
|
201
|
+
201 throw new Error(`Cannot operate on ${scopeName} without authentication`)
|
|
202
|
+
202 }
|
|
203
|
+
203 return
|
|
204
|
+
204 }
|
|
205
|
+
205
|
|
206
|
+
206 if (isOwnershipDisabled(scopeOptions)) return
|
|
207
|
+
207
|
|
208
|
+
208 const field = ownershipField()
|
|
209
|
+
209 const scope = scopes[scopeName]
|
|
210
|
+
210 const schemaInfo = scope?.vars?.schemaInfo
|
|
211
|
+
211 const fieldSchema = schemaInfo?.schemaStructure?.[field]
|
|
212
|
+
212 const hasField = !!fieldSchema
|
|
213
|
+
213 const shouldSet = scopeOptions?.ownership === true || (scopeOptions?.ownership === undefined && hasField)
|
|
214
|
+
214 if (!shouldSet || !hasField) return
|
|
215
|
+
215
|
|
216
|
+
216 if (context.auth.roles?.includes?.('admin')) {
|
|
217
|
+
217 return
|
|
218
|
+
218 }
|
|
219
|
+
219
|
|
220
|
+
220 if (!context.inputRecord?.data) return
|
|
221
|
+
221
|
|
222
|
+
222 const relationships = context.inputRecord.data.relationships = context.inputRecord.data.relationships || {}
|
|
223
|
+
223 const attributes = context.inputRecord.data.attributes = context.inputRecord.data.attributes || {}
|
|
224
|
+
224
|
|
225
|
+
225 const relationshipName = fieldSchema.as || fieldSchema.belongsTo || field
|
|
226
|
+
226
|
|
227
|
+
227 if (fieldSchema.belongsTo) {
|
|
228
|
+
228 relationships[relationshipName] = {
|
|
229
|
+
229 data: {
|
|
230
|
+
230 type: fieldSchema.belongsTo || config.ownership.userResource || 'users',
|
|
231
|
+
231 id: String(context.auth.userId)
|
|
232
|
+
232 }
|
|
233
|
+
233 }
|
|
234
|
+
234 } else {
|
|
235
|
+
235 attributes[field] = context.auth.userId
|
|
236
|
+
236 }
|
|
237
|
+
237 }
|
|
238
|
+
238
|
|
239
|
+
239 addHook('beforeProcessingPost', 'rest-auth-auto-set-owner', {}, ({ context, scopeName, scopes, scopeOptions }) => {
|
|
240
|
+
240 setOwnerOnInput({ context, scopeName, scopes, scopeOptions })
|
|
241
|
+
241 })
|
|
242
|
+
242 addHook('beforeProcessingPatch', 'rest-auth-auto-set-owner', {}, ({ context, scopeName, scopes, scopeOptions }) => {
|
|
243
|
+
243 setOwnerOnInput({ context, scopeName, scopes, scopeOptions })
|
|
244
|
+
244 })
|
|
245
|
+
245 addHook('beforeProcessingPut', 'rest-auth-auto-set-owner', {}, ({ context, scopeName, scopes, scopeOptions }) => {
|
|
246
|
+
246 setOwnerOnInput({ context, scopeName, scopes, scopeOptions })
|
|
247
|
+
247 })
|
|
248
|
+
248
|
|
249
|
+
249 addHook('knexQueryFiltering', 'rest-auth-filter-by-owner', { sequence: -40 }, ({ context, scopes, scopeOptions }) => {
|
|
250
|
+
250 if (!config.ownership.enabled || !config.ownership.filterByOwner) return
|
|
251
|
+
251
|
|
252
|
+
252 const { query, tableName, scopeName } = context.knexQuery || {}
|
|
253
|
+
253
|
|
254
|
+
254 if (!query || !tableName) {
|
|
255
|
+
255 throw new Error('AccessPlugin: knexQuery must provide query and tableName for ownership filtering')
|
|
256
|
+
256 }
|
|
257
|
+
257
|
|
258
|
+
258 if (config.ownership.excludeResources.includes(scopeName)) return
|
|
259
|
+
259 if (!context.auth?.userId) {
|
|
260
|
+
260 if (config.ownership.requireOwnership) {
|
|
261
|
+
261 throw new Error(`Cannot query ${scopeName} without authentication`)
|
|
262
|
+
262 }
|
|
263
|
+
263 return
|
|
264
|
+
264 }
|
|
265
|
+
265
|
|
266
|
+
266 if (context.auth.roles?.includes?.('admin')) return
|
|
267
|
+
267 if (isOwnershipDisabled(scopeOptions)) return
|
|
268
|
+
268
|
|
269
|
+
269 const field = ownershipField()
|
|
270
|
+
270 const scope = scopes[scopeName]
|
|
271
|
+
271 const schemaInfo = scope?.vars?.schemaInfo
|
|
272
|
+
272 const hasField = !!schemaInfo?.schemaStructure?.[field]
|
|
273
|
+
273 const shouldFilter = scopeOptions?.ownership === true || (scopeOptions?.ownership === undefined && hasField)
|
|
274
|
+
274 if (!shouldFilter || !hasField) return
|
|
275
|
+
275
|
|
276
|
+
276 const storageAdapter = resolveStorageAdapter(scopeName, context)
|
|
277
|
+
277 if (storageAdapter && !context.storageAdapter) {
|
|
278
|
+
278 context.storageAdapter = storageAdapter
|
|
279
|
+
279 }
|
|
280
|
+
280
|
|
281
|
+
281 const columnRef = translateColumnReference({ field, tableName, scopeName, hookContext: context })
|
|
282
|
+
282 const ownerValue = translateFilterValue({ field, value: context.auth.userId, scopeName, hookContext: context })
|
|
283
|
+
283
|
|
284
|
+
284 if (ownerValue === null) {
|
|
285
|
+
285 query.whereNull(columnRef)
|
|
286
|
+
286 } else {
|
|
287
|
+
287 query.where(columnRef, ownerValue)
|
|
288
|
+
288 }
|
|
289
|
+
289 })
|
|
290
|
+
290
|
|
291
|
+
291 addHook('checkPermissions', 'rest-auth-enforce', { sequence: -100 }, async ({ context, scope, scopeName }) => {
|
|
292
|
+
292 const operation = context.method
|
|
293
|
+
293 const scopeVars = scope?.vars
|
|
294
|
+
294 const authRules = scopeVars?.authRules
|
|
295
|
+
295 const minimalRecord = context.originalContext?.minimalRecord
|
|
296
|
+
296
|
|
297
|
+
297 if (!authRules) return
|
|
298
|
+
298 const rules = authRules[operation]
|
|
299
|
+
299 if (!rules) {
|
|
300
|
+
300 throw new Error(`Operation '${operation}' not allowed on resource '${scopeName}'`)
|
|
301
|
+
301 }
|
|
302
|
+
302
|
|
303
|
+
303 let passed = false
|
|
304
|
+
304 const failures = []
|
|
305
|
+
305
|
|
306
|
+
306 for (const rule of rules) {
|
|
307
|
+
307 try {
|
|
308
|
+
308 const [checkerName, ...paramParts] = rule.split(':')
|
|
309
|
+
309 const checker = state.authCheckers.get(checkerName)
|
|
310
|
+
310 const param = paramParts.join(':') || undefined
|
|
311
|
+
311 if (!checker) {
|
|
312
|
+
312 throw new Error(`Unknown auth rule: ${rule}`)
|
|
313
|
+
313 }
|
|
314
|
+
314
|
|
315
|
+
315 const result = await checker(context.originalContext || context, {
|
|
316
|
+
316 existingRecord: minimalRecord,
|
|
317
|
+
317 scopeVars,
|
|
318
|
+
318 param
|
|
319
|
+
319 })
|
|
320
|
+
320
|
|
321
|
+
321 if (result) {
|
|
322
|
+
322 passed = true
|
|
323
|
+
323 break
|
|
324
|
+
324 }
|
|
325
|
+
325
|
|
326
|
+
326 failures.push(rule)
|
|
327
|
+
327 } catch (error) {
|
|
328
|
+
328 failures.push(`${rule} (error: ${error.message})`)
|
|
329
|
+
329 }
|
|
330
|
+
330 }
|
|
331
|
+
331
|
|
332
|
+
332 if (!passed) {
|
|
333
|
+
333 const err = new Error(
|
|
334
|
+
334 `Access denied. Required one of: ${rules.join(', ')}. Failed checks: ${failures.join(', ')}`
|
|
335
|
+
335 )
|
|
336
|
+
336 err.statusCode = 403
|
|
337
|
+
337 throw err
|
|
338
|
+
338 }
|
|
339
|
+
339
|
|
340
|
+
340 context.authGranted = true
|
|
341
|
+
341 })
|
|
342
|
+
342
|
|
343
|
+
343 addHook('checkPermissions', 'rest-auth-check-get-ownership', { sequence: -80 }, ({ context, scope, scopeName, scopeOptions }) => {
|
|
344
|
+
344 if (!config.ownership.enabled || !config.ownership.filterByOwner) return
|
|
345
|
+
345 if (config.ownership.excludeResources.includes(scopeName)) return
|
|
346
|
+
346
|
|
347
|
+
347 if (!['get', 'put', 'patch', 'delete'].includes(context.method)) return
|
|
348
|
+
348
|
|
349
|
+
349 const auth = context.originalContext?.auth || context.auth
|
|
350
|
+
350 if (!auth?.userId) return
|
|
351
|
+
351 if (auth.roles?.includes?.('admin')) return
|
|
352
|
+
352 if (isOwnershipDisabled(scopeOptions)) return
|
|
353
|
+
353
|
|
354
|
+
354 const field = ownershipField()
|
|
355
|
+
355 const schemaInfo = scope?.vars?.schemaInfo || {}
|
|
356
|
+
356 const hasField = !!schemaInfo?.schemaStructure?.[field]
|
|
357
|
+
357 const shouldCheck = scopeOptions?.ownership === true || (scopeOptions?.ownership === undefined && hasField)
|
|
358
|
+
358 if (!shouldCheck) return
|
|
359
|
+
359
|
|
360
|
+
360 const record = context.originalContext?.minimalRecord
|
|
361
|
+
361 if (!record) return
|
|
362
|
+
362
|
|
363
|
+
363 const idProperty = schemaInfo.idProperty || 'id'
|
|
364
|
+
364 const userId = String(auth.userId)
|
|
365
|
+
365
|
|
366
|
+
366 // Ownership by primary key (e.g., users modifying themselves)
|
|
367
|
+
367 if (field === idProperty) {
|
|
368
|
+
368 if (record.id !== undefined && record.id !== null && String(record.id) !== userId) {
|
|
369
|
+
369 throw new RestApiResourceError('Resource not found', { subtype: 'not_found' })
|
|
370
|
+
370 }
|
|
371
|
+
371 return
|
|
372
|
+
372 }
|
|
373
|
+
373
|
|
374
|
+
374 const ownershipStatus = evaluateOwnership({
|
|
375
|
+
375 record,
|
|
376
|
+
376 field,
|
|
377
|
+
377 schemaInfo,
|
|
378
|
+
378 userId
|
|
379
|
+
379 })
|
|
380
|
+
380
|
|
381
|
+
381 if (ownershipStatus === 'mismatch') {
|
|
382
|
+
382 throw new RestApiResourceError('Resource not found', { subtype: 'not_found' })
|
|
383
|
+
383 }
|
|
384
|
+
384 })
|
|
385
|
+
385
|
|
386
|
+
386 if (!helpers.auth) {
|
|
387
|
+
387 helpers.auth = {}
|
|
388
|
+
388 }
|
|
389
|
+
389
|
|
390
|
+
390 helpers.auth.requireAuth = function requireAuth (context) {
|
|
391
|
+
391 if (!context.auth?.userId) {
|
|
392
|
+
392 const error = new Error('Authentication required')
|
|
393
|
+
393 error.statusCode = 401
|
|
394
|
+
394 throw error
|
|
395
|
+
395 }
|
|
396
|
+
396 return context.auth
|
|
397
|
+
397 }
|
|
398
|
+
398
|
|
399
|
+
399 helpers.auth.requireOwnership = function requireOwnership (context, resourceOrUserId) {
|
|
400
|
+
400 const auth = helpers.auth.requireAuth(context)
|
|
401
|
+
401 const field = ownershipField()
|
|
402
|
+
402
|
|
403
|
+
403 let ownerId
|
|
404
|
+
404
|
|
405
|
+
405 if (typeof resourceOrUserId === 'object' && resourceOrUserId !== null) {
|
|
406
|
+
406 ownerId = resourceOrUserId[field]
|
|
407
|
+
407 if (!ownerId) {
|
|
408
|
+
408 throw new Error(`Resource does not have ownership field '${field}'`)
|
|
409
|
+
409 }
|
|
410
|
+
410 } else if (resourceOrUserId !== undefined) {
|
|
411
|
+
411 ownerId = resourceOrUserId
|
|
412
|
+
412 } else if (context.existingRecord) {
|
|
413
|
+
413 ownerId = context.existingRecord[field]
|
|
414
|
+
414 if (!ownerId) {
|
|
415
|
+
415 throw new Error(`Resource does not have ownership field '${field}'`)
|
|
416
|
+
416 }
|
|
417
|
+
417 } else {
|
|
418
|
+
418 throw new Error('No resource or user ID provided for ownership check')
|
|
419
|
+
419 }
|
|
420
|
+
420
|
|
421
|
+
421 if (String(auth.userId) !== String(ownerId)) {
|
|
422
|
+
422 const error = new Error('Access denied: you do not own this resource')
|
|
423
|
+
423 error.statusCode = 403
|
|
424
|
+
424 throw error
|
|
425
|
+
425 }
|
|
426
|
+
426
|
|
427
|
+
427 return auth
|
|
428
|
+
428 }
|
|
429
|
+
429
|
|
430
|
+
430 helpers.auth.registerChecker = function registerChecker (name, checkerFn) {
|
|
431
|
+
431 state.authCheckers.set(name, checkerFn)
|
|
432
|
+
432 }
|
|
433
|
+
433
|
|
434
|
+
434 helpers.auth.checkPermission = async function checkPermission (context, rules, options = {}) {
|
|
435
|
+
435 if (!rules || rules.length === 0) return true
|
|
436
|
+
436
|
|
437
|
+
437 const { existingRecord, scopeVars } = options
|
|
438
|
+
438
|
|
439
|
+
439 for (const rule of rules) {
|
|
440
|
+
440 const [checkerName, ...paramParts] = rule.split(':')
|
|
441
|
+
441 const checker = state.authCheckers.get(checkerName)
|
|
442
|
+
442 if (!checker) continue
|
|
443
|
+
443
|
|
444
|
+
444 const param = paramParts.join(':') || undefined
|
|
445
|
+
445 if (await checker(context, { existingRecord, scopeVars, param })) {
|
|
446
|
+
446 return true
|
|
447
|
+
447 }
|
|
448
|
+
448 }
|
|
449
|
+
449
|
|
450
|
+
450 return false
|
|
451
|
+
451 }
|
|
452
|
+
452
|
|
453
|
+
453 helpers.auth.cleanup = function cleanup () {
|
|
454
|
+
454 state.authCheckers.clear()
|
|
455
|
+
455 }
|
|
456
|
+
456 }
|
|
457
|
+
457 }
|
|
458
|
+
458
|
|
459
|
+
459 export default AccessPlugin
|
package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-cors-plugin.js.txt
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
1 export const CorsPlugin = {
|
|
2
|
+
2 name: 'rest-api-cors',
|
|
3
|
+
3 dependencies: ['rest-api'],
|
|
4
|
+
4
|
|
5
|
+
5 async install ({ api, addHook, vars, helpers, log, pluginOptions, runHooks }) {
|
|
6
|
+
6 // Get CORS configuration
|
|
7
|
+
7 const corsOptions = pluginOptions || {}
|
|
8
|
+
8
|
|
9
|
+
9 // Store configuration - using a plain object for runtime updates
|
|
10
|
+
10 const corsConfig = {
|
|
11
|
+
11 // Origin configuration
|
|
12
|
+
12 origin: corsOptions.origin || '*', // Can be string, regex, array, or function
|
|
13
|
+
13 credentials: corsOptions.credentials !== undefined ? corsOptions.credentials : true,
|
|
14
|
+
14
|
|
15
|
+
15 // Allowed methods
|
|
16
|
+
16 methods: corsOptions.methods || ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
17
|
+
17
|
|
18
|
+
18 // Allowed headers
|
|
19
|
+
19 allowedHeaders: corsOptions.allowedHeaders || [
|
|
20
|
+
20 'Content-Type',
|
|
21
|
+
21 'Authorization',
|
|
22
|
+
22 'X-Requested-With',
|
|
23
|
+
23 'X-HTTP-Method-Override',
|
|
24
|
+
24 'Accept',
|
|
25
|
+
25 'Origin'
|
|
26
|
+
26 ],
|
|
27
|
+
27
|
|
28
|
+
28 // Exposed headers (for client to read)
|
|
29
|
+
29 exposedHeaders: corsOptions.exposedHeaders || [
|
|
30
|
+
30 'X-Total-Count',
|
|
31
|
+
31 'X-Page-Count',
|
|
32
|
+
32 'Link',
|
|
33
|
+
33 'Location'
|
|
34
|
+
34 ],
|
|
35
|
+
35
|
|
36
|
+
36 // Preflight cache
|
|
37
|
+
37 maxAge: corsOptions.maxAge || 86400, // 24 hours
|
|
38
|
+
38
|
|
39
|
+
39 // Options success status
|
|
40
|
+
40 optionsSuccessStatus: corsOptions.optionsSuccessStatus || 204
|
|
41
|
+
41 }
|
|
42
|
+
42
|
|
43
|
+
43 // Store reference in vars for access in hooks
|
|
44
|
+
44 vars.cors = corsConfig
|
|
45
|
+
45
|
|
46
|
+
46 // Helper to check if origin is allowed
|
|
47
|
+
47 function isOriginAllowed (origin, allowedOrigin) {
|
|
48
|
+
48 if (!origin) return !corsConfig.credentials // No origin = same-origin request
|
|
49
|
+
49
|
|
50
|
+
50 // String match
|
|
51
|
+
51 if (typeof allowedOrigin === 'string') {
|
|
52
|
+
52 return allowedOrigin === '*' || allowedOrigin === origin
|
|
53
|
+
53 }
|
|
54
|
+
54
|
|
55
|
+
55 // Regex match
|
|
56
|
+
56 if (allowedOrigin instanceof RegExp) {
|
|
57
|
+
57 return allowedOrigin.test(origin)
|
|
58
|
+
58 }
|
|
59
|
+
59
|
|
60
|
+
60 // Array of allowed origins
|
|
61
|
+
61 if (Array.isArray(allowedOrigin)) {
|
|
62
|
+
62 return allowedOrigin.some(allowed => isOriginAllowed(origin, allowed))
|
|
63
|
+
63 }
|
|
64
|
+
64
|
|
65
|
+
65 // Function check
|
|
66
|
+
66 if (typeof allowedOrigin === 'function') {
|
|
67
|
+
67 return allowedOrigin(origin)
|
|
68
|
+
68 }
|
|
69
|
+
69
|
|
70
|
+
70 return false
|
|
71
|
+
71 }
|
|
72
|
+
72
|
|
73
|
+
73 // Check if transport plugin is available
|
|
74
|
+
74 if (!vars.transport) {
|
|
75
|
+
75 log.error('CORS plugin requires a transport plugin (express, koa, etc.) to be installed')
|
|
76
|
+
76 return
|
|
77
|
+
77 }
|
|
78
|
+
78
|
|
79
|
+
79 // Register OPTIONS handler for all routes
|
|
80
|
+
80 const optionsHandler = async ({ context, headers }) => {
|
|
81
|
+
81 const origin = headers?.origin
|
|
82
|
+
82
|
|
83
|
+
83 log.debug('CORS OPTIONS request', { origin })
|
|
84
|
+
84
|
|
85
|
+
85 // Check if origin is allowed
|
|
86
|
+
86 if (isOriginAllowed(origin, corsConfig.origin)) {
|
|
87
|
+
87 const responseHeaders = {
|
|
88
|
+
88 'Access-Control-Allow-Methods': corsConfig.methods.join(', '),
|
|
89
|
+
89 'Access-Control-Allow-Headers': corsConfig.allowedHeaders.join(', '),
|
|
90
|
+
90 'Access-Control-Max-Age': String(corsConfig.maxAge)
|
|
91
|
+
91 }
|
|
92
|
+
92
|
|
93
|
+
93 // Set origin header
|
|
94
|
+
94 if (corsConfig.origin === '*' && !corsConfig.credentials) {
|
|
95
|
+
95 responseHeaders['Access-Control-Allow-Origin'] = '*'
|
|
96
|
+
96 } else if (origin) {
|
|
97
|
+
97 responseHeaders['Access-Control-Allow-Origin'] = origin
|
|
98
|
+
98 responseHeaders['Vary'] = 'Origin'
|
|
99
|
+
99 }
|
|
100
|
+
100
|
|
101
|
+
101 // Set credentials if enabled
|
|
102
|
+
102 if (corsConfig.credentials) {
|
|
103
|
+
103 responseHeaders['Access-Control-Allow-Credentials'] = 'true'
|
|
104
|
+
104 }
|
|
105
|
+
105
|
|
106
|
+
106 return {
|
|
107
|
+
107 statusCode: corsConfig.optionsSuccessStatus,
|
|
108
|
+
108 headers: responseHeaders,
|
|
109
|
+
109 body: null
|
|
110
|
+
110 }
|
|
111
|
+
111 } else {
|
|
112
|
+
112 // Origin not allowed
|
|
113
|
+
113 log.warn('CORS origin not allowed', { origin })
|
|
114
|
+
114 return {
|
|
115
|
+
115 statusCode: 403,
|
|
116
|
+
116 body: { error: 'CORS origin not allowed' }
|
|
117
|
+
117 }
|
|
118
|
+
118 }
|
|
119
|
+
119 }
|
|
120
|
+
120
|
|
121
|
+
121 // Register OPTIONS route for all paths
|
|
122
|
+
122 await api.addRoute({
|
|
123
|
+
123 method: 'OPTIONS',
|
|
124
|
+
124 path: vars.transport.matchAll,
|
|
125
|
+
125 handler: optionsHandler
|
|
126
|
+
126 })
|
|
127
|
+
127
|
|
128
|
+
128 // Hook to handle CORS headers for all responses
|
|
129
|
+
129 addHook('transport:response', 'cors-headers', { order: -1000 }, async ({ context }) => {
|
|
130
|
+
130 // Transport data is now nested in context
|
|
131
|
+
131 const { request, response } = context.transport || {}
|
|
132
|
+
132
|
|
133
|
+
133 if (!request) {
|
|
134
|
+
134 log.error('CORS: request is undefined in transport:response hook')
|
|
135
|
+
135 return
|
|
136
|
+
136 }
|
|
137
|
+
137
|
|
138
|
+
138 const origin = request.headers?.origin
|
|
139
|
+
139 const method = request.method?.toUpperCase()
|
|
140
|
+
140
|
|
141
|
+
141 log.debug('CORS processing response', {
|
|
142
|
+
142 origin,
|
|
143
|
+
143 method,
|
|
144
|
+
144 path: request.path
|
|
145
|
+
145 })
|
|
146
|
+
146
|
|
147
|
+
147 // Skip if this was an OPTIONS request (already handled by route)
|
|
148
|
+
148 if (method === 'OPTIONS') {
|
|
149
|
+
149 return
|
|
150
|
+
150 }
|
|
151
|
+
151
|
|
152
|
+
152 // Check if origin is allowed
|
|
153
|
+
153 if (isOriginAllowed(origin, corsConfig.origin)) {
|
|
154
|
+
154 // Set origin header
|
|
155
|
+
155 if (corsConfig.origin === '*' && !corsConfig.credentials) {
|
|
156
|
+
156 response.headers['Access-Control-Allow-Origin'] = '*'
|
|
157
|
+
157 } else if (origin) {
|
|
158
|
+
158 response.headers['Access-Control-Allow-Origin'] = origin
|
|
159
|
+
159 response.headers['Vary'] = 'Origin'
|
|
160
|
+
160 }
|
|
161
|
+
161
|
|
162
|
+
162 // Set credentials if enabled
|
|
163
|
+
163 if (corsConfig.credentials) {
|
|
164
|
+
164 response.headers['Access-Control-Allow-Credentials'] = 'true'
|
|
165
|
+
165 }
|
|
166
|
+
166
|
|
167
|
+
167 // Set exposed headers
|
|
168
|
+
168 if (corsConfig.exposedHeaders.length > 0) {
|
|
169
|
+
169 response.headers['Access-Control-Expose-Headers'] = corsConfig.exposedHeaders.join(', ')
|
|
170
|
+
170 }
|
|
171
|
+
171 } else if (origin) {
|
|
172
|
+
172 // Origin not allowed - don't set any CORS headers
|
|
173
|
+
173 log.warn('CORS origin not allowed for response', {
|
|
174
|
+
174 origin,
|
|
175
|
+
175 allowedOrigins: corsConfig.origin
|
|
176
|
+
176 })
|
|
177
|
+
177 }
|
|
178
|
+
178 })
|
|
179
|
+
179
|
|
180
|
+
180 log.info('CORS plugin installed', {
|
|
181
|
+
181 origin: corsConfig.origin,
|
|
182
|
+
182 credentials: corsConfig.credentials,
|
|
183
|
+
183 methods: corsConfig.methods
|
|
184
|
+
184 })
|
|
185
|
+
185 }
|
|
186
|
+
186 }
|
|
187
|
+
187
|
|
188
|
+
188 /*
|
|
189
|
+
189 Usage examples:
|
|
190
|
+
190
|
|
191
|
+
191 // Basic usage - allow all origins
|
|
192
|
+
192 await api.use(CorsPlugin);
|
|
193
|
+
193
|
|
194
|
+
194 // Specific origin
|
|
195
|
+
195 await api.use(CorsPlugin, {
|
|
196
|
+
196 'rest-api-cors': {
|
|
197
|
+
197 origin: 'https://app.example.com'
|
|
198
|
+
198 }
|
|
199
|
+
199 });
|
|
200
|
+
200
|
|
201
|
+
201 // Multiple origins
|
|
202
|
+
202 await api.use(CorsPlugin, {
|
|
203
|
+
203 'rest-api-cors': {
|
|
204
|
+
204 origin: ['https://app.example.com', 'https://admin.example.com']
|
|
205
|
+
205 }
|
|
206
|
+
206 });
|
|
207
|
+
207
|
|
208
|
+
208 // Dynamic origin with regex
|
|
209
|
+
209 await api.use(CorsPlugin, {
|
|
210
|
+
210 'rest-api-cors': {
|
|
211
|
+
211 origin: /^https:\/\/.*\.example\.com$/
|
|
212
|
+
212 }
|
|
213
|
+
213 });
|
|
214
|
+
214
|
|
215
|
+
215 // Function-based origin check
|
|
216
|
+
216 await api.use(CorsPlugin, {
|
|
217
|
+
217 'rest-api-cors': {
|
|
218
|
+
218 origin: (origin) => {
|
|
219
|
+
219 // Custom logic to determine if origin is allowed
|
|
220
|
+
220 return myAllowedOrigins.includes(origin);
|
|
221
|
+
221 }
|
|
222
|
+
222 }
|
|
223
|
+
223 });
|
|
224
|
+
224
|
|
225
|
+
225 // Full configuration
|
|
226
|
+
226 await api.use(CorsPlugin, {
|
|
227
|
+
227 'rest-api-cors': {
|
|
228
|
+
228 origin: 'https://app.example.com',
|
|
229
|
+
229 credentials: true,
|
|
230
|
+
230 methods: ['GET', 'POST', 'PATCH', 'DELETE'],
|
|
231
|
+
231 allowedHeaders: ['Content-Type', 'Authorization', 'X-Custom-Header'],
|
|
232
|
+
232 exposedHeaders: ['X-Total-Count', 'X-RateLimit-Remaining'],
|
|
233
|
+
233 maxAge: 3600 // 1 hour
|
|
234
|
+
234 }
|
|
235
|
+
235 });
|
|
236
|
+
236
|
|
237
|
+
237 */
|