fasp_client 0.5.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 +7 -0
- data/LICENSE.md +9 -0
- data/README.md +119 -0
- data/Rakefile +8 -0
- data/app/controllers/fasp_client/application_controller.rb +44 -0
- data/app/controllers/fasp_client/backfill_requests_controller.rb +21 -0
- data/app/controllers/fasp_client/event_subscriptions_controller.rb +31 -0
- data/app/controllers/fasp_client/providers_controller.rb +46 -0
- data/app/helpers/fasp_client/application_helper.rb +11 -0
- data/app/jobs/fasp_client/lifecycle_announcement_job.rb +7 -0
- data/app/models/concerns/fasp_client/data_sharing/lifecycle.rb +36 -0
- data/app/models/fasp_client/application_record.rb +7 -0
- data/app/models/fasp_client/backfill_request.rb +9 -0
- data/app/models/fasp_client/event_subscription.rb +28 -0
- data/app/models/fasp_client/provider.rb +115 -0
- data/app/services/fasp_client/account_search_service.rb +16 -0
- data/app/services/fasp_client/capability_activation_service.rb +27 -0
- data/app/services/fasp_client/follow_recommendation_service.rb +16 -0
- data/app/services/fasp_client/http_request_service.rb +32 -0
- data/app/services/fasp_client/provider_info_service.rb +23 -0
- data/app/views/fasp_client/providers/edit.html.erb +29 -0
- data/app/views/fasp_client/providers/index.html.erb +13 -0
- data/config/routes.rb +6 -0
- data/db/migrate/20250801150509_create_fasp_client_providers.rb +19 -0
- data/db/migrate/20250905153345_create_fasp_client_event_subscriptions.rb +11 -0
- data/db/migrate/20250908164011_create_fasp_client_backfill_requests.rb +11 -0
- data/lib/fasp_client/configuration.rb +15 -0
- data/lib/fasp_client/ed25519_signing_key_coder.rb +11 -0
- data/lib/fasp_client/engine.rb +15 -0
- data/lib/fasp_client/version.rb +3 -0
- data/lib/fasp_client.rb +13 -0
- data/lib/generators/fasp_client/install/USAGE +9 -0
- data/lib/generators/fasp_client/install/install_generator.rb +13 -0
- data/lib/generators/fasp_client/install/templates/fasp_client.rb +16 -0
- data/lib/generators/fasp_client/views/USAGE +8 -0
- data/lib/generators/fasp_client/views/views_generator.rb +9 -0
- data/lib/tasks/fasp_client_tasks.rake +4 -0
- metadata +221 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 79b0748608e7c9b2fc0c9cb096954fa37b8270b8467c839175d0799ce809afec
|
4
|
+
data.tar.gz: 7f0c2a9d5d9cef162b00edde1711d52d175ce726725db8e79ddea15aa5ace00c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3c93b8087b5fb053fde2c40cfab4e8fe212c10416a80791aa1bb74935f3e2a689aa0368847f20f06f28bec1d1ea6abd65e316757f4d37b25d6064c0f67293f53
|
7
|
+
data.tar.gz: 5261bfcaf33617dc941f57e07f8bf0689be42d48bfcbbb382aa6d50523797e555dad3765c2407907f0d1e72192542e0caca796ddbb0c6bc07aae60c37f0d5646
|
data/LICENSE.md
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
# The MIT License
|
2
|
+
|
3
|
+
Copyright 2025 James Smith <james@floppy.org.uk>
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
6
|
+
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
8
|
+
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
# FaspClient
|
2
|
+
A Rails engine that implements the non-provider side of the [Fediverse Auxiliary Service Provider (FASP)](https://fediscovery.org) standard.
|
3
|
+
|
4
|
+
## Features
|
5
|
+
|
6
|
+
* [Base URL discovery](https://github.com/mastodon/fediverse_auxiliary_service_provider_specifications/blob/main/general/v0.1/protocol_basics.md#base-url) ✅
|
7
|
+
* [Request integrity](https://github.com/mastodon/fediverse_auxiliary_service_provider_specifications/blob/main/general/v0.1/protocol_basics.md#request-integrity) ✅
|
8
|
+
* [Authentication](https://github.com/mastodon/fediverse_auxiliary_service_provider_specifications/blob/main/general/v0.1/protocol_basics.md#authentication) ✅
|
9
|
+
* [Provider registration](https://github.com/mastodon/fediverse_auxiliary_service_provider_specifications/blob/main/general/v0.1/registration.md) ✅
|
10
|
+
* [Accepting registration requests](https://github.com/mastodon/fediverse_auxiliary_service_provider_specifications/blob/main/general/v0.1/registration.md) ✅
|
11
|
+
* [Selecting capabilities](https://github.com/mastodon/fediverse_auxiliary_service_provider_specifications/blob/main/general/v0.1/registration.md#selecting-capabilities) ✅
|
12
|
+
* [Fetching FASP information](https://github.com/mastodon/fediverse_auxiliary_service_provider_specifications/blob/main/general/v0.1/provider_info.md) ✅
|
13
|
+
* [Capability APIs](https://github.com/mastodon/fediverse_auxiliary_service_provider_specifications/blob/main/general/v0.1/provider_specifications.md) ⏳
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
Add to your application's Gemfile:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
gem "fasp_client"
|
21
|
+
```
|
22
|
+
|
23
|
+
Install configuration and run migrations (you'll also want to do this when upgrading).
|
24
|
+
|
25
|
+
```shell
|
26
|
+
bin/rails generate fasp_client:install && bin/rails db:migrate
|
27
|
+
```
|
28
|
+
|
29
|
+
You will probably want to customise the view templates used for editing and listing providers.
|
30
|
+
You can copy the default views like so:
|
31
|
+
|
32
|
+
```shell
|
33
|
+
bin/rails generate fasp_client:views
|
34
|
+
```
|
35
|
+
|
36
|
+
## Usage
|
37
|
+
|
38
|
+
Mount the engine in your `routes.rb` file:
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
mount FaspClient::Engine => "/fasp"
|
42
|
+
```
|
43
|
+
|
44
|
+
Add the base URL to your nodeinfo metadata:
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
"faspBaseUrl" => Rails.application.routes.url_helpers.fasp_client_url
|
48
|
+
```
|
49
|
+
|
50
|
+
If you're using [Federails](https://gitlab.com/experimentslabs/federails), you can add this to the metadata using the configuration option (currently only on the `nodeinfo-metadata` branch):
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
conf.nodeinfo_metadata = -> do
|
54
|
+
{"faspBaseUrl" => Rails.application.routes.url_helpers.fasp_client_url}
|
55
|
+
end
|
56
|
+
```
|
57
|
+
|
58
|
+
Edit `config/initializers/fasp_client.rb` and add customise the template `authenticate` method. It should return true if the current user should be able to access the provider approval/edit pages.
|
59
|
+
|
60
|
+
Once you've done that, you can sign up to FASP providers using the URL of your site, and you should be able to register, approve, and choose capabilities.
|
61
|
+
|
62
|
+
### Capabilities
|
63
|
+
|
64
|
+
#### account_search
|
65
|
+
|
66
|
+
Get a list of accounts matching the provided search term, returned as a simple array of account URIs. Provide the optional `limit` parameter to control now many results are returned (default = 20).
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
my_provider.account_search("mastodon", limit: 5)
|
70
|
+
=> ["https://mastodon.social/users/Gargron"]
|
71
|
+
```
|
72
|
+
|
73
|
+
#### data_sharing
|
74
|
+
|
75
|
+
Automatically shares data from your app to the FASP on demand. To configure one of your models to be included in data sharing, include the appropriate concern and set the configuration:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
class MyModel < ApplicationRecord
|
79
|
+
include FaspClient::DataSharing::Lifecycle
|
80
|
+
|
81
|
+
fasp_share_lifecycle category: "account", uri_method: :my_canonical_uri
|
82
|
+
|
83
|
+
def my_canonical_uri
|
84
|
+
# Any method that returns the canonical activitypub URI for this object
|
85
|
+
end
|
86
|
+
end
|
87
|
+
```
|
88
|
+
|
89
|
+
Only lifecycle events are currently supported, trending events will be implemented in future. Valid categories are currently `account`, or `content` (see [the spec](https://github.com/mastodon/fediverse_auxiliary_service_provider_specifications/blob/main/discovery/data_sharing/v0.1/data_sharing.md) for details).
|
90
|
+
|
91
|
+
The actual announcements are then sent to all subscribed providers by a background ActiveJob, using the `default` queue. You can set a specific queue name by adding an optional `queue` parameter to `fasp_share_lifecycle`:
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
fasp_share_lifecycle category: "account", uri_method: :my_canonical_uri, queue: "fasp_broadcasts"
|
95
|
+
```
|
96
|
+
|
97
|
+
Only new lifecycle events are currently sent. Whilst backfill requests can be made, they aren't serviced yet (see https://github.com/manyfold3d/fasp_client/issues/25).
|
98
|
+
|
99
|
+
#### follow_recommendation
|
100
|
+
|
101
|
+
Get a list of follow recommendations, returned as a simple array of account URIs. The account URI argument is required by the [spec](https://github.com/mastodon/fediverse_auxiliary_service_provider_specifications/blob/main/discovery/follow_recommendation/v0.1/follow_recommendation.md), but won't necessarily affect the results, depending on the server implementation.
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
my_provider.follow_recommendation(your_account_uri)
|
105
|
+
=> [
|
106
|
+
"https://mastodon.me.uk/users/Floppy",
|
107
|
+
"https://mastodon.social/users/Gargron"
|
108
|
+
]
|
109
|
+
```
|
110
|
+
|
111
|
+
## License
|
112
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
113
|
+
|
114
|
+
## Credits
|
115
|
+
|
116
|
+
This code was originally written for the [Manyfold](https://github.com/manyfold3d/manyfold) project, which is funded through [NGI0 Entrust](https://nlnet.nl/entrust), a fund established by [NLnet](https://nlnet.nl) with financial support from the European Commission's [Next Generation Internet](https://ngi.eu) program. Learn more at the [NLnet project page](https://nlnet.nl/project/Personal-3D-archive).
|
117
|
+
|
118
|
+
[<img src="https://nlnet.nl/logo/banner.png" alt="NLnet foundation logo" width="20%" />](https://nlnet.nl)
|
119
|
+
[<img src="https://nlnet.nl/image/logos/NGI0_tag.svg" alt="NGI Zero Logo" width="20%" />](https://nlnet.nl/entrust)
|
data/Rakefile
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
module FaspClient
|
2
|
+
class ApplicationController < FaspClient::Configuration.instance.controller_base.constantize
|
3
|
+
layout FaspClient::Configuration.instance.layout
|
4
|
+
|
5
|
+
after_action :sign_response
|
6
|
+
|
7
|
+
def fasp_client_controller?
|
8
|
+
true
|
9
|
+
end
|
10
|
+
|
11
|
+
def get_provider
|
12
|
+
head :unauthorized and return unless request.headers.key?("Signature-Input")
|
13
|
+
m = request.headers["Signature-Input"].match(/keyid=\"([^\"]+)\"/)
|
14
|
+
@provider = FaspClient::Provider.find_by(uuid: m[1]) if m
|
15
|
+
head :unauthorized if @provider.nil?
|
16
|
+
end
|
17
|
+
|
18
|
+
def verify_request
|
19
|
+
head :unauthorized unless @provider.valid_request?(request)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def authenticate
|
25
|
+
head :forbidden unless FaspClient::Configuration.instance.authenticate.call(request)
|
26
|
+
end
|
27
|
+
|
28
|
+
def sign_response
|
29
|
+
if @provider
|
30
|
+
response["date"] = Time.now.utc.to_s
|
31
|
+
response["content-digest"] = "sha-256=:"+Digest::SHA256.base64digest(response.body || "")+":"
|
32
|
+
Linzer.sign!(
|
33
|
+
response,
|
34
|
+
key: @provider.local_linzer_key,
|
35
|
+
components: %w[@status content-digest],
|
36
|
+
label: "sig1",
|
37
|
+
params: {
|
38
|
+
created: Time.now.utc.to_i
|
39
|
+
}
|
40
|
+
)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module FaspClient
|
2
|
+
class BackfillRequestsController < ApplicationController
|
3
|
+
wrap_parameters :backfill_request, include: [ :category, :maxCount ]
|
4
|
+
before_action :get_provider
|
5
|
+
before_action :verify_request
|
6
|
+
|
7
|
+
def create
|
8
|
+
options = params.deep_transform_keys(&:underscore).expect(backfill_request: [ :category, :max_count ])
|
9
|
+
req = FaspClient::BackfillRequest.create(options.merge(fasp_client_provider: @provider))
|
10
|
+
if req.valid?
|
11
|
+
respond_to do |format|
|
12
|
+
format.json do
|
13
|
+
render json: { "backfillRequest" => { "id" => req.id.to_s } }, status: :created
|
14
|
+
end
|
15
|
+
end
|
16
|
+
else
|
17
|
+
head :unprocessable_content
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module FaspClient
|
2
|
+
class EventSubscriptionsController < ApplicationController
|
3
|
+
wrap_parameters :event_subscription, include: [ :category, :subscriptionType ]
|
4
|
+
before_action :get_provider
|
5
|
+
before_action :verify_request
|
6
|
+
|
7
|
+
def create
|
8
|
+
options = params.deep_transform_keys(&:underscore).expect(event_subscription: [ :category, :subscription_type ])
|
9
|
+
if options[:category] == "content" && options[:subscription_type] == "trends"
|
10
|
+
head :not_implemented
|
11
|
+
return
|
12
|
+
end
|
13
|
+
sub = FaspClient::EventSubscription.create(options.merge(fasp_client_provider: @provider))
|
14
|
+
if sub.valid?
|
15
|
+
respond_to do |format|
|
16
|
+
format.json do
|
17
|
+
render json: { "subscription" => { "id" => sub.id.to_s } }, status: :created
|
18
|
+
end
|
19
|
+
end
|
20
|
+
else
|
21
|
+
head :unprocessable_content
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def destroy
|
26
|
+
@subscription = @provider.fasp_client_event_subscriptions.find(params[:id])
|
27
|
+
@subscription.destroy
|
28
|
+
head :no_content
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module FaspClient
|
2
|
+
class ProvidersController < ApplicationController
|
3
|
+
wrap_parameters :provider, include: [ :name, :baseUrl, :serverId, :publicKey ]
|
4
|
+
protect_from_forgery with: :null_session, only: :create
|
5
|
+
before_action :authenticate, except: [ :create ]
|
6
|
+
before_action :get_provider, except: [ :create, :index ]
|
7
|
+
|
8
|
+
def create
|
9
|
+
attributes = params.deep_transform_keys(&:underscore).expect(provider: [ :name, :base_url, :server_id, :public_key ])
|
10
|
+
@provider = Provider.create(attributes)
|
11
|
+
if @provider.valid?
|
12
|
+
render json: {
|
13
|
+
faspId: @provider.uuid,
|
14
|
+
publicKey: Base64.strict_encode64(@provider.ed25519_signing_key.verify_key.to_bytes),
|
15
|
+
registrationCompletionUri: edit_provider_url(@provider)
|
16
|
+
}, status: :created
|
17
|
+
else
|
18
|
+
head :bad_request
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def index
|
23
|
+
@providers = Provider.all
|
24
|
+
end
|
25
|
+
|
26
|
+
def update
|
27
|
+
provider_params = params.expect(provider: [ :status, enable_capability: [ :id, :version ], disable_capability: [ :id, :version ] ])
|
28
|
+
head :bad_request and return unless provider_params
|
29
|
+
if provider_params[:enable_capability]
|
30
|
+
success = @provider.enable(provider_params[:enable_capability][:id], provider_params[:enable_capability][:version])
|
31
|
+
redirect_back_or_to edit_provider_path(@provider), notice: success ? "Enabled" : "Failed"
|
32
|
+
elsif provider_params[:disable_capability]
|
33
|
+
success = @provider.disable(provider_params[:disable_capability][:id], provider_params[:disable_capability][:version])
|
34
|
+
redirect_back_or_to edit_provider_path(@provider), notice: success ? "Disabled" : "Failed"
|
35
|
+
elsif @provider.update!(provider_params)
|
36
|
+
redirect_back_or_to edit_provider_path(@provider)
|
37
|
+
else
|
38
|
+
head :bad_request
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def get_provider
|
43
|
+
@provider = Provider.find_by(id: params[:id])
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
class FaspClient::LifecycleAnnouncementJob < ::ApplicationJob
|
2
|
+
def perform(event_type:, category:, uri:)
|
3
|
+
FaspClient::EventSubscription.where(category: category, subscription_type: "lifecycle").find_each do |sub|
|
4
|
+
sub.announce_lifecycle event_type: event_type, uri: uri
|
5
|
+
end
|
6
|
+
end
|
7
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module FaspClient
|
2
|
+
module DataSharing
|
3
|
+
module Lifecycle
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
class_methods do
|
7
|
+
def fasp_share_lifecycle(category:, uri_method:, queue: "default")
|
8
|
+
self.fasp_uri_method = uri_method
|
9
|
+
self.fasp_category = category
|
10
|
+
self.fasp_job_queue = queue
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
included do
|
15
|
+
cattr_accessor :fasp_category
|
16
|
+
cattr_accessor :fasp_uri_method
|
17
|
+
cattr_accessor :fasp_job_queue
|
18
|
+
|
19
|
+
after_commit -> { fasp_emit_lifecycle_announcement "new" }, on: :create
|
20
|
+
after_commit -> { fasp_emit_lifecycle_announcement "update" }, on: :update
|
21
|
+
before_destroy -> { fasp_emit_lifecycle_announcement "delete" }
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def fasp_object_uri
|
27
|
+
raise NotImplementedError("set object URI method using `fasp_share_lifecycle`") unless respond_to?(fasp_uri_method)
|
28
|
+
send(fasp_uri_method)
|
29
|
+
end
|
30
|
+
|
31
|
+
def fasp_emit_lifecycle_announcement(event_type)
|
32
|
+
LifecycleAnnouncementJob.set(queue: fasp_job_queue).perform_later(event_type: event_type, category: fasp_category, uri: fasp_object_uri)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module FaspClient
|
2
|
+
class BackfillRequest < ApplicationRecord
|
3
|
+
belongs_to :fasp_client_provider, class_name: "FaspClient::Provider"
|
4
|
+
|
5
|
+
validates :fasp_client_provider, presence: true
|
6
|
+
validates :category, presence: true, inclusion: FaspClient::ApplicationRecord::CATEGORIES
|
7
|
+
validates :max_count, presence: true
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module FaspClient
|
2
|
+
class EventSubscription < ApplicationRecord
|
3
|
+
belongs_to :fasp_client_provider, class_name: "FaspClient::Provider"
|
4
|
+
|
5
|
+
validates :fasp_client_provider, presence: true
|
6
|
+
validates :category, presence: true, inclusion: FaspClient::ApplicationRecord::CATEGORIES
|
7
|
+
validates :subscription_type, presence: true, inclusion: [ "lifecycle", "trends" ], if: -> { category == "content" }
|
8
|
+
validates :subscription_type, presence: true, inclusion: [ "lifecycle" ], unless: -> { category == "content" }
|
9
|
+
|
10
|
+
def announce_lifecycle(event_type:, uri:)
|
11
|
+
return if subscription_type != "lifecycle"
|
12
|
+
Rails.logger.debug("Announcing #{subscription_type} event: #{event_type} #{category} #{uri}")
|
13
|
+
request = Net::HTTP::Post.new(URI(fasp_client_provider.base_url + "/data_sharing/v0/announcements"))
|
14
|
+
request.body = {
|
15
|
+
source: {
|
16
|
+
subscription: {
|
17
|
+
id: id.to_s
|
18
|
+
}
|
19
|
+
},
|
20
|
+
category: category,
|
21
|
+
eventType: event_type,
|
22
|
+
objectUris: [ uri ]
|
23
|
+
}.to_json
|
24
|
+
request.content_type = "application/json"
|
25
|
+
HttpRequestService.new(provider: fasp_client_provider).execute!(request)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require "ed25519"
|
2
|
+
require "fasp_client/ed25519_signing_key_coder"
|
3
|
+
|
4
|
+
module FaspClient
|
5
|
+
class Provider < ApplicationRecord
|
6
|
+
enum :status, { pending: nil, approved: 1, denied: -1 }, default: :pending, validate: true
|
7
|
+
|
8
|
+
has_many :fasp_client_event_subscriptions, class_name: "FaspClient::EventSubscription", foreign_key: "fasp_client_provider_id"
|
9
|
+
has_many :fasp_client_backfill_requests, class_name: "FaspClient::BackfillRequest", foreign_key: "fasp_client_provider_id"
|
10
|
+
|
11
|
+
validates :uuid, presence: true
|
12
|
+
validates :name, presence: true
|
13
|
+
validates :base_url, presence: true
|
14
|
+
validates :server_id, presence: true
|
15
|
+
validates :public_key, presence: true
|
16
|
+
validates :ed25519_signing_key, presence: true
|
17
|
+
|
18
|
+
serialize :ed25519_signing_key, coder: FaspClient::Ed25519SigningKeyCoder
|
19
|
+
|
20
|
+
attribute :capabilities, :json, default: []
|
21
|
+
attribute :privacy_policy, :json, default: []
|
22
|
+
|
23
|
+
before_validation on: :create do
|
24
|
+
self.uuid ||= SecureRandom.uuid
|
25
|
+
self.ed25519_signing_key ||= Ed25519::SigningKey.generate
|
26
|
+
end
|
27
|
+
|
28
|
+
before_save :fetch_provider_info, if: -> { approved? && status_changed? }
|
29
|
+
|
30
|
+
def verify_key
|
31
|
+
Ed25519::VerifyKey.new(Base64.strict_decode64(public_key))
|
32
|
+
end
|
33
|
+
|
34
|
+
def fingerprint
|
35
|
+
Digest::SHA256.base64digest(verify_key)
|
36
|
+
end
|
37
|
+
|
38
|
+
def has_capability?(capability, version = nil)
|
39
|
+
version ? capability_versions(capability).include?(version) : capability_versions(capability).any?
|
40
|
+
end
|
41
|
+
|
42
|
+
def capability_versions(capability)
|
43
|
+
capabilities.filter_map { |it| it["version"] if it["id"] == capability.to_s }
|
44
|
+
end
|
45
|
+
|
46
|
+
def capability_ids
|
47
|
+
capabilities.map { |it| it["id"] }.uniq.map(&:to_sym)
|
48
|
+
end
|
49
|
+
|
50
|
+
def fetch_provider_info
|
51
|
+
assign_attributes(ProviderInfoService.new(provider: self).to_provider_attributes) if approved?
|
52
|
+
end
|
53
|
+
|
54
|
+
def enable(capability, version)
|
55
|
+
return unless has_capability?(capability, version)
|
56
|
+
CapabilityActivationService.new(provider: self, capability: capability, version: version).enable!
|
57
|
+
end
|
58
|
+
|
59
|
+
def disable(capability, version)
|
60
|
+
return unless has_capability?(capability, version)
|
61
|
+
CapabilityActivationService.new(provider: self, capability: capability, version: version).disable!
|
62
|
+
end
|
63
|
+
|
64
|
+
def follow_recommendation(account_uri)
|
65
|
+
FollowRecommendationService.new(provider: self).for(account_uri: account_uri)
|
66
|
+
end
|
67
|
+
|
68
|
+
def account_search(query, limit: 20)
|
69
|
+
AccountSearchService.new(provider: self).search(query: query, limit: limit)
|
70
|
+
end
|
71
|
+
|
72
|
+
def valid_request?(request)
|
73
|
+
HttpRequestService.new(provider: self).verified?(request)
|
74
|
+
end
|
75
|
+
|
76
|
+
def local_linzer_key
|
77
|
+
asn1 = OpenSSL::ASN1.Sequence(
|
78
|
+
[
|
79
|
+
OpenSSL::ASN1::Integer(OpenSSL::BN.new(0)),
|
80
|
+
OpenSSL::ASN1.Sequence(
|
81
|
+
[
|
82
|
+
OpenSSL::ASN1.ObjectId("ED25519")
|
83
|
+
]
|
84
|
+
),
|
85
|
+
OpenSSL::ASN1.OctetString(OpenSSL::ASN1.OctetString(ed25519_signing_key.to_bytes).to_der)
|
86
|
+
]
|
87
|
+
)
|
88
|
+
pem = <<~PEM
|
89
|
+
-----BEGIN PRIVATE KEY-----
|
90
|
+
#{Base64.strict_encode64(asn1.to_der)}
|
91
|
+
-----END PRIVATE KEY-----
|
92
|
+
PEM
|
93
|
+
Linzer.new_ed25519_key(pem, server_id)
|
94
|
+
end
|
95
|
+
|
96
|
+
def fasp_linzer_key
|
97
|
+
asn1 = OpenSSL::ASN1.Sequence(
|
98
|
+
[
|
99
|
+
OpenSSL::ASN1.Sequence(
|
100
|
+
[
|
101
|
+
OpenSSL::ASN1.ObjectId("ED25519")
|
102
|
+
]
|
103
|
+
),
|
104
|
+
OpenSSL::ASN1.BitString(verify_key.to_bytes)
|
105
|
+
]
|
106
|
+
)
|
107
|
+
pem = <<~PEM
|
108
|
+
-----BEGIN PUBLIC KEY-----
|
109
|
+
#{Base64.strict_encode64(asn1.to_der)}
|
110
|
+
-----END PUBLIC KEY-----
|
111
|
+
PEM
|
112
|
+
Linzer.new_ed25519_key(pem, uuid)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require "linzer"
|
2
|
+
|
3
|
+
module FaspClient
|
4
|
+
class AccountSearchService
|
5
|
+
def initialize(provider:)
|
6
|
+
@provider = provider
|
7
|
+
end
|
8
|
+
|
9
|
+
def search(query:, limit: 20)
|
10
|
+
response = HttpRequestService.new(provider: @provider).execute!(
|
11
|
+
Net::HTTP::Get.new(URI(@provider.base_url + "/account_search/v0/search?limit=#{limit}&term=" + CGI.escape(query)))
|
12
|
+
)
|
13
|
+
JSON.parse(response.body)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module FaspClient
|
2
|
+
class CapabilityActivationService
|
3
|
+
def initialize(provider:, capability:, version:)
|
4
|
+
@provider = provider
|
5
|
+
@capability = capability
|
6
|
+
@major_version = version.split(".").first
|
7
|
+
end
|
8
|
+
|
9
|
+
def enable!
|
10
|
+
HttpRequestService.new(provider: @provider).execute!(
|
11
|
+
Net::HTTP::Post.new(uri)
|
12
|
+
).code == "204"
|
13
|
+
end
|
14
|
+
|
15
|
+
def disable!
|
16
|
+
HttpRequestService.new(provider: @provider).execute!(
|
17
|
+
Net::HTTP::Delete.new(uri)
|
18
|
+
).code == "204"
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def uri
|
24
|
+
URI("#{@provider.base_url}/capabilities/#{@capability}/#{@major_version}/activation")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require "linzer"
|
2
|
+
|
3
|
+
module FaspClient
|
4
|
+
class FollowRecommendationService
|
5
|
+
def initialize(provider:)
|
6
|
+
@provider = provider
|
7
|
+
end
|
8
|
+
|
9
|
+
def for(account_uri:)
|
10
|
+
response = HttpRequestService.new(provider: @provider).execute!(
|
11
|
+
Net::HTTP::Get.new(URI(@provider.base_url + "/follow_recommendation/v0/accounts?accountUri=" + CGI.escape(account_uri)))
|
12
|
+
)
|
13
|
+
JSON.parse(response.body)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module FaspClient
|
2
|
+
class HttpRequestService
|
3
|
+
def initialize(provider:)
|
4
|
+
@provider = provider
|
5
|
+
end
|
6
|
+
|
7
|
+
def execute!(request)
|
8
|
+
request["date"] = Time.now.utc.to_s
|
9
|
+
request["Content-Digest"] = "sha-256=:"+Digest::SHA256.base64digest(request.body || "")+":"
|
10
|
+
Linzer.sign!(
|
11
|
+
request,
|
12
|
+
key: @provider.local_linzer_key,
|
13
|
+
components: %w[@method @target-uri content-digest],
|
14
|
+
label: "sig1",
|
15
|
+
params: {
|
16
|
+
created: Time.now.utc.to_i
|
17
|
+
}
|
18
|
+
)
|
19
|
+
response = nil
|
20
|
+
Net::HTTP.start request.uri.hostname, request.uri.port, use_ssl: (request.uri.scheme == "https") do |http|
|
21
|
+
response = http.request request
|
22
|
+
end
|
23
|
+
response
|
24
|
+
end
|
25
|
+
|
26
|
+
def verified?(request_or_response)
|
27
|
+
Linzer.verify!(request_or_response, key: @provider.fasp_linzer_key)
|
28
|
+
rescue Linzer::Error
|
29
|
+
false
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require "linzer"
|
2
|
+
|
3
|
+
module FaspClient
|
4
|
+
class ProviderInfoService
|
5
|
+
def initialize(provider:)
|
6
|
+
@provider = provider
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_provider_attributes
|
10
|
+
response = HttpRequestService.new(provider: @provider).execute!(
|
11
|
+
Net::HTTP::Get.new(URI(@provider.base_url + "/provider_info"))
|
12
|
+
)
|
13
|
+
json ||= JSON.parse(response.body)
|
14
|
+
json.slice(
|
15
|
+
"capabilities",
|
16
|
+
"privacyPolicy",
|
17
|
+
"signInUrl",
|
18
|
+
"contactEmail",
|
19
|
+
"fediverseAccount"
|
20
|
+
).deep_transform_keys(&:underscore).deep_symbolize_keys
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
<%= notice %>
|
2
|
+
|
3
|
+
<h1>Provider: <%= link_to @provider.name, @provider.base_url %></h1>
|
4
|
+
|
5
|
+
<div>
|
6
|
+
<p>Requested at: <%= @provider.created_at %></p>
|
7
|
+
<p>Status: <%= @provider.status %></p>
|
8
|
+
|
9
|
+
<% if @provider.pending? %>
|
10
|
+
<p>
|
11
|
+
Fingerprint:
|
12
|
+
<code>
|
13
|
+
<%= @provider.fingerprint %>
|
14
|
+
</code>
|
15
|
+
</p>
|
16
|
+
<%= button_to "Approve", @provider, method: :patch, params: {provider: {status: "approved"}}, form: {style: "display: inline"} %>
|
17
|
+
<%= button_to "Deny", @provider, method: :patch, params: {provider: {status: "denied"}}, form: {style: "display: inline"} %>
|
18
|
+
<% else %>
|
19
|
+
<table>
|
20
|
+
<% @provider.capabilities.each do |cap| %>
|
21
|
+
<tr>
|
22
|
+
<td><%= cap["id"] %> (<%= cap["version"] %>)</td>
|
23
|
+
<td><%= button_to "Enable", @provider, method: :patch, params: {provider: {enable_capability: {id: cap["id"], version: cap["version"]}}}, form: {style: "display: inline"} %></td>
|
24
|
+
<td><%= button_to "Disable", @provider, method: :patch, params: {provider: {disable_capability: {id: cap["id"], version: cap["version"]}}}, form: {style: "display: inline"} %></td>
|
25
|
+
</tr>
|
26
|
+
<% end %>
|
27
|
+
</table>
|
28
|
+
<% end %>
|
29
|
+
</div>
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<h1>FASP Providers</h1>
|
2
|
+
|
3
|
+
<% @providers.each do |provider| %>
|
4
|
+
<div>
|
5
|
+
<h2>
|
6
|
+
<%= link_to provider.name, provider.base_url %>
|
7
|
+
</h2>
|
8
|
+
<p>Requested at: <%= provider.created_at %></p>
|
9
|
+
<p>Status: <%= provider.status %></p>
|
10
|
+
<%= link_to "Edit", edit_provider_path(provider) %>
|
11
|
+
</div>
|
12
|
+
|
13
|
+
<% end %>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
FaspClient::Engine.routes.draw do
|
2
|
+
post "/registration" => "providers#create"
|
3
|
+
resources :providers, only: [ :index, :edit, :update ]
|
4
|
+
resources :event_subscriptions, only: [ :create, :destroy ], path: "data_sharing/v0/event_subscriptions"
|
5
|
+
resources :backfill_requests, only: [ :create ], path: "data_sharing/v0/backfill_requests"
|
6
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class CreateFaspClientProviders < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :fasp_client_providers do |t|
|
4
|
+
t.string :uuid
|
5
|
+
t.string :name
|
6
|
+
t.string :base_url
|
7
|
+
t.string :server_id
|
8
|
+
t.string :public_key
|
9
|
+
t.string :ed25519_signing_key
|
10
|
+
t.integer :status
|
11
|
+
t.json :capabilities
|
12
|
+
t.json :privacy_policy
|
13
|
+
t.string :sign_in_url
|
14
|
+
t.string :contact_email
|
15
|
+
t.string :fediverse_account
|
16
|
+
t.timestamps
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class CreateFaspClientEventSubscriptions < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :fasp_client_event_subscriptions do |t|
|
4
|
+
t.references :fasp_client_provider, null: false, foreign_key: true
|
5
|
+
t.string :category
|
6
|
+
t.string :subscription_type
|
7
|
+
|
8
|
+
t.timestamps
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class CreateFaspClientBackfillRequests < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :fasp_client_backfill_requests do |t|
|
4
|
+
t.references :fasp_client_provider, null: false, foreign_key: true
|
5
|
+
t.string :category
|
6
|
+
t.integer :max_count
|
7
|
+
|
8
|
+
t.timestamps
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module FaspClient
|
2
|
+
class Configuration
|
3
|
+
include Singleton
|
4
|
+
|
5
|
+
attr_accessor :authenticate
|
6
|
+
attr_accessor :layout
|
7
|
+
attr_accessor :controller_base
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@authenticate = ->(request) { }
|
11
|
+
@layout = "application"
|
12
|
+
@controller_base = "ActionController::Base"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require "linzer"
|
2
|
+
|
3
|
+
module FaspClient
|
4
|
+
class Engine < ::Rails::Engine
|
5
|
+
isolate_namespace FaspClient
|
6
|
+
|
7
|
+
config.to_prepare do
|
8
|
+
# Include main application helpers so we can allow it to override views
|
9
|
+
FaspClient::ApplicationController.helper Rails.application.helpers
|
10
|
+
|
11
|
+
Linzer::Message.register_adapter(ActionDispatch::Request, Linzer::Message::Adapter::Rack::Request)
|
12
|
+
Linzer::Message.register_adapter(ActionDispatch::Response, Linzer::Message::Adapter::Rack::Response)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/fasp_client.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
module FaspClient
|
2
|
+
class InstallGenerator < Rails::Generators::Base
|
3
|
+
source_root File.expand_path("templates", __dir__)
|
4
|
+
|
5
|
+
def create_initializer_file
|
6
|
+
copy_file "fasp_client.rb", Rails.root.join("config", "initializers", "fasp_client.rb")
|
7
|
+
end
|
8
|
+
|
9
|
+
def generate_migrations
|
10
|
+
rake "fasp_client:install:migrations"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
FaspClient.configure do |conf|
|
2
|
+
conf.authenticate = ->(request) do
|
3
|
+
# Return a truthy value if the current user should be able to access and edit FASP providers, otherwise
|
4
|
+
# return falsey. For example:
|
5
|
+
#
|
6
|
+
# Warden / Devise:
|
7
|
+
# request.env["warden"]&.user&.is_administrator?
|
8
|
+
end
|
9
|
+
|
10
|
+
# Configure the layout name that should be used for views; defaults to "application"
|
11
|
+
# conf.layout = "application"
|
12
|
+
|
13
|
+
# For even tighter integration, you might want to use your own ApplicationController as the base
|
14
|
+
# class for FaspClient controllers.
|
15
|
+
# conf.controller_base = "::ApplicationController"
|
16
|
+
end
|
metadata
ADDED
@@ -0,0 +1,221 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fasp_client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.5.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- James Smith
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: rails
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '8.0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '8.0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: ed25519
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '1.4'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '1.4'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: linzer
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0.7'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0.7'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: rubocop-rails-omakase
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '1.1'
|
61
|
+
type: :development
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '1.1'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: rspec-rails
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '8.0'
|
75
|
+
type: :development
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '8.0'
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: byebug
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '12.0'
|
89
|
+
type: :development
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '12.0'
|
96
|
+
- !ruby/object:Gem::Dependency
|
97
|
+
name: rspec-uuid
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0.6'
|
103
|
+
type: :development
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0.6'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: vcr
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '6.3'
|
117
|
+
type: :development
|
118
|
+
prerelease: false
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - "~>"
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '6.3'
|
124
|
+
- !ruby/object:Gem::Dependency
|
125
|
+
name: webmock
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - "~>"
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '3.25'
|
131
|
+
type: :development
|
132
|
+
prerelease: false
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - "~>"
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '3.25'
|
138
|
+
- !ruby/object:Gem::Dependency
|
139
|
+
name: factory_bot
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - "~>"
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '6.5'
|
145
|
+
type: :development
|
146
|
+
prerelease: false
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - "~>"
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '6.5'
|
152
|
+
description: A Rails engine that implements the non-provider side of the Fediverse
|
153
|
+
Auxiliary Service Provider (FASP) standard.
|
154
|
+
email:
|
155
|
+
- james@floppy.org.uk
|
156
|
+
executables: []
|
157
|
+
extensions: []
|
158
|
+
extra_rdoc_files: []
|
159
|
+
files:
|
160
|
+
- LICENSE.md
|
161
|
+
- README.md
|
162
|
+
- Rakefile
|
163
|
+
- app/controllers/fasp_client/application_controller.rb
|
164
|
+
- app/controllers/fasp_client/backfill_requests_controller.rb
|
165
|
+
- app/controllers/fasp_client/event_subscriptions_controller.rb
|
166
|
+
- app/controllers/fasp_client/providers_controller.rb
|
167
|
+
- app/helpers/fasp_client/application_helper.rb
|
168
|
+
- app/jobs/fasp_client/lifecycle_announcement_job.rb
|
169
|
+
- app/models/concerns/fasp_client/data_sharing/lifecycle.rb
|
170
|
+
- app/models/fasp_client/application_record.rb
|
171
|
+
- app/models/fasp_client/backfill_request.rb
|
172
|
+
- app/models/fasp_client/event_subscription.rb
|
173
|
+
- app/models/fasp_client/provider.rb
|
174
|
+
- app/services/fasp_client/account_search_service.rb
|
175
|
+
- app/services/fasp_client/capability_activation_service.rb
|
176
|
+
- app/services/fasp_client/follow_recommendation_service.rb
|
177
|
+
- app/services/fasp_client/http_request_service.rb
|
178
|
+
- app/services/fasp_client/provider_info_service.rb
|
179
|
+
- app/views/fasp_client/providers/edit.html.erb
|
180
|
+
- app/views/fasp_client/providers/index.html.erb
|
181
|
+
- config/routes.rb
|
182
|
+
- db/migrate/20250801150509_create_fasp_client_providers.rb
|
183
|
+
- db/migrate/20250905153345_create_fasp_client_event_subscriptions.rb
|
184
|
+
- db/migrate/20250908164011_create_fasp_client_backfill_requests.rb
|
185
|
+
- lib/fasp_client.rb
|
186
|
+
- lib/fasp_client/configuration.rb
|
187
|
+
- lib/fasp_client/ed25519_signing_key_coder.rb
|
188
|
+
- lib/fasp_client/engine.rb
|
189
|
+
- lib/fasp_client/version.rb
|
190
|
+
- lib/generators/fasp_client/install/USAGE
|
191
|
+
- lib/generators/fasp_client/install/install_generator.rb
|
192
|
+
- lib/generators/fasp_client/install/templates/fasp_client.rb
|
193
|
+
- lib/generators/fasp_client/views/USAGE
|
194
|
+
- lib/generators/fasp_client/views/views_generator.rb
|
195
|
+
- lib/tasks/fasp_client_tasks.rake
|
196
|
+
homepage: https://github.com/manyfold3d/fasp_client
|
197
|
+
licenses:
|
198
|
+
- MIT
|
199
|
+
metadata:
|
200
|
+
allowed_push_host: https://rubygems.org
|
201
|
+
homepage_uri: https://github.com/manyfold3d/fasp_client
|
202
|
+
source_code_uri: https://github.com/manyfold3d/fasp_client
|
203
|
+
changelog_uri: https://github.com/manyfold3d/fasp_client/releases
|
204
|
+
rdoc_options: []
|
205
|
+
require_paths:
|
206
|
+
- lib
|
207
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
208
|
+
requirements:
|
209
|
+
- - ">="
|
210
|
+
- !ruby/object:Gem::Version
|
211
|
+
version: '0'
|
212
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
213
|
+
requirements:
|
214
|
+
- - ">="
|
215
|
+
- !ruby/object:Gem::Version
|
216
|
+
version: '0'
|
217
|
+
requirements: []
|
218
|
+
rubygems_version: 3.6.9
|
219
|
+
specification_version: 4
|
220
|
+
summary: A Rails engine for FASP client apps
|
221
|
+
test_files: []
|