wefunder 0.1.0.beta1

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 (361) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +263 -0
  4. data/lib/wefunder/client.rb +283 -0
  5. data/lib/wefunder/errors.rb +87 -0
  6. data/lib/wefunder/oauth.rb +135 -0
  7. data/lib/wefunder/pagination.rb +52 -0
  8. data/lib/wefunder/token_manager.rb +80 -0
  9. data/lib/wefunder/transport.rb +93 -0
  10. data/lib/wefunder/version.rb +5 -0
  11. data/lib/wefunder/webhooks.rb +238 -0
  12. data/lib/wefunder.rb +26 -0
  13. data/lib/wefunder_generated/api/activity_api.rb +234 -0
  14. data/lib/wefunder_generated/api/attribution_api.rb +182 -0
  15. data/lib/wefunder_generated/api/attribution_partners_api.rb +392 -0
  16. data/lib/wefunder_generated/api/attribution_webhooks_api.rb +504 -0
  17. data/lib/wefunder_generated/api/campaigns_api.rb +82 -0
  18. data/lib/wefunder_generated/api/companies_api.rb +101 -0
  19. data/lib/wefunder_generated/api/explore_api.rb +633 -0
  20. data/lib/wefunder_generated/api/installations_api.rb +405 -0
  21. data/lib/wefunder_generated/api/intents_api.rb +301 -0
  22. data/lib/wefunder_generated/api/investment_delta_api.rb +99 -0
  23. data/lib/wefunder_generated/api/investments_api.rb +238 -0
  24. data/lib/wefunder_generated/api/portfolio_api.rb +166 -0
  25. data/lib/wefunder_generated/api/syndicate_deals_api.rb +381 -0
  26. data/lib/wefunder_generated/api/syndicate_members_api.rb +976 -0
  27. data/lib/wefunder_generated/api/syndicate_portfolio_api.rb +178 -0
  28. data/lib/wefunder_generated/api/syndicate_statistics_api.rb +85 -0
  29. data/lib/wefunder_generated/api/syndicates_api.rb +226 -0
  30. data/lib/wefunder_generated/api/users_api.rb +329 -0
  31. data/lib/wefunder_generated/api/webhook_endpoints_api.rb +539 -0
  32. data/lib/wefunder_generated/api_client.rb +437 -0
  33. data/lib/wefunder_generated/api_error.rb +58 -0
  34. data/lib/wefunder_generated/configuration.rb +392 -0
  35. data/lib/wefunder_generated/models/anonymized_attribution.rb +298 -0
  36. data/lib/wefunder_generated/models/anonymized_attribution_attribution.rb +330 -0
  37. data/lib/wefunder_generated/models/anonymized_attribution_status.rb +260 -0
  38. data/lib/wefunder_generated/models/attributed_investment_list_envelope.rb +226 -0
  39. data/lib/wefunder_generated/models/attributed_investment_list_envelope_data_inner.rb +105 -0
  40. data/lib/wefunder_generated/models/attributed_investment_list_envelope_meta.rb +295 -0
  41. data/lib/wefunder_generated/models/attribution_campaign_access.rb +241 -0
  42. data/lib/wefunder_generated/models/attribution_campaign_summary.rb +251 -0
  43. data/lib/wefunder_generated/models/attribution_campaign_summary_list_envelope.rb +216 -0
  44. data/lib/wefunder_generated/models/attribution_me.rb +235 -0
  45. data/lib/wefunder_generated/models/attribution_me_envelope.rb +214 -0
  46. data/lib/wefunder_generated/models/attribution_partner.rb +275 -0
  47. data/lib/wefunder_generated/models/attribution_stats.rb +266 -0
  48. data/lib/wefunder_generated/models/attribution_stats_by_campaign_inner.rb +232 -0
  49. data/lib/wefunder_generated/models/attribution_stats_by_source_inner.rb +232 -0
  50. data/lib/wefunder_generated/models/attribution_stats_envelope.rb +214 -0
  51. data/lib/wefunder_generated/models/attribution_stats_period.rb +223 -0
  52. data/lib/wefunder_generated/models/attribution_stats_quality_breakdown.rb +236 -0
  53. data/lib/wefunder_generated/models/attribution_stats_totals.rb +245 -0
  54. data/lib/wefunder_generated/models/attribution_user.rb +232 -0
  55. data/lib/wefunder_generated/models/audit_event.rb +234 -0
  56. data/lib/wefunder_generated/models/audit_event_attributes.rb +391 -0
  57. data/lib/wefunder_generated/models/audit_event_list_envelope.rb +225 -0
  58. data/lib/wefunder_generated/models/bulk_invite_link_create_input.rb +243 -0
  59. data/lib/wefunder_generated/models/bulk_invite_link_envelope.rb +237 -0
  60. data/lib/wefunder_generated/models/bulk_invite_link_envelope_errors_inner.rb +233 -0
  61. data/lib/wefunder_generated/models/bulk_invite_link_envelope_meta.rb +232 -0
  62. data/lib/wefunder_generated/models/campaign.rb +232 -0
  63. data/lib/wefunder_generated/models/campaign_attributes.rb +301 -0
  64. data/lib/wefunder_generated/models/campaign_list_envelope.rb +225 -0
  65. data/lib/wefunder_generated/models/comment_proposal_envelope.rb +274 -0
  66. data/lib/wefunder_generated/models/comment_proposal_envelope_draft.rb +260 -0
  67. data/lib/wefunder_generated/models/comment_proposal_envelope_proposal.rb +286 -0
  68. data/lib/wefunder_generated/models/comment_proposal_envelope_related_questions_inner.rb +264 -0
  69. data/lib/wefunder_generated/models/company.rb +234 -0
  70. data/lib/wefunder_generated/models/company_attributes.rb +318 -0
  71. data/lib/wefunder_generated/models/company_attributes_location.rb +235 -0
  72. data/lib/wefunder_generated/models/company_disclosures.rb +268 -0
  73. data/lib/wefunder_generated/models/company_disclosures_attributes.rb +434 -0
  74. data/lib/wefunder_generated/models/company_disclosures_attributes_business.rb +255 -0
  75. data/lib/wefunder_generated/models/company_disclosures_attributes_capital_structure_inner.rb +255 -0
  76. data/lib/wefunder_generated/models/company_disclosures_attributes_current_position.rb +281 -0
  77. data/lib/wefunder_generated/models/company_disclosures_attributes_filing.rb +256 -0
  78. data/lib/wefunder_generated/models/company_disclosures_attributes_financial_statements.rb +233 -0
  79. data/lib/wefunder_generated/models/company_disclosures_attributes_outstanding_debts.rb +227 -0
  80. data/lib/wefunder_generated/models/company_disclosures_attributes_outstanding_debts_items_inner.rb +255 -0
  81. data/lib/wefunder_generated/models/company_disclosures_attributes_outstanding_notes_inner.rb +284 -0
  82. data/lib/wefunder_generated/models/company_disclosures_attributes_prior_offerings_inner.rb +266 -0
  83. data/lib/wefunder_generated/models/company_disclosures_attributes_ratios.rb +272 -0
  84. data/lib/wefunder_generated/models/company_disclosures_attributes_related_parties.rb +226 -0
  85. data/lib/wefunder_generated/models/company_disclosures_attributes_related_parties_parties_inner.rb +265 -0
  86. data/lib/wefunder_generated/models/company_disclosures_attributes_use_of_funds_inner.rb +226 -0
  87. data/lib/wefunder_generated/models/company_disclosures_attributes_voting_power_inner.rb +236 -0
  88. data/lib/wefunder_generated/models/company_disclosures_envelope.rb +214 -0
  89. data/lib/wefunder_generated/models/company_envelope.rb +214 -0
  90. data/lib/wefunder_generated/models/company_pitch.rb +268 -0
  91. data/lib/wefunder_generated/models/company_pitch_attributes.rb +311 -0
  92. data/lib/wefunder_generated/models/company_pitch_attributes_perks.rb +248 -0
  93. data/lib/wefunder_generated/models/company_pitch_attributes_story.rb +255 -0
  94. data/lib/wefunder_generated/models/company_pitch_envelope.rb +214 -0
  95. data/lib/wefunder_generated/models/company_question.rb +268 -0
  96. data/lib/wefunder_generated/models/company_question_attributes.rb +329 -0
  97. data/lib/wefunder_generated/models/company_question_attributes_answers_inner.rb +253 -0
  98. data/lib/wefunder_generated/models/company_question_list_envelope.rb +225 -0
  99. data/lib/wefunder_generated/models/company_question_list_envelope_meta.rb +301 -0
  100. data/lib/wefunder_generated/models/company_question_person.rb +247 -0
  101. data/lib/wefunder_generated/models/company_search_result.rb +268 -0
  102. data/lib/wefunder_generated/models/company_search_result_attributes.rb +266 -0
  103. data/lib/wefunder_generated/models/company_search_result_list_envelope.rb +225 -0
  104. data/lib/wefunder_generated/models/company_search_result_list_envelope_meta.rb +223 -0
  105. data/lib/wefunder_generated/models/company_totals.rb +249 -0
  106. data/lib/wefunder_generated/models/company_update.rb +269 -0
  107. data/lib/wefunder_generated/models/company_update_attributes.rb +350 -0
  108. data/lib/wefunder_generated/models/company_update_attributes_author.rb +215 -0
  109. data/lib/wefunder_generated/models/company_update_envelope.rb +214 -0
  110. data/lib/wefunder_generated/models/company_update_list_envelope.rb +225 -0
  111. data/lib/wefunder_generated/models/company_update_list_envelope_meta.rb +243 -0
  112. data/lib/wefunder_generated/models/connected_app.rb +233 -0
  113. data/lib/wefunder_generated/models/connected_app_attributes.rb +258 -0
  114. data/lib/wefunder_generated/models/connected_app_list_envelope.rb +216 -0
  115. data/lib/wefunder_generated/models/connected_app_revocation.rb +223 -0
  116. data/lib/wefunder_generated/models/connected_app_revocation_envelope.rb +214 -0
  117. data/lib/wefunder_generated/models/convertible_note_security.rb +266 -0
  118. data/lib/wefunder_generated/models/convertible_note_security_terms.rb +266 -0
  119. data/lib/wefunder_generated/models/create_installation_request.rb +294 -0
  120. data/lib/wefunder_generated/models/create_installation_token_request.rb +216 -0
  121. data/lib/wefunder_generated/models/create_intent_request.rb +320 -0
  122. data/lib/wefunder_generated/models/create_partner_invite_request.rb +287 -0
  123. data/lib/wefunder_generated/models/create_webhook_endpoint_request.rb +291 -0
  124. data/lib/wefunder_generated/models/create_webhook_subscription_request.rb +263 -0
  125. data/lib/wefunder_generated/models/current_raise.rb +288 -0
  126. data/lib/wefunder_generated/models/deal_investor.rb +234 -0
  127. data/lib/wefunder_generated/models/deal_investor_attributes.rb +321 -0
  128. data/lib/wefunder_generated/models/deal_investor_list_envelope.rb +225 -0
  129. data/lib/wefunder_generated/models/deal_investor_list_envelope_meta.rb +252 -0
  130. data/lib/wefunder_generated/models/debt_security.rb +266 -0
  131. data/lib/wefunder_generated/models/debt_security_terms.rb +235 -0
  132. data/lib/wefunder_generated/models/delete_webhook_endpoint200_response.rb +214 -0
  133. data/lib/wefunder_generated/models/delete_webhook_endpoint200_response_data.rb +232 -0
  134. data/lib/wefunder_generated/models/disclosure_document.rb +224 -0
  135. data/lib/wefunder_generated/models/disclosure_person.rb +254 -0
  136. data/lib/wefunder_generated/models/eligible_target_list_envelope.rb +225 -0
  137. data/lib/wefunder_generated/models/eligible_target_list_envelope_data_inner.rb +295 -0
  138. data/lib/wefunder_generated/models/equity_security.rb +266 -0
  139. data/lib/wefunder_generated/models/equity_security_terms.rb +271 -0
  140. data/lib/wefunder_generated/models/error.rb +214 -0
  141. data/lib/wefunder_generated/models/error_error.rb +254 -0
  142. data/lib/wefunder_generated/models/exemption.rb +271 -0
  143. data/lib/wefunder_generated/models/fiscal_year_financials.rb +296 -0
  144. data/lib/wefunder_generated/models/follow_state_envelope.rb +214 -0
  145. data/lib/wefunder_generated/models/follow_state_envelope_data.rb +268 -0
  146. data/lib/wefunder_generated/models/follow_state_envelope_data_attributes.rb +225 -0
  147. data/lib/wefunder_generated/models/followed_company.rb +268 -0
  148. data/lib/wefunder_generated/models/followed_company_attributes.rb +275 -0
  149. data/lib/wefunder_generated/models/followed_company_list_envelope.rb +225 -0
  150. data/lib/wefunder_generated/models/full_attribution.rb +272 -0
  151. data/lib/wefunder_generated/models/full_attribution_attribution.rb +326 -0
  152. data/lib/wefunder_generated/models/full_attribution_investor.rb +242 -0
  153. data/lib/wefunder_generated/models/full_attribution_status.rb +257 -0
  154. data/lib/wefunder_generated/models/fund_security.rb +267 -0
  155. data/lib/wefunder_generated/models/installation.rb +233 -0
  156. data/lib/wefunder_generated/models/installation_attributes.rb +307 -0
  157. data/lib/wefunder_generated/models/installation_attributes_target.rb +267 -0
  158. data/lib/wefunder_generated/models/installation_envelope.rb +223 -0
  159. data/lib/wefunder_generated/models/installation_list_envelope.rb +225 -0
  160. data/lib/wefunder_generated/models/installation_list_envelope_meta.rb +214 -0
  161. data/lib/wefunder_generated/models/installation_token_envelope.rb +232 -0
  162. data/lib/wefunder_generated/models/installation_token_envelope_token.rb +252 -0
  163. data/lib/wefunder_generated/models/intent.rb +234 -0
  164. data/lib/wefunder_generated/models/intent_attributes.rb +375 -0
  165. data/lib/wefunder_generated/models/intent_attributes_resource_id.rb +106 -0
  166. data/lib/wefunder_generated/models/intent_envelope.rb +214 -0
  167. data/lib/wefunder_generated/models/intent_list_envelope.rb +225 -0
  168. data/lib/wefunder_generated/models/intent_preview_envelope.rb +214 -0
  169. data/lib/wefunder_generated/models/intent_preview_envelope_data.rb +257 -0
  170. data/lib/wefunder_generated/models/intent_preview_envelope_data_attributes.rb +262 -0
  171. data/lib/wefunder_generated/models/intent_review.rb +234 -0
  172. data/lib/wefunder_generated/models/investment_change_list_envelope.rb +225 -0
  173. data/lib/wefunder_generated/models/investment_change_list_envelope_meta.rb +285 -0
  174. data/lib/wefunder_generated/models/investment_delta_record.rb +504 -0
  175. data/lib/wefunder_generated/models/investment_delta_record_amounts.rb +251 -0
  176. data/lib/wefunder_generated/models/investment_delta_record_blockers_inner.rb +224 -0
  177. data/lib/wefunder_generated/models/investment_delta_record_contracts_inner.rb +234 -0
  178. data/lib/wefunder_generated/models/investment_delta_record_investor.rb +286 -0
  179. data/lib/wefunder_generated/models/investment_envelope.rb +223 -0
  180. data/lib/wefunder_generated/models/investment_envelope_meta.rb +270 -0
  181. data/lib/wefunder_generated/models/investment_list_envelope.rb +225 -0
  182. data/lib/wefunder_generated/models/investment_list_envelope_meta.rb +288 -0
  183. data/lib/wefunder_generated/models/investment_session.rb +232 -0
  184. data/lib/wefunder_generated/models/investment_session_attributes.rb +384 -0
  185. data/lib/wefunder_generated/models/investment_session_attributes_events_inner.rb +232 -0
  186. data/lib/wefunder_generated/models/investment_session_create_input.rb +331 -0
  187. data/lib/wefunder_generated/models/investment_session_envelope.rb +223 -0
  188. data/lib/wefunder_generated/models/investment_session_list_envelope.rb +225 -0
  189. data/lib/wefunder_generated/models/investment_session_list_envelope_meta.rb +242 -0
  190. data/lib/wefunder_generated/models/investor.rb +233 -0
  191. data/lib/wefunder_generated/models/investor_attributes.rb +262 -0
  192. data/lib/wefunder_generated/models/investor_envelope.rb +214 -0
  193. data/lib/wefunder_generated/models/investor_list_envelope.rb +225 -0
  194. data/lib/wefunder_generated/models/invite_link.rb +233 -0
  195. data/lib/wefunder_generated/models/invite_link_attributes.rb +375 -0
  196. data/lib/wefunder_generated/models/invite_link_attributes_events_inner.rb +243 -0
  197. data/lib/wefunder_generated/models/invite_link_create_input.rb +295 -0
  198. data/lib/wefunder_generated/models/invite_link_envelope.rb +223 -0
  199. data/lib/wefunder_generated/models/invite_link_list_envelope.rb +225 -0
  200. data/lib/wefunder_generated/models/invite_link_update_input.rb +227 -0
  201. data/lib/wefunder_generated/models/invite_syndicate_member_request.rb +277 -0
  202. data/lib/wefunder_generated/models/marketing_partner.rb +295 -0
  203. data/lib/wefunder_generated/models/marketing_partner_envelope.rb +214 -0
  204. data/lib/wefunder_generated/models/member_investment.rb +234 -0
  205. data/lib/wefunder_generated/models/member_investment_attributes.rb +301 -0
  206. data/lib/wefunder_generated/models/member_investment_list_envelope.rb +225 -0
  207. data/lib/wefunder_generated/models/member_investment_list_envelope_meta.rb +225 -0
  208. data/lib/wefunder_generated/models/my_company.rb +268 -0
  209. data/lib/wefunder_generated/models/my_company_attributes.rb +257 -0
  210. data/lib/wefunder_generated/models/my_company_list_envelope.rb +225 -0
  211. data/lib/wefunder_generated/models/offering.rb +267 -0
  212. data/lib/wefunder_generated/models/offering_attributes.rb +469 -0
  213. data/lib/wefunder_generated/models/offering_attributes_intended_security.rb +258 -0
  214. data/lib/wefunder_generated/models/offering_attributes_security.rb +105 -0
  215. data/lib/wefunder_generated/models/offering_envelope.rb +214 -0
  216. data/lib/wefunder_generated/models/offering_list_envelope.rb +225 -0
  217. data/lib/wefunder_generated/models/offering_list_envelope_meta.rb +265 -0
  218. data/lib/wefunder_generated/models/offering_stats_envelope.rb +223 -0
  219. data/lib/wefunder_generated/models/offering_stats_envelope_data.rb +253 -0
  220. data/lib/wefunder_generated/models/offering_stats_envelope_data_by_status_value.rb +232 -0
  221. data/lib/wefunder_generated/models/offering_stats_envelope_data_total.rb +233 -0
  222. data/lib/wefunder_generated/models/offering_stats_envelope_meta.rb +258 -0
  223. data/lib/wefunder_generated/models/offering_warnings_inner.rb +257 -0
  224. data/lib/wefunder_generated/models/other_security.rb +267 -0
  225. data/lib/wefunder_generated/models/other_security_terms.rb +235 -0
  226. data/lib/wefunder_generated/models/pagination_meta.rb +234 -0
  227. data/lib/wefunder_generated/models/pagination_meta_next_cursor.rb +106 -0
  228. data/lib/wefunder_generated/models/partner_investment.rb +232 -0
  229. data/lib/wefunder_generated/models/partner_investment_attributes.rb +305 -0
  230. data/lib/wefunder_generated/models/partner_investment_attributes_accreditation.rb +289 -0
  231. data/lib/wefunder_generated/models/partner_investment_envelope.rb +214 -0
  232. data/lib/wefunder_generated/models/partner_investment_list_envelope.rb +225 -0
  233. data/lib/wefunder_generated/models/partner_investor.rb +233 -0
  234. data/lib/wefunder_generated/models/partner_investor_attributes.rb +264 -0
  235. data/lib/wefunder_generated/models/partner_investor_list_envelope.rb +225 -0
  236. data/lib/wefunder_generated/models/partner_invite.rb +360 -0
  237. data/lib/wefunder_generated/models/partner_invite_envelope.rb +214 -0
  238. data/lib/wefunder_generated/models/partner_invite_list_envelope.rb +216 -0
  239. data/lib/wefunder_generated/models/partner_webhook_event.rb +257 -0
  240. data/lib/wefunder_generated/models/partner_webhook_event_envelope.rb +214 -0
  241. data/lib/wefunder_generated/models/partner_webhook_subscription.rb +232 -0
  242. data/lib/wefunder_generated/models/partner_webhook_subscription_attributes.rb +243 -0
  243. data/lib/wefunder_generated/models/partner_webhook_subscription_create_input.rb +239 -0
  244. data/lib/wefunder_generated/models/partner_webhook_subscription_envelope.rb +214 -0
  245. data/lib/wefunder_generated/models/partner_webhook_subscription_list_envelope.rb +216 -0
  246. data/lib/wefunder_generated/models/partner_webhook_subscription_with_secret.rb +239 -0
  247. data/lib/wefunder_generated/models/partner_webhook_subscription_with_secret_all_of_attributes.rb +215 -0
  248. data/lib/wefunder_generated/models/partner_webhook_subscription_with_secret_envelope.rb +214 -0
  249. data/lib/wefunder_generated/models/past_round.rb +309 -0
  250. data/lib/wefunder_generated/models/perk_tier.rb +226 -0
  251. data/lib/wefunder_generated/models/pitch_block.rb +375 -0
  252. data/lib/wefunder_generated/models/pitch_block_links_inner.rb +226 -0
  253. data/lib/wefunder_generated/models/portfolio_company_ref.rb +235 -0
  254. data/lib/wefunder_generated/models/portfolio_holding.rb +258 -0
  255. data/lib/wefunder_generated/models/portfolio_position.rb +268 -0
  256. data/lib/wefunder_generated/models/portfolio_position_attributes.rb +456 -0
  257. data/lib/wefunder_generated/models/portfolio_position_list_envelope.rb +225 -0
  258. data/lib/wefunder_generated/models/portfolio_position_list_envelope_meta.rb +234 -0
  259. data/lib/wefunder_generated/models/portfolio_security.rb +323 -0
  260. data/lib/wefunder_generated/models/portfolio_security_owner.rb +258 -0
  261. data/lib/wefunder_generated/models/portfolio_security_terms.rb +257 -0
  262. data/lib/wefunder_generated/models/portfolio_summary_envelope.rb +214 -0
  263. data/lib/wefunder_generated/models/portfolio_summary_envelope_data.rb +223 -0
  264. data/lib/wefunder_generated/models/portfolio_summary_envelope_data_attributes.rb +292 -0
  265. data/lib/wefunder_generated/models/portfolio_summary_envelope_data_attributes_position_counts.rb +242 -0
  266. data/lib/wefunder_generated/models/preview_intent_request.rb +262 -0
  267. data/lib/wefunder_generated/models/register_as_partner_request.rb +232 -0
  268. data/lib/wefunder_generated/models/reorder_syndicate_members_request.rb +224 -0
  269. data/lib/wefunder_generated/models/revenue_share_security.rb +266 -0
  270. data/lib/wefunder_generated/models/revenue_share_security_terms.rb +300 -0
  271. data/lib/wefunder_generated/models/safe_security.rb +266 -0
  272. data/lib/wefunder_generated/models/safe_security_terms.rb +257 -0
  273. data/lib/wefunder_generated/models/security.rb +66 -0
  274. data/lib/wefunder_generated/models/security_summary.rb +272 -0
  275. data/lib/wefunder_generated/models/spv.rb +232 -0
  276. data/lib/wefunder_generated/models/spv_attributes.rb +353 -0
  277. data/lib/wefunder_generated/models/spv_attributes_target_company.rb +223 -0
  278. data/lib/wefunder_generated/models/spv_cancel_intent_envelope.rb +224 -0
  279. data/lib/wefunder_generated/models/spv_cancel_intent_envelope_meta.rb +214 -0
  280. data/lib/wefunder_generated/models/spv_close_intent_envelope.rb +224 -0
  281. data/lib/wefunder_generated/models/spv_close_intent_envelope_meta.rb +214 -0
  282. data/lib/wefunder_generated/models/spv_create_input.rb +274 -0
  283. data/lib/wefunder_generated/models/spv_envelope.rb +214 -0
  284. data/lib/wefunder_generated/models/spv_list_envelope.rb +225 -0
  285. data/lib/wefunder_generated/models/spv_metrics.rb +261 -0
  286. data/lib/wefunder_generated/models/spv_settings.rb +275 -0
  287. data/lib/wefunder_generated/models/spv_status.rb +232 -0
  288. data/lib/wefunder_generated/models/spv_status_attributes.rb +289 -0
  289. data/lib/wefunder_generated/models/spv_status_envelope.rb +223 -0
  290. data/lib/wefunder_generated/models/spv_terms.rb +348 -0
  291. data/lib/wefunder_generated/models/spv_with_meta_envelope.rb +223 -0
  292. data/lib/wefunder_generated/models/spv_with_meta_envelope_meta.rb +214 -0
  293. data/lib/wefunder_generated/models/syndicate.rb +234 -0
  294. data/lib/wefunder_generated/models/syndicate_attributes.rb +412 -0
  295. data/lib/wefunder_generated/models/syndicate_attributes_annual_subscription.rb +236 -0
  296. data/lib/wefunder_generated/models/syndicate_attributes_traditional_fund.rb +225 -0
  297. data/lib/wefunder_generated/models/syndicate_deal.rb +234 -0
  298. data/lib/wefunder_generated/models/syndicate_deal_attributes.rb +435 -0
  299. data/lib/wefunder_generated/models/syndicate_deal_attributes_exemption.rb +105 -0
  300. data/lib/wefunder_generated/models/syndicate_deal_attributes_security.rb +105 -0
  301. data/lib/wefunder_generated/models/syndicate_deal_close_intent_envelope.rb +224 -0
  302. data/lib/wefunder_generated/models/syndicate_deal_close_intent_envelope_meta.rb +214 -0
  303. data/lib/wefunder_generated/models/syndicate_deal_detail_envelope.rb +214 -0
  304. data/lib/wefunder_generated/models/syndicate_deal_envelope.rb +214 -0
  305. data/lib/wefunder_generated/models/syndicate_deal_finalize_intent_envelope.rb +224 -0
  306. data/lib/wefunder_generated/models/syndicate_deal_finalize_intent_envelope_meta.rb +214 -0
  307. data/lib/wefunder_generated/models/syndicate_deal_list_envelope.rb +225 -0
  308. data/lib/wefunder_generated/models/syndicate_detail_envelope.rb +214 -0
  309. data/lib/wefunder_generated/models/syndicate_list_envelope.rb +225 -0
  310. data/lib/wefunder_generated/models/syndicate_member.rb +234 -0
  311. data/lib/wefunder_generated/models/syndicate_member_attributes.rb +650 -0
  312. data/lib/wefunder_generated/models/syndicate_member_envelope.rb +214 -0
  313. data/lib/wefunder_generated/models/syndicate_member_list_envelope.rb +225 -0
  314. data/lib/wefunder_generated/models/syndicate_portfolio_summary_envelope.rb +214 -0
  315. data/lib/wefunder_generated/models/syndicate_portfolio_summary_envelope_data.rb +223 -0
  316. data/lib/wefunder_generated/models/syndicate_portfolio_summary_envelope_data_attributes.rb +302 -0
  317. data/lib/wefunder_generated/models/syndicate_portfolio_summary_envelope_data_attributes_deal_counts.rb +242 -0
  318. data/lib/wefunder_generated/models/syndicate_statistics.rb +224 -0
  319. data/lib/wefunder_generated/models/syndicate_statistics_attributes.rb +277 -0
  320. data/lib/wefunder_generated/models/syndicate_statistics_envelope.rb +214 -0
  321. data/lib/wefunder_generated/models/tag_ref.rb +238 -0
  322. data/lib/wefunder_generated/models/target_company_input.rb +258 -0
  323. data/lib/wefunder_generated/models/test_webhook_endpoint_request.rb +249 -0
  324. data/lib/wefunder_generated/models/update_syndicate_member_request.rb +221 -0
  325. data/lib/wefunder_generated/models/update_syndicate_member_request_member.rb +251 -0
  326. data/lib/wefunder_generated/models/update_syndicate_request.rb +221 -0
  327. data/lib/wefunder_generated/models/update_syndicate_request_syndicate.rb +232 -0
  328. data/lib/wefunder_generated/models/update_webhook_endpoint_request.rb +252 -0
  329. data/lib/wefunder_generated/models/user.rb +233 -0
  330. data/lib/wefunder_generated/models/user_attributes.rb +300 -0
  331. data/lib/wefunder_generated/models/user_envelope.rb +214 -0
  332. data/lib/wefunder_generated/models/validation_error.rb +215 -0
  333. data/lib/wefunder_generated/models/validation_error_error.rb +234 -0
  334. data/lib/wefunder_generated/models/validation_error_error_details_inner.rb +232 -0
  335. data/lib/wefunder_generated/models/webhook_delivery.rb +318 -0
  336. data/lib/wefunder_generated/models/webhook_endpoint.rb +232 -0
  337. data/lib/wefunder_generated/models/webhook_endpoint_attributes.rb +327 -0
  338. data/lib/wefunder_generated/models/webhook_endpoint_envelope.rb +223 -0
  339. data/lib/wefunder_generated/models/webhook_endpoint_envelope_meta.rb +214 -0
  340. data/lib/wefunder_generated/models/webhook_endpoint_list_envelope.rb +225 -0
  341. data/lib/wefunder_generated/models/webhook_endpoint_list_envelope_meta.rb +233 -0
  342. data/lib/wefunder_generated/models/webhook_endpoint_test_result_envelope.rb +223 -0
  343. data/lib/wefunder_generated/models/webhook_endpoint_test_result_envelope_data.rb +306 -0
  344. data/lib/wefunder_generated/models/webhook_subscription.rb +304 -0
  345. data/lib/wefunder_generated/models/webhook_subscription_envelope.rb +214 -0
  346. data/lib/wefunder_generated/models/webhook_subscription_list_envelope.rb +225 -0
  347. data/lib/wefunder_generated/models/webhook_subscription_list_envelope_meta.rb +225 -0
  348. data/lib/wefunder_generated/models/webhook_subscription_with_recent_deliveries_envelope.rb +214 -0
  349. data/lib/wefunder_generated/models/webhook_subscription_with_recent_deliveries_envelope_data.rb +322 -0
  350. data/lib/wefunder_generated/models/webhook_subscription_with_secret2_envelope.rb +214 -0
  351. data/lib/wefunder_generated/models/webhook_subscription_with_secret2_envelope_data.rb +320 -0
  352. data/lib/wefunder_generated/models/webhook_subscription_with_secret_envelope.rb +214 -0
  353. data/lib/wefunder_generated/models/webhook_subscription_with_secret_envelope_data.rb +320 -0
  354. data/lib/wefunder_generated/models/webhook_test_result.rb +233 -0
  355. data/lib/wefunder_generated/models/webhook_test_result_envelope.rb +214 -0
  356. data/lib/wefunder_generated/models/wefunder_round.rb +330 -0
  357. data/lib/wefunder_generated/models/wefunder_round_exemption.rb +104 -0
  358. data/lib/wefunder_generated/models/wefunder_round_security.rb +104 -0
  359. data/lib/wefunder_generated/version.rb +15 -0
  360. data/lib/wefunder_generated.rb +382 -0
  361. metadata +456 -0
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "faraday"
5
+ require "json"
6
+ require "securerandom"
7
+ require "uri"
8
+
9
+ module Wefunder
10
+ # OAuth 2.0 helpers: authorization_code + PKCE (user flows) and client_credentials
11
+ # (server-to-server, read:public). Refresh tokens ROTATE — every refresh returns a NEW
12
+ # refresh token that must be persisted (the client / TokenManager does this for you).
13
+ #
14
+ # HOST SPLIT: /token (+ refresh) is on the API gateway, which routes test/live by the
15
+ # credential's mode, so ONE token host serves both modes. /authorize is the browser consent
16
+ # redirect: wefunder.com for live, oauth.wefunder-sandbox.com for pk_test_ client ids.
17
+ # Overrides: specific (authorize_base_url / token_base_url) > oauth_base_url alias > default.
18
+ module OAuth
19
+ DEFAULT_AUTHORIZE_BASE_URL = "https://wefunder.com/oauth"
20
+ SANDBOX_AUTHORIZE_BASE_URL = "https://oauth.wefunder-sandbox.com/oauth"
21
+ DEFAULT_TOKEN_BASE_URL = "https://api.wefunder.com/oauth"
22
+ SANDBOX_CLIENT_ID_PREFIX = "pk_test_"
23
+
24
+ # The token endpoint answered non-2xx.
25
+ class TokenError < StandardError
26
+ attr_reader :status, :body
27
+
28
+ def initialize(status, body)
29
+ super("OAuth token request failed (#{status}): #{body}")
30
+ @status = status
31
+ @body = body
32
+ end
33
+ end
34
+
35
+ # A token set. +expires_at+ is unix seconds (Float) or nil.
36
+ TokenSet = Struct.new(:access_token, :refresh_token, :expires_at, :scope, :token_type, keyword_init: true) do
37
+ def self.from_token_response(raw, now)
38
+ expires_in = raw["expires_in"] || raw[:expires_in]
39
+ new(
40
+ access_token: (raw["access_token"] || raw[:access_token]).to_s,
41
+ refresh_token: raw["refresh_token"] || raw[:refresh_token],
42
+ expires_at: expires_in ? now + expires_in.to_f : nil,
43
+ scope: raw["scope"] || raw[:scope],
44
+ token_type: raw["token_type"] || raw[:token_type]
45
+ )
46
+ end
47
+ end
48
+
49
+ Pkce = Struct.new(:code_verifier, :code_challenge, :code_challenge_method, keyword_init: true)
50
+
51
+ module_function
52
+
53
+ def resolve_token_base(token_base_url: nil, oauth_base_url: nil, **)
54
+ token_base_url || oauth_base_url || DEFAULT_TOKEN_BASE_URL
55
+ end
56
+
57
+ # Sandbox consent host for pk_test_ client ids, live otherwise.
58
+ def default_authorize_base(client_id)
59
+ client_id.start_with?(SANDBOX_CLIENT_ID_PREFIX) ? SANDBOX_AUTHORIZE_BASE_URL : DEFAULT_AUTHORIZE_BASE_URL
60
+ end
61
+
62
+ # base64url without padding (no +base64+ gem dependency — it leaves the default gems in Ruby 3.4).
63
+ def base64url(bytes)
64
+ [bytes].pack("m0").tr("+/", "-_").delete("=")
65
+ end
66
+
67
+ # RFC 7636 S256: base64url(sha256(ascii(verifier))) without padding.
68
+ def pkce_challenge(code_verifier)
69
+ base64url(Digest::SHA256.digest(code_verifier))
70
+ end
71
+
72
+ # A fresh PKCE verifier/challenge pair (RFC 7636, S256).
73
+ def generate_pkce
74
+ verifier = base64url(SecureRandom.random_bytes(32))
75
+ Pkce.new(code_verifier: verifier, code_challenge: pkce_challenge(verifier), code_challenge_method: "S256")
76
+ end
77
+
78
+ # The URL to redirect a user to for the authorization_code + PKCE flow. +state+ is an
79
+ # opaque CSRF token you generate and verify on the callback.
80
+ def create_authorization_url(client_id:, redirect_uri:, scopes:, state:, pkce: nil, code_challenge: nil,
81
+ authorize_base_url: nil, oauth_base_url: nil, token_base_url: nil)
82
+ _ = token_base_url # accepted for symmetry with the token helpers; never affects /authorize
83
+ challenge = code_challenge || pkce&.code_challenge
84
+ raise ArgumentError, "create_authorization_url: provide pkce: or code_challenge:" unless challenge
85
+
86
+ base = authorize_base_url || oauth_base_url || default_authorize_base(client_id)
87
+ query = URI.encode_www_form(
88
+ response_type: "code", client_id: client_id, redirect_uri: redirect_uri, scope: Array(scopes).join(" "),
89
+ state: state, code_challenge: challenge, code_challenge_method: "S256"
90
+ )
91
+ "#{base}/authorize?#{query}"
92
+ end
93
+
94
+ # Exchange an authorization code (+ PKCE verifier) for a token set. Public (PKCE) clients
95
+ # omit +client_secret+; confidential clients pass it.
96
+ def exchange_code(client_id:, code:, redirect_uri:, code_verifier:, client_secret: nil, faraday: nil, now: nil, **hosts)
97
+ params = { grant_type: "authorization_code", client_id: client_id, code: code, redirect_uri: redirect_uri,
98
+ code_verifier: code_verifier }
99
+ params[:client_secret] = client_secret if client_secret
100
+ post_token(resolve_token_base(**hosts), params, faraday, now)
101
+ end
102
+
103
+ # Mint an application token (server-to-server). cc tokens carry no refresh token.
104
+ def client_credentials_grant(client_id:, client_secret:, scopes: nil, faraday: nil, now: nil, **hosts)
105
+ params = { grant_type: "client_credentials", client_id: client_id, client_secret: client_secret }
106
+ params[:scope] = Array(scopes).join(" ") if scopes && !Array(scopes).empty?
107
+ post_token(resolve_token_base(**hosts), params, faraday, now)
108
+ end
109
+
110
+ # Refresh an access token. CRITICAL: the result carries a NEW refresh token (rotation) —
111
+ # persist it. Reusing the old one after rotation is a permanent 401.
112
+ def refresh_token(client_id:, refresh_token:, client_secret: nil, faraday: nil, now: nil, **hosts)
113
+ params = { grant_type: "refresh_token", client_id: client_id, refresh_token: refresh_token }
114
+ params[:client_secret] = client_secret if client_secret
115
+ post_token(resolve_token_base(**hosts), params, faraday, now)
116
+ end
117
+
118
+ # +faraday+ is an optional callable that configures the connection (tests inject an
119
+ # adapter: +->(conn) { conn.adapter :test, stubs }+).
120
+ def post_token(base, params, faraday, now)
121
+ conn = Faraday.new do |c|
122
+ faraday&.call(c)
123
+ c.adapter Faraday.default_adapter unless faraday
124
+ end
125
+ response = conn.post("#{base}/token", URI.encode_www_form(params),
126
+ "Content-Type" => "application/x-www-form-urlencoded")
127
+ raise TokenError.new(response.status, response.body.to_s) unless (200..299).cover?(response.status)
128
+
129
+ TokenSet.from_token_response(JSON.parse(response.body.to_s), (now || -> { Time.now.to_f }).call)
130
+ end
131
+ end
132
+
133
+ TokenSet = OAuth::TokenSet
134
+ Pkce = OAuth::Pkce
135
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wefunder
4
+ # Lazy auto-pagination over an OPAQUE cursor. Different endpoints back the cursor with an
5
+ # int id, an ISO timestamp, or an offset, so it is never inspected — +meta.next_cursor+ is
6
+ # fed back verbatim. Stop when +meta.has_more+ is false, +next_cursor+ is nil/absent, or a
7
+ # cursor repeats (loop guard). The Investment Delta API always sends +next_cursor+, even on
8
+ # the last page, so +has_more+ terminates there.
9
+ module Pagination
10
+ module_function
11
+
12
+ # +(items, has_more, next_cursor)+ from a Hash page (string or symbol keys) or a generated
13
+ # +*ListEnvelope+ object.
14
+ def page_parts(page)
15
+ data = read(page, :data) || []
16
+ meta = read(page, :meta)
17
+ has_more = read(meta, :has_more)
18
+ [Array(data), [true, false].include?(has_more) ? has_more : nil, read(meta, :next_cursor)]
19
+ end
20
+
21
+ def read(obj, key)
22
+ return nil if obj.nil?
23
+ return (obj.key?(key.to_s) ? obj[key.to_s] : obj[key]) if obj.is_a?(Hash)
24
+
25
+ obj.respond_to?(key) ? obj.public_send(key) : nil
26
+ end
27
+
28
+ # Yields every item across all pages, lazily. +fetch_page.call(nil)+ is the first page.
29
+ def paginate(fetch_page = nil, &block)
30
+ fetch = fetch_page || block
31
+ Enumerator.new do |y|
32
+ cursor = nil
33
+ seen = {}
34
+ loop do
35
+ items, has_more, next_cursor = page_parts(fetch.call(cursor))
36
+ items.each { |item| y << item }
37
+ break if has_more == false || next_cursor.nil? || seen.key?(next_cursor)
38
+
39
+ seen[next_cursor] = true
40
+ cursor = next_cursor
41
+ end
42
+ end
43
+ end
44
+
45
+ def collect(fetch_page = nil, &)
46
+ paginate(fetch_page, &).to_a
47
+ end
48
+ end
49
+
50
+ def self.paginate(fetch_page = nil, &) = Pagination.paginate(fetch_page, &)
51
+ def self.collect(fetch_page = nil, &) = Pagination.collect(fetch_page, &)
52
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wefunder
4
+ # Holds the live token state and owns recovery + persistence. Two strategies, picked
5
+ # automatically: refresh_token ROTATION (a refresh token + client_id present) or
6
+ # client_credentials RE-MINT (built from cc grant inputs; cc tokens carry no refresh token).
7
+ # Concurrent callers coalesce: the transport passes the token it just used, and if the
8
+ # manager already holds a different one, another caller recovered in the meantime and the
9
+ # current set is returned without a round-trip. The rotated set is persisted (store +
10
+ # callback) BEFORE the retried request can use it. Thread-safe (Mutex).
11
+ class TokenManager
12
+ DEFAULT_EXPIRY_LEEWAY_SECONDS = 30.0
13
+
14
+ attr_reader :current
15
+
16
+ def initialize(tokens, client_id: nil, client_secret: nil, re_mint: nil, on_token_refresh: nil, store: nil,
17
+ faraday: nil, now: nil, expiry_leeway_seconds: DEFAULT_EXPIRY_LEEWAY_SECONDS,
18
+ token_base_url: nil, oauth_base_url: nil)
19
+ @current = tokens
20
+ @client_id = client_id
21
+ @client_secret = client_secret
22
+ @re_mint = re_mint
23
+ @on_token_refresh = on_token_refresh
24
+ @store = store
25
+ @faraday = faraday
26
+ @now = now || -> { Time.now.to_f }
27
+ @leeway = expiry_leeway_seconds
28
+ @token_base_url = OAuth.resolve_token_base(token_base_url: token_base_url, oauth_base_url: oauth_base_url)
29
+ @lock = Mutex.new
30
+ end
31
+
32
+ # True if an expired token can be recovered (rotate a refresh token or re-mint).
33
+ def can_refresh?
34
+ can_rotate? || !@re_mint.nil?
35
+ end
36
+
37
+ # A valid access token, refreshing proactively if expired / within the leeway.
38
+ def access_token
39
+ refresh(stale_token: @current.access_token) if near_expiry? && can_refresh?
40
+ @current.access_token
41
+ end
42
+
43
+ # Recover the token (after a 401 or proactively). If +stale_token+ is no longer the current
44
+ # token, someone else already recovered and the current set is returned.
45
+ def refresh(stale_token: nil)
46
+ @lock.synchronize do
47
+ return @current if stale_token && @current.access_token != stale_token
48
+
49
+ previous_refresh = @current.refresh_token
50
+ nxt =
51
+ if can_rotate?
52
+ set = OAuth.refresh_token(client_id: @client_id, client_secret: @client_secret, refresh_token: previous_refresh,
53
+ token_base_url: @token_base_url, faraday: @faraday, now: @now)
54
+ # Some servers omit a fresh refresh_token on rotation-disabled flows; keep the old one.
55
+ set.refresh_token ||= previous_refresh
56
+ set
57
+ elsif @re_mint
58
+ @re_mint.call
59
+ else
60
+ raise AuthError, "Access token expired and no refresh token / re-mint capability is configured."
61
+ end
62
+ @current = nxt
63
+ @store&.save(nxt)
64
+ @on_token_refresh&.call(nxt)
65
+ nxt
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def can_rotate?
72
+ !@current.refresh_token.nil? && !@current.refresh_token.empty? && !@client_id.nil?
73
+ end
74
+
75
+ def near_expiry?
76
+ exp = @current.expires_at
77
+ !exp.nil? && @now.call >= exp - @leeway
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+
5
+ module Wefunder
6
+ # The retry/refresh Faraday middleware. Owns the Authorization header (reads the managed
7
+ # token per attempt) and applies the contract-driven policy:
8
+ # * 401 → recover the token (rotation or re-mint) and retry ONCE, any method.
9
+ # * 429 → honour X-RateLimit-Reset (NOT Retry-After) and retry, bounded.
10
+ # * 5xx / network error → retry IDEMPOTENT methods only (GET/HEAD/OPTIONS) with jittered
11
+ # exponential backoff. Writes are never auto-retried.
12
+ # Every attempt re-sends the same body (Faraday bodies are strings), which is what made
13
+ # the equivalent Node fix necessary.
14
+ class Transport < Faraday::Middleware
15
+ IDEMPOTENT_METHODS = %w[GET HEAD OPTIONS].freeze
16
+ RATE_LIMIT_RESET_HEADER = "x-ratelimit-reset"
17
+ NETWORK_ERRORS = [Faraday::ConnectionFailed, Faraday::TimeoutError, Faraday::SSLError].freeze
18
+
19
+ RetryOptions = Struct.new(:max_retries, :base_delay_ms, :max_delay_ms, keyword_init: true) do
20
+ def self.defaults = new(max_retries: 2, base_delay_ms: 250.0, max_delay_ms: 60_000.0)
21
+ end
22
+
23
+ # How long to wait for a 429. X-RateLimit-Reset may be absolute epoch SECONDS (values
24
+ # above ~1e9) or a delta in seconds. Absent/non-numeric → 1s; clamped to [0, max].
25
+ def self.rate_limit_wait_ms(reset_header, now_ms, max_delay_ms)
26
+ fallback = [1000.0, max_delay_ms].min
27
+ return fallback if reset_header.nil?
28
+
29
+ n = Float(reset_header, exception: false)
30
+ return fallback if n.nil?
31
+
32
+ wait = n > 1_000_000_000 ? (n * 1000) - now_ms : n * 1000
33
+ wait.clamp(0.0, max_delay_ms)
34
+ end
35
+
36
+ # min(base·2ⁿ + base·2ⁿ·0.5·random, max) for retry attempt n (1-based).
37
+ def self.backoff_ms(attempt, opts, random_value)
38
+ exp = opts.base_delay_ms * (2**attempt)
39
+ [exp + (exp * 0.5 * random_value), opts.max_delay_ms].min
40
+ end
41
+
42
+ def initialize(app, token_manager: nil, retry_options: nil, sleep: nil, now_ms: nil, random: nil)
43
+ super(app)
44
+ @token_manager = token_manager
45
+ @retry = retry_options || RetryOptions.defaults
46
+ @sleep = sleep || ->(ms) { Kernel.sleep(ms / 1000.0) }
47
+ @now_ms = now_ms || -> { Time.now.to_f * 1000 }
48
+ @random = random || -> { Kernel.rand }
49
+ end
50
+
51
+ def call(env)
52
+ idempotent = IDEMPOTENT_METHODS.include?(env.method.to_s.upcase)
53
+ token = @token_manager&.access_token
54
+ refreshed = false
55
+ attempt = 0
56
+ loop do
57
+ env.request_headers["Authorization"] = "Bearer #{token}" if token
58
+ response = nil
59
+ error = nil
60
+ begin
61
+ response = @app.call(env.dup)
62
+ rescue *NETWORK_ERRORS => e
63
+ error = e
64
+ end
65
+ status = response&.status
66
+
67
+ if status == 401 && !refreshed && @token_manager&.can_refresh?
68
+ refreshed = true
69
+ token = @token_manager.refresh(stale_token: token).access_token
70
+ next
71
+ end
72
+
73
+ delay = nil
74
+ if status == 429 && attempt < @retry.max_retries
75
+ delay = self.class.rate_limit_wait_ms(response.headers[RATE_LIMIT_RESET_HEADER], @now_ms.call, @retry.max_delay_ms)
76
+ elsif (error || (status && status >= 500)) && idempotent && attempt < @retry.max_retries
77
+ delay = self.class.backoff_ms(attempt + 1, @retry, @random.call)
78
+ end
79
+ if delay
80
+ attempt += 1
81
+ @sleep.call(delay)
82
+ next
83
+ end
84
+
85
+ return response if response
86
+
87
+ raise error
88
+ end
89
+ end
90
+ end
91
+
92
+ RetryOptions = Transport::RetryOptions
93
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wefunder
4
+ VERSION = "0.1.0.beta1"
5
+ end
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "openssl"
5
+
6
+ module Wefunder
7
+ # Webhook verification + parsing for Wefunder platform events. The scheme is a single
8
+ # Stripe-style header:
9
+ # Wefunder-Signature: t=<unix seconds>,v1=<hex>[,v1=<hex>]
10
+ # v1 = HMAC-SHA256(secret, "<t>.<raw_body>")
11
+ # During a secret rotation the header carries one +v1+ per active secret; a delivery is
12
+ # valid if ANY matches. Consumers enforce a replay tolerance on +t+ (documented: 5 min).
13
+ # Every delivery body is the envelope +{id: "evt_…", event, created_at, mode, data}+ —
14
+ # +id+ is the dedup key. The retired attribution scheme (+X-Wefunder-Signature: sha256=…+
15
+ # + +X-Wefunder-Timestamp+) is still accepted by +construct_event+ when the new header is
16
+ # absent, so an attribution subscriber isn't stranded.
17
+ module Webhooks
18
+ SIGNATURE_HEADER = "wefunder-signature"
19
+ DEFAULT_TOLERANCE_SECONDS = 300
20
+ LEGACY_SIGNATURE_HEADER = "x-wefunder-signature"
21
+ LEGACY_TIMESTAMP_HEADER = "x-wefunder-timestamp"
22
+ LEGACY_EVENT_HEADER = "x-wefunder-event"
23
+ LEGACY_DELIVERY_ID_HEADER = "x-wefunder-delivery-id"
24
+
25
+ FAILURE_REASONS = %w[missing_header malformed_header timestamp_out_of_tolerance signature_mismatch
26
+ invalid_payload].freeze
27
+ FAILURE_MESSAGES = {
28
+ "missing_header" => "Missing Wefunder-Signature header",
29
+ "malformed_header" => "Wefunder-Signature header is malformed (expected t=<unix>,v1=<hex>)",
30
+ "timestamp_out_of_tolerance" => "Webhook timestamp is outside the replay tolerance",
31
+ "signature_mismatch" => "Webhook signature verification failed",
32
+ "invalid_payload" => "Webhook body is not a JSON object"
33
+ }.freeze
34
+
35
+ # Every event name in the catalog. Kept in step with the spec's +events+ enum by
36
+ # spec/webhooks_spec.rb, which fails when the two diverge.
37
+ EVENT_NAMES = %w[
38
+ investment.created investment.reinstated investment.canceled investment.converted
39
+ investment.amount_changed investment.executed investment.changed
40
+ offering.opened offering.closing offering.closed offering.canceled
41
+ investment_session.created investment_session.started investment_session.completed
42
+ investment_session.expired investment_session.canceled
43
+ syndicate_member.invited syndicate_member.reinvited syndicate_member.applied
44
+ syndicate_member.approved syndicate_member.joined
45
+ ].freeze
46
+
47
+ # Raised by +construct_event+. Respond 400 and don't process.
48
+ class SignatureError < StandardError
49
+ attr_reader :reason
50
+
51
+ def initialize(reason)
52
+ super(FAILURE_MESSAGES.fetch(reason))
53
+ @reason = reason
54
+ end
55
+ end
56
+
57
+ # A verified, parsed delivery. +timestamp+ is the signed +t+ (unix seconds).
58
+ Event = Struct.new(:id, :event, :created_at, :mode, :data, :timestamp, keyword_init: true) do
59
+ def known? = EVENT_NAMES.include?(event)
60
+ end
61
+
62
+ ParsedHeader = Struct.new(:timestamp, :signatures, keyword_init: true)
63
+
64
+ module_function
65
+
66
+ # HMAC-SHA256(secret, "<timestamp>.<raw body>") as lowercase hex.
67
+ def compute_signature(secret, timestamp, payload)
68
+ OpenSSL::HMAC.hexdigest("SHA256", secret, "#{timestamp}.".b + payload.to_s.b)
69
+ end
70
+
71
+ # Parse +t=<unix>,v1=<hex>[,v1=<hex>]+. nil when the shape is wrong. Unknown keys (a
72
+ # future +v2+) are ignored on purpose.
73
+ def parse_signature_header(header)
74
+ timestamp = nil
75
+ signatures = []
76
+ header.to_s.split(",").each do |part|
77
+ key, eq, value = part.partition("=")
78
+ next if eq.empty?
79
+
80
+ key = key.strip
81
+ value = value.strip
82
+ if key == "t"
83
+ return nil unless value.match?(/\A\d+\z/)
84
+
85
+ timestamp = value.to_i
86
+ elsif key == "v1" && !value.empty?
87
+ signatures << value
88
+ end
89
+ end
90
+ return nil if timestamp.nil? || signatures.empty?
91
+
92
+ ParsedHeader.new(timestamp: timestamp, signatures: signatures)
93
+ end
94
+
95
+ # Build a +Wefunder-Signature+ header value for +payload+ (for your own tests — the API
96
+ # signs real deliveries). +additional_secrets+ adds one +v1+ per secret (rotation window).
97
+ def sign(payload, secret, timestamp: nil, additional_secrets: [])
98
+ t = timestamp || Time.now.to_i
99
+ entries = [secret, *additional_secrets].map { |s| "v1=#{compute_signature(s, t, payload)}" }
100
+ ["t=#{t}", *entries].join(",")
101
+ end
102
+
103
+ # The reason a delivery is NOT valid, or nil when it is. Constant-time compare against
104
+ # every +v1+; enforces the timestamp tolerance (0 disables it).
105
+ def check_signature(payload, header, secret, tolerance_seconds: DEFAULT_TOLERANCE_SECONDS, now: nil)
106
+ parsed = parse_signature_header(header)
107
+ return "malformed_header" unless parsed
108
+ if tolerance_seconds.positive? && (now_f(now) - parsed.timestamp).abs > tolerance_seconds
109
+ return "timestamp_out_of_tolerance"
110
+ end
111
+
112
+ expected = compute_signature(secret, parsed.timestamp, payload)
113
+ parsed.signatures.any? { |sig| secure_equal?(sig, expected) } ? nil : "signature_mismatch"
114
+ end
115
+
116
+ # Legacy attribution scheme: +X-Wefunder-Signature: sha256=<hex>+ + +X-Wefunder-Timestamp+.
117
+ def verify_legacy(payload, signature, timestamp, secret, tolerance_seconds: DEFAULT_TOLERANCE_SECONDS, now: nil)
118
+ ts = Integer(timestamp.to_s, exception: false)
119
+ return false if ts.nil?
120
+ return false if tolerance_seconds.positive? && (now_f(now) - ts).abs > tolerance_seconds
121
+
122
+ secure_equal?(signature.to_s, "sha256=#{compute_signature(secret, timestamp, payload)}")
123
+ end
124
+
125
+ # true iff the signature is valid and (when tolerance > 0) the timestamp is within
126
+ # tolerance. Never raises. Pass +header:+ (the Wefunder-Signature value) or, for a legacy
127
+ # attribution subscription, +signature:+ + +timestamp:+.
128
+ def verify(payload, secret, header: nil, signature: nil, timestamp: nil,
129
+ tolerance_seconds: DEFAULT_TOLERANCE_SECONDS, now: nil)
130
+ return check_signature(payload, header, secret, tolerance_seconds: tolerance_seconds, now: now).nil? if header
131
+ return false if signature.nil? || timestamp.nil?
132
+
133
+ verify_legacy(payload, signature, timestamp, secret, tolerance_seconds: tolerance_seconds, now: now)
134
+ end
135
+
136
+ # Verify a delivery and parse its envelope in one step. Raises SignatureError (with
137
+ # +reason+) on any failure. +payload+ is the RAW request body; +headers+ is any
138
+ # case-insensitive mapping (Rack env style keys are accepted too), or the header value itself.
139
+ def construct_event(payload, headers, secret, tolerance_seconds: DEFAULT_TOLERANCE_SECONDS, now: nil)
140
+ get = header_getter(headers)
141
+ header = get.call(SIGNATURE_HEADER)
142
+ unless header
143
+ legacy_sig = get.call(LEGACY_SIGNATURE_HEADER)
144
+ legacy_ts = get.call(LEGACY_TIMESTAMP_HEADER)
145
+ if legacy_sig && legacy_ts
146
+ return construct_legacy_event(payload, get, legacy_sig, legacy_ts, secret, tolerance_seconds, now)
147
+ end
148
+
149
+ raise SignatureError, "missing_header"
150
+ end
151
+ failure = check_signature(payload, header, secret, tolerance_seconds: tolerance_seconds, now: now)
152
+ raise SignatureError, failure if failure
153
+
154
+ parsed = parse_signature_header(header)
155
+ env = parse_envelope(payload)
156
+ Event.new(id: env["id"].to_s, event: env["event"].to_s, created_at: env["created_at"].to_s,
157
+ mode: mode_of(env["mode"]), data: env["data"], timestamp: parsed.timestamp)
158
+ end
159
+
160
+ # Call the handler registered for +event.event+, falling back to "default". Returns true
161
+ # if a handler ran. Handlers are callables (procs/lambdas/method objects).
162
+ def dispatch(event, handlers)
163
+ handler = pick_handler(event, handlers)
164
+ return false unless handler
165
+
166
+ handler.call(event)
167
+ true
168
+ end
169
+
170
+ # --- internals -----------------------------------------------------------------------
171
+
172
+ def now_f(now) = (now || -> { Time.now.to_f }).call
173
+
174
+ def secure_equal?(a, b)
175
+ return false unless a.bytesize == b.bytesize
176
+
177
+ OpenSSL.fixed_length_secure_compare(a, b)
178
+ end
179
+
180
+ def header_getter(headers)
181
+ return ->(name) { name == SIGNATURE_HEADER ? headers : nil } if headers.is_a?(String)
182
+
183
+ lowered = {}
184
+ headers.each do |k, v|
185
+ key = k.to_s.downcase
186
+ key = key.delete_prefix("http_").tr("_", "-") if key.start_with?("http_") # Rack env style
187
+ lowered[key] = v
188
+ end
189
+ lambda do |name|
190
+ value = lowered[name]
191
+ value = value.first if value.is_a?(Array)
192
+ value&.to_s
193
+ end
194
+ end
195
+
196
+ def parse_envelope(payload)
197
+ parsed = JSON.parse(payload.to_s)
198
+ raise SignatureError, "invalid_payload" unless parsed.is_a?(Hash)
199
+
200
+ parsed
201
+ rescue JSON::ParserError
202
+ raise SignatureError, "invalid_payload"
203
+ end
204
+
205
+ def mode_of(value) = value == "test" ? "test" : "live"
206
+
207
+ def construct_legacy_event(payload, get, signature, timestamp, secret, tolerance_seconds, now)
208
+ unless verify_legacy(payload, signature, timestamp, secret, tolerance_seconds: tolerance_seconds, now: now)
209
+ raise SignatureError, "signature_mismatch"
210
+ end
211
+
212
+ env = parse_envelope(payload)
213
+ Event.new(id: (env["id"] || get.call(LEGACY_DELIVERY_ID_HEADER)).to_s,
214
+ event: (env["event"] || get.call(LEGACY_EVENT_HEADER)).to_s,
215
+ created_at: env["created_at"].to_s, mode: mode_of(env["mode"]),
216
+ data: env.key?("data") ? env["data"] : env, timestamp: timestamp.to_i)
217
+ end
218
+
219
+ def pick_handler(event, handlers)
220
+ specific = event.event == "default" ? nil : (handlers[event.event] || handlers[event.event.to_sym])
221
+ specific || handlers["default"] || handlers[:default]
222
+ end
223
+ end
224
+
225
+ WebhookEvent = Webhooks::Event
226
+ WebhookSignatureError = Webhooks::SignatureError
227
+ WEBHOOK_EVENT_NAMES = Webhooks::EVENT_NAMES
228
+
229
+ class << self
230
+ def construct_event(...) = Webhooks.construct_event(...)
231
+ def verify_webhook(...) = Webhooks.verify(...)
232
+ def check_webhook_signature(...) = Webhooks.check_signature(...)
233
+ def sign_webhook(...) = Webhooks.sign(...)
234
+ def compute_webhook_signature(...) = Webhooks.compute_signature(...)
235
+ def parse_signature_header(...) = Webhooks.parse_signature_header(...)
236
+ def dispatch_webhook(...) = Webhooks.dispatch(...)
237
+ end
238
+ end
data/lib/wefunder.rb ADDED
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Official Ruby SDK for the Wefunder API (beta).
4
+ #
5
+ # wf = Wefunder::Client.from_client_credentials(client_id: ..., client_secret: ..., scopes: ["read:public"])
6
+ # wf.offerings.all(sort: "newest").each { |offering| puts offering.id }
7
+ require "wefunder_generated"
8
+ require_relative "wefunder/version"
9
+ require_relative "wefunder/errors"
10
+ require_relative "wefunder/oauth"
11
+ require_relative "wefunder/token_manager"
12
+ require_relative "wefunder/transport"
13
+ require_relative "wefunder/pagination"
14
+ require_relative "wefunder/webhooks"
15
+ require_relative "wefunder/client"
16
+
17
+ module Wefunder
18
+ class << self
19
+ def generate_pkce = OAuth.generate_pkce
20
+ def pkce_challenge(...) = OAuth.pkce_challenge(...)
21
+ def create_authorization_url(...) = OAuth.create_authorization_url(...)
22
+ def exchange_code(...) = OAuth.exchange_code(...)
23
+ def client_credentials_grant(...) = OAuth.client_credentials_grant(...)
24
+ def refresh_token(...) = OAuth.refresh_token(...)
25
+ end
26
+ end