better_auth-sinatra 0.6.1 → 0.7.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 +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +33 -3
- data/lib/better_auth/sinatra/configuration.rb +1 -0
- data/lib/better_auth/sinatra/extension.rb +7 -0
- data/lib/better_auth/sinatra/helpers.rb +48 -11
- data/lib/better_auth/sinatra/migration.rb +118 -2
- data/lib/better_auth/sinatra/mounted_app.rb +37 -6
- data/lib/better_auth/sinatra/tasks.rb +3 -6
- data/lib/better_auth/sinatra/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 14d141007853cbd143bb23e5ac0557790a28bfb65e386b340e545d9cd0cb55c0
|
|
4
|
+
data.tar.gz: 6b21abf9d5f9b8e8633c83f9255e7861818e310bf0c614b1368e4530460eeaaa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a1766d030b0eb07b9e0ad84896b8b0d8efe4d3f2f01d9eb46a95e5fdb9a2f2e629904d924108a970d4fb70308e68e5cd6480832789e2edc5bd4a5d1a5167899f
|
|
7
|
+
data.tar.gz: 48323869049ff91d30ad0f0b465e89c8802d88b5f3d92df37f1d08c9f401e9fcfb5ffdc38fcf2d9a828296739a913dba3171e71bd00667a852111b1a98a9707d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.7.0 - 2026-05-05
|
|
6
|
+
|
|
7
|
+
- Fixed auth dispatch when Rack splits mounted paths across `SCRIPT_NAME` and `PATH_INFO`.
|
|
8
|
+
- Rejected `better_auth at: "/"` to avoid capturing every Sinatra route.
|
|
9
|
+
- Stopped swallowing real migration bookkeeping query errors while preserving empty-state behavior for missing schema tables.
|
|
10
|
+
- Split simple single-line multi-statement SQL migration files.
|
|
11
|
+
- Passed versioned `secrets` through Sinatra configuration to core auth.
|
|
12
|
+
- Warned when `better_auth` is configured more than once on the same Sinatra app class.
|
|
13
|
+
- Returned JSON-shaped 401 responses from `require_authentication` when JSON is preferred.
|
|
14
|
+
- Removed duplicate Rake task wiring and clarified `better_auth:routes` output.
|
|
15
|
+
- Documented mount path, Rack nesting, SQL migration, and helper auth caveats.
|
|
16
|
+
|
|
3
17
|
## 0.1.1 - 2026-04-29
|
|
4
18
|
|
|
5
19
|
- Fixed mounted base-path propagation when creating the Sinatra auth instance.
|
data/README.md
CHANGED
|
@@ -39,9 +39,26 @@ class App < Sinatra::Base
|
|
|
39
39
|
end
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
The extension mounts the core Rack app at `/api/auth` by default. The
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
The extension mounts the core Rack app at `/api/auth` by default. The mount path
|
|
43
|
+
cannot be `/`, because that would capture every Sinatra route before the app can
|
|
44
|
+
handle it. The core app still owns routes such as `/ok`, `/sign-up/email`,
|
|
45
|
+
`/sign-in/email`, and plugin endpoints.
|
|
46
|
+
|
|
47
|
+
`better_auth at:` sets the path prefix that Better Auth uses as its core
|
|
48
|
+
`base_path`. The adapter supports two common Rack mount patterns:
|
|
49
|
+
|
|
50
|
+
- Natural Sinatra nesting: mount the Sinatra app under a parent `Rack::URLMap`,
|
|
51
|
+
for example at `/api`, and configure `better_auth at: "/auth"`. Auth routes
|
|
52
|
+
are available at `/api/auth/*`.
|
|
53
|
+
- Shared auth mount: mount the Sinatra app itself at the same path as auth, for
|
|
54
|
+
example `/api/auth`, and configure `better_auth at: "/api/auth"`. The adapter
|
|
55
|
+
reconstructs the logical path from Rack `SCRIPT_NAME` and `PATH_INFO`.
|
|
56
|
+
|
|
57
|
+
When using reverse proxies, `Rack::URLMap`, or another parent app, make sure the
|
|
58
|
+
`PATH_INFO` visible to Sinatra still aligns with the configured auth prefix.
|
|
59
|
+
`SCRIPT_NAME` handling depends on the Rack server and mount stack, so verify
|
|
60
|
+
redirect URLs and cookie paths in integration tests when mounting below a
|
|
61
|
+
sub-path.
|
|
45
62
|
|
|
46
63
|
## Helpers
|
|
47
64
|
|
|
@@ -51,6 +68,14 @@ endpoints.
|
|
|
51
68
|
- `require_authentication`
|
|
52
69
|
|
|
53
70
|
`require_authentication` halts with `401` when no Better Auth user is present.
|
|
71
|
+
Requests that prefer JSON receive the same JSON error shape used by the core
|
|
72
|
+
router.
|
|
73
|
+
|
|
74
|
+
Sinatra helpers resolve sessions through the core `get-session` API path, so
|
|
75
|
+
Better Auth plugin hooks that affect session lookup, such as the bearer plugin,
|
|
76
|
+
run for `current_user` and `require_authentication`. Helper session lookup may
|
|
77
|
+
emit Better Auth `Set-Cookie` headers when stale cookies need to be cleared or
|
|
78
|
+
session cookies need to be refreshed.
|
|
54
79
|
|
|
55
80
|
## Rake Tasks
|
|
56
81
|
|
|
@@ -78,6 +103,11 @@ Sinatra does not include a Rails-style database layer or migration command.
|
|
|
78
103
|
This adapter uses Better Auth core SQL adapters for migrations. Set
|
|
79
104
|
`BETTER_AUTH_DIALECT=postgres`, `mysql`, or `sqlite` when generating SQL.
|
|
80
105
|
|
|
106
|
+
Generated SQL should keep one statement per line ending with `;`. The migration
|
|
107
|
+
runner handles simple single-line multi-statement files, but hand-edited SQL
|
|
108
|
+
with semicolons inside string literals can confuse the splitter. DDL rollback
|
|
109
|
+
behavior depends on the database, so back up production data before migrating.
|
|
110
|
+
|
|
81
111
|
ActiveRecord-backed Sinatra migrations are not supported yet. Apps that already
|
|
82
112
|
use `sinatra-activerecord` can still configure Better Auth manually, but the v1
|
|
83
113
|
Rake tasks do not emit ActiveRecord migrations.
|
|
@@ -11,6 +11,13 @@ module BetterAuth
|
|
|
11
11
|
module ClassMethods
|
|
12
12
|
def better_auth(at: BetterAuth::Configuration::DEFAULT_BASE_PATH, auth: nil, **overrides)
|
|
13
13
|
mount_path = normalize_better_auth_mount_path(at)
|
|
14
|
+
if mount_path == "/"
|
|
15
|
+
raise ArgumentError,
|
|
16
|
+
"better_auth mount path cannot be '/' (it would capture every request). " \
|
|
17
|
+
"Use a prefix such as #{BetterAuth::Configuration::DEFAULT_BASE_PATH.inspect}."
|
|
18
|
+
end
|
|
19
|
+
warn "[better_auth-sinatra] better_auth is already configured for this app; the new configuration will be appended." if respond_to?(:better_auth_auth)
|
|
20
|
+
|
|
14
21
|
config = BetterAuth::Sinatra.configuration.copy
|
|
15
22
|
yield config if block_given?
|
|
16
23
|
config.base_path = mount_path
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
3
5
|
module BetterAuth
|
|
4
6
|
module Sinatra
|
|
5
7
|
module Helpers
|
|
@@ -20,11 +22,29 @@ module BetterAuth
|
|
|
20
22
|
def require_authentication
|
|
21
23
|
return true if authenticated?
|
|
22
24
|
|
|
25
|
+
if prefers_json_response?
|
|
26
|
+
error = BetterAuth::APIError.new("UNAUTHORIZED")
|
|
27
|
+
halt 401, {"content-type" => "application/json"}, JSON.generate(error.to_h)
|
|
28
|
+
end
|
|
29
|
+
|
|
23
30
|
halt 401, ""
|
|
24
31
|
end
|
|
25
32
|
|
|
26
33
|
private
|
|
27
34
|
|
|
35
|
+
def prefers_json_response?
|
|
36
|
+
accept = request.env["HTTP_ACCEPT"].to_s
|
|
37
|
+
return false if accept.empty? || accept == "*/*"
|
|
38
|
+
|
|
39
|
+
preferred = request.preferred_type(["application/json", "text/html"]) if request.respond_to?(:preferred_type)
|
|
40
|
+
return preferred.to_s == "application/json" if preferred
|
|
41
|
+
|
|
42
|
+
accept.split(",").any? do |entry|
|
|
43
|
+
media_type = entry.split(";", 2).first.to_s.strip
|
|
44
|
+
media_type == "application/json" || media_type.end_with?("+json")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
28
48
|
def better_auth_session_data
|
|
29
49
|
return request.env["better_auth.session"] if request.env.key?("better_auth.session")
|
|
30
50
|
|
|
@@ -33,18 +53,35 @@ module BetterAuth
|
|
|
33
53
|
|
|
34
54
|
def resolve_better_auth_session
|
|
35
55
|
auth = better_auth_auth
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
method: request.request_method,
|
|
40
|
-
query: request.GET,
|
|
41
|
-
body: {},
|
|
42
|
-
params: params,
|
|
43
|
-
headers: {"cookie" => request.env["HTTP_COOKIE"]},
|
|
44
|
-
context: auth.context,
|
|
45
|
-
request: request
|
|
56
|
+
result = auth.api.get_session(
|
|
57
|
+
headers: better_auth_request_headers,
|
|
58
|
+
return_headers: true
|
|
46
59
|
)
|
|
47
|
-
|
|
60
|
+
apply_better_auth_response_headers(result[:headers] || result["headers"] || {})
|
|
61
|
+
result[:response] || result["response"]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def better_auth_request_headers
|
|
65
|
+
request.env.each_with_object({}) do |(key, value), headers|
|
|
66
|
+
case key
|
|
67
|
+
when "CONTENT_TYPE"
|
|
68
|
+
headers["content-type"] = value if value
|
|
69
|
+
when "CONTENT_LENGTH"
|
|
70
|
+
headers["content-length"] = value if value
|
|
71
|
+
else
|
|
72
|
+
next unless key.start_with?("HTTP_")
|
|
73
|
+
|
|
74
|
+
headers[key.delete_prefix("HTTP_").downcase.tr("_", "-")] = value
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def apply_better_auth_response_headers(headers)
|
|
80
|
+
set_cookie = headers["set-cookie"] || headers["Set-Cookie"] || headers[:set_cookie]
|
|
81
|
+
return if set_cookie.to_s.empty?
|
|
82
|
+
|
|
83
|
+
existing = response.headers["set-cookie"].to_s
|
|
84
|
+
response.headers["set-cookie"] = [existing, set_cookie.to_s].reject(&:empty?).join("\n")
|
|
48
85
|
end
|
|
49
86
|
|
|
50
87
|
def better_auth_auth
|
|
@@ -6,6 +6,13 @@ module BetterAuth
|
|
|
6
6
|
module Sinatra
|
|
7
7
|
module Migration
|
|
8
8
|
DEFAULT_MIGRATIONS_PATH = "db/better_auth/migrate"
|
|
9
|
+
MISSING_MIGRATIONS_TABLE_MESSAGES = [
|
|
10
|
+
/no such table/i,
|
|
11
|
+
/relation .* does not exist/i,
|
|
12
|
+
/table .* doesn't exist/i,
|
|
13
|
+
/undefined table/i,
|
|
14
|
+
/invalid object name/i
|
|
15
|
+
].freeze
|
|
9
16
|
|
|
10
17
|
class UnsupportedAdapterError < StandardError; end
|
|
11
18
|
|
|
@@ -83,7 +90,11 @@ module BetterAuth
|
|
|
83
90
|
def applied_migrations(connection, dialect)
|
|
84
91
|
rows = execute_sql(connection, "SELECT #{quote("version", dialect)} FROM #{quote("better_auth_schema_migrations", dialect)};")
|
|
85
92
|
Array(rows).map { |row| row["version"] || row[:version] }
|
|
86
|
-
rescue
|
|
93
|
+
rescue UnsupportedAdapterError
|
|
94
|
+
raise
|
|
95
|
+
rescue => error
|
|
96
|
+
raise error unless missing_schema_migrations_table?(error)
|
|
97
|
+
|
|
87
98
|
[]
|
|
88
99
|
end
|
|
89
100
|
|
|
@@ -109,7 +120,112 @@ module BetterAuth
|
|
|
109
120
|
end
|
|
110
121
|
|
|
111
122
|
def statements(sql)
|
|
112
|
-
sql.
|
|
123
|
+
normalized = sql.to_s.gsub("\r\n", "\n").strip
|
|
124
|
+
return [] if normalized.empty?
|
|
125
|
+
|
|
126
|
+
split_sql_statements(normalized)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def split_sql_statements(sql)
|
|
130
|
+
output = []
|
|
131
|
+
buffer = +""
|
|
132
|
+
index = 0
|
|
133
|
+
quote = nil
|
|
134
|
+
line_comment = false
|
|
135
|
+
block_comment = false
|
|
136
|
+
dollar_tag = nil
|
|
137
|
+
|
|
138
|
+
while index < sql.length
|
|
139
|
+
state = {
|
|
140
|
+
quote: quote,
|
|
141
|
+
line_comment: line_comment,
|
|
142
|
+
block_comment: block_comment,
|
|
143
|
+
dollar_tag: dollar_tag
|
|
144
|
+
}
|
|
145
|
+
index, quote, line_comment, block_comment, dollar_tag = scan_sql_character(sql, buffer, index, state, output)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
tail = buffer.strip
|
|
149
|
+
output << tail unless tail.empty?
|
|
150
|
+
output
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def scan_sql_character(sql, buffer, index, state, output)
|
|
154
|
+
char = sql[index]
|
|
155
|
+
next_char = sql[index + 1]
|
|
156
|
+
quote = state[:quote]
|
|
157
|
+
line_comment = state[:line_comment]
|
|
158
|
+
block_comment = state[:block_comment]
|
|
159
|
+
dollar_tag = state[:dollar_tag]
|
|
160
|
+
|
|
161
|
+
return scan_line_comment(buffer, index, char) + [quote, false, block_comment, dollar_tag] if line_comment && char == "\n"
|
|
162
|
+
return scan_line_comment(buffer, index, char) + [quote, true, block_comment, dollar_tag] if line_comment
|
|
163
|
+
return scan_block_comment(buffer, index, char, next_char, quote, line_comment, dollar_tag) if block_comment
|
|
164
|
+
return scan_dollar_quote(sql, buffer, index, char, quote, line_comment, block_comment, dollar_tag) if dollar_tag
|
|
165
|
+
return scan_quoted_string(sql, buffer, index, char, quote, line_comment, block_comment, dollar_tag) if quote
|
|
166
|
+
return [index + 2, quote, true, block_comment, dollar_tag].tap { buffer << char << next_char } if char == "-" && next_char == "-"
|
|
167
|
+
return [index + 2, quote, line_comment, true, dollar_tag].tap { buffer << char << next_char } if char == "/" && next_char == "*"
|
|
168
|
+
|
|
169
|
+
tag = dollar_quote_tag_at(sql, index)
|
|
170
|
+
return [index + tag.length, quote, line_comment, block_comment, tag].tap { buffer << tag } if tag
|
|
171
|
+
return [index + 1, char, line_comment, block_comment, dollar_tag].tap { buffer << char } if char == "'" || char == "\""
|
|
172
|
+
|
|
173
|
+
if char == ";"
|
|
174
|
+
statement = buffer.strip
|
|
175
|
+
output << statement unless statement.empty?
|
|
176
|
+
buffer.clear
|
|
177
|
+
return [index + 1, quote, line_comment, block_comment, dollar_tag]
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
buffer << char
|
|
181
|
+
[index + 1, quote, line_comment, block_comment, dollar_tag]
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def scan_line_comment(buffer, index, char)
|
|
185
|
+
buffer << char
|
|
186
|
+
[index + 1]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def scan_block_comment(buffer, index, char, next_char, quote, line_comment, dollar_tag)
|
|
190
|
+
buffer << char
|
|
191
|
+
if char == "*" && next_char == "/"
|
|
192
|
+
buffer << next_char
|
|
193
|
+
[index + 2, quote, line_comment, false, dollar_tag]
|
|
194
|
+
else
|
|
195
|
+
[index + 1, quote, line_comment, true, dollar_tag]
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def scan_dollar_quote(sql, buffer, index, char, quote, line_comment, block_comment, dollar_tag)
|
|
200
|
+
if sql[index, dollar_tag.length] == dollar_tag
|
|
201
|
+
buffer << dollar_tag
|
|
202
|
+
[index + dollar_tag.length, quote, line_comment, block_comment, nil]
|
|
203
|
+
else
|
|
204
|
+
buffer << char
|
|
205
|
+
[index + 1, quote, line_comment, block_comment, dollar_tag]
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def scan_quoted_string(sql, buffer, index, char, quote, line_comment, block_comment, dollar_tag)
|
|
210
|
+
buffer << char
|
|
211
|
+
if char == quote && sql[index + 1] == quote
|
|
212
|
+
buffer << sql[index + 1]
|
|
213
|
+
[index + 2, quote, line_comment, block_comment, dollar_tag]
|
|
214
|
+
elsif char == quote
|
|
215
|
+
[index + 1, nil, line_comment, block_comment, dollar_tag]
|
|
216
|
+
else
|
|
217
|
+
[index + 1, quote, line_comment, block_comment, dollar_tag]
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def dollar_quote_tag_at(sql, index)
|
|
222
|
+
match = sql[index..]&.match(/\A\$[A-Za-z_][A-Za-z0-9_]*\$|\A\$\$/)
|
|
223
|
+
match&.[](0)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def missing_schema_migrations_table?(error)
|
|
227
|
+
message = error.message.to_s
|
|
228
|
+
MISSING_MIGRATIONS_TABLE_MESSAGES.any? { |pattern| message.match?(pattern) }
|
|
113
229
|
end
|
|
114
230
|
|
|
115
231
|
def quote(identifier, dialect)
|
|
@@ -10,9 +10,12 @@ module BetterAuth
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def call(env)
|
|
13
|
-
return
|
|
13
|
+
return @app.call(env) unless mount_matches?(env)
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
rewritten_path = mounted_path_info(env)
|
|
16
|
+
next_env = env.merge("PATH_INFO" => rewritten_path)
|
|
17
|
+
next_env["SCRIPT_NAME"] = "" if shared_mount_rewrite?(env, rewritten_path)
|
|
18
|
+
auth.call(next_env)
|
|
16
19
|
end
|
|
17
20
|
|
|
18
21
|
private
|
|
@@ -23,11 +26,39 @@ module BetterAuth
|
|
|
23
26
|
@auth
|
|
24
27
|
end
|
|
25
28
|
|
|
26
|
-
def
|
|
27
|
-
|
|
28
|
-
return true if @mount_path == "/"
|
|
29
|
+
def mount_matches?(env)
|
|
30
|
+
return false if @mount_path == "/"
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
path_info = normalize_path(env["PATH_INFO"])
|
|
33
|
+
return true if path_info == @mount_path || path_info.start_with?("#{@mount_path}/")
|
|
34
|
+
|
|
35
|
+
full = full_request_path(env)
|
|
36
|
+
full == @mount_path || full.start_with?("#{@mount_path}/")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def full_request_path(env)
|
|
40
|
+
script = env.fetch("SCRIPT_NAME", "").to_s
|
|
41
|
+
path = env.fetch("PATH_INFO", "").to_s
|
|
42
|
+
normalize_path("#{script}#{path}")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def mounted_path_info(env)
|
|
46
|
+
path_info = normalize_path(env["PATH_INFO"])
|
|
47
|
+
return path_info if path_info == @mount_path || path_info.start_with?("#{@mount_path}/")
|
|
48
|
+
|
|
49
|
+
script_name = normalize_path(env["SCRIPT_NAME"])
|
|
50
|
+
prefix = (script_name == "/") ? @mount_path : script_name
|
|
51
|
+
return path_info if path_info == prefix || path_info.start_with?("#{prefix}/")
|
|
52
|
+
|
|
53
|
+
normalize_path("#{prefix}/#{path_info.delete_prefix("/")}")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def shared_mount_rewrite?(env, rewritten_path)
|
|
57
|
+
script_name = normalize_path(env["SCRIPT_NAME"])
|
|
58
|
+
original_path = normalize_path(env["PATH_INFO"])
|
|
59
|
+
script_name != "/" &&
|
|
60
|
+
!original_path.start_with?("#{@mount_path}/") &&
|
|
61
|
+
rewritten_path.start_with?("#{@mount_path}/")
|
|
31
62
|
end
|
|
32
63
|
|
|
33
64
|
def normalize_path(path)
|
|
@@ -30,11 +30,6 @@ namespace :better_auth do
|
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
desc "Create the Better Auth SQL migration"
|
|
34
|
-
task "generate:migration" do
|
|
35
|
-
Rake::Task["better_auth:generate:migration"].invoke
|
|
36
|
-
end
|
|
37
|
-
|
|
38
33
|
desc "Run pending Better Auth SQL migrations"
|
|
39
34
|
task :migrate do
|
|
40
35
|
BetterAuth::Sinatra.load_app_config
|
|
@@ -44,6 +39,8 @@ namespace :better_auth do
|
|
|
44
39
|
desc "Print Better Auth Sinatra mount information"
|
|
45
40
|
task :routes do
|
|
46
41
|
BetterAuth::Sinatra.load_app_config
|
|
47
|
-
|
|
42
|
+
mount_path = BetterAuth::Sinatra.configuration.base_path
|
|
43
|
+
puts "#{mount_path}/* -> BetterAuth.auth"
|
|
44
|
+
puts "Core routes are handled by Better Auth; use the OpenAPI plugin or HTTP API docs for endpoint details."
|
|
48
45
|
end
|
|
49
46
|
end
|