haveapi 0.31.2 → 0.32.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 (123) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/benchmarks/documentation.rb +29 -6
  4. data/benchmarks/endpoints.rb +153 -24
  5. data/benchmarks/spec/client_examples_spec.rb +109 -37
  6. data/doc/client-examples.md +218 -45
  7. data/doc/documentation-oauth.md +188 -0
  8. data/doc/documentation-ui.md +192 -0
  9. data/doc/index.md +3 -6
  10. data/doc/performance.md +48 -14
  11. data/haveapi.gemspec +1 -1
  12. data/lib/haveapi/authentication/chain.rb +19 -0
  13. data/lib/haveapi/authentication/oauth2/provider.rb +7 -1
  14. data/lib/haveapi/client_example.rb +143 -15
  15. data/lib/haveapi/client_examples/curl.rb +30 -20
  16. data/lib/haveapi/client_examples/dynamic_helpers.rb +1 -1
  17. data/lib/haveapi/client_examples/go_client.rb +9 -2
  18. data/lib/haveapi/client_examples/http.rb +9 -4
  19. data/lib/haveapi/client_examples/js_async_client.rb +7 -0
  20. data/lib/haveapi/client_examples/js_client.rb +47 -36
  21. data/lib/haveapi/client_examples/php_client.rb +161 -75
  22. data/lib/haveapi/client_examples/python_async_client.rb +3 -0
  23. data/lib/haveapi/client_examples/python_client.rb +61 -47
  24. data/lib/haveapi/client_examples/ruby_cli.rb +27 -18
  25. data/lib/haveapi/client_examples/ruby_client.rb +80 -31
  26. data/lib/haveapi/client_examples/rust_client.rb +20 -9
  27. data/lib/haveapi/client_examples/ts_async_client.rb +7 -0
  28. data/lib/haveapi/client_examples/ts_client.rb +3 -0
  29. data/lib/haveapi/client_examples/ts_generated_client.rb +10 -2
  30. data/lib/haveapi/documentation_context.rb +7 -2
  31. data/lib/haveapi/documentation_helpers.rb +223 -0
  32. data/lib/haveapi/documentation_oauth.rb +411 -0
  33. data/lib/haveapi/documentation_search.rb +225 -0
  34. data/lib/haveapi/documentation_search_index.rb +79 -0
  35. data/lib/haveapi/documentation_validators.rb +179 -0
  36. data/lib/haveapi/public/css/documentation.css +343 -0
  37. data/lib/haveapi/public/js/documentation.js +992 -0
  38. data/lib/haveapi/server.rb +144 -86
  39. data/lib/haveapi/shared_documentation_helpers.rb +54 -0
  40. data/lib/haveapi/version.rb +1 -1
  41. data/lib/haveapi/views/doc_layout.erb +16 -0
  42. data/lib/haveapi/views/doc_sidebars/create-client.erb +3 -3
  43. data/lib/haveapi/views/doc_sidebars/json-schema.erb +1 -1
  44. data/lib/haveapi/views/doc_sidebars/protocol.erb +5 -5
  45. data/lib/haveapi/views/documentation/search_form.erb +10 -0
  46. data/lib/haveapi/views/documentation/search_index.erb +1 -0
  47. data/lib/haveapi/views/documentation/search_results.erb +20 -0
  48. data/lib/haveapi/views/index.erb +2 -4
  49. data/lib/haveapi/views/main_layout.erb +12 -6
  50. data/lib/haveapi/views/shared_documentation.erb +12 -0
  51. data/lib/haveapi/views/shared_sidebar.erb +25 -0
  52. data/lib/haveapi/views/version_page/auth_body.erb +12 -33
  53. data/lib/haveapi/views/version_page/client_overview.erb +40 -0
  54. data/lib/haveapi/views/version_page/example_panels.erb +18 -0
  55. data/lib/haveapi/views/version_page/resource_body.erb +91 -132
  56. data/lib/haveapi/views/version_page/resource_directory.erb +14 -0
  57. data/lib/haveapi/views/version_page/usage.erb +7 -0
  58. data/lib/haveapi/views/version_page.erb +36 -117
  59. data/lib/haveapi/views/version_sidebar/auth_nav.erb +12 -16
  60. data/lib/haveapi/views/version_sidebar/auth_resource_nav.erb +19 -0
  61. data/lib/haveapi/views/version_sidebar/authentication_control.erb +26 -0
  62. data/lib/haveapi/views/version_sidebar/authentication_status.erb +3 -0
  63. data/lib/haveapi/views/version_sidebar/brand.erb +31 -0
  64. data/lib/haveapi/views/version_sidebar/resource_nav.erb +11 -20
  65. data/lib/haveapi/views/version_sidebar.erb +59 -48
  66. data/spec/authentication/basic_spec.rb +10 -10
  67. data/spec/authentication/oauth2_chain_spec.rb +72 -0
  68. data/spec/authentication/oauth2_token_validation_spec.rb +38 -0
  69. data/spec/client_examples/assets/documentation_test.cjs +817 -0
  70. data/spec/client_examples/context_performance_spec.rb +5 -4
  71. data/spec/client_examples/context_spec.rb +23 -2
  72. data/spec/client_examples/detail_spec.rb +175 -0
  73. data/spec/client_examples/dynamic_acceptance.md +59 -26
  74. data/spec/client_examples/dynamic_acceptance.rb +164 -44
  75. data/spec/client_examples/dynamic_acceptance_spec.rb +140 -23
  76. data/spec/client_examples/dynamic_fixture.rb +4 -3
  77. data/spec/client_examples/dynamic_js_spec.rb +20 -11
  78. data/spec/client_examples/dynamic_metadata_spec.rb +69 -0
  79. data/spec/client_examples/dynamic_mounted_paths_spec.rb +9 -7
  80. data/spec/client_examples/dynamic_python_spec.rb +62 -11
  81. data/spec/client_examples/go_spec.rb +14 -1
  82. data/spec/client_examples/presentation_spec.rb +247 -0
  83. data/spec/client_examples/profile_mount_spec.rb +40 -13
  84. data/spec/client_examples/rust_client_spec.rb +35 -1
  85. data/spec/client_examples/shell_spec.rb +268 -0
  86. data/spec/client_examples/syntax_spec.rb +192 -0
  87. data/spec/client_examples/ts_generated_client_spec.rb +15 -2
  88. data/spec/client_examples/typescript_profiles_spec.rb +1 -1
  89. data/spec/documentation/auth_filtering_spec.rb +9 -4
  90. data/spec/documentation/canonical_routes_spec.rb +89 -0
  91. data/spec/documentation/client_examples_spec.rb +39 -5
  92. data/spec/documentation/client_overview_spec.rb +145 -0
  93. data/spec/documentation/current_user_html_escaping_spec.rb +7 -2
  94. data/spec/documentation/example_variants_spec.rb +93 -0
  95. data/spec/documentation/fixtures/search_index.json +476 -0
  96. data/spec/documentation/identity_and_guides_spec.rb +130 -0
  97. data/spec/documentation/identity_context_spec.rb +180 -0
  98. data/spec/documentation/navigation_spec.rb +296 -0
  99. data/spec/documentation/oauth_fixture_spec.rb +39 -0
  100. data/spec/documentation/oauth_spec.rb +1058 -0
  101. data/spec/documentation/oauth_transport_spec.rb +45 -0
  102. data/spec/documentation/performance_spec.rb +47 -6
  103. data/spec/documentation/reference_headings_spec.rb +190 -0
  104. data/spec/documentation/search_index_routes_spec.rb +202 -0
  105. data/spec/documentation/search_index_spec.rb +85 -0
  106. data/spec/documentation/search_routes_spec.rb +199 -0
  107. data/spec/documentation/search_spec.rb +134 -0
  108. data/spec/documentation/shared_pages_spec.rb +240 -0
  109. data/spec/documentation/validators_spec.rb +123 -0
  110. data/spec/documentation_spec.rb +3 -3
  111. data/spec/packaging_spec.rb +21 -2
  112. data/spec/performance/documentation_identity_fixture_spec.rb +47 -0
  113. data/spec/performance/documentation_shapes_spec.rb +97 -0
  114. data/spec/performance/event_baseline_spec.rb +97 -2
  115. data/spec/performance/json_activation_spec.rb +78 -1
  116. data/spec/performance/release_lineage_spec.rb +4 -3
  117. data/spec/performance/runner_spec.rb +1 -0
  118. data/test_support/all_client_performance_fixture.rb +6 -10
  119. data/test_support/documentation_oauth_fixture.rb +54 -0
  120. data/test_support/documentation_performance_fixture.rb +19 -1
  121. data/test_support/dynamic_documentation_api.rb +35 -10
  122. data/test_support/dynamic_python_login_spec.rb +3 -11
  123. metadata +52 -3
@@ -1,5 +1,7 @@
1
1
  # Client examples in API documentation
2
2
 
3
+ For browser sign-in to the documentation, see [OAuth sign-in](documentation-oauth.md).
4
+
3
5
  The Ruby server renders examples from the current user's API description.
4
6
  JavaScript and TypeScript examples cover callback and async clients; Python
5
7
  examples cover sync and async clients. These clients need no server
@@ -8,18 +10,39 @@ Ruby, PHP, CLI, curl and HTTP examples are also available. A sample
8
10
  that a client cannot represent shows a limitation instead of dropping input or
9
11
  inventing a successful result.
10
12
 
11
- Go, Rust and generated TypeScript tabs require a profile for a generated SDK.
12
- Their package versions are independent of HaveAPI and the API version. Without a
13
- profile, the server
14
- hides those clients from the overview and all initialization, authentication and
15
- action panels. Install the matching generator gem only when configuring its
16
- profile; ordinary servers do not depend on these generators.
17
-
18
- ## Configure a generated SDK
19
-
20
- Register profiles before mounting the server. For an SDK that matches the
21
- current
22
- API, configure its package identity:
13
+ Examples default to **Simple**. Action examples assume an initialized and, when
14
+ needed, authenticated `api`; initialization and authentication examples are on
15
+ separate pages. The Examples selector stays visible and offers **With error
16
+ handling** when the current page has examples with additional error handling
17
+ for that client. Examples without a distinct error-handling form remain Simple.
18
+ Both levels preserve the same sample values, path arguments and operation
19
+ semantics. Extra setup alone does not create another choice; built-in
20
+ limitations have one Simple panel.
21
+ See [the documentation UI guide](documentation-ui.md) for navigation and saved
22
+ display preferences.
23
+
24
+ Examples use placeholder credentials: `user` / `secret`, `example-token` and
25
+ `example-access-token`. Replace them with your application's values. Renderers
26
+ use these public literals rather than reading credentials from the environment,
27
+ current user, profile settings or private defaults.
28
+
29
+ Token-login and challenge inputs use the authorized description's field names,
30
+ types and finite machine-readable choices. Required inputs remain present, and
31
+ the lifetime is explicitly fixed. Custom validation may require different
32
+ values; an unrepresentable required field produces a limitation. Continuation
33
+ names and inputs follow the advertised actions, while the runtime supplies and
34
+ propagates intermediate tokens.
35
+
36
+ Go, Rust and TypeScript API clients require a configured profile. Their package
37
+ versions are independent of HaveAPI and the API version. Without a profile, the
38
+ server omits those clients from the overview and example panels. Install the
39
+ matching generator gem when configuring its profile; ordinary servers do not
40
+ depend on these generators.
41
+
42
+ ## Configure an API client
43
+
44
+ Register profiles before mounting the server. For an API client that matches the
45
+ current API, configure its package identity:
23
46
 
24
47
  ```ruby
25
48
  api.documentation_clients('1.0',
@@ -40,30 +63,27 @@ api.mount('/api/')
40
63
  matching generator in the server's Ruby environment: `haveapi-go-client`,
41
64
  `haveapi-rust-client`, or the source-distributed `haveapi-typescript-client`.
42
65
  See the [TypeScript generator guide](../clients/typescript/README.md) for its
43
- setup. An explicit `generator_version` requires the installed generator to
44
- report
66
+ setup. An explicit `generator_version` requires the installed generator to report
45
67
  that version; the server never downloads one. This version is independent of the
46
- SDK and API versions.
68
+ API client and API versions.
47
69
 
48
70
  At the end of mount, the server builds one private structural model per
49
- configured
50
- API version. The model includes authentication resources, routes and changes
51
- from
52
- post-mount hooks, so canonical names and parent arguments use the complete API
71
+ configured API version. The model includes authentication resources, routes and
72
+ changes from post-mount hooks, so canonical names and parent arguments use the complete API
53
73
  declaration. The private copy excludes examples, prose and parameter defaults.
54
74
  HTML requests use the current user's authorized description, fields and samples;
55
75
  they never expose the private model or its initialization diagnostics. Without
56
76
  a configured profile, the server loads no optional generator and hides the Go,
57
- Rust and generated TypeScript panels. The callback and async TypeScript
77
+ Rust and TypeScript API client panels. The callback and async TypeScript
58
78
  examples remain separate and need no profile.
59
79
 
60
- Automatic profiles assume that the installed SDK matches the current server's
61
- generation contract. A package name or version does not prove its contents.
62
- An older SDK can lack new actions or use different names after a collision or
63
- route change. Check actual SDK compatibility before advertising it; use a saved
64
- schema override below when the SDK represents an older or restricted API.
80
+ Automatic profiles assume that the installed API client matches the current
81
+ server's generation contract. A package name or version does not prove its
82
+ contents. An older API client can lack new actions or use different names after
83
+ a collision or route change. Check compatibility before advertising a client;
84
+ use a saved-schema override for an older or restricted API.
65
85
 
66
- ### Optional SDK versions and sources
86
+ ### Optional API client versions and sources
67
87
 
68
88
  Set `module_version: 'v1.2.3'` for Go, `crate_version: '1.2.3'` for Rust or
69
89
  `package_version: '1.2.3'` for TypeScript when a specific package version is
@@ -89,13 +109,15 @@ examples also show their Tokio and serde_json requirements.
89
109
 
90
110
  TypeScript accepts ordinary lowercase npm package names, including scoped names,
91
111
  and optional concrete SemVer versions. Tags, version ranges, Git URLs, local
92
- paths
93
- and custom factories are not profile options. Configure a package that users can
94
- install; the server does not check its availability or publish it. Generated
95
- snippets use the package's public `createClient` export and the published
96
- `haveapi-client` 0.30.2 runtime.
112
+ paths and custom factories are not profile options. Configure a package that
113
+ users can install; the server does not check its availability or publish it.
114
+ TypeScript examples call the package's typed properties through its public
115
+ `createClient` export. The profile does not detect or select the application's
116
+ installed `haveapi-client` runtime. The installation command above uses the
117
+ version supported by the [TypeScript generator](../clients/typescript/README.md);
118
+ the examples do not report it as a configured runtime version.
97
119
 
98
- For an unpublished Rust SDK, supply an HTTPS Git URL and a full commit SHA:
120
+ For an unpublished Rust API client, supply an HTTPS Git URL and a full commit SHA:
99
121
 
100
122
  ```ruby
101
123
  api.documentation_clients('7.0',
@@ -122,8 +144,8 @@ credentials, query strings, fragments or control characters; revisions must be
122
144
  40 hexadecimal characters. Branches, tags, local paths and custom registries are
123
145
  not supported. An explicit crate version additionally constrains the selected
124
146
  source package. The server never fetches the repository or runs these user
125
- installation commands. These settings do not regenerate or publish an SDK and
126
- do not establish that this particular SDK matches a given API surface.
147
+ installation commands. These settings do not regenerate or publish an API client
148
+ or establish that it matches a given API surface.
127
149
 
128
150
  ### Saved-schema overrides
129
151
 
@@ -153,9 +175,9 @@ api.documentation_clients('1.0',
153
175
 
154
176
  Paths name trusted absolute local files. `description_path` is Go's saved
155
177
  machine-readable description, not HTML. For Rust and TypeScript, `snapshot_path`
156
- selects the saved schema and capture metadata. These files select an exact SDK
157
- shape in place of the current
158
- server declaration. Keep the files private and retain them with the SDK release.
178
+ selects the saved schema and capture metadata. These files select an exact API
179
+ client shape in place of the current server declaration. Keep them private and
180
+ retain them with the API client release.
159
181
 
160
182
  Rust's legacy `generator_config_path` supplies the crate name/version and full
161
183
  packaging configuration. It is mutually exclusive with inline `crate_name` and
@@ -174,9 +196,9 @@ server application. An older explicitly pinned generator can serve a suitable
174
196
  legacy file profile, but automatic/inline Rust mode requires the new factory
175
197
  interface and never guesses a fallback.
176
198
 
177
- A saved SDK can still serve examples after unrelated live additions. Each sample
178
- must match its saved route, layout and supplied parameter contracts; unavailable
179
- or stale samples show a limitation. Hidden actions, fields, defaults and samples
199
+ A saved API client can still serve examples after unrelated live additions. Each
200
+ sample must match its saved route, layout and supplied parameter contracts;
201
+ unavailable or stale samples show a limitation. Hidden actions, fields, defaults and samples
180
202
  remain hidden from the current user. The server never fetches descriptions,
181
203
  regenerates packages or runs a compiler/formatter during an HTML request.
182
204
 
@@ -192,6 +214,11 @@ Existing renderer positional constructors and `init`, `auth(method, desc)` and
192
214
  The registry binds a request-local `HaveAPI::ClientExample::Context` through
193
215
  `with_context`. It holds the selected version, URLs including the API mount,
194
216
  the filtered description, and the current resource, action and sample.
217
+ Its `detail` reader is exactly `:simple` or `:thorough`, defaulting to `:simple`.
218
+ Both construction and `context.with(detail: ...)` reject other values, including
219
+ strings and nil. Changing detail shares the existing owned data, as other
220
+ context derivations do. The base renderer's `thorough?` helper reads this value;
221
+ legacy positional renderer instances use simple behavior.
195
222
  Construction copies and freezes Hash, Array and String data without freezing
196
223
  the caller's containers. Each request owns its description snapshot.
197
224
  `context.with(...)` creates a distinct frozen context, sharing unchanged fields
@@ -215,6 +242,74 @@ strings, escaped once by the templates. A stable class `id` identifies a
215
242
  variant; `code` selects its syntax language. `available?(context)` controls the
216
243
  overview and every panel before rendering.
217
244
 
245
+ Renderer classes declare presentation with `language 'python'`,
246
+ `language_label 'Python'` and, for a client variant, `variant 'Async'`. These
247
+ class methods also read the metadata. The language is a stable lower-case group
248
+ ID, independent of the syntax lexer and exact client ID. An unspecified language
249
+ defaults to the renderer's own `id`, its language label defaults to `label`, and
250
+ its variant is nil. Metadata is declared on each class; a subclass without its
251
+ own metadata gets those defaults. Keep client IDs and registry order stable.
252
+
253
+ The class query `detail_levels_for(kind, context:, result:, auth_method: nil,
254
+ auth_description: nil)` selects the meaningful levels for each example group.
255
+ `result` is the already-rendered Simple result. Return a frozen `[:simple]` or
256
+ `[:simple, :thorough]`; other values are errors. The base implementation returns
257
+ the inherited `detail_levels` declaration, which defaults to both levels for
258
+ custom renderers. A custom Simple limitation does not suppress its distinct
259
+ thorough implementation. Direct rendering at either Context detail remains
260
+ supported, independently of the choices advertised in HTML.
261
+
262
+ Ruby and PHP offer both levels for representable initialization, authentication
263
+ and actions. Ruby OAuth remains unsupported. Their constructors are lazy, so
264
+ With error handling runs setup explicitly and handles validation, API,
265
+ authentication and protocol failures. Ruby rescues `ValidationError` before its
266
+ `ActionFailed` parent and reads response errors only from an actual `Response`.
267
+ PHP uses its framework exceptions and `getErrors()` where supported, and
268
+ rethrows unknown dependency exceptions. Its OAuth session, state, PKCE, redirect
269
+ and token exchange remain part of the example.
270
+
271
+ Callback JavaScript and TypeScript offer both levels for actions. Initialization
272
+ and authentication already check the ready result and offer Simple only. Async
273
+ JavaScript, async TypeScript and Python offer both for representable examples.
274
+
275
+ Go's Simple actions already check transport and API failures. Both levels are
276
+ available only for successful blocking actions with an authorized operation-status
277
+ helper. Its initialization and credential setters make no request and offer
278
+ Simple only. Rust offers both for representable code, with explicit handling of
279
+ fallible construction and send results. Its authentication setters configure
280
+ credentials without validating them over the network.
281
+
282
+ Typed TypeScript initialization is Simple only. Supported Basic/token
283
+ authentication and actions offer both with awaited `AsyncError` handling; OAuth
284
+ remains a limitation. CLI, curl and HTTP offer Simple only: the CLI reports
285
+ failures and exits nonzero, while the transcripts show protocol responses.
286
+ All built-in limitation results offer Simple only.
287
+
288
+ Go, Rust and TypeScript wrappers delegate to their configured adapter's
289
+ `detail_levels_for(kind:, context:, result:, mode:, auth_method: nil,
290
+ auth_description: nil)` when it implements that query. Older adapters retain
291
+ the wrapper's declared levels, including a distinct thorough result after a
292
+ Simple limitation. Those adapters may still offer identical code at both levels.
293
+ A query that returns invalid levels or raises an error does not use this fallback.
294
+ Capability queries must not render again or inspect unrelated resources.
295
+ The HTML producer renders each advertised detail once and
296
+ publishes its choices in the example group's `data-example-details` map. Client
297
+ metadata contains the ordered union for the current page; clients without any
298
+ example groups get Simple. Neither the group nor page choices change the saved
299
+ detail preference.
300
+
301
+ Simple action renderers use the established `api` and show the actual call and
302
+ relevant result. Initialize and authenticate it using the separate examples.
303
+ Python async fragments belong inside an async function; their comment makes that
304
+ context explicit. Python setup examples use a minimal async function and
305
+ `asyncio.run`, while sync examples run at top level. Neither level has a
306
+ `__main__` guard. Thorough sync examples use top-level `try`/`except`.
307
+
308
+ Translate and validate samples before selecting presentation detail. Keep exact
309
+ names, parent arguments, false, zero, empty strings, nullable values and blocking
310
+ operation behavior. Do not shorten examples by dropping unsupported input or
311
+ removing lines from an already rendered program.
312
+
218
313
  Generated-client adapters belong to their generator gems and consume context
219
314
  readers without depending on the server gem. They resolve names using the
220
315
  complete private generator model and never run a compiler, formatter or
@@ -223,11 +318,87 @@ in an HTML request. Keep adapter rendering read-only: configured adapters are
223
318
  frozen
224
319
  and shared, while authorized description snapshots belong to individual
225
320
  requests.
321
+ Adapters keep their existing `render(kind:, context:, mode:, ...)` signature and
322
+ read `context.detail` directly. Test contexts must provide the reader. An older
323
+ third-party adapter can ignore this additive field and retain the wrapper's
324
+ declared choices without implementing the new capability query.
226
325
  Derived contexts within one request share unchanged owned data. Supplying a
227
326
  resource, action or sample still copies that supplied subtree, so a resource
228
327
  with
229
328
  many actions can remain costly to render.
230
329
 
330
+ ## Example source style
331
+
332
+ Generated examples follow the conventions of their output language. These rules
333
+ apply to initialization, authentication and action examples, including inherited
334
+ callback, sync, async and blocking variants. Keep Simple and With error handling
335
+ consistent where both are supported.
336
+
337
+ | Language | Source conventions |
338
+ | --- | --- |
339
+ | PHP | Four-space blocks and array nesting; single quotes for ordinary static strings without interpolation; spaces around concatenation. Keep escaped double quotes where control strings or newline output require them. |
340
+ | Ruby | Two-space blocks and single quotes for plain authored strings. Keep `dump`, `inspect` and PP encoding for sample data. |
341
+ | JavaScript / TypeScript | Two-space blocks, double-quoted strings and import specifiers, semicolons, and conventional spacing after commas and colons and inside import/object braces. Preserve safe computed keys and typed property access. |
342
+ | Python | Four-space suites and the existing double-quoted data encoding. Omit empty optional import slots; use one blank line before module-level executable code and two around top-level async definitions. |
343
+ | Go | Tabs, gofmt-compatible spacing and separate standard-library/client import groups. Keep native double-quoted string encoding. |
344
+ | Rust | Four-space blocks, conventional Rust spacing/imports and the existing string encoder in both async and blocking examples. |
345
+ | CLI / curl | Preserve shell continuations. Serialize data first, then encode every complete dynamic shell argument with `Shellwords.escape`; this includes URLs, headers, option names and values. |
346
+ | HTTP | Preserve the protocol/JSON transcript, especially the header/body separator. Shell quoting rules do not apply. |
347
+
348
+ At known fragment boundaries, omit absent fragments and separate logical blocks
349
+ with one useful blank line, subject to the language rules above. Avoid leading
350
+ or trailing blank padding, duplicate separators and whitespace on empty lines.
351
+ Keep formatting in the owning language renderer or adapter. Do not normalize
352
+ whole rendered programs or custom-renderer output with a shared regex or
353
+ formatter, or add a runtime formatter dependency. Tests may use native formatters.
354
+
355
+ For CLI and curl, keep authored shell syntax separate from argument data. Encode
356
+ each argument once after its value has been serialized; surrounding raw data
357
+ with single or double quotes is insufficient. Check generated commands with
358
+ temporary fake executables: require successful execution, exact captured argument
359
+ values and order, and no unintended command execution. Parse captured curl JSON
360
+ to verify that shell encoding preserves the complete request body.
361
+
362
+ CLI and curl examples can show response output after the executable command.
363
+ Keep every physical response line inert when the complete block is pasted into a
364
+ shell. Normalize CRLF and bare CR to LF only within display-only response
365
+ fragments, then prefix every resulting line with `# `. Do not normalize or
366
+ comment the request command, and do not use shell escaping as a substitute for
367
+ commenting each physical response line. Regression tests must execute the
368
+ complete displayed block with temporary fake executables, preserve the exact
369
+ request arguments and curl JSON, and prove that hostile multiline response
370
+ values stay visible without being evaluated.
371
+
372
+ Keep encoded data separate from authored syntax. Preserve runtime values,
373
+ Unicode, apostrophes, backslashes, dollar signs, control bytes, empty values,
374
+ false, zero and null. PHP control strings may need double quotes with dollar
375
+ signs, backslashes and quotes escaped and PHP-valid control escapes; JSON
376
+ `\u` escapes are not a PHP string encoder. Never turn a newline into literal
377
+ backslash-n or let wrapper indentation change literal bytes.
378
+
379
+ Formatting changes must preserve public sample credentials, request inputs,
380
+ error handlers and control flow, direct-call/custom-adapter compatibility, and
381
+ meaningful detail choices. Do not add panels solely to show different formatting.
382
+
383
+ When changing a renderer:
384
+
385
+ - Inspect relevant initialization, authentication and action output for affected
386
+ variants and both direct detail inputs. Cover absent optional fragments and
387
+ literal edge cases where the change can affect them.
388
+ - Use existing [server example specs](../servers/ruby/spec/client_examples/) for
389
+ PHP literal round trips and real handlers, Ruby syntax, Python AST/separation,
390
+ detail behavior and custom whitespace. Use the owning
391
+ [Go documentation specs](../clients/go/spec/unit/documentation_spec.rb),
392
+ [TypeScript documentation specs](../clients/typescript/spec/unit/) and
393
+ [Rust development checks](../clients/rust/docs/development.md) when affected.
394
+ TypeScript coverage includes source, strict compilation and performance specs.
395
+ - Search neighboring HTML, fixture and performance assertions for exact source
396
+ spelling. Update intentional quote/import/spacing expectations while retaining
397
+ semantic assertions, traversal/allocation bounds and strict compiler checks.
398
+ - Follow [development commands](agent-instructions/development.md) and the
399
+ existing verification requirements. Select checks by affected language and
400
+ behavior; an unrelated edit does not require every language toolchain.
401
+
231
402
  ## Verification
232
403
 
233
404
  `make test-documentation` checks renderers, contexts, profiles, rendered HTML
@@ -268,9 +439,11 @@ The target synchronizes shared documentation before running the harness.
268
439
  Standard
269
440
  output is JSON; shell/dependency preparation happens before the redirected
270
441
  command.
271
- No network server, database, generated SDK compilation or formatter is involved.
272
- The synthetic fixture uses Basic users `admin` and `member`, both with the local
273
- test password `fixture-only`.
442
+ No network server, database, API client compilation or formatter is involved.
443
+ The synthetic fixture has users `admin` and `member`. Direct-render and description
444
+ lanes use Basic authentication with the local password `fixture-only`. HTML
445
+ requests use real documentation OAuth cookies on supported sources, with token
446
+ validation on each request; historical sources without that feature use Basic.
274
447
 
275
448
  Defaults measure two shapes with eight ordinary actions each: eight resources
276
449
  with one action, and one resource with eight actions. Each ordinary action also
@@ -351,8 +524,8 @@ failure class, while concentrated-resource costs and other future regressions
351
524
  still need representative measurements.
352
525
 
353
526
  Each client also has acceptance commands that check executable snippets against
354
- installed packages or a generated SDK. These use
355
- rendered source against disposable fixtures; syntax checks alone do not
527
+ installed packages or an API client. These run rendered source against
528
+ disposable fixtures; syntax checks alone do not
356
529
  establish that client calls or authentication work. See each client's
357
530
  development documentation for its test environment. Highlighter rebuilding is
358
531
  described in [utils/highlight](../utils/highlight/README.md).
@@ -0,0 +1,188 @@
1
+ # OAuth sign-in for HTML documentation
2
+
3
+ The Ruby server can use an existing OAuth provider to sign readers into its HTML
4
+ documentation. This is an optional confidential client: the server exchanges
5
+ authorization codes and keeps the client secret out of the browser. Passwords,
6
+ MFA, account recovery and optional SSO remain with the provider.
7
+
8
+ ## Configure the client
9
+
10
+ Register a dedicated confidential client with your OAuth provider. Use the exact
11
+ HTTPS callback `<API mount>_documentation/oauth/callback`, fixed access-token
12
+ expiry and no refresh token. Keep existing default OAuth clients unchanged.
13
+
14
+ Configure the documentation client before mounting the server:
15
+
16
+ ```ruby
17
+ api.documentation_oauth(
18
+ client_id: ENV.fetch('DOCUMENTATION_CLIENT_ID'),
19
+ client_secret: ENV.fetch('DOCUMENTATION_CLIENT_SECRET'),
20
+ redirect_uri: 'https://docs.example.test/api/_documentation/oauth/callback',
21
+ session_secret: File.binread('/run/secrets/documentation-session'),
22
+ scope: ['all'],
23
+ session_lifetime: 3600
24
+ )
25
+ api.mount('/api/')
26
+ ```
27
+
28
+ Generate at least 64 bytes of cryptographically random session secret material
29
+ once, keep it private, and supply the same value to every worker. Do not generate
30
+ it on each startup. Changing it signs existing documentation sessions out. Client
31
+ credentials and session secrets must not be committed or included in diagnostics.
32
+
33
+ `scope` defaults to `[]`. Request only the scopes needed by the application.
34
+ vpsAdmin uses `['all']` to describe the actions available to the signed-in user's
35
+ role. Its resulting bearer token can also authorize API requests, so keeping the
36
+ documentation cookie out of API authentication is essential.
37
+
38
+ `session_lifetime` defaults to 3600 seconds and accepts integers from 1 to 86400.
39
+ The actual session ends at the earlier of this limit and the provider's access
40
+ token expiry. No refresh token is stored or used.
41
+
42
+ `switch_user` defaults to `false` and accepts only `true` or `false`. Enable it
43
+ only after verifying that a fresh authorization for this dedicated client lets
44
+ the reader enter different credentials. This option asserts that the operator
45
+ has checked that behavior; it does not discover account-selection support or
46
+ send `prompt` or `select_account` parameters.
47
+
48
+ `switch_user_revoke_params` defaults to `{}` and accepts a static hash of string
49
+ keys and string values. These fields are added to the revoke POST only for a
50
+ validated Switch user request. They are never sent during ordinary Sign out,
51
+ authorization or token exchange. The required `client_id`, `client_secret`,
52
+ `token` and `token_type_hint` fields cannot be configured through this option.
53
+ The server copies and freezes the hash and its strings at startup; browser
54
+ fields and later changes to the original hash cannot change the revoke payload.
55
+ The option has no effect when `switch_user` is `false`.
56
+
57
+ For example, a dedicated nondefault vpsAdmin client can allow SSO and use the
58
+ provider's `close_sso` revoke parameter when switching:
59
+
60
+ ```ruby
61
+ api.documentation_oauth(
62
+ # Include the client, callback, session and scope options shown above.
63
+ switch_user: true,
64
+ switch_user_revoke_params: { 'close_sso' => '1' }
65
+ )
66
+ ```
67
+
68
+ Verify that the provider accepts these parameters and that the next authorization
69
+ allows different credentials. Revoking the documentation token alone can leave
70
+ SSO available when another active authorization shares it. Provider-specific
71
+ SSO closure need not revoke that other authorization's access token. A dedicated
72
+ client with SSO disabled is another way to permit account switching.
73
+
74
+ The callback must be an absolute HTTPS URL with the exact mounted callback path,
75
+ without credentials, query or fragment. The server takes authorize, token and
76
+ revoke endpoints from its default version's configured OAuth provider metadata
77
+ at mount time. Those endpoints must also use HTTPS. It does not discover an
78
+ issuer from a browser request or follow endpoint redirects. Missing providers,
79
+ invalid configuration and repeated or late configuration fail startup.
80
+
81
+ ## HTTPS and worker requirements
82
+
83
+ Serve the documentation at the configured HTTPS origin. The Rack request scheme
84
+ and host must reflect that origin. When using a reverse proxy, configure trusted
85
+ forwarding in the server/proxy; do not trust arbitrary client-supplied forwarding
86
+ headers. A private development CA must be trusted by both the reader's browser
87
+ and the server's standard HTTPS trust store. Never disable peer verification.
88
+
89
+ Token and revoke exchanges use bounded HTTPS requests. If the provider shares
90
+ the documentation server's public origin, the server needs enough concurrent
91
+ request capacity to handle a token exchange while the callback is running.
92
+ A single-request worker cannot serve both sides of that exchange.
93
+
94
+ ## Browser session and access checks
95
+
96
+ Sign-in uses state and S256 PKCE, with a ten-minute encrypted pending cookie.
97
+ Return navigation contains validated documentation version/page/resource fields
98
+ and an optional bounded fragment, rather than an arbitrary return URL. Callback
99
+ errors offer a retry without exposing provider responses or exception details.
100
+ Successful callbacks redirect promptly to a clean documentation URL.
101
+ Failed, cancelled and replayed callbacks consume pending state and preserve an
102
+ existing authenticated documentation session.
103
+
104
+ Authenticated cookies contain only the access token, expiry and logout CSRF
105
+ nonce. ActiveSupport MessageEncryptor protects them with AES-256-GCM and JSON;
106
+ separate purposes bind pending and authenticated cookies to the client, callback
107
+ and API mount. Cookies are Secure, HttpOnly, SameSite=Lax, mount-scoped and have
108
+ no Domain attribute. Complete cookies are limited to 3800 bytes; oversized token
109
+ responses fail sign-in rather than producing truncated sessions. Browser storage
110
+ used for documentation preferences never contains tokens.
111
+
112
+ HTML documentation GET and HEAD requests, including the versions index and
113
+ search-index representations, use only the documentation cookie on its configured
114
+ HTTPS origin. Without that configuration or a valid cookie, documentation is
115
+ anonymous. Basic credentials, Bearer/custom token headers and query credentials
116
+ do not select its identity. With API credentials for user A and a valid
117
+ documentation cookie for user B, the reference and its account controls use B.
118
+ Invalid or conflicting API credentials do not affect that selection.
119
+
120
+ Each network page request decrypts and validates the cookie, then validates its
121
+ token through the requested version's OAuth2 provider and runs the existing
122
+ post-authentication hook before describing resources. It passes the original
123
+ request and the cookie token separately, without changing request headers or
124
+ parameters. Identity and roles are never taken from a cached user ID. A revoked
125
+ or expired session returns to the public view; an inaccessible selected resource
126
+ is 404. Conditional search-index requests perform the same validation.
127
+
128
+ The standard OAuth2 provider delegates this explicit validation to its existing
129
+ `config.find_user_by_access_token(request, token)` callback. A custom provider
130
+ named `oauth2` must implement `authenticate_access_token(request, token)` to
131
+ support documentation sessions. Unsupported versions or providers cannot fall
132
+ back to Basic, Token or another API authenticator. Ordinary API actions, OPTIONS
133
+ descriptions, mutations and provider endpoints retain their complete existing
134
+ authentication chain and credential-conflict rules. The documentation cookie
135
+ does not authenticate those operations.
136
+
137
+ Account sign-in, sign-out and switching belong to this documentation session.
138
+ The compatibility GET URLs `_login` and `_logout` redirect with HTTP 303 to the
139
+ default version overview without authenticating, challenging, clearing cookies or
140
+ revoking tokens. They ignore arbitrary return destinations. API Basic challenges
141
+ continue to work, and documentation switching does not erase cached Basic
142
+ credentials or sign out of an API client.
143
+
144
+ Personalized HTML and authentication responses use private, no-store caching.
145
+ Documentation pages use `Referrer-Policy: strict-origin` so native sign-out and
146
+ Switch user forms retain their same-origin `Origin` header. Referrers contain at
147
+ most the origin and are omitted on HTTPS-to-HTTP downgrades. Login, callback,
148
+ sign-out and authentication error responses use `Referrer-Policy: no-referrer`.
149
+ Configure access and error logs so callback query strings, tokens, passwords and
150
+ client credentials are not retained.
151
+
152
+ ## Sign-out and recovery
153
+
154
+ Sign-out is a same-origin POST with a session-bound CSRF nonce. The server
155
+ attempts access-token revocation and clears local authenticated and pending
156
+ cookies. If revocation fails, the response states that local sign-out succeeded
157
+ but the server session may remain until expiry. It does not claim a successful
158
+ revocation, queue tokens for retry, or request a whole-account SSO sign-out.
159
+
160
+ When `switch_user: true`, readers authenticated by a documentation cookie see
161
+ Switch user in Documentation settings. It submits the existing same-origin,
162
+ CSRF-protected logout POST with one URL-encoded scalar `switch_user=1`.
163
+ Disabled, duplicate, array and other invalid switch fields or invalid origin/CSRF
164
+ are rejected before sign-out or revocation. Ordinary Sign out sends no switch
165
+ field. API credentials on the request do not affect these cookie-session operations.
166
+
167
+ After successful local sign-out and revocation, switching returns 303 to the
168
+ fixed documentation login route for the default version. That login creates new
169
+ state and PKCE values; the old pending attempt and session are cleared. No return
170
+ URL or scope supplied by the switch request changes that destination. Cancelling
171
+ the new authorization leaves the old documentation session signed out. A revoke
172
+ failure retains the 502 response described above and does not continue to login.
173
+ Switching can request provider-specific SSO behavior through the configured
174
+ revoke parameters; HaveAPI does not interpret them or guarantee whole-account
175
+ sign-out. Ordinary Sign out still revokes only the documentation token without
176
+ those parameters.
177
+
178
+ To disable this feature, remove its optional configuration before starting older
179
+ documentation code. In particular, remove `switch_user_revoke_params` before
180
+ rolling back to a HaveAPI version that does not support it; the unknown option
181
+ would prevent startup. Upgrade HaveAPI before enabling the option, including on
182
+ each worker that loads the configuration. Existing API data needs no migration
183
+ or rollback. Cookies expire or are ignored. Disable or revoke the dedicated
184
+ OAuth client through normal provider administration when needed; documentation
185
+ rollback does not delete clients automatically. Without this configuration, documentation remains
186
+ anonymous and offers no authentication controls; ordinary API authentication is
187
+ unchanged. Older documentation code may use API credentials and legacy browser
188
+ sign-in routes again. Rolling back requires no cookie or database migration.