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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +263 -0
- data/lib/wefunder/client.rb +283 -0
- data/lib/wefunder/errors.rb +87 -0
- data/lib/wefunder/oauth.rb +135 -0
- data/lib/wefunder/pagination.rb +52 -0
- data/lib/wefunder/token_manager.rb +80 -0
- data/lib/wefunder/transport.rb +93 -0
- data/lib/wefunder/version.rb +5 -0
- data/lib/wefunder/webhooks.rb +238 -0
- data/lib/wefunder.rb +26 -0
- data/lib/wefunder_generated/api/activity_api.rb +234 -0
- data/lib/wefunder_generated/api/attribution_api.rb +182 -0
- data/lib/wefunder_generated/api/attribution_partners_api.rb +392 -0
- data/lib/wefunder_generated/api/attribution_webhooks_api.rb +504 -0
- data/lib/wefunder_generated/api/campaigns_api.rb +82 -0
- data/lib/wefunder_generated/api/companies_api.rb +101 -0
- data/lib/wefunder_generated/api/explore_api.rb +633 -0
- data/lib/wefunder_generated/api/installations_api.rb +405 -0
- data/lib/wefunder_generated/api/intents_api.rb +301 -0
- data/lib/wefunder_generated/api/investment_delta_api.rb +99 -0
- data/lib/wefunder_generated/api/investments_api.rb +238 -0
- data/lib/wefunder_generated/api/portfolio_api.rb +166 -0
- data/lib/wefunder_generated/api/syndicate_deals_api.rb +381 -0
- data/lib/wefunder_generated/api/syndicate_members_api.rb +976 -0
- data/lib/wefunder_generated/api/syndicate_portfolio_api.rb +178 -0
- data/lib/wefunder_generated/api/syndicate_statistics_api.rb +85 -0
- data/lib/wefunder_generated/api/syndicates_api.rb +226 -0
- data/lib/wefunder_generated/api/users_api.rb +329 -0
- data/lib/wefunder_generated/api/webhook_endpoints_api.rb +539 -0
- data/lib/wefunder_generated/api_client.rb +437 -0
- data/lib/wefunder_generated/api_error.rb +58 -0
- data/lib/wefunder_generated/configuration.rb +392 -0
- data/lib/wefunder_generated/models/anonymized_attribution.rb +298 -0
- data/lib/wefunder_generated/models/anonymized_attribution_attribution.rb +330 -0
- data/lib/wefunder_generated/models/anonymized_attribution_status.rb +260 -0
- data/lib/wefunder_generated/models/attributed_investment_list_envelope.rb +226 -0
- data/lib/wefunder_generated/models/attributed_investment_list_envelope_data_inner.rb +105 -0
- data/lib/wefunder_generated/models/attributed_investment_list_envelope_meta.rb +295 -0
- data/lib/wefunder_generated/models/attribution_campaign_access.rb +241 -0
- data/lib/wefunder_generated/models/attribution_campaign_summary.rb +251 -0
- data/lib/wefunder_generated/models/attribution_campaign_summary_list_envelope.rb +216 -0
- data/lib/wefunder_generated/models/attribution_me.rb +235 -0
- data/lib/wefunder_generated/models/attribution_me_envelope.rb +214 -0
- data/lib/wefunder_generated/models/attribution_partner.rb +275 -0
- data/lib/wefunder_generated/models/attribution_stats.rb +266 -0
- data/lib/wefunder_generated/models/attribution_stats_by_campaign_inner.rb +232 -0
- data/lib/wefunder_generated/models/attribution_stats_by_source_inner.rb +232 -0
- data/lib/wefunder_generated/models/attribution_stats_envelope.rb +214 -0
- data/lib/wefunder_generated/models/attribution_stats_period.rb +223 -0
- data/lib/wefunder_generated/models/attribution_stats_quality_breakdown.rb +236 -0
- data/lib/wefunder_generated/models/attribution_stats_totals.rb +245 -0
- data/lib/wefunder_generated/models/attribution_user.rb +232 -0
- data/lib/wefunder_generated/models/audit_event.rb +234 -0
- data/lib/wefunder_generated/models/audit_event_attributes.rb +391 -0
- data/lib/wefunder_generated/models/audit_event_list_envelope.rb +225 -0
- data/lib/wefunder_generated/models/bulk_invite_link_create_input.rb +243 -0
- data/lib/wefunder_generated/models/bulk_invite_link_envelope.rb +237 -0
- data/lib/wefunder_generated/models/bulk_invite_link_envelope_errors_inner.rb +233 -0
- data/lib/wefunder_generated/models/bulk_invite_link_envelope_meta.rb +232 -0
- data/lib/wefunder_generated/models/campaign.rb +232 -0
- data/lib/wefunder_generated/models/campaign_attributes.rb +301 -0
- data/lib/wefunder_generated/models/campaign_list_envelope.rb +225 -0
- data/lib/wefunder_generated/models/comment_proposal_envelope.rb +274 -0
- data/lib/wefunder_generated/models/comment_proposal_envelope_draft.rb +260 -0
- data/lib/wefunder_generated/models/comment_proposal_envelope_proposal.rb +286 -0
- data/lib/wefunder_generated/models/comment_proposal_envelope_related_questions_inner.rb +264 -0
- data/lib/wefunder_generated/models/company.rb +234 -0
- data/lib/wefunder_generated/models/company_attributes.rb +318 -0
- data/lib/wefunder_generated/models/company_attributes_location.rb +235 -0
- data/lib/wefunder_generated/models/company_disclosures.rb +268 -0
- data/lib/wefunder_generated/models/company_disclosures_attributes.rb +434 -0
- data/lib/wefunder_generated/models/company_disclosures_attributes_business.rb +255 -0
- data/lib/wefunder_generated/models/company_disclosures_attributes_capital_structure_inner.rb +255 -0
- data/lib/wefunder_generated/models/company_disclosures_attributes_current_position.rb +281 -0
- data/lib/wefunder_generated/models/company_disclosures_attributes_filing.rb +256 -0
- data/lib/wefunder_generated/models/company_disclosures_attributes_financial_statements.rb +233 -0
- data/lib/wefunder_generated/models/company_disclosures_attributes_outstanding_debts.rb +227 -0
- data/lib/wefunder_generated/models/company_disclosures_attributes_outstanding_debts_items_inner.rb +255 -0
- data/lib/wefunder_generated/models/company_disclosures_attributes_outstanding_notes_inner.rb +284 -0
- data/lib/wefunder_generated/models/company_disclosures_attributes_prior_offerings_inner.rb +266 -0
- data/lib/wefunder_generated/models/company_disclosures_attributes_ratios.rb +272 -0
- data/lib/wefunder_generated/models/company_disclosures_attributes_related_parties.rb +226 -0
- data/lib/wefunder_generated/models/company_disclosures_attributes_related_parties_parties_inner.rb +265 -0
- data/lib/wefunder_generated/models/company_disclosures_attributes_use_of_funds_inner.rb +226 -0
- data/lib/wefunder_generated/models/company_disclosures_attributes_voting_power_inner.rb +236 -0
- data/lib/wefunder_generated/models/company_disclosures_envelope.rb +214 -0
- data/lib/wefunder_generated/models/company_envelope.rb +214 -0
- data/lib/wefunder_generated/models/company_pitch.rb +268 -0
- data/lib/wefunder_generated/models/company_pitch_attributes.rb +311 -0
- data/lib/wefunder_generated/models/company_pitch_attributes_perks.rb +248 -0
- data/lib/wefunder_generated/models/company_pitch_attributes_story.rb +255 -0
- data/lib/wefunder_generated/models/company_pitch_envelope.rb +214 -0
- data/lib/wefunder_generated/models/company_question.rb +268 -0
- data/lib/wefunder_generated/models/company_question_attributes.rb +329 -0
- data/lib/wefunder_generated/models/company_question_attributes_answers_inner.rb +253 -0
- data/lib/wefunder_generated/models/company_question_list_envelope.rb +225 -0
- data/lib/wefunder_generated/models/company_question_list_envelope_meta.rb +301 -0
- data/lib/wefunder_generated/models/company_question_person.rb +247 -0
- data/lib/wefunder_generated/models/company_search_result.rb +268 -0
- data/lib/wefunder_generated/models/company_search_result_attributes.rb +266 -0
- data/lib/wefunder_generated/models/company_search_result_list_envelope.rb +225 -0
- data/lib/wefunder_generated/models/company_search_result_list_envelope_meta.rb +223 -0
- data/lib/wefunder_generated/models/company_totals.rb +249 -0
- data/lib/wefunder_generated/models/company_update.rb +269 -0
- data/lib/wefunder_generated/models/company_update_attributes.rb +350 -0
- data/lib/wefunder_generated/models/company_update_attributes_author.rb +215 -0
- data/lib/wefunder_generated/models/company_update_envelope.rb +214 -0
- data/lib/wefunder_generated/models/company_update_list_envelope.rb +225 -0
- data/lib/wefunder_generated/models/company_update_list_envelope_meta.rb +243 -0
- data/lib/wefunder_generated/models/connected_app.rb +233 -0
- data/lib/wefunder_generated/models/connected_app_attributes.rb +258 -0
- data/lib/wefunder_generated/models/connected_app_list_envelope.rb +216 -0
- data/lib/wefunder_generated/models/connected_app_revocation.rb +223 -0
- data/lib/wefunder_generated/models/connected_app_revocation_envelope.rb +214 -0
- data/lib/wefunder_generated/models/convertible_note_security.rb +266 -0
- data/lib/wefunder_generated/models/convertible_note_security_terms.rb +266 -0
- data/lib/wefunder_generated/models/create_installation_request.rb +294 -0
- data/lib/wefunder_generated/models/create_installation_token_request.rb +216 -0
- data/lib/wefunder_generated/models/create_intent_request.rb +320 -0
- data/lib/wefunder_generated/models/create_partner_invite_request.rb +287 -0
- data/lib/wefunder_generated/models/create_webhook_endpoint_request.rb +291 -0
- data/lib/wefunder_generated/models/create_webhook_subscription_request.rb +263 -0
- data/lib/wefunder_generated/models/current_raise.rb +288 -0
- data/lib/wefunder_generated/models/deal_investor.rb +234 -0
- data/lib/wefunder_generated/models/deal_investor_attributes.rb +321 -0
- data/lib/wefunder_generated/models/deal_investor_list_envelope.rb +225 -0
- data/lib/wefunder_generated/models/deal_investor_list_envelope_meta.rb +252 -0
- data/lib/wefunder_generated/models/debt_security.rb +266 -0
- data/lib/wefunder_generated/models/debt_security_terms.rb +235 -0
- data/lib/wefunder_generated/models/delete_webhook_endpoint200_response.rb +214 -0
- data/lib/wefunder_generated/models/delete_webhook_endpoint200_response_data.rb +232 -0
- data/lib/wefunder_generated/models/disclosure_document.rb +224 -0
- data/lib/wefunder_generated/models/disclosure_person.rb +254 -0
- data/lib/wefunder_generated/models/eligible_target_list_envelope.rb +225 -0
- data/lib/wefunder_generated/models/eligible_target_list_envelope_data_inner.rb +295 -0
- data/lib/wefunder_generated/models/equity_security.rb +266 -0
- data/lib/wefunder_generated/models/equity_security_terms.rb +271 -0
- data/lib/wefunder_generated/models/error.rb +214 -0
- data/lib/wefunder_generated/models/error_error.rb +254 -0
- data/lib/wefunder_generated/models/exemption.rb +271 -0
- data/lib/wefunder_generated/models/fiscal_year_financials.rb +296 -0
- data/lib/wefunder_generated/models/follow_state_envelope.rb +214 -0
- data/lib/wefunder_generated/models/follow_state_envelope_data.rb +268 -0
- data/lib/wefunder_generated/models/follow_state_envelope_data_attributes.rb +225 -0
- data/lib/wefunder_generated/models/followed_company.rb +268 -0
- data/lib/wefunder_generated/models/followed_company_attributes.rb +275 -0
- data/lib/wefunder_generated/models/followed_company_list_envelope.rb +225 -0
- data/lib/wefunder_generated/models/full_attribution.rb +272 -0
- data/lib/wefunder_generated/models/full_attribution_attribution.rb +326 -0
- data/lib/wefunder_generated/models/full_attribution_investor.rb +242 -0
- data/lib/wefunder_generated/models/full_attribution_status.rb +257 -0
- data/lib/wefunder_generated/models/fund_security.rb +267 -0
- data/lib/wefunder_generated/models/installation.rb +233 -0
- data/lib/wefunder_generated/models/installation_attributes.rb +307 -0
- data/lib/wefunder_generated/models/installation_attributes_target.rb +267 -0
- data/lib/wefunder_generated/models/installation_envelope.rb +223 -0
- data/lib/wefunder_generated/models/installation_list_envelope.rb +225 -0
- data/lib/wefunder_generated/models/installation_list_envelope_meta.rb +214 -0
- data/lib/wefunder_generated/models/installation_token_envelope.rb +232 -0
- data/lib/wefunder_generated/models/installation_token_envelope_token.rb +252 -0
- data/lib/wefunder_generated/models/intent.rb +234 -0
- data/lib/wefunder_generated/models/intent_attributes.rb +375 -0
- data/lib/wefunder_generated/models/intent_attributes_resource_id.rb +106 -0
- data/lib/wefunder_generated/models/intent_envelope.rb +214 -0
- data/lib/wefunder_generated/models/intent_list_envelope.rb +225 -0
- data/lib/wefunder_generated/models/intent_preview_envelope.rb +214 -0
- data/lib/wefunder_generated/models/intent_preview_envelope_data.rb +257 -0
- data/lib/wefunder_generated/models/intent_preview_envelope_data_attributes.rb +262 -0
- data/lib/wefunder_generated/models/intent_review.rb +234 -0
- data/lib/wefunder_generated/models/investment_change_list_envelope.rb +225 -0
- data/lib/wefunder_generated/models/investment_change_list_envelope_meta.rb +285 -0
- data/lib/wefunder_generated/models/investment_delta_record.rb +504 -0
- data/lib/wefunder_generated/models/investment_delta_record_amounts.rb +251 -0
- data/lib/wefunder_generated/models/investment_delta_record_blockers_inner.rb +224 -0
- data/lib/wefunder_generated/models/investment_delta_record_contracts_inner.rb +234 -0
- data/lib/wefunder_generated/models/investment_delta_record_investor.rb +286 -0
- data/lib/wefunder_generated/models/investment_envelope.rb +223 -0
- data/lib/wefunder_generated/models/investment_envelope_meta.rb +270 -0
- data/lib/wefunder_generated/models/investment_list_envelope.rb +225 -0
- data/lib/wefunder_generated/models/investment_list_envelope_meta.rb +288 -0
- data/lib/wefunder_generated/models/investment_session.rb +232 -0
- data/lib/wefunder_generated/models/investment_session_attributes.rb +384 -0
- data/lib/wefunder_generated/models/investment_session_attributes_events_inner.rb +232 -0
- data/lib/wefunder_generated/models/investment_session_create_input.rb +331 -0
- data/lib/wefunder_generated/models/investment_session_envelope.rb +223 -0
- data/lib/wefunder_generated/models/investment_session_list_envelope.rb +225 -0
- data/lib/wefunder_generated/models/investment_session_list_envelope_meta.rb +242 -0
- data/lib/wefunder_generated/models/investor.rb +233 -0
- data/lib/wefunder_generated/models/investor_attributes.rb +262 -0
- data/lib/wefunder_generated/models/investor_envelope.rb +214 -0
- data/lib/wefunder_generated/models/investor_list_envelope.rb +225 -0
- data/lib/wefunder_generated/models/invite_link.rb +233 -0
- data/lib/wefunder_generated/models/invite_link_attributes.rb +375 -0
- data/lib/wefunder_generated/models/invite_link_attributes_events_inner.rb +243 -0
- data/lib/wefunder_generated/models/invite_link_create_input.rb +295 -0
- data/lib/wefunder_generated/models/invite_link_envelope.rb +223 -0
- data/lib/wefunder_generated/models/invite_link_list_envelope.rb +225 -0
- data/lib/wefunder_generated/models/invite_link_update_input.rb +227 -0
- data/lib/wefunder_generated/models/invite_syndicate_member_request.rb +277 -0
- data/lib/wefunder_generated/models/marketing_partner.rb +295 -0
- data/lib/wefunder_generated/models/marketing_partner_envelope.rb +214 -0
- data/lib/wefunder_generated/models/member_investment.rb +234 -0
- data/lib/wefunder_generated/models/member_investment_attributes.rb +301 -0
- data/lib/wefunder_generated/models/member_investment_list_envelope.rb +225 -0
- data/lib/wefunder_generated/models/member_investment_list_envelope_meta.rb +225 -0
- data/lib/wefunder_generated/models/my_company.rb +268 -0
- data/lib/wefunder_generated/models/my_company_attributes.rb +257 -0
- data/lib/wefunder_generated/models/my_company_list_envelope.rb +225 -0
- data/lib/wefunder_generated/models/offering.rb +267 -0
- data/lib/wefunder_generated/models/offering_attributes.rb +469 -0
- data/lib/wefunder_generated/models/offering_attributes_intended_security.rb +258 -0
- data/lib/wefunder_generated/models/offering_attributes_security.rb +105 -0
- data/lib/wefunder_generated/models/offering_envelope.rb +214 -0
- data/lib/wefunder_generated/models/offering_list_envelope.rb +225 -0
- data/lib/wefunder_generated/models/offering_list_envelope_meta.rb +265 -0
- data/lib/wefunder_generated/models/offering_stats_envelope.rb +223 -0
- data/lib/wefunder_generated/models/offering_stats_envelope_data.rb +253 -0
- data/lib/wefunder_generated/models/offering_stats_envelope_data_by_status_value.rb +232 -0
- data/lib/wefunder_generated/models/offering_stats_envelope_data_total.rb +233 -0
- data/lib/wefunder_generated/models/offering_stats_envelope_meta.rb +258 -0
- data/lib/wefunder_generated/models/offering_warnings_inner.rb +257 -0
- data/lib/wefunder_generated/models/other_security.rb +267 -0
- data/lib/wefunder_generated/models/other_security_terms.rb +235 -0
- data/lib/wefunder_generated/models/pagination_meta.rb +234 -0
- data/lib/wefunder_generated/models/pagination_meta_next_cursor.rb +106 -0
- data/lib/wefunder_generated/models/partner_investment.rb +232 -0
- data/lib/wefunder_generated/models/partner_investment_attributes.rb +305 -0
- data/lib/wefunder_generated/models/partner_investment_attributes_accreditation.rb +289 -0
- data/lib/wefunder_generated/models/partner_investment_envelope.rb +214 -0
- data/lib/wefunder_generated/models/partner_investment_list_envelope.rb +225 -0
- data/lib/wefunder_generated/models/partner_investor.rb +233 -0
- data/lib/wefunder_generated/models/partner_investor_attributes.rb +264 -0
- data/lib/wefunder_generated/models/partner_investor_list_envelope.rb +225 -0
- data/lib/wefunder_generated/models/partner_invite.rb +360 -0
- data/lib/wefunder_generated/models/partner_invite_envelope.rb +214 -0
- data/lib/wefunder_generated/models/partner_invite_list_envelope.rb +216 -0
- data/lib/wefunder_generated/models/partner_webhook_event.rb +257 -0
- data/lib/wefunder_generated/models/partner_webhook_event_envelope.rb +214 -0
- data/lib/wefunder_generated/models/partner_webhook_subscription.rb +232 -0
- data/lib/wefunder_generated/models/partner_webhook_subscription_attributes.rb +243 -0
- data/lib/wefunder_generated/models/partner_webhook_subscription_create_input.rb +239 -0
- data/lib/wefunder_generated/models/partner_webhook_subscription_envelope.rb +214 -0
- data/lib/wefunder_generated/models/partner_webhook_subscription_list_envelope.rb +216 -0
- data/lib/wefunder_generated/models/partner_webhook_subscription_with_secret.rb +239 -0
- data/lib/wefunder_generated/models/partner_webhook_subscription_with_secret_all_of_attributes.rb +215 -0
- data/lib/wefunder_generated/models/partner_webhook_subscription_with_secret_envelope.rb +214 -0
- data/lib/wefunder_generated/models/past_round.rb +309 -0
- data/lib/wefunder_generated/models/perk_tier.rb +226 -0
- data/lib/wefunder_generated/models/pitch_block.rb +375 -0
- data/lib/wefunder_generated/models/pitch_block_links_inner.rb +226 -0
- data/lib/wefunder_generated/models/portfolio_company_ref.rb +235 -0
- data/lib/wefunder_generated/models/portfolio_holding.rb +258 -0
- data/lib/wefunder_generated/models/portfolio_position.rb +268 -0
- data/lib/wefunder_generated/models/portfolio_position_attributes.rb +456 -0
- data/lib/wefunder_generated/models/portfolio_position_list_envelope.rb +225 -0
- data/lib/wefunder_generated/models/portfolio_position_list_envelope_meta.rb +234 -0
- data/lib/wefunder_generated/models/portfolio_security.rb +323 -0
- data/lib/wefunder_generated/models/portfolio_security_owner.rb +258 -0
- data/lib/wefunder_generated/models/portfolio_security_terms.rb +257 -0
- data/lib/wefunder_generated/models/portfolio_summary_envelope.rb +214 -0
- data/lib/wefunder_generated/models/portfolio_summary_envelope_data.rb +223 -0
- data/lib/wefunder_generated/models/portfolio_summary_envelope_data_attributes.rb +292 -0
- data/lib/wefunder_generated/models/portfolio_summary_envelope_data_attributes_position_counts.rb +242 -0
- data/lib/wefunder_generated/models/preview_intent_request.rb +262 -0
- data/lib/wefunder_generated/models/register_as_partner_request.rb +232 -0
- data/lib/wefunder_generated/models/reorder_syndicate_members_request.rb +224 -0
- data/lib/wefunder_generated/models/revenue_share_security.rb +266 -0
- data/lib/wefunder_generated/models/revenue_share_security_terms.rb +300 -0
- data/lib/wefunder_generated/models/safe_security.rb +266 -0
- data/lib/wefunder_generated/models/safe_security_terms.rb +257 -0
- data/lib/wefunder_generated/models/security.rb +66 -0
- data/lib/wefunder_generated/models/security_summary.rb +272 -0
- data/lib/wefunder_generated/models/spv.rb +232 -0
- data/lib/wefunder_generated/models/spv_attributes.rb +353 -0
- data/lib/wefunder_generated/models/spv_attributes_target_company.rb +223 -0
- data/lib/wefunder_generated/models/spv_cancel_intent_envelope.rb +224 -0
- data/lib/wefunder_generated/models/spv_cancel_intent_envelope_meta.rb +214 -0
- data/lib/wefunder_generated/models/spv_close_intent_envelope.rb +224 -0
- data/lib/wefunder_generated/models/spv_close_intent_envelope_meta.rb +214 -0
- data/lib/wefunder_generated/models/spv_create_input.rb +274 -0
- data/lib/wefunder_generated/models/spv_envelope.rb +214 -0
- data/lib/wefunder_generated/models/spv_list_envelope.rb +225 -0
- data/lib/wefunder_generated/models/spv_metrics.rb +261 -0
- data/lib/wefunder_generated/models/spv_settings.rb +275 -0
- data/lib/wefunder_generated/models/spv_status.rb +232 -0
- data/lib/wefunder_generated/models/spv_status_attributes.rb +289 -0
- data/lib/wefunder_generated/models/spv_status_envelope.rb +223 -0
- data/lib/wefunder_generated/models/spv_terms.rb +348 -0
- data/lib/wefunder_generated/models/spv_with_meta_envelope.rb +223 -0
- data/lib/wefunder_generated/models/spv_with_meta_envelope_meta.rb +214 -0
- data/lib/wefunder_generated/models/syndicate.rb +234 -0
- data/lib/wefunder_generated/models/syndicate_attributes.rb +412 -0
- data/lib/wefunder_generated/models/syndicate_attributes_annual_subscription.rb +236 -0
- data/lib/wefunder_generated/models/syndicate_attributes_traditional_fund.rb +225 -0
- data/lib/wefunder_generated/models/syndicate_deal.rb +234 -0
- data/lib/wefunder_generated/models/syndicate_deal_attributes.rb +435 -0
- data/lib/wefunder_generated/models/syndicate_deal_attributes_exemption.rb +105 -0
- data/lib/wefunder_generated/models/syndicate_deal_attributes_security.rb +105 -0
- data/lib/wefunder_generated/models/syndicate_deal_close_intent_envelope.rb +224 -0
- data/lib/wefunder_generated/models/syndicate_deal_close_intent_envelope_meta.rb +214 -0
- data/lib/wefunder_generated/models/syndicate_deal_detail_envelope.rb +214 -0
- data/lib/wefunder_generated/models/syndicate_deal_envelope.rb +214 -0
- data/lib/wefunder_generated/models/syndicate_deal_finalize_intent_envelope.rb +224 -0
- data/lib/wefunder_generated/models/syndicate_deal_finalize_intent_envelope_meta.rb +214 -0
- data/lib/wefunder_generated/models/syndicate_deal_list_envelope.rb +225 -0
- data/lib/wefunder_generated/models/syndicate_detail_envelope.rb +214 -0
- data/lib/wefunder_generated/models/syndicate_list_envelope.rb +225 -0
- data/lib/wefunder_generated/models/syndicate_member.rb +234 -0
- data/lib/wefunder_generated/models/syndicate_member_attributes.rb +650 -0
- data/lib/wefunder_generated/models/syndicate_member_envelope.rb +214 -0
- data/lib/wefunder_generated/models/syndicate_member_list_envelope.rb +225 -0
- data/lib/wefunder_generated/models/syndicate_portfolio_summary_envelope.rb +214 -0
- data/lib/wefunder_generated/models/syndicate_portfolio_summary_envelope_data.rb +223 -0
- data/lib/wefunder_generated/models/syndicate_portfolio_summary_envelope_data_attributes.rb +302 -0
- data/lib/wefunder_generated/models/syndicate_portfolio_summary_envelope_data_attributes_deal_counts.rb +242 -0
- data/lib/wefunder_generated/models/syndicate_statistics.rb +224 -0
- data/lib/wefunder_generated/models/syndicate_statistics_attributes.rb +277 -0
- data/lib/wefunder_generated/models/syndicate_statistics_envelope.rb +214 -0
- data/lib/wefunder_generated/models/tag_ref.rb +238 -0
- data/lib/wefunder_generated/models/target_company_input.rb +258 -0
- data/lib/wefunder_generated/models/test_webhook_endpoint_request.rb +249 -0
- data/lib/wefunder_generated/models/update_syndicate_member_request.rb +221 -0
- data/lib/wefunder_generated/models/update_syndicate_member_request_member.rb +251 -0
- data/lib/wefunder_generated/models/update_syndicate_request.rb +221 -0
- data/lib/wefunder_generated/models/update_syndicate_request_syndicate.rb +232 -0
- data/lib/wefunder_generated/models/update_webhook_endpoint_request.rb +252 -0
- data/lib/wefunder_generated/models/user.rb +233 -0
- data/lib/wefunder_generated/models/user_attributes.rb +300 -0
- data/lib/wefunder_generated/models/user_envelope.rb +214 -0
- data/lib/wefunder_generated/models/validation_error.rb +215 -0
- data/lib/wefunder_generated/models/validation_error_error.rb +234 -0
- data/lib/wefunder_generated/models/validation_error_error_details_inner.rb +232 -0
- data/lib/wefunder_generated/models/webhook_delivery.rb +318 -0
- data/lib/wefunder_generated/models/webhook_endpoint.rb +232 -0
- data/lib/wefunder_generated/models/webhook_endpoint_attributes.rb +327 -0
- data/lib/wefunder_generated/models/webhook_endpoint_envelope.rb +223 -0
- data/lib/wefunder_generated/models/webhook_endpoint_envelope_meta.rb +214 -0
- data/lib/wefunder_generated/models/webhook_endpoint_list_envelope.rb +225 -0
- data/lib/wefunder_generated/models/webhook_endpoint_list_envelope_meta.rb +233 -0
- data/lib/wefunder_generated/models/webhook_endpoint_test_result_envelope.rb +223 -0
- data/lib/wefunder_generated/models/webhook_endpoint_test_result_envelope_data.rb +306 -0
- data/lib/wefunder_generated/models/webhook_subscription.rb +304 -0
- data/lib/wefunder_generated/models/webhook_subscription_envelope.rb +214 -0
- data/lib/wefunder_generated/models/webhook_subscription_list_envelope.rb +225 -0
- data/lib/wefunder_generated/models/webhook_subscription_list_envelope_meta.rb +225 -0
- data/lib/wefunder_generated/models/webhook_subscription_with_recent_deliveries_envelope.rb +214 -0
- data/lib/wefunder_generated/models/webhook_subscription_with_recent_deliveries_envelope_data.rb +322 -0
- data/lib/wefunder_generated/models/webhook_subscription_with_secret2_envelope.rb +214 -0
- data/lib/wefunder_generated/models/webhook_subscription_with_secret2_envelope_data.rb +320 -0
- data/lib/wefunder_generated/models/webhook_subscription_with_secret_envelope.rb +214 -0
- data/lib/wefunder_generated/models/webhook_subscription_with_secret_envelope_data.rb +320 -0
- data/lib/wefunder_generated/models/webhook_test_result.rb +233 -0
- data/lib/wefunder_generated/models/webhook_test_result_envelope.rb +214 -0
- data/lib/wefunder_generated/models/wefunder_round.rb +330 -0
- data/lib/wefunder_generated/models/wefunder_round_exemption.rb +104 -0
- data/lib/wefunder_generated/models/wefunder_round_security.rb +104 -0
- data/lib/wefunder_generated/version.rb +15 -0
- data/lib/wefunder_generated.rb +382 -0
- 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,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
|