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.
Files changed (173) hide show
  1. package/audit/00-charter.md +103 -0
  2. package/audit/01-scope-map.md +84 -0
  3. package/audit/02-coverage-matrix.csv +174 -0
  4. package/audit/03-invariants.md +123 -0
  5. package/audit/04-standards-rubric.md +56 -0
  6. package/audit/05-competitor-rubric.md +77 -0
  7. package/audit/06-findings.md +160 -0
  8. package/audit/07-risk-register.csv +10 -0
  9. package/audit/08-remediation-roadmap.md +177 -0
  10. package/audit/09-signoff-check.md +23 -0
  11. package/audit/10-final-synthesis.md +29 -0
  12. package/audit/evidence/code/all-files-json-rest-api.txt +168 -0
  13. package/audit/evidence/code/all-files-json-rest-schema.txt +12 -0
  14. package/audit/evidence/code/boundary-leak-checks.txt +72 -0
  15. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__bulk-operations-plugin.js.txt +480 -0
  16. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__express-plugin.js.txt +532 -0
  17. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__busboy-detector.js.txt +175 -0
  18. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__formidable-detector.js.txt +138 -0
  19. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__request-helpers.js.txt +222 -0
  20. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__file-handling-plugin.js.txt +342 -0
  21. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__dbTablesOperations.js.txt +304 -0
  22. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__fractional-positioning.js.txt +188 -0
  23. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__compile-schemas.js.txt +336 -0
  24. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__connectors-query-parser.js.txt +165 -0
  25. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-capabilities.js.txt +265 -0
  26. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-value-normalizers.js.txt +369 -0
  27. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__default-data-helpers.js.txt +72 -0
  28. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__field-utils.js.txt +139 -0
  29. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-constants.js.txt +45 -0
  30. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-field-helpers.js.txt +396 -0
  31. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-json-api-transformers.js.txt +162 -0
  32. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__payload-validators.js.txt +1314 -0
  33. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__schema-helpers.js.txt +488 -0
  34. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__scope-validations.js.txt +484 -0
  35. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__simplified-helpers.js.txt +857 -0
  36. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-cross-table-search.js.txt +565 -0
  37. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-json-api-transformers-querying.js.txt +485 -0
  38. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-pagination-helpers.js.txt +560 -0
  39. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-process-includes.js.txt +167 -0
  40. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers-base.js.txt +109 -0
  41. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers.js.txt +1064 -0
  42. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-relationship-includes.js.txt +1983 -0
  43. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-window-queries.js.txt +328 -0
  44. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__url-helpers.js.txt +119 -0
  45. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__storage__storage-adapter.js.txt +160 -0
  46. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__many-to-many-manipulations.js.txt +300 -0
  47. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__relationship-processor.js.txt +245 -0
  48. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-access.js.txt +459 -0
  49. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-cors-plugin.js.txt +237 -0
  50. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-knex-plugin.js.txt +1135 -0
  51. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-multihome-plugin.js.txt +314 -0
  52. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__compile-resource-schemas.js.txt +7 -0
  53. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-relationship-routes.js.txt +71 -0
  54. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-scope-routes.js.txt +102 -0
  55. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__turn-scope-init-into-vars.js.txt +49 -0
  56. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__validate-include-configurations.js.txt +39 -0
  57. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__add-route.js.txt +23 -0
  58. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__check-permissions.js.txt +13 -0
  59. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__common.js.txt +639 -0
  60. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete-relationship.js.txt +117 -0
  61. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete.js.txt +104 -0
  62. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__enrich-attributes.js.txt +199 -0
  63. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-related.js.txt +353 -0
  64. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-relationship.js.txt +61 -0
  65. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get.js.txt +184 -0
  66. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch-relationship.js.txt +55 -0
  67. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch.js.txt +191 -0
  68. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post-relationship.js.txt +110 -0
  69. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post.js.txt +168 -0
  70. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__put.js.txt +253 -0
  71. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__query.js.txt +207 -0
  72. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__release.js.txt +12 -0
  73. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin.js.txt +165 -0
  74. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-positioning-plugin.js.txt +668 -0
  75. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__socketio-plugin.js.txt +621 -0
  76. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__local-storage.js.txt +218 -0
  77. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__s3-storage.js.txt +124 -0
  78. package/audit/evidence/code/critical-pass1/json-rest-schema__src__CorePlugin.js.txt +269 -0
  79. package/audit/evidence/code/critical-pass1/json-rest-schema__src__Schema.js.txt +242 -0
  80. package/audit/evidence/code/critical-pass1/json-rest-schema__src__index.js.txt +77 -0
  81. package/audit/evidence/code/critical-pass1-risk-keywords.txt +238 -0
  82. package/audit/evidence/code/critical-pass1-summary.csv +67 -0
  83. package/audit/evidence/code/critical-pass2-summary.csv +67 -0
  84. package/audit/evidence/code/early-silent-zones-no-test-token.txt +57 -0
  85. package/audit/evidence/code/error-model-signals.txt +98 -0
  86. package/audit/evidence/code/git-files-json-rest-api.txt +171 -0
  87. package/audit/evidence/code/git-files-json-rest-schema.txt +27 -0
  88. package/audit/evidence/code/hook-flow-signals.txt +282 -0
  89. package/audit/evidence/code/idproperty-signals.txt +157 -0
  90. package/audit/evidence/code/import-cycles-non-anyapi.txt +3 -0
  91. package/audit/evidence/code/import-hotspots-non-anyapi.txt +96 -0
  92. package/audit/evidence/code/imports-json-rest-api.txt +354 -0
  93. package/audit/evidence/code/imports-json-rest-schema.txt +15 -0
  94. package/audit/evidence/code/in-scope-files-json-rest-api.txt +160 -0
  95. package/audit/evidence/code/in-scope-files-json-rest-schema.txt +13 -0
  96. package/audit/evidence/code/non-anyapi-core-files.txt +101 -0
  97. package/audit/evidence/code/permission-signals.txt +193 -0
  98. package/audit/evidence/code/query-include-signals.txt +687 -0
  99. package/audit/evidence/code/transaction-signals.txt +28 -0
  100. package/audit/evidence/code/validation-signals.txt +650 -0
  101. package/audit/evidence/code/write-path-signals.txt +61 -0
  102. package/audit/evidence/docs/docs-claims-signals.txt +1075 -0
  103. package/audit/evidence/docs/schema-docs-claims-signals.txt +179 -0
  104. package/audit/evidence/external/corpus-freeze.md +21 -0
  105. package/audit/evidence/external/npm-competitor-metadata.txt +70 -0
  106. package/audit/evidence/external/npm-download-signals.txt +23 -0
  107. package/audit/evidence/external/npm-jsonapi-server.txt +8 -0
  108. package/audit/evidence/external/npm-modern-jsonapi-metadata.txt +38 -0
  109. package/audit/evidence/external/readme-fortune-signals.txt +26 -0
  110. package/audit/evidence/external/readme-fortune.md +165 -0
  111. package/audit/evidence/external/readme-json-api-serializer-signals.txt +80 -0
  112. package/audit/evidence/external/readme-json-api-serializer.md +690 -0
  113. package/audit/evidence/external/readme-jsonapi-serde-server-signals.txt +0 -0
  114. package/audit/evidence/external/readme-jsonapi-serde-server.md +22 -0
  115. package/audit/evidence/external/readme-jsonapi-serializer-signals.txt +63 -0
  116. package/audit/evidence/external/readme-jsonapi-serializer.md +315 -0
  117. package/audit/evidence/external/readme-jsonapi-server-signals.txt +19 -0
  118. package/audit/evidence/external/readme-jsonapi-server.md +104 -0
  119. package/audit/evidence/external/readme-nestjs-json-api-signals.txt +4 -0
  120. package/audit/evidence/external/readme-nestjs-json-api.md +94 -0
  121. package/audit/evidence/external/readme-yaser-json-api-nestjs-signals.txt +47 -0
  122. package/audit/evidence/external/readme-yaser-json-api-nestjs.md +447 -0
  123. package/audit/evidence/external/standard-jsonapi-snippets.txt +84 -0
  124. package/audit/evidence/external/standard-rfc-snippets.txt +168 -0
  125. package/audit/evidence/external/standards-sources.txt +6 -0
  126. package/audit/evidence/runtime/baseline-commits.txt +2 -0
  127. package/audit/evidence/runtime/db-adapter-assumptions.txt +3 -0
  128. package/audit/evidence/runtime/environment.txt +4 -0
  129. package/audit/evidence/runtime/invariant-runtime-mapping.md +38 -0
  130. package/audit/evidence/runtime/probe-patch-relationship-error.exitcode +1 -0
  131. package/audit/evidence/runtime/probe-patch-relationship-error.log +76 -0
  132. package/audit/evidence/runtime/probe-patch-relationship-error.mjs +67 -0
  133. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.exitcode +1 -0
  134. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.log +62 -0
  135. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.mjs +47 -0
  136. package/audit/evidence/runtime/test-exitcodes.txt +4 -0
  137. package/audit/evidence/runtime/test-summary.md +107 -0
  138. package/audit/evidence/runtime/trace-anchors.txt +61 -0
  139. package/audit/evidence/tests/access-multitenancy-test-signals.txt +125 -0
  140. package/audit/evidence/tests/failing-tests-summary.txt +25 -0
  141. package/audit/evidence/tests/npm-test-json-rest-api.exitcode +1 -0
  142. package/audit/evidence/tests/npm-test-json-rest-api.log +5885 -0
  143. package/audit/evidence/tests/npm-test-json-rest-schema.exitcode +1 -0
  144. package/audit/evidence/tests/npm-test-json-rest-schema.log +356 -0
  145. package/audit/evidence/tests/query-include-test-signals.txt +948 -0
  146. package/audit/evidence/tests/relationship-test-signals.txt +223 -0
  147. package/audit/evidence/tests/skipped-tests.txt +10 -0
  148. package/audit/evidence/tests/test-suite-list-non-anyapi.txt +30 -0
  149. package/audit/evidence/tests/test-suite-list.txt +32 -0
  150. package/audit/subsystems/docs-drift.md +49 -0
  151. package/audit/subsystems/pass2-cross-review.md +131 -0
  152. package/audit/subsystems/plugins-access.md +51 -0
  153. package/audit/subsystems/plugins-bulk.md +50 -0
  154. package/audit/subsystems/plugins-cors.md +48 -0
  155. package/audit/subsystems/plugins-file.md +53 -0
  156. package/audit/subsystems/plugins-multihome.md +50 -0
  157. package/audit/subsystems/plugins-positioning.md +54 -0
  158. package/audit/subsystems/plugins-socketio.md +50 -0
  159. package/audit/subsystems/query-engine.md +61 -0
  160. package/audit/subsystems/relationships.md +55 -0
  161. package/audit/subsystems/rest-orchestration.md +66 -0
  162. package/audit/subsystems/schema-core.md +64 -0
  163. package/audit/subsystems/storage-knex.md +57 -0
  164. package/audit/subsystems/tests.md +48 -0
  165. package/audit/subsystems/transport-express.md +55 -0
  166. package/audit/traces/trace-read-get.md +22 -0
  167. package/audit/traces/trace-read-query.md +25 -0
  168. package/audit/traces/trace-relationship-endpoints.md +31 -0
  169. package/audit/traces/trace-write-delete.md +17 -0
  170. package/audit/traces/trace-write-patch.md +26 -0
  171. package/audit/traces/trace-write-post.md +22 -0
  172. package/audit/traces/trace-write-put.md +23 -0
  173. package/package.json +5 -5
@@ -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
@@ -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 */