app_bridge 4.0.0-x86_64-linux → 5.0.0-x86_64-linux

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e2ef17c1b654b1c1b81501984696d28346c42c3650759cad8e28f1ce10fdb427
4
- data.tar.gz: 366015ee7c93a0dab3a90573994d83913083b7661f5cc5617d44137db019333c
3
+ metadata.gz: 42a6e98398ec928fdd633a6062f47600331ddee76382366fafbc87b5fcecc2f4
4
+ data.tar.gz: de9cb9e4c3169a3f9e70d57eff0a585dc7bc0620fb29e1cc7aa5aab412093728
5
5
  SHA512:
6
- metadata.gz: bf5c9d65f7f7bc6849455f23e1502c4ef917de7df323ae407c6195d34c0697b15502351c301f831da8c736a21a63afaf4d9613fe3510bbbc256e52e047290dbf
7
- data.tar.gz: 5a4e1c0a5c04fd3ac2c30696bd6c59c6f5d953918add386094e622e63e989f796dc9a84410c0483345cec6fee8de10d0772a5db6163d9cdd325ccb7764feab1f
6
+ metadata.gz: 2dd0825cdcba66bd2862ba78937e8f5ccb58856f95c503a9fb25577c5c4f49540c3a6ff15e91c8c2a0e21a0d94062b6f60506070695d04a5cba75d8c5a55e675
7
+ data.tar.gz: 0aff9c1d8fe5ac5c0f85303e50dc13df59848d11e5559b9cf86a197af01a93d74ddd813fa640d32a9959e65ab27efa01ce792827bbae23dfe8d47d4fceba25f9
data/.tool-versions CHANGED
@@ -1 +1 @@
1
- ruby 3.4.8
1
+ ruby 3.4.9
data/README.md CHANGED
@@ -103,6 +103,67 @@ AppBridge.file_uploader = ->(file_data) {
103
103
 
104
104
  The gem automatically replaces file data with the return value (in this example blob IDs) before returning the action response.
105
105
 
106
+ ### Multipart Form Data
107
+
108
+ For `multipart/form-data`, you must build the body manually and set the `Content-Type` header with a boundary. In `standout:app@4.1.0` you can send raw bytes via `body-bytes`; earlier versions only support a string body.
109
+
110
+ #### In your WASM connector (Rust):
111
+
112
+ ```rust
113
+ let boundary = "----app-bridge-boundary";
114
+
115
+ let mut body = Vec::new();
116
+ body.extend_from_slice(format!(
117
+ "--{b}\r\n\
118
+ Content-Disposition: form-data; name=\"metadata\"\r\n\r\n\
119
+ {metadata}\r\n\
120
+ --{b}\r\n\
121
+ Content-Disposition: form-data; name=\"file\"; filename=\"invoice.pdf\"\r\n\
122
+ Content-Type: application/pdf\r\n\r\n",
123
+ b = boundary,
124
+ metadata = metadata_json,
125
+ ).as_bytes());
126
+ body.extend_from_slice(&file_bytes);
127
+ body.extend_from_slice(format!("\r\n--{b}--\r\n", b = boundary).as_bytes());
128
+
129
+ let response = RequestBuilder::new()
130
+ .method(Method::Post)
131
+ .url(upload_url)
132
+ .header("Content-Type", format!("multipart/form-data; boundary={}", boundary))
133
+ .body_bytes(body)
134
+ .send()?;
135
+ ```
136
+
137
+ If you are targeting `standout:app@4.0.0` or earlier, you can only send a string body. That means multipart uploads must be base64-encoded and the receiving API must support `Content-Transfer-Encoding: base64`.
138
+
139
+ ## Retry With Reference
140
+
141
+ `standout:app@4.1.0` introduces a structured retry error for actions. Connectors can return a retry error with a reference and status, and the platform can pass that back on subsequent retries via `action-context.reference-object`.
142
+
143
+ ### In your WASM connector (Rust)
144
+
145
+ ```rust
146
+ return Err(AppError {
147
+ code: ErrorCode::RetryWithReference(ReferenceObject {
148
+ reference: request_id.to_string(),
149
+ status: status.to_string(),
150
+ }),
151
+ message: "Background request still processing".to_string(),
152
+ });
153
+ ```
154
+
155
+ ### In the Ruby platform
156
+
157
+ When the connector returns `retry-with-reference`, the bridge raises `AppBridge::RetryWithReferenceError`. You can access the structured fields:
158
+
159
+ ```ruby
160
+ rescue AppBridge::RetryWithReferenceError => e
161
+ e.reference # => "ref-123"
162
+ e.status # => "queued"
163
+ e.message # => "Background request still processing after 1 attempts."
164
+ end
165
+ ```
166
+
106
167
  ## Backward Compatibility
107
168
 
108
169
  The gem supports **multi-version WIT interfaces**, allowing connectors built against older WIT versions to continue working when the gem is updated.
@@ -244,6 +305,12 @@ impl v5::standout::app::new_feature::Host for AppState {
244
305
  - **Gradual migration**: Update connectors to new WIT versions at your own pace
245
306
  - **Type safety**: Each version's types are converted to the latest version internally
246
307
 
308
+ - **Connections interface** (WIT 5.0.0+): Embeds `connection-config` JSON in WASM; platform extracts it at version upload
309
+
310
+ ## Connection configuration (WIT 5)
311
+
312
+ Connectors embed `connection-config.json` via `include_str!` and export it through the `connections` interface. Integrationer reads it **once when the WASM version is saved**, validates against [connection-config-schema.json](ext/app_bridge/docs/connection-config-schema.json), and stores it on the version. Runtime connection flows use the stored JSON, not a live WASM call.
313
+
247
314
  ## Development
248
315
 
249
316
  To contribute or modify this gem, ensure you have the following dependencies installed:
data/Rakefile CHANGED
@@ -1,12 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
5
-
6
- require "rubocop/rake_task"
7
-
8
- RuboCop::RakeTask.new
9
-
10
4
  require "rb_sys/extensiontask"
11
5
 
12
6
  task build: :compile
@@ -17,12 +11,21 @@ RbSys::ExtensionTask.new("app_bridge", GEMSPEC) do |ext|
17
11
  ext.lib_dir = "lib/app_bridge"
18
12
  end
19
13
 
20
- # Load all project specific rake tasks
21
- Dir.glob(File.expand_path("tasks/**/*.rake", __dir__)).each { |file| load file }
14
+ if ENV.key?("RUBY_TARGET")
15
+ task default: :compile
16
+ else
17
+ require "rspec/core/rake_task"
18
+ require "rubocop/rake_task"
22
19
 
23
- # Set test mode environment variable for specs
24
- RSpec::Core::RakeTask.new(:spec) do |_t|
25
- ENV["APP_BRIDGE_TEST_MODE"] = "1"
26
- end
20
+ RuboCop::RakeTask.new
27
21
 
28
- task default: %i[fixtures compile rust:test spec rubocop]
22
+ # Load all project specific rake tasks
23
+ Dir.glob(File.expand_path("tasks/**/*.rake", __dir__)).each { |file| load file }
24
+
25
+ # Set test mode environment variable for specs
26
+ RSpec::Core::RakeTask.new(:spec) do |_t|
27
+ ENV["APP_BRIDGE_TEST_MODE"] = "1"
28
+ end
29
+
30
+ task default: %i[fixtures compile rust:test spec rubocop]
31
+ end
@@ -0,0 +1,500 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:standout:schemas:connection-config:2",
4
+ "title": "Connection Config",
5
+ "description": "Declares how a connector authenticates and what runtime data the platform passes to actions/triggers. OAuth uses RFC 8414 authorization_server metadata and RFC 9728 protected_resource at the top level, with Standout-specific oauth_client credentials and platform hooks alongside. Generated at connector build time (see base-connector-tools). The platform does not fetch well-known metadata at runtime.",
6
+ "type": "object",
7
+ "required": ["strategies", "default_strategy", "runtime"],
8
+ "properties": {
9
+ "protected_resource": {
10
+ "$ref": "#/$defs/protected_resource",
11
+ "description": "RFC 9728 protected resource metadata for the API. runtime.base_url defaults to resource when omitted."
12
+ },
13
+ "strategies": {
14
+ "type": "array",
15
+ "minItems": 0,
16
+ "description": "Connection strategies. An empty array disables WASM-driven connections.",
17
+ "items": { "$ref": "#/$defs/strategy" }
18
+ },
19
+ "default_strategy": {
20
+ "type": "string",
21
+ "description": "ID of the strategy used when connecting"
22
+ },
23
+ "connection_schema": {
24
+ "type": "object",
25
+ "description": "JSON Schema for user input before connection (shared fallback). Field values listed in strategy storage are available as {{field}} in templates."
26
+ },
27
+ "runtime": {
28
+ "$ref": "#/$defs/runtime",
29
+ "description": "Default runtime payload merged with strategy.runtime. Headers and optional base_url override."
30
+ },
31
+ "post_connect": { "$ref": "#/$defs/post_connect_hook" }
32
+ },
33
+ "$defs": {
34
+ "templated_value": {
35
+ "description": "Literal string or template resolved by the platform. Use {{field}} for connection storage keys and {{env:NAME}} for app environment variables.",
36
+ "oneOf": [
37
+ { "type": "string" },
38
+ {
39
+ "type": "object",
40
+ "required": ["template"],
41
+ "additionalProperties": false,
42
+ "properties": {
43
+ "template": {
44
+ "type": "string",
45
+ "description": "e.g. Bearer {{access_token}}, {{username}}:{{password}}, {{env:CLIENT_ID}}"
46
+ },
47
+ "as": {
48
+ "enum": ["basic_authorization"],
49
+ "description": "Interpolate template then encode as HTTP Basic (Basic + Base64)"
50
+ }
51
+ }
52
+ }
53
+ ]
54
+ },
55
+ "templated_uri": {
56
+ "description": "Static URI or template that resolves to a URI at runtime",
57
+ "oneOf": [
58
+ { "type": "string", "format": "uri" },
59
+ {
60
+ "type": "object",
61
+ "required": ["template"],
62
+ "additionalProperties": false,
63
+ "properties": {
64
+ "template": {
65
+ "type": "string",
66
+ "description": "e.g. https://api.example.com/v1, https://{{subdomain}}.example.com/api, {{env:API_BASE_URL}}"
67
+ }
68
+ }
69
+ }
70
+ ]
71
+ },
72
+ "protected_resource": {
73
+ "type": "object",
74
+ "description": "RFC 9728 OAuth Protected Resource Metadata",
75
+ "required": ["resource", "authorization_servers"],
76
+ "properties": {
77
+ "resource": {
78
+ "type": "string",
79
+ "format": "uri",
80
+ "description": "API resource identifier (typically the API base URL)"
81
+ },
82
+ "authorization_servers": {
83
+ "type": "array",
84
+ "minItems": 1,
85
+ "items": { "type": "string", "format": "uri" },
86
+ "description": "Issuer URLs of authorization servers that can issue tokens for this resource"
87
+ },
88
+ "scopes_supported": {
89
+ "type": "array",
90
+ "items": { "type": "string" },
91
+ "description": "Scopes supported when accessing this resource"
92
+ },
93
+ "bearer_methods_supported": {
94
+ "type": "array",
95
+ "items": { "type": "string" },
96
+ "description": "e.g. header"
97
+ }
98
+ }
99
+ },
100
+ "authorization_server": {
101
+ "type": "object",
102
+ "description": "RFC 8414 OAuth Authorization Server Metadata (server capabilities and endpoints)",
103
+ "required": ["issuer", "token_endpoint"],
104
+ "properties": {
105
+ "issuer": {
106
+ "type": "string",
107
+ "format": "uri",
108
+ "description": "Authorization server issuer identifier"
109
+ },
110
+ "authorization_endpoint": {
111
+ "type": "string",
112
+ "format": "uri",
113
+ "description": "OAuth authorization endpoint"
114
+ },
115
+ "token_endpoint": {
116
+ "type": "string",
117
+ "format": "uri",
118
+ "description": "OAuth token endpoint"
119
+ },
120
+ "response_types_supported": {
121
+ "type": "array",
122
+ "items": { "type": "string" },
123
+ "description": "e.g. code"
124
+ },
125
+ "grant_types_supported": {
126
+ "type": "array",
127
+ "items": { "type": "string" },
128
+ "description": "e.g. authorization_code, refresh_token, client_credentials"
129
+ },
130
+ "scopes_supported": {
131
+ "type": "array",
132
+ "items": { "type": "string" },
133
+ "description": "Scopes the authorization server supports"
134
+ },
135
+ "code_challenge_methods_supported": {
136
+ "type": "array",
137
+ "items": { "type": "string" },
138
+ "description": "PKCE methods supported, e.g. S256"
139
+ },
140
+ "token_endpoint_auth_methods_supported": {
141
+ "type": "array",
142
+ "items": { "type": "string" },
143
+ "description": "e.g. client_secret_basic, client_secret_post"
144
+ },
145
+ "revocation_endpoint": {
146
+ "type": "string",
147
+ "format": "uri"
148
+ },
149
+ "introspection_endpoint": {
150
+ "type": "string",
151
+ "format": "uri"
152
+ },
153
+ "registration_endpoint": {
154
+ "type": "string",
155
+ "format": "uri"
156
+ }
157
+ },
158
+ "allOf": [
159
+ {
160
+ "if": {
161
+ "required": ["grant_types_supported"],
162
+ "properties": {
163
+ "grant_types_supported": {
164
+ "type": "array",
165
+ "contains": { "const": "client_credentials" },
166
+ "not": { "contains": { "const": "authorization_code" } }
167
+ }
168
+ }
169
+ },
170
+ "then": {},
171
+ "else": {
172
+ "required": ["authorization_endpoint"]
173
+ }
174
+ }
175
+ ]
176
+ },
177
+ "oauth_client": {
178
+ "type": "object",
179
+ "description": "Standout client registration and OAuth request choices (not part of RFC 8414)",
180
+ "required": ["client_id", "client_secret"],
181
+ "properties": {
182
+ "grant_type": {
183
+ "enum": ["authorization_code", "client_credentials"],
184
+ "default": "authorization_code",
185
+ "description": "OAuth grant used when connecting"
186
+ },
187
+ "response_type": {
188
+ "type": "string",
189
+ "default": "code",
190
+ "description": "Override response_type; defaults to code or first response_types_supported"
191
+ },
192
+ "scopes": {
193
+ "type": "array",
194
+ "items": { "type": "string" },
195
+ "description": "Fixed scope list when scopes_from_field is not used"
196
+ },
197
+ "scopes_from_field": {
198
+ "type": "string",
199
+ "description": "Connection form field containing selected scopes (array of strings)"
200
+ },
201
+ "scopes_always_include": {
202
+ "type": "array",
203
+ "items": { "type": "string" },
204
+ "description": "Scopes always appended to scopes_from_field"
205
+ },
206
+ "client_id": {
207
+ "$ref": "#/$defs/templated_value",
208
+ "description": "OAuth client_id, e.g. {{env:OAUTH_CLIENT_ID}}"
209
+ },
210
+ "client_secret": {
211
+ "$ref": "#/$defs/templated_value",
212
+ "description": "OAuth client_secret, e.g. {{env:OAUTH_CLIENT_SECRET}}"
213
+ },
214
+ "authorization_parameters": {
215
+ "type": "object",
216
+ "description": "Extra query parameters appended to the authorization request",
217
+ "additionalProperties": { "type": "string" }
218
+ },
219
+ "authorization_parameters_from_form": {
220
+ "type": "object",
221
+ "description": "Map connection form field values to authorization query parameters",
222
+ "additionalProperties": {
223
+ "type": "object",
224
+ "additionalProperties": {
225
+ "type": "object",
226
+ "additionalProperties": { "type": "string" }
227
+ }
228
+ }
229
+ },
230
+ "token_request_method": {
231
+ "enum": ["POST", "GET"],
232
+ "default": "POST"
233
+ },
234
+ "token_request_parameters": {
235
+ "type": "object",
236
+ "description": "Extra parameters sent to the token endpoint",
237
+ "additionalProperties": { "type": "string" }
238
+ }
239
+ }
240
+ },
241
+ "strategy": {
242
+ "type": "object",
243
+ "required": ["id", "type"],
244
+ "properties": {
245
+ "id": { "type": "string" },
246
+ "title": {
247
+ "type": "string",
248
+ "description": "Label shown when the user picks authentication type before connecting"
249
+ },
250
+ "type": {
251
+ "enum": ["oauth2", "basic_auth", "api_key", "session", "none", "custom"]
252
+ },
253
+ "authorization_server": {
254
+ "$ref": "#/$defs/authorization_server",
255
+ "description": "RFC 8414 metadata (required when type is oauth2)"
256
+ },
257
+ "oauth_client": {
258
+ "$ref": "#/$defs/oauth_client",
259
+ "description": "Client credentials and request options (required when type is oauth2)"
260
+ },
261
+ "session": {
262
+ "$ref": "#/$defs/session",
263
+ "description": "Session login configuration (required when type is session)"
264
+ },
265
+ "token_response": {
266
+ "$ref": "#/$defs/token_response",
267
+ "description": "Maps token response fields to storage keys. Optional for oauth2 — platform defaults to standard RFC 6749 field names."
268
+ },
269
+ "refresh": {
270
+ "$ref": "#/$defs/refresh",
271
+ "description": "Token refresh policy. Optional for oauth2 — platform defaults to enabled for authorization_code."
272
+ },
273
+ "storage": {
274
+ "description": "Fields persisted in connection storage. Array of keys or object with optional display titles. Available as {{field}} in runtime templates.",
275
+ "oneOf": [
276
+ {
277
+ "type": "array",
278
+ "items": { "type": "string" },
279
+ "minItems": 1
280
+ },
281
+ {
282
+ "type": "object",
283
+ "minProperties": 1,
284
+ "additionalProperties": { "$ref": "#/$defs/storage_field" }
285
+ }
286
+ ]
287
+ },
288
+ "connection_schema": {
289
+ "type": "object",
290
+ "description": "Strategy-specific pre-connection form schema (JSON Schema)"
291
+ },
292
+ "form_persist": {
293
+ "type": "object",
294
+ "description": "Map pre-connection form fields to account storage keys after connect",
295
+ "additionalProperties": {
296
+ "oneOf": [
297
+ { "type": "string" },
298
+ {
299
+ "type": "object",
300
+ "required": ["as"],
301
+ "properties": {
302
+ "as": { "type": "string" },
303
+ "join": {
304
+ "type": "string",
305
+ "description": "Join array values before persisting, e.g. a space for OAuth scopes"
306
+ }
307
+ }
308
+ }
309
+ ]
310
+ }
311
+ },
312
+ "runtime": {
313
+ "$ref": "#/$defs/runtime",
314
+ "description": "Strategy-specific runtime override"
315
+ }
316
+ },
317
+ "allOf": [
318
+ {
319
+ "if": {
320
+ "properties": { "type": { "const": "oauth2" } },
321
+ "required": ["type"]
322
+ },
323
+ "then": { "required": ["authorization_server", "oauth_client"] }
324
+ },
325
+ {
326
+ "if": {
327
+ "properties": { "type": { "const": "session" } },
328
+ "required": ["type"]
329
+ },
330
+ "then": { "required": ["session", "token_response"] }
331
+ }
332
+ ]
333
+ },
334
+ "session": {
335
+ "type": "object",
336
+ "required": ["token_endpoint"],
337
+ "additionalProperties": false,
338
+ "description": "HTTP request that creates an API session",
339
+ "properties": {
340
+ "token_endpoint": {
341
+ "$ref": "#/$defs/templated_uri",
342
+ "description": "Login URL that returns a session token"
343
+ },
344
+ "method": {
345
+ "enum": ["GET", "POST", "PUT", "PATCH", "DELETE"],
346
+ "default": "POST"
347
+ },
348
+ "headers": {
349
+ "type": "object",
350
+ "additionalProperties": { "$ref": "#/$defs/templated_value" }
351
+ },
352
+ "body": {
353
+ "type": "object",
354
+ "additionalProperties": { "$ref": "#/$defs/templated_value" }
355
+ },
356
+ "body_format": {
357
+ "enum": ["json", "form"],
358
+ "default": "json"
359
+ },
360
+ "token_source": {
361
+ "enum": ["body", "header"],
362
+ "default": "body",
363
+ "description": "Whether token_response reads from JSON response body or a response header"
364
+ },
365
+ "token_header": {
366
+ "type": "string",
367
+ "description": "Response header name when token_source is header"
368
+ },
369
+ "success_status": {
370
+ "type": "array",
371
+ "items": { "type": "integer" },
372
+ "default": [200]
373
+ }
374
+ }
375
+ },
376
+ "token_response": {
377
+ "type": "object",
378
+ "description": "Maps token or session response fields to connection storage keys",
379
+ "properties": {
380
+ "access_token": { "type": "string" },
381
+ "refresh_token": { "type": "string" },
382
+ "expires_at": {
383
+ "type": "object",
384
+ "properties": {
385
+ "from": { "type": "string" },
386
+ "type": { "enum": ["relative_seconds", "absolute_timestamp", "iso8601"] }
387
+ }
388
+ }
389
+ }
390
+ },
391
+ "refresh": {
392
+ "type": "object",
393
+ "properties": {
394
+ "enabled": { "type": "boolean" },
395
+ "grant_type": { "enum": ["refresh_token", "client_credentials"] },
396
+ "token_endpoint": {
397
+ "$ref": "#/$defs/templated_uri",
398
+ "description": "Override authorization_server.token_endpoint for refresh"
399
+ },
400
+ "expires_latency": {
401
+ "type": "integer",
402
+ "minimum": 0,
403
+ "description": "Seconds to reduce token validity by before refresh (oauth2 expires_latency)"
404
+ }
405
+ }
406
+ },
407
+ "storage_field": {
408
+ "type": "object",
409
+ "description": "Storage field metadata for account display and connect forms.",
410
+ "additionalProperties": false,
411
+ "properties": {
412
+ "title": {
413
+ "type": "string",
414
+ "description": "Label shown for this field"
415
+ },
416
+ "description": {
417
+ "type": "string",
418
+ "description": "Help text shown on connect forms. Not persisted."
419
+ }
420
+ }
421
+ },
422
+ "post_connect_hook": {
423
+ "type": "object",
424
+ "description": "Platform-run validation after connect. Uses resolved runtime headers and base_url for the active strategy.",
425
+ "required": ["request"],
426
+ "properties": {
427
+ "request": {
428
+ "type": "object",
429
+ "properties": {
430
+ "method": {
431
+ "enum": ["GET", "POST", "PUT", "PATCH", "DELETE"],
432
+ "default": "GET"
433
+ },
434
+ "path": {
435
+ "type": "string",
436
+ "description": "Appended to runtime base_url, e.g. /v1/me"
437
+ },
438
+ "url": {
439
+ "$ref": "#/$defs/templated_uri",
440
+ "description": "Optional full URL override instead of base_url + path"
441
+ },
442
+ "body": {
443
+ "type": "object",
444
+ "additionalProperties": { "$ref": "#/$defs/templated_value" }
445
+ },
446
+ "body_format": {
447
+ "enum": ["json", "form"],
448
+ "default": "json"
449
+ }
450
+ },
451
+ "anyOf": [
452
+ { "required": ["path"] },
453
+ { "required": ["url"] }
454
+ ]
455
+ },
456
+ "success_status": {
457
+ "type": "array",
458
+ "items": { "type": "integer" },
459
+ "default": [200]
460
+ },
461
+ "persist": {
462
+ "type": "object",
463
+ "description": "Map response values to connection storage keys",
464
+ "additionalProperties": {
465
+ "oneOf": [
466
+ {
467
+ "type": "string",
468
+ "description": "Dot path from response context, e.g. body.account.id"
469
+ },
470
+ {
471
+ "type": "object",
472
+ "required": ["from"],
473
+ "additionalProperties": false,
474
+ "properties": {
475
+ "from": { "type": "string" },
476
+ "as": { "enum": ["string"] }
477
+ }
478
+ }
479
+ ]
480
+ }
481
+ }
482
+ }
483
+ },
484
+ "runtime": {
485
+ "type": "object",
486
+ "description": "Runtime payload for actions/triggers. base_url defaults to protected_resource.resource when omitted.",
487
+ "properties": {
488
+ "base_url": { "$ref": "#/$defs/templated_uri" },
489
+ "headers": {
490
+ "type": "object",
491
+ "additionalProperties": { "$ref": "#/$defs/templated_value" }
492
+ },
493
+ "fields": {
494
+ "type": "object",
495
+ "additionalProperties": { "$ref": "#/$defs/templated_value" }
496
+ }
497
+ }
498
+ }
499
+ }
500
+ }