app_bridge 4.1.1 → 5.0.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.
data/Cargo.toml CHANGED
@@ -5,4 +5,4 @@
5
5
  [workspace]
6
6
  members = ["./ext/app_bridge"]
7
7
  resolver = "2"
8
- exclude = ["./spec/fixtures/components/rust_app", "./spec/fixtures/components/rust_app_v3"]
8
+ exclude = ["./spec/fixtures/components/rust_app", "./spec/fixtures/components/rust_app_v3", "./spec/fixtures/components/rust_app_v5"]
data/README.md CHANGED
@@ -305,6 +305,12 @@ impl v5::standout::app::new_feature::Host for AppState {
305
305
  - **Gradual migration**: Update connectors to new WIT versions at your own pace
306
306
  - **Type safety**: Each version's types are converted to the latest version internally
307
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
+
308
314
  ## Development
309
315
 
310
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
@@ -2,7 +2,7 @@
2
2
  name = "app_bridge"
3
3
  # When updating the version, please also update the version in the
4
4
  # lib/app_bridge/version.rb file to keep them in sync.
5
- version = "4.1.1"
5
+ version = "5.0.0"
6
6
  edition = "2021"
7
7
  authors = ["Alexander Ross <ross@standout.se>"]
8
8
  publish = false
@@ -11,14 +11,16 @@ publish = false
11
11
  crate-type = ["cdylib"]
12
12
 
13
13
  [dependencies]
14
- magnus = { version = "0.7.1" }
15
- wasmtime = "33.0.2"
16
- wasmtime-wasi = "33.0.2"
14
+ magnus = { version = "0.8.2" }
15
+ wasmtime = "44.0.2"
16
+ wasmtime-wasi = "44.0.2"
17
+ wasmtime-wasi-io = "44.0.2"
17
18
  reqwest = { version = "0.12", features = ["blocking", "json", "native-tls-vendored"] }
18
19
  base64 = "0.22"
19
20
  infer = "0.16"
20
21
  serde = { version = "1.0", features = ["derive"] }
21
22
  serde_json = "1.0"
23
+ rustls-webpki = "0.103.13"
22
24
 
23
25
  [dev-dependencies]
24
26
  httpmock = "0.8.2"
@@ -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
+ }
@@ -1,10 +1,11 @@
1
- use crate::component::{v3, v4, v4_1};
1
+ use crate::component::{v3, v4, v4_1, v5};
2
2
  use crate::component::v4::standout::app::http::Request;
3
3
  use reqwest::blocking::Client;
4
4
  use std::collections::HashMap;
5
5
  use std::sync::{Arc, Mutex};
6
6
  use wasmtime::component::ResourceTable;
7
- use wasmtime_wasi::p2::{WasiCtx, WasiCtxBuilder, WasiView, IoView};
7
+ use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView};
8
+ use wasmtime_wasi_io::IoView;
8
9
 
9
10
  pub struct AppState {
10
11
  ctx: WasiCtx,
@@ -56,20 +57,24 @@ macro_rules! impl_host_for_version {
56
57
  impl_host_for_version!(v3);
57
58
  impl_host_for_version!(v4);
58
59
  impl_host_for_version!(v4_1);
60
+ impl_host_for_version!(v5);
59
61
 
60
62
  // ============================================================================
61
63
  // WASI implementations
62
64
  // ============================================================================
63
65
 
64
- impl IoView for AppState {
65
- fn table(&mut self) -> &mut ResourceTable {
66
- &mut self.table
66
+ impl WasiView for AppState {
67
+ fn ctx(&mut self) -> WasiCtxView<'_> {
68
+ WasiCtxView {
69
+ ctx: &mut self.ctx,
70
+ table: &mut self.table,
71
+ }
67
72
  }
68
73
  }
69
74
 
70
- impl WasiView for AppState {
71
- fn ctx(&mut self) -> &mut WasiCtx {
72
- &mut self.ctx
75
+ impl IoView for AppState {
76
+ fn table(&mut self) -> &mut ResourceTable {
77
+ &mut self.table
73
78
  }
74
79
  }
75
80