fopost-rails 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5959178d911d67f15c611ae03d778c338acb8c90e0f0d40b5b6484ba5b0608c6
4
- data.tar.gz: 4ed0650b489d85ad122a10ee30787456eca0606846c8bfcdab4aeb42ccf8fe23
3
+ metadata.gz: 66538248d3e80cefb4a338b87150c210d74f4cb4beccb9b581b9c17be5ee78ab
4
+ data.tar.gz: 456c6e82e21492da699bc289602099d11d18b4ce45d55fc6304a9dacb7a5fc1f
5
5
  SHA512:
6
- metadata.gz: dc18b76fcd3d73197793b2803bd7b427b86d8d9a4e194528c70009b47712841ff8317eeaecb957920506ddea4b9b7200afafe23588a178e7a4e93f997007ad34
7
- data.tar.gz: f89fa93ca20e7f221a0e3e7fd90c18dfd9853a4e964b20bfe9ddfdc47a90831212e74232fa130976ca4c2f10610094afae7f8f27d9b76b6078b0cf695607ed8f
6
+ metadata.gz: 658fe9d5a6693fb5c283362cc468f36f0e674312c1527f2d89984c0743a36c0c9a983c3002ad98043855395e8516f05efaad80e68582ff84b7156fdfcd902422
7
+ data.tar.gz: 56ee14f403c563b9f78cc3b36a87bd57811ed1716583131612c14b29f7f01eca0e1be025e925d6d90c315967f27273d3306c899383b748a165d7147c5a0116a1
data/CHANGELOG.md CHANGED
@@ -4,6 +4,20 @@ All notable changes to this gem are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the gem follows
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.2.0] - 2026-10-18
8
+
9
+ ### Added
10
+
11
+ - `POST /fopost/articles` on the mounted engine receives the blog's website target. The `fopost`
12
+ gem verifies the signature; `article.published` reaches `config.on_article_published` and the
13
+ `fopost.article.published` notification, and a returned URL is echoed as `{ "url": ... }`.
14
+ - `website_secret` setting (`FOPOST_WEBSITE_SECRET`), falling back to `webhook_secret`.
15
+
16
+ ### Changed
17
+
18
+ - Requires `fopost` `~> 0.3`, now resolved from RubyGems instead of the GitHub source.
19
+ - The README points at `fopost.com/dashboard` instead of the retired app host.
20
+
7
21
  ## [0.1.0] - 2026-08-30
8
22
 
9
23
  Initial release.
@@ -17,4 +31,5 @@ Initial release.
17
31
  - A mountable engine that verifies incoming webhook signatures and republishes them as
18
32
  `ActiveSupport::Notifications` events.
19
33
 
34
+ [0.2.0]: https://github.com/fopost/fopost-rails/releases/tag/v0.2.0
20
35
  [0.1.0]: https://github.com/fopost/fopost-rails/releases/tag/v0.1.0
data/README.md CHANGED
@@ -36,7 +36,7 @@ bin/rails generate fopost:install
36
36
 
37
37
  ## Configure
38
38
 
39
- Create an API key at [app.fopost.com/api-keys](https://app.fopost.com/api-keys) and put it
39
+ Create an API key at [fopost.com/dashboard/api-keys](https://fopost.com/dashboard/api-keys) and put it
40
40
  somewhere the app can read it:
41
41
 
42
42
  ```bash
@@ -67,6 +67,7 @@ Every setting resolves the same way — **what you set explicitly wins, then Rai
67
67
  | `max_retries` | `max_retries` | `FOPOST_MAX_RETRIES` | `3` |
68
68
  | `default_workspace_id` | `default_workspace_id` | `FOPOST_WORKSPACE_ID` | none |
69
69
  | `webhook_secret` | `webhook_secret` | `FOPOST_WEBHOOK_SECRET` | none |
70
+ | `website_secret` | `website_secret` | `FOPOST_WEBSITE_SECRET` | falls back to `webhook_secret` |
70
71
  | `queue_name` | `queue_name` | `FOPOST_QUEUE` | `default` |
71
72
 
72
73
  Set them in the initializer:
@@ -88,7 +89,7 @@ config.fopost.default_workspace_id = 'ws_...'
88
89
 
89
90
  `Fopost::Rails.client` is a configured `Fopost::Client`, memoized and safe to call from any
90
91
  thread. The [`fopost` gem README](https://github.com/fopost/fopost-ruby) documents the full
91
- resource surface — `posts`, `accounts`, `workspaces`, `labels`, `ai`.
92
+ resource surface — `posts`, `accounts`, `workspaces`, `labels`, `ai`, `blog`.
92
93
 
93
94
  ```ruby
94
95
  class PostsController < ApplicationController
@@ -203,6 +204,49 @@ post '/fopost/webhooks',
203
204
  }
204
205
  ```
205
206
 
207
+ ## Receive blog articles
208
+
209
+ The same engine mount also serves `POST /fopost/articles`, the receiver for the blog's **Website**
210
+ target. Set that target to `https://your-app.example/fopost/articles`, copy the website secret it
211
+ shows you once into `config.website_secret`, and hand FoPost a callback:
212
+
213
+ ```ruby
214
+ # config/initializers/fopost.rb
215
+ Fopost::Rails.configure do |config|
216
+ config.website_secret = Rails.application.credentials.dig(:fopost, :website_secret)
217
+
218
+ config.on_article_published = lambda do |article|
219
+ record = Article.create!(
220
+ title: article.title,
221
+ slug: article.slug,
222
+ body: article.body_html,
223
+ excerpt: article.excerpt,
224
+ tags: article.tags,
225
+ published_at: article.published_at
226
+ )
227
+ Rails.application.routes.url_helpers.article_url(record)
228
+ end
229
+ end
230
+ ```
231
+
232
+ `article` is a `Fopost::BlogArticlePublished` from the `fopost` gem: `title`, `slug`, `excerpt`,
233
+ `meta_title`, `meta_description`, `body_markdown`, `body_html`, `featured_image`, `images`, `tags`,
234
+ `keyword`, `word_count`, `published_at`. Image URLs stay fetchable for six hours, so store your own
235
+ copy. Return the URL the article now lives at and the receiver answers `{ "url": "..." }`; return
236
+ anything else and it answers `{ "ok": true }`.
237
+
238
+ Prefer events? Every verified `article.published` also fires
239
+ `ActiveSupport::Notifications.instrument('fopost.article.published', ...)` with `:article`, `:data`,
240
+ `:timestamp` and `:delivery_id`, whether or not a callback is set.
241
+
242
+ Verification is the `fopost` gem's `Fopost::Webhooks.verify`: the timestamped
243
+ `X-FoPost-Signature-256` over the raw body, rejected outside a 300 second window. A bad signature
244
+ is a `401` and the callback never runs; a test delivery (`article.test`) answers `{ "ok": true }`.
245
+ To sign one yourself in a request spec, use `Fopost::Webhooks.sign("#{timestamp}.#{body}", secret)`.
246
+
247
+ Managing the blog itself (settings, keywords, articles) is `Fopost::Rails.client.blog`, documented
248
+ in the [`fopost` gem README](https://github.com/fopost/fopost-ruby#blog).
249
+
206
250
  ## Testing your app
207
251
 
208
252
  Swap the client for one wired to your own transport and nothing touches the network:
@@ -223,7 +267,7 @@ separate on purpose and never depend on each other.
223
267
  ## Links
224
268
 
225
269
  - Documentation: [fopost.com/docs](https://fopost.com/docs)
226
- - API keys: [app.fopost.com/api-keys](https://app.fopost.com/api-keys)
270
+ - API keys: [fopost.com/dashboard/api-keys](https://fopost.com/dashboard/api-keys)
227
271
  - The SDK this wraps: [`fopost`](https://github.com/fopost/fopost-ruby)
228
272
  - Issues: [github.com/fopost/fopost-rails/issues](https://github.com/fopost/fopost-rails/issues)
229
273
  - Support: [fopost.com/contact](https://fopost.com/contact)
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'active_support/notifications'
5
+
6
+ module Fopost
7
+ module Rails
8
+ # Receives the blog's website target: FoPost posts here when an article is
9
+ # published. The signature is verified by the `fopost` gem over the raw
10
+ # body; a mismatch is a 401 and nothing runs.
11
+ #
12
+ # # config/initializers/fopost.rb
13
+ # config.on_article_published = ->(article) { Article.import!(article).url }
14
+ #
15
+ # The callback may return the URL the article now lives at, which is
16
+ # echoed back as `{ "url": ... }`. `fopost.article.published` also fires as
17
+ # an ActiveSupport::Notifications event for subscribers.
18
+ class ArticlesController < ActionController::API
19
+ def create
20
+ secret = Fopost::Rails.config.website_secret || Fopost::Rails.config.webhook_secret
21
+ return head :service_unavailable if secret.nil? || secret.to_s.empty?
22
+
23
+ event = Fopost::Webhooks.verify(request.raw_post, request.headers, secret)
24
+ return render json: { ok: true } unless event.event == 'article.published'
25
+
26
+ url = deliver(event)
27
+ render json: url.is_a?(String) && !url.empty? ? { url: url } : { ok: true }
28
+ rescue Fopost::WebhookSignatureError
29
+ head :unauthorized
30
+ end
31
+
32
+ private
33
+
34
+ def deliver(event)
35
+ article = event.article
36
+ notification = {
37
+ event: event.event,
38
+ article: article,
39
+ data: event.data,
40
+ timestamp: event.timestamp,
41
+ delivery_id: request.headers[Fopost::Webhooks::DELIVERY_HEADER]
42
+ }
43
+ ActiveSupport::Notifications.instrument('fopost.article.published', notification)
44
+
45
+ Fopost::Rails.config.on_article_published&.call(article)
46
+ end
47
+ end
48
+ end
49
+ end
data/config/routes.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  Fopost::Rails::Engine.routes.draw do
4
4
  post '/webhooks', to: 'webhooks#create', as: :webhooks
5
+ post '/articles', to: 'articles#create', as: :articles
5
6
 
6
7
  # So mounting straight onto the endpoint path also works:
7
8
  # mount Fopost::Rails::Engine => '/fopost/webhooks'
@@ -15,7 +15,7 @@ module Fopost
15
15
  # end
16
16
  class Configuration
17
17
  SETTINGS = %i[
18
- api_key base_url timeout max_retries default_workspace_id webhook_secret queue_name
18
+ api_key base_url timeout max_retries default_workspace_id webhook_secret website_secret queue_name
19
19
  ].freeze
20
20
 
21
21
  ENV_KEYS = {
@@ -25,6 +25,7 @@ module Fopost
25
25
  max_retries: 'FOPOST_MAX_RETRIES',
26
26
  default_workspace_id: 'FOPOST_WORKSPACE_ID',
27
27
  webhook_secret: 'FOPOST_WEBHOOK_SECRET',
28
+ website_secret: 'FOPOST_WEBSITE_SECRET',
28
29
  queue_name: 'FOPOST_QUEUE'
29
30
  }.freeze
30
31
 
@@ -37,6 +38,11 @@ module Fopost
37
38
 
38
39
  attr_writer(*SETTINGS)
39
40
 
41
+ # Called with a Fopost::BlogArticlePublished for every verified
42
+ # `article.published` delivery. Return the URL the article now lives at
43
+ # and the receiver echoes it back to FoPost.
44
+ attr_accessor :on_article_published
45
+
40
46
  def api_key
41
47
  resolve(:api_key)
42
48
  end
@@ -65,6 +71,12 @@ module Fopost
65
71
  resolve(:webhook_secret)
66
72
  end
67
73
 
74
+ # The blog's website secret, shown once when the website target is set.
75
+ # The article receiver falls back to `webhook_secret` when it is unset.
76
+ def website_secret
77
+ resolve(:website_secret)
78
+ end
79
+
68
80
  def queue_name
69
81
  resolve(:queue_name)
70
82
  end
@@ -83,6 +95,7 @@ module Fopost
83
95
  # Drop every explicit value, so the next read falls back again.
84
96
  def reset!
85
97
  SETTINGS.each { |key| instance_variable_set(:"@#{key}", nil) }
98
+ @on_article_published = nil
86
99
  self
87
100
  end
88
101
 
@@ -92,7 +105,9 @@ module Fopost
92
105
 
93
106
  # Keeps the key out of logs and consoles.
94
107
  def inspect
95
- redacted = to_h.merge(api_key: mask(api_key), webhook_secret: mask(webhook_secret))
108
+ redacted = to_h.merge(
109
+ api_key: mask(api_key), webhook_secret: mask(webhook_secret), website_secret: mask(website_secret)
110
+ )
96
111
  "#<Fopost::Rails::Configuration #{redacted.map { |k, v| "#{k}=#{v.inspect}" }.join(' ')}>"
97
112
  end
98
113
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Fopost
4
4
  module Rails
5
- VERSION = '0.1.0'
5
+ VERSION = '0.2.0'
6
6
  end
7
7
  end
@@ -22,7 +22,7 @@ module Fopost
22
22
  bin/rails credentials:edit # fopost: { api_key: fp_... }
23
23
  # or export FOPOST_API_KEY=fp_...
24
24
 
25
- Create a key at https://app.fopost.com/api-keys.
25
+ Create a key at https://fopost.com/dashboard/api-keys.
26
26
  TEXT
27
27
  end
28
28
  end
@@ -6,7 +6,7 @@
6
6
  # then Rails credentials under `fopost:`, then the environment. Leave a line
7
7
  # commented to take the fallback.
8
8
  Fopost::Rails.configure do |config|
9
- # Create a key at https://app.fopost.com/api-keys.
9
+ # Create a key at https://fopost.com/dashboard/api-keys.
10
10
  # Falls back to credentials `fopost: api_key:`, then ENV['FOPOST_API_KEY'].
11
11
  # config.api_key = nil
12
12
 
@@ -20,6 +20,13 @@ Fopost::Rails.configure do |config|
20
20
  # Shown once, when you create a webhook. Required by the mounted endpoint.
21
21
  # config.webhook_secret = nil
22
22
 
23
+ # Shown once, when you set the blog's website target. Used by /fopost/articles;
24
+ # falls back to webhook_secret.
25
+ # config.website_secret = nil
26
+
27
+ # Receives a Fopost::BlogArticlePublished; return the URL you published it at.
28
+ # config.on_article_published = ->(article) { Article.import!(article).url }
29
+
23
30
  # ActiveJob queue for PublishJob and CreatePostJob.
24
31
  # config.queue_name = 'default'
25
32
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fopost-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - FoPost
@@ -36,14 +36,14 @@ dependencies:
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: '0.1'
39
+ version: '0.3'
40
40
  type: :runtime
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '0.1'
46
+ version: '0.3'
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: railties
49
49
  requirement: !ruby/object:Gem::Requirement
@@ -74,6 +74,7 @@ files:
74
74
  - CHANGELOG.md
75
75
  - LICENSE
76
76
  - README.md
77
+ - app/controllers/fopost/rails/articles_controller.rb
77
78
  - app/controllers/fopost/rails/webhooks_controller.rb
78
79
  - config/routes.rb
79
80
  - lib/fopost-rails.rb