postnhost 0.1.1 → 0.2.1

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -0
  3. data/README.md +27 -0
  4. data/app/assets/builds/postnhost/application.css +1 -1
  5. data/app/controllers/postnhost/api/base_controller.rb +34 -0
  6. data/app/controllers/postnhost/api/v1/commands_controller.rb +27 -0
  7. data/app/controllers/postnhost/api/v1/mcp_controller.rb +53 -0
  8. data/app/controllers/postnhost/api_tokens_controller.rb +34 -0
  9. data/app/controllers/postnhost/articles/translations_controller.rb +2 -9
  10. data/app/controllers/postnhost/articles_controller.rb +5 -6
  11. data/app/controllers/postnhost/categories/translations_controller.rb +2 -9
  12. data/app/controllers/postnhost/management_preview_controller.rb +17 -0
  13. data/app/controllers/postnhost/oauth/authorizations_controller.rb +50 -0
  14. data/app/controllers/postnhost/oauth/metadata_controller.rb +25 -0
  15. data/app/controllers/postnhost/oauth/public_controller.rb +16 -0
  16. data/app/controllers/postnhost/oauth/registrations_controller.rb +29 -0
  17. data/app/controllers/postnhost/oauth/tokens_controller.rb +19 -0
  18. data/app/controllers/postnhost/pages/translations_controller.rb +2 -9
  19. data/app/controllers/postnhost/sessions_controller.rb +2 -1
  20. data/app/models/postnhost/api_token.rb +26 -0
  21. data/app/models/postnhost/oauth_client.rb +29 -0
  22. data/app/models/postnhost/oauth_grant.rb +32 -0
  23. data/app/models/postnhost/user.rb +3 -0
  24. data/app/services/postnhost/api/v1/openapi.rb +27 -0
  25. data/app/services/postnhost/management/catalog.rb +151 -0
  26. data/app/services/postnhost/management/command.rb +174 -0
  27. data/app/services/postnhost/management/content_actions.rb +75 -0
  28. data/app/services/postnhost/management/input.rb +49 -0
  29. data/app/services/postnhost/management/mcp.rb +54 -0
  30. data/app/services/postnhost/management/preview.rb +25 -0
  31. data/app/services/postnhost/management/resources.rb +30 -0
  32. data/app/services/postnhost/management/schema_fields.rb +25 -0
  33. data/app/services/postnhost/management/serializer.rb +40 -0
  34. data/app/services/postnhost/management/settings_actions.rb +64 -0
  35. data/app/services/postnhost/management/upload.rb +18 -0
  36. data/app/services/postnhost/translation/enqueue.rb +59 -0
  37. data/app/views/layouts/postnhost/_sidebar.html.erb +7 -0
  38. data/app/views/postnhost/api_tokens/index.html.erb +92 -0
  39. data/app/views/postnhost/oauth/authorizations/new.html.erb +18 -0
  40. data/config/routes.rb +21 -0
  41. data/db/migrate/20261008120000_create_api_tokens.rb +12 -0
  42. data/db/migrate/20261025120000_add_mcp_oauth.rb +23 -0
  43. data/lib/postnhost/engine.rb +7 -0
  44. data/lib/postnhost/oauth_discovery.rb +22 -0
  45. data/lib/postnhost/version.rb +1 -1
  46. metadata +32 -1
@@ -0,0 +1,64 @@
1
+ module Postnhost
2
+ module Management
3
+ class SettingsActions < BaseService
4
+ include Postnhost::Settings::NavigationManageable
5
+
6
+ def initialize(action:, record:, arguments:)
7
+ @action = action
8
+ @record = record
9
+ @arguments = arguments
10
+ end
11
+
12
+ def call
13
+ return upload if action.start_with?("upload_")
14
+
15
+ if %w[remove_logo remove_og_image].include?(action)
16
+ field = action == "remove_logo" ? :site_logo : :og_image
17
+ record.update!(field => nil)
18
+ return record
19
+ end
20
+
21
+ validate_locale!
22
+ case action
23
+ when "get_navigation"
24
+ @setting = record
25
+ @selected_navigation_locale_key = arguments.fetch("locale")
26
+ @default_navigation_locale_key = Language.blog_default&.html_lang || I18n.default_locale.to_s
27
+ @navigation = record.current_navigation
28
+ { tree: navigation_tree_for_editor, targets: navigation_target_options }
29
+ when "replace_navigation"
30
+ tree = arguments.fetch("tree")
31
+ record.current_navigation.replace_tree!(tree:, locale_key: arguments.fetch("locale"))
32
+ { navigation: record.current_navigation.navigation_items.to_a }
33
+ when "get_translations"
34
+ Settings::I18nOverrides.editable_translations_for_locale(arguments.fetch("locale"), record.locale_overrides)
35
+ when "update_translations"
36
+ overrides = Settings::I18nOverrides.merge_locale_overrides(record.locale_overrides, arguments.fetch("locale"), arguments.fetch("translations"))
37
+ record.update!(locale_overrides: overrides)
38
+ record
39
+ else
40
+ raise ArgumentError, "Unsupported action"
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ attr_reader :action, :record, :arguments
47
+
48
+ def validate_locale!
49
+ raise ArgumentError, "Locale must belong to a configured language" unless Language.exists?(html_lang: arguments["locale"])
50
+ end
51
+
52
+ def upload
53
+ file = Upload.open(arguments.fetch("file"))
54
+ if action == "upload_image"
55
+ result = record.upload_inline_image(file)
56
+ return result[:success] ? success({ url: result[:url] }) : failure(result[:error])
57
+ end
58
+ field = { "upload_cover" => :cover_image, "upload_avatar" => :avatar_file, "upload_logo" => :site_logo, "upload_og_image" => :og_image }.fetch(action)
59
+ record.update!(field => file)
60
+ { url: record.public_send(field).url }
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,18 @@
1
+ require "base64"
2
+ require "stringio"
3
+
4
+ module Postnhost
5
+ module Management
6
+ module Upload
7
+ def self.open(payload)
8
+ filename = payload.fetch("filename")
9
+ raise ArgumentError, "Invalid filename" if filename.blank? || filename != File.basename(filename) || filename.include?("\\")
10
+
11
+ file = StringIO.new(Base64.strict_decode64(payload.fetch("base64")))
12
+ file.define_singleton_method(:original_filename) { filename }
13
+ file.define_singleton_method(:content_type) { payload.fetch("content_type") }
14
+ file
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,59 @@
1
+ module Postnhost
2
+ module Translation
3
+ class Enqueue < BaseService
4
+ def initialize(record:, language_ids:)
5
+ @record = record
6
+ @language_ids = language_ids
7
+ end
8
+
9
+ def call
10
+ return success({ variants: [], job_ids: [] }) if language_ids.empty?
11
+
12
+ kind = record.class.model_name.element
13
+ return failure("Publish the source before generating translations") if record.respond_to?(:published?) && !record.published?
14
+
15
+ languages = language_ids.map { |id| Language.find(id) }
16
+ primary_id = record.respond_to?(:language_id) ? record.language_id : Language.blog_default&.id
17
+ return failure("Cannot translate into the source language") if languages.any? { |language| language.id == primary_id }
18
+
19
+ variants = record.class.transaction do
20
+ languages.map { |language| record.public_send("#{kind}_variants").create!(language:, generating: true) }
21
+ end
22
+ enqueue_variants(variants, Translation.const_get("#{kind.camelize}VariantJob", false))
23
+ rescue ActiveRecord::RecordInvalid => e
24
+ failure(e.record.errors.full_messages)
25
+ rescue ActiveRecord::RecordNotFound
26
+ failure("Language not found", status: :not_found)
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :record, :language_ids
32
+
33
+ def enqueue_variants(variants, job_class)
34
+ jobs = []
35
+ variants.each do |variant|
36
+ job = enqueue(job_class, variant)
37
+ unless job
38
+ unqueued = variants.drop(jobs.size)
39
+ unqueued.each(&:destroy!)
40
+ return ServiceResult.new(
41
+ value: { variants: variants.take(jobs.size), job_ids: jobs.map(&:job_id), failed_language_ids: unqueued.map(&:language_id) },
42
+ errors: ["Could not queue all translations. Retry the failed languages."], status: :service_unavailable
43
+ )
44
+ end
45
+ jobs << job
46
+ end
47
+ success({ variants:, job_ids: jobs.map(&:job_id) }, status: :accepted)
48
+ end
49
+
50
+ def enqueue(job_class, variant)
51
+ job = job_class.new(record.id, variant.language_id)
52
+ job_class.perform_later(job)
53
+ rescue StandardError => e
54
+ Rails.logger.error("Postnhost translation enqueue failed (#{e.class})")
55
+ job if job&.successfully_enqueued?
56
+ end
57
+ end
58
+ end
59
+ end
@@ -58,6 +58,13 @@
58
58
  <span class="text-sm font-medium">Settings</span>
59
59
  <% end %>
60
60
 
61
+ <%= link_to api_tokens_path, class: "group flex items-center space-x-3 text-gray-700 hover:text-gray-900 hover:bg-white font-medium px-3 py-2.5 rounded-xl transition-all duration-200 #{controller_name == 'api_tokens' ? 'bg-white text-gray-900' : ''}" do %>
62
+ <div class="flex-shrink-0 <%= controller_name == 'api_tokens' ? 'text-gray-900' : 'text-gray-500 group-hover:text-gray-700' %>">
63
+ <%= icon("code-bracket", css_class: "h-5 w-5") %>
64
+ </div>
65
+ <span class="text-sm font-medium">API &amp; MCP</span>
66
+ <% end %>
67
+
61
68
  <%= link_to edit_template_path, class: "group flex items-center space-x-3 text-gray-700 hover:text-gray-900 hover:bg-white font-medium px-3 py-2.5 rounded-xl transition-all duration-200 #{current_page?(edit_template_path) || controller_name == 'templates' ? 'bg-white text-gray-900' : ''}" do %>
62
69
  <div class="flex-shrink-0 #{current_page?(edit_template_path) || controller_name == 'templates' ? 'text-gray-900' : 'text-gray-500 group-hover:text-gray-700'}">
63
70
  <%= icon("eye", css_class: "h-5 w-5") %>
@@ -0,0 +1,92 @@
1
+ <% content_for :title, "API & MCP" %>
2
+ <% content_for :head do %>
3
+ <meta name="turbo-cache-control" content="no-cache">
4
+ <% end %>
5
+
6
+ <div class="space-y-8">
7
+ <%= render "postnhost/shared/elements/page_header", title: "API & MCP access", description: "Connect agents and integrations to your CMS." %>
8
+
9
+ <% if @secret.present? %>
10
+ <section class="rounded-2xl border border-green-200 bg-green-50 p-6 space-y-3" aria-label="New token">
11
+ <div class="flex items-center gap-2 text-green-800">
12
+ <%= icon("check-circle", css_class: "h-5 w-5") %>
13
+ <h2 class="font-medium">Token created</h2>
14
+ </div>
15
+ <p class="text-sm text-green-800">Copy this token now. It is shown only once.</p>
16
+ <code class="block rounded-xl border border-green-200 bg-white p-4 text-sm text-gray-900 break-all select-all"><%= @secret %></code>
17
+ </section>
18
+ <% end %>
19
+
20
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
21
+ <section class="lg:col-span-2 <%= form_card_classes %>" aria-labelledby="create-token-title">
22
+ <div>
23
+ <h2 id="create-token-title" class="text-lg font-medium text-gray-900">Create an access token</h2>
24
+ <p class="mt-2 text-sm text-gray-500">Tokens grant your CMS permissions. Give each integration its own token so you can revoke access separately.</p>
25
+ </div>
26
+ <%= form_with scope: :api_token, url: api_tokens_path, data: { turbo: false }, class: "space-y-6" do |form| %>
27
+ <div>
28
+ <%= form.label :name, class: form_label_classes %>
29
+ <%= form.text_field :name, required: true, placeholder: "e.g. Writing agent", class: form_input_classes %>
30
+ </div>
31
+ <%= form.submit "Create token", class: button_classes %>
32
+ <% end %>
33
+ </section>
34
+
35
+ <section class="<%= form_card_classes %>" aria-labelledby="connection-title">
36
+ <div>
37
+ <h2 id="connection-title" class="text-lg font-medium text-gray-900">Connection details</h2>
38
+ <p class="mt-2 text-sm text-gray-500">Connect an MCP client by signing in through your browser, or use a token for the management API.</p>
39
+ </div>
40
+ <dl class="space-y-5">
41
+ <div>
42
+ <dt class="text-sm font-medium text-gray-700">Management API · v1</dt>
43
+ <dd class="mt-2 rounded-xl bg-gray-50 p-3 text-xs text-gray-900 break-all"><code><%= api_v1_commands_url %></code></dd>
44
+ </div>
45
+ <div>
46
+ <dt class="text-sm font-medium text-gray-700">MCP · v1</dt>
47
+ <dd class="mt-2 rounded-xl bg-gray-50 p-3 text-xs text-gray-900 break-all"><code><%= api_v1_mcp_url %></code></dd>
48
+ <dd class="mt-2 text-xs text-gray-500">Connect using Streamable HTTP and OAuth. Access remains active until revoked.</dd>
49
+ </div>
50
+ <div>
51
+ <dt class="text-sm font-medium text-gray-700">Authorization header</dt>
52
+ <dd class="mt-2 rounded-xl bg-gray-50 p-3 text-xs text-gray-900 break-all"><code>Bearer &lt;your-token&gt;</code></dd>
53
+ </div>
54
+ </dl>
55
+ </section>
56
+ </div>
57
+
58
+ <section class="space-y-4" aria-labelledby="access-tokens-title">
59
+ <div>
60
+ <h2 id="access-tokens-title" class="text-lg font-medium text-gray-900">Access tokens</h2>
61
+ <p class="mt-2 text-sm text-gray-500">Manage the tokens you have created for your account.</p>
62
+ </div>
63
+ <% if @api_tokens.any? %>
64
+ <%= render "postnhost/shared/elements/table_container", headers: [
65
+ { text: "Token", class: "col-span-8" },
66
+ { text: "Actions", class: "col-span-4 text-right" }
67
+ ] do %>
68
+ <% @api_tokens.each do |token| %>
69
+ <div class="<%= table_row_classes %>">
70
+ <div class="grid grid-cols-12 gap-4 items-center">
71
+ <div class="col-span-8 min-w-0 space-y-2">
72
+ <p class="font-medium text-gray-900 break-words"><%= token.name %></p>
73
+ <% if token.revoked_at? %>
74
+ <span class="inline-flex <%= stats_badge_classes(variant: :gray) %>">Revoked</span>
75
+ <% else %>
76
+ <span class="inline-flex <%= stats_badge_classes(variant: :green) %>">Active</span>
77
+ <% end %>
78
+ </div>
79
+ <div class="col-span-4 flex justify-end">
80
+ <% unless token.revoked_at? %>
81
+ <%= button_to "Revoke", api_token_path(token), method: :delete, class: button_classes(variant: :danger) %>
82
+ <% end %>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ <% end %>
87
+ <% end %>
88
+ <% else %>
89
+ <%= render "postnhost/shared/elements/empty_state", icon: "code-bracket", title: "No access tokens", description: "Create a token above to connect your first agent or integration." %>
90
+ <% end %>
91
+ </section>
92
+ </div>
@@ -0,0 +1,18 @@
1
+ <% content_for :title, "Connect MCP" %>
2
+
3
+ <section class="max-w-xl space-y-6" aria-labelledby="oauth-title">
4
+ <h1 id="oauth-title" class="text-xl font-medium text-gray-900">Connect <%= @client.name %>?</h1>
5
+ <p class="text-gray-700">This application will be able to read and manage your CMS content and settings, including publishing and deleting content.</p>
6
+ <p class="text-sm text-gray-500">Access remains active until you revoke it in API &amp; MCP.</p>
7
+ <dl class="space-y-2">
8
+ <dt class="text-sm font-medium text-gray-700">Application callback</dt>
9
+ <dd class="text-sm text-gray-700 break-all"><%= @oauth_request[:redirect_uri] %></dd>
10
+ </dl>
11
+ <%= form_with url: oauth_authorize_path, data: { turbo: false }, class: "flex gap-3" do |form| %>
12
+ <% @oauth_request.each do |key, value| %>
13
+ <%= hidden_field_tag key, value %>
14
+ <% end %>
15
+ <%= form.button "Connect", name: "decision", value: "approve", class: button_classes %>
16
+ <%= form.button "Cancel", name: "decision", value: "deny", class: button_classes(variant: :secondary) %>
17
+ <% end %>
18
+ </section>
data/config/routes.rb CHANGED
@@ -33,6 +33,16 @@ Postnhost::Engine.routes.draw do
33
33
  end
34
34
 
35
35
  # Admin routes
36
+ namespace :oauth do
37
+ get "resource_metadata", to: "metadata#resource"
38
+ get "server_metadata", to: "metadata#server"
39
+ get "authorize", to: "authorizations#new"
40
+ post "authorize", to: "authorizations#create"
41
+ post "register", to: "registrations#create"
42
+ post "token", to: "tokens#create"
43
+ end
44
+
45
+ resources :api_tokens, only: %i[index create destroy]
36
46
  resource :onboarding, only: %i[show update], controller: "onboarding"
37
47
  resource :session, only: %i[new create destroy], controller: "sessions"
38
48
  resources :users, only: %i[index new create edit update destroy] do
@@ -106,6 +116,17 @@ Postnhost::Engine.routes.draw do
106
116
  resources :site_scripts, only: %i[create update destroy], controller: "settings/site_scripts"
107
117
  end
108
118
 
119
+ # Management API and MCP
120
+ namespace :api do
121
+ namespace :v1 do
122
+ get "openapi", to: "commands#schema", as: :openapi
123
+ get "commands", to: "commands#index", as: :commands
124
+ post "commands/:command", to: "commands#create", as: :command
125
+ post "mcp", to: "mcp#create", as: :mcp
126
+ match "mcp", to: "mcp#unsupported", via: %i[get delete]
127
+ end
128
+ end
129
+
109
130
  # Public static pages from host app views:
110
131
  # app/views/postnhost/static_pages/*.html.erb
111
132
  get "/:slug", to: "public/static_pages#show", as: :public_static_page
@@ -0,0 +1,12 @@
1
+ class CreateApiTokens < ActiveRecord::Migration[7.2]
2
+ def change
3
+ create_table :postnhost_api_tokens do |t|
4
+ t.references :user, null: false, foreign_key: { to_table: :postnhost_users }
5
+ t.string :name, null: false
6
+ t.string :token_digest, null: false
7
+ t.datetime :revoked_at
8
+ t.timestamps
9
+ end
10
+ add_index :postnhost_api_tokens, :token_digest, unique: true
11
+ end
12
+ end
@@ -0,0 +1,23 @@
1
+ class AddMcpOauth < ActiveRecord::Migration[7.2]
2
+ def change
3
+ create_table :postnhost_oauth_clients do |t|
4
+ t.string :client_id, null: false
5
+ t.string :name, null: false
6
+ t.json :redirect_uris, null: false
7
+ t.timestamps
8
+ end
9
+ add_index :postnhost_oauth_clients, :client_id, unique: true
10
+
11
+ create_table :postnhost_oauth_grants do |t|
12
+ t.references :oauth_client, null: false, foreign_key: { to_table: :postnhost_oauth_clients }
13
+ t.references :user, null: false, foreign_key: { to_table: :postnhost_users }
14
+ t.string :code_digest, null: false
15
+ t.string :code_challenge, null: false
16
+ t.string :redirect_uri, null: false
17
+ t.string :resource, null: false
18
+ t.datetime :expires_at, null: false
19
+ end
20
+ add_index :postnhost_oauth_grants, :code_digest, unique: true
21
+ add_column :postnhost_api_tokens, :resource, :string
22
+ end
23
+ end
@@ -1,7 +1,14 @@
1
+ require_relative "oauth_discovery"
2
+
1
3
  module Postnhost
2
4
  class Engine < ::Rails::Engine
3
5
  isolate_namespace Postnhost
4
6
 
7
+ initializer "postnhost.oauth" do |app|
8
+ app.middleware.use Postnhost::OauthDiscovery
9
+ app.config.filter_parameters += %i[code code_verifier access_token]
10
+ end
11
+
5
12
  # Initialize configuration before other initializers
6
13
  initializer "postnhost.configuration", before: :load_config_initializers do
7
14
  Postnhost.configure
@@ -0,0 +1,22 @@
1
+ module Postnhost
2
+ # Well-known discovery lives at the origin even when the engine is mounted at /blog.
3
+ class OauthDiscovery
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ path = env["PATH_INFO"]
10
+ if env["REQUEST_METHOD"] == "GET" && path.match?(%r{\A/\.well-known/oauth-authorization-server(?:/.*)?\z})
11
+ status, headers, body = @app.call(env)
12
+ return [status, headers, body] unless status == 404
13
+
14
+ body.close if body.respond_to?(:close)
15
+ mount = path.delete_prefix("/.well-known/oauth-authorization-server")
16
+ return @app.call(env.merge("PATH_INFO" => "#{mount}/oauth/server_metadata"))
17
+ end
18
+
19
+ @app.call(env)
20
+ end
21
+ end
22
+ end
@@ -1,3 +1,3 @@
1
1
  module Postnhost
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.1"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: postnhost
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kirill Shevchenko
@@ -323,6 +323,10 @@ files:
323
323
  - app/controllers/concerns/postnhost/settings/navigation_manageable.rb
324
324
  - app/controllers/concerns/postnhost/settings/schema_manageable.rb
325
325
  - app/controllers/concerns/postnhost/users/schema_manageable.rb
326
+ - app/controllers/postnhost/api/base_controller.rb
327
+ - app/controllers/postnhost/api/v1/commands_controller.rb
328
+ - app/controllers/postnhost/api/v1/mcp_controller.rb
329
+ - app/controllers/postnhost/api_tokens_controller.rb
326
330
  - app/controllers/postnhost/application_controller.rb
327
331
  - app/controllers/postnhost/article_variants_controller.rb
328
332
  - app/controllers/postnhost/articles/cover_images_controller.rb
@@ -333,6 +337,12 @@ files:
333
337
  - app/controllers/postnhost/categories_controller.rb
334
338
  - app/controllers/postnhost/category_variants_controller.rb
335
339
  - app/controllers/postnhost/languages_controller.rb
340
+ - app/controllers/postnhost/management_preview_controller.rb
341
+ - app/controllers/postnhost/oauth/authorizations_controller.rb
342
+ - app/controllers/postnhost/oauth/metadata_controller.rb
343
+ - app/controllers/postnhost/oauth/public_controller.rb
344
+ - app/controllers/postnhost/oauth/registrations_controller.rb
345
+ - app/controllers/postnhost/oauth/tokens_controller.rb
336
346
  - app/controllers/postnhost/onboarding_controller.rb
337
347
  - app/controllers/postnhost/page_variants_controller.rb
338
348
  - app/controllers/postnhost/pages/images_controller.rb
@@ -399,6 +409,7 @@ files:
399
409
  - app/models/concerns/postnhost/primary_language_guard.rb
400
410
  - app/models/concerns/postnhost/public_revision_touch.rb
401
411
  - app/models/concerns/postnhost/schema_localizable.rb
412
+ - app/models/postnhost/api_token.rb
402
413
  - app/models/postnhost/application_record.rb
403
414
  - app/models/postnhost/article.rb
404
415
  - app/models/postnhost/article_author.rb
@@ -410,6 +421,8 @@ files:
410
421
  - app/models/postnhost/language.rb
411
422
  - app/models/postnhost/navigation.rb
412
423
  - app/models/postnhost/navigation_item.rb
424
+ - app/models/postnhost/oauth_client.rb
425
+ - app/models/postnhost/oauth_grant.rb
413
426
  - app/models/postnhost/page.rb
414
427
  - app/models/postnhost/page_variant.rb
415
428
  - app/models/postnhost/public_site_revision.rb
@@ -426,9 +439,21 @@ files:
426
439
  - app/models/postnhost/template.rb
427
440
  - app/models/postnhost/user.rb
428
441
  - app/models/postnhost/version.rb
442
+ - app/services/postnhost/api/v1/openapi.rb
429
443
  - app/services/postnhost/base_service.rb
430
444
  - app/services/postnhost/constraints/public_locale_constraint.rb
431
445
  - app/services/postnhost/constraints/public_route_constraint.rb
446
+ - app/services/postnhost/management/catalog.rb
447
+ - app/services/postnhost/management/command.rb
448
+ - app/services/postnhost/management/content_actions.rb
449
+ - app/services/postnhost/management/input.rb
450
+ - app/services/postnhost/management/mcp.rb
451
+ - app/services/postnhost/management/preview.rb
452
+ - app/services/postnhost/management/resources.rb
453
+ - app/services/postnhost/management/schema_fields.rb
454
+ - app/services/postnhost/management/serializer.rb
455
+ - app/services/postnhost/management/settings_actions.rb
456
+ - app/services/postnhost/management/upload.rb
432
457
  - app/services/postnhost/onboarding/step_processor.rb
433
458
  - app/services/postnhost/onboarding/steps/base.rb
434
459
  - app/services/postnhost/onboarding/steps/choose_languages.rb
@@ -465,6 +490,7 @@ files:
465
490
  - app/services/postnhost/translation/article_variant_translator.rb
466
491
  - app/services/postnhost/translation/category_variant_translator.rb
467
492
  - app/services/postnhost/translation/content_translator.rb
493
+ - app/services/postnhost/translation/enqueue.rb
468
494
  - app/services/postnhost/translation/openai_llm_client.rb
469
495
  - app/services/postnhost/translation/page_variant_translator.rb
470
496
  - app/uploaders/base_uploader.rb
@@ -486,6 +512,7 @@ files:
486
512
  - app/views/layouts/postnhost/public_header/_dashboard_sign_in.html.erb
487
513
  - app/views/layouts/postnhost/public_header/_language_switcher.html.erb
488
514
  - app/views/layouts/postnhost/public_header/_logo.html.erb
515
+ - app/views/postnhost/api_tokens/index.html.erb
489
516
  - app/views/postnhost/article_variants/_variants_list.html.erb
490
517
  - app/views/postnhost/article_variants/edit.html.erb
491
518
  - app/views/postnhost/article_variants/index.html.erb
@@ -517,6 +544,7 @@ files:
517
544
  - app/views/postnhost/languages/index.html.erb
518
545
  - app/views/postnhost/languages/new.html.erb
519
546
  - app/views/postnhost/languages/show.html.erb
547
+ - app/views/postnhost/oauth/authorizations/new.html.erb
520
548
  - app/views/postnhost/onboarding/_post_install_checklist.html.erb
521
549
  - app/views/postnhost/onboarding/_step_1.html.erb
522
550
  - app/views/postnhost/onboarding/_step_2.html.erb
@@ -669,6 +697,8 @@ files:
669
697
  - db/migrate/20260528000002_create_page_variants.rb
670
698
  - db/migrate/20260709090000_create_snapshots.rb
671
699
  - db/migrate/20260726130000_create_site_scripts.rb
700
+ - db/migrate/20261008120000_create_api_tokens.rb
701
+ - db/migrate/20261025120000_add_mcp_oauth.rb
672
702
  - lib/generators/postnhost/install/install_generator.rb
673
703
  - lib/generators/postnhost/install/templates/carrierwave.rb
674
704
  - lib/generators/postnhost/install/templates/postnhost.rb
@@ -683,6 +713,7 @@ files:
683
713
  - lib/postnhost/css_build.rb
684
714
  - lib/postnhost/css_scope.rb
685
715
  - lib/postnhost/engine.rb
716
+ - lib/postnhost/oauth_discovery.rb
686
717
  - lib/postnhost/sample_data.rb
687
718
  - lib/postnhost/settings/i18n_overrides.rb
688
719
  - lib/postnhost/settings/i18n_patch.rb