app_bridge 4.1.0-aarch64-linux → 5.0.0-aarch64-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 +4 -4
- data/.tool-versions +1 -1
- data/README.md +6 -0
- data/Rakefile +16 -13
- data/ext/app_bridge/docs/connection-config-schema.json +500 -0
- data/ext/app_bridge/wit/v4_1/world.wit +2 -0
- data/ext/app_bridge/wit/v5/world.wit +363 -0
- data/lib/app_bridge/3.3/app_bridge.so +0 -0
- data/lib/app_bridge/3.4/app_bridge.so +0 -0
- data/lib/app_bridge/{3.2 → 4.0}/app_bridge.so +0 -0
- data/lib/app_bridge/app.rb +10 -0
- data/lib/app_bridge/connection_config_validator.rb +101 -0
- data/lib/app_bridge/version.rb +1 -1
- data/lib/app_bridge.rb +2 -0
- data/tasks/fixtures.rake +25 -4
- metadata +24 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7682962940ed84a4fb2094e10cd617180918e0d81b40c7582f1d60e0adda6b60
|
|
4
|
+
data.tar.gz: 2cc9bafd2f197425a7953321b3a6c0ad91a6e008b30904ca7299363fdc699fee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 023e6c2edb75406ae15ee0a80e13773c54ec482b915cb0afa6008127d3b62dd31ba56285365560eeb338d8ddb4876a2d937da5c6449a7dee005c5cc4433ccf0c
|
|
7
|
+
data.tar.gz: 24ced7ec7713fc4c22600e12a61793b978bb76a7459f265d8c41230b286cc0b0db84d8227638afefb08cc01e5c725c8ec50986807d934dc0706893f5a60353f6
|
data/.tool-versions
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
ruby 3.4.
|
|
1
|
+
ruby 3.4.9
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
RSpec::Core::RakeTask.new(:spec) do |_t|
|
|
25
|
-
ENV["APP_BRIDGE_TEST_MODE"] = "1"
|
|
26
|
-
end
|
|
20
|
+
RuboCop::RakeTask.new
|
|
27
21
|
|
|
28
|
-
|
|
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
|
+
}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
package standout:app@5.0.0;
|
|
2
|
+
|
|
3
|
+
interface types {
|
|
4
|
+
// The trigger-store is a string that is used to store data between trigger
|
|
5
|
+
// invocations. It is unique per trigger instance and is persisted between
|
|
6
|
+
// invocations.
|
|
7
|
+
//
|
|
8
|
+
// You can store any string here. We suggest that you use a serialized
|
|
9
|
+
// JSON object or similar since that will give you some flexibility if you
|
|
10
|
+
// need to add more data to the store.
|
|
11
|
+
type trigger-store = string;
|
|
12
|
+
|
|
13
|
+
record connection {
|
|
14
|
+
id: string,
|
|
15
|
+
name: string,
|
|
16
|
+
// The connection data is a JSON object serialized into a string. The JSON root
|
|
17
|
+
// will always be an object.
|
|
18
|
+
serialized-data: string,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
record trigger-context {
|
|
22
|
+
// Trigger ID is a unique identifier for the trigger that is requested to be
|
|
23
|
+
// invoked.
|
|
24
|
+
trigger-id: string,
|
|
25
|
+
|
|
26
|
+
// The connection that the trigger is invoked for.
|
|
27
|
+
// Connection is required for all trigger operations.
|
|
28
|
+
connection: connection,
|
|
29
|
+
|
|
30
|
+
// The store will contain the data that was stored in the trigger store the
|
|
31
|
+
// last time the trigger was invoked.
|
|
32
|
+
store: trigger-store,
|
|
33
|
+
|
|
34
|
+
// The input data for the trigger, serialized as a JSON object string.
|
|
35
|
+
// This contains the input data from the trigger configuration form.
|
|
36
|
+
serialized-input: string,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
record action-context {
|
|
40
|
+
// Action ID is a unique identifier for the action that is requested to be
|
|
41
|
+
// invoked.
|
|
42
|
+
action-id: string,
|
|
43
|
+
|
|
44
|
+
// The connection that the action is invoked for.
|
|
45
|
+
// Connection is required for all action operations.
|
|
46
|
+
connection: connection,
|
|
47
|
+
|
|
48
|
+
// The input data for the action, serialized as a JSON object string.
|
|
49
|
+
// This contains the data passed from the previous step in the workflow.
|
|
50
|
+
serialized-input: string,
|
|
51
|
+
|
|
52
|
+
// Optional reference information when the platform retries an action.
|
|
53
|
+
reference-object: option<reference-object>,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
record trigger-response {
|
|
57
|
+
// The trigger events, each event will be used to spawn a new workflow
|
|
58
|
+
// execution in Standouts integration platform.
|
|
59
|
+
events: list<trigger-event>,
|
|
60
|
+
|
|
61
|
+
// The updated store will be stored and used the next time the trigger is
|
|
62
|
+
// invoked.
|
|
63
|
+
store: trigger-store,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
record action-response {
|
|
67
|
+
// The output data from the action, serialized as a JSON object string.
|
|
68
|
+
// This contains the data that will be passed to the next step in the workflow.
|
|
69
|
+
// The data must be a valid JSON object (not an array or primitive).
|
|
70
|
+
serialized-output: string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
record trigger-event {
|
|
74
|
+
// The ID of the trigger event
|
|
75
|
+
//
|
|
76
|
+
// If the connection used for the given instance of the trigger is the same,
|
|
77
|
+
// as seen before. Then the event will be ignored.
|
|
78
|
+
//
|
|
79
|
+
// A scheduler could therefore use a timestamp as the ID, to ensure that
|
|
80
|
+
// the event is only triggered once per given time.
|
|
81
|
+
//
|
|
82
|
+
// A trigger that acts on created orders in a e-commerce system could use
|
|
83
|
+
// the order ID as the ID, to ensure that the event is only triggered once
|
|
84
|
+
// per order.
|
|
85
|
+
//
|
|
86
|
+
// A trigger that acts on updated orders in a e-commerce system could use
|
|
87
|
+
// the order ID in combination with an updated at timestamp as the ID, to
|
|
88
|
+
// ensure that the event is only triggered once per order update.
|
|
89
|
+
id: string,
|
|
90
|
+
|
|
91
|
+
// Serialized data must be a JSON object serialized into a string
|
|
92
|
+
// Note that it is important that the root is an object, not an array,
|
|
93
|
+
// or another primitive type.
|
|
94
|
+
serialized-data: string,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Retry reference payload returned with error-code.retry-with-reference.
|
|
98
|
+
record reference-object {
|
|
99
|
+
/// Reference ID provided for retrying this request later.
|
|
100
|
+
reference: string,
|
|
101
|
+
|
|
102
|
+
/// Status describing the retry state.
|
|
103
|
+
status: string,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
record app-error {
|
|
107
|
+
/// The error code identifying the type of failure.
|
|
108
|
+
code: error-code,
|
|
109
|
+
|
|
110
|
+
/// A human-readable message describing the error in more detail.
|
|
111
|
+
message: string,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// An enumeration of error codes that can be returned by a trigger implementation.
|
|
115
|
+
/// These codes help the platform and plugin developers distinguish between different types of failures.
|
|
116
|
+
variant error-code {
|
|
117
|
+
/// Authentication failed. Typically due to an invalid or expired API key or token.
|
|
118
|
+
unauthenticated,
|
|
119
|
+
|
|
120
|
+
/// Authorization failed. The connection is valid but does not have the necessary permissions.
|
|
121
|
+
forbidden,
|
|
122
|
+
|
|
123
|
+
/// The trigger is misconfigured. For example, a required setting is missing or invalid.
|
|
124
|
+
misconfigured,
|
|
125
|
+
|
|
126
|
+
/// The target system does not support a required feature or endpoint.
|
|
127
|
+
unsupported,
|
|
128
|
+
|
|
129
|
+
/// The target system is rate-limiting requests. Try again later.
|
|
130
|
+
rate-limit,
|
|
131
|
+
|
|
132
|
+
/// The request timed out. The target system did not respond in time.
|
|
133
|
+
timeout,
|
|
134
|
+
|
|
135
|
+
/// The target system is currently unavailable or unreachable.
|
|
136
|
+
unavailable,
|
|
137
|
+
|
|
138
|
+
/// An unexpected internal error occurred in the plugin.
|
|
139
|
+
internal-error,
|
|
140
|
+
|
|
141
|
+
/// The response from the external system could not be parsed or was in an invalid format.
|
|
142
|
+
malformed-response,
|
|
143
|
+
|
|
144
|
+
/// A catch-all for all other types of errors. Should include a descriptive message.
|
|
145
|
+
other,
|
|
146
|
+
|
|
147
|
+
/// Retry the request using a reference identifier.
|
|
148
|
+
retry-with-reference(reference-object),
|
|
149
|
+
|
|
150
|
+
/// Complete the current workflow execution.
|
|
151
|
+
complete-workflow,
|
|
152
|
+
|
|
153
|
+
/// Complete the parent step execution.
|
|
154
|
+
complete-parent,
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
interface triggers {
|
|
160
|
+
use types.{trigger-context, trigger-event, trigger-response, app-error};
|
|
161
|
+
|
|
162
|
+
trigger-ids: func() -> result<list<string>, app-error>;
|
|
163
|
+
|
|
164
|
+
// Get the input schema for a specific trigger
|
|
165
|
+
// Returns a JSON Schema Draft 2020-12 schema as a string
|
|
166
|
+
// The schema may vary based on the connection in the context
|
|
167
|
+
// The trigger-id is extracted from the context
|
|
168
|
+
input-schema: func(context: trigger-context) -> result<string, app-error>;
|
|
169
|
+
|
|
170
|
+
// Get the output schema for a specific trigger
|
|
171
|
+
// Returns a JSON Schema Draft 2020-12 schema as a string
|
|
172
|
+
// The schema may vary based on the connection in the context
|
|
173
|
+
// The trigger-id is extracted from the context
|
|
174
|
+
output-schema: func(context: trigger-context) -> result<string, app-error>;
|
|
175
|
+
|
|
176
|
+
// Fetch events
|
|
177
|
+
//
|
|
178
|
+
// There are some limitations to the function:
|
|
179
|
+
// - It must return a `trigger-response` within 30 seconds
|
|
180
|
+
// - It must return less than or equal to 100 `trigger-response.events`
|
|
181
|
+
// - It must not return more than 64 kB of data in the `trigger-response.store`
|
|
182
|
+
//
|
|
183
|
+
// If you need to fetch more events, you can return up to 100 events and then
|
|
184
|
+
// store the data needed for you to remember where you left off in the store.
|
|
185
|
+
// The next time the trigger is invoked, you can use the store to continue
|
|
186
|
+
// where you left off.
|
|
187
|
+
//
|
|
188
|
+
// If you do not pass the limitations the return value will be ignored. We
|
|
189
|
+
// will not handle any events and we persist the store that was returned in
|
|
190
|
+
// the response.
|
|
191
|
+
//
|
|
192
|
+
// That also means that you should implement your fetch event function in a
|
|
193
|
+
// way that it can be called multiple times using the same context and return
|
|
194
|
+
// the same events. That will ensure that the user that is building an
|
|
195
|
+
// integration with your trigger will not miss any events if your system is
|
|
196
|
+
// down for a short period of time.
|
|
197
|
+
fetch-events: func(context: trigger-context) -> result<trigger-response, app-error>;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
interface actions {
|
|
201
|
+
use types.{action-context, action-response, app-error};
|
|
202
|
+
|
|
203
|
+
action-ids: func() -> result<list<string>, app-error>;
|
|
204
|
+
|
|
205
|
+
// Get the input schema for a specific action
|
|
206
|
+
// Returns a JSON Schema Draft 2020-12 schema as a string
|
|
207
|
+
// The schema may vary based on the connection in the context
|
|
208
|
+
// The action-id is extracted from the context
|
|
209
|
+
input-schema: func(context: action-context) -> result<string, app-error>;
|
|
210
|
+
|
|
211
|
+
// Get the output schema for a specific action
|
|
212
|
+
// Returns a JSON Schema Draft 2020-12 schema as a string
|
|
213
|
+
// The schema may vary based on the connection in the context
|
|
214
|
+
// The action-id is extracted from the context
|
|
215
|
+
output-schema: func(context: action-context) -> result<string, app-error>;
|
|
216
|
+
|
|
217
|
+
// Execute an action
|
|
218
|
+
//
|
|
219
|
+
// There are some limitations to the function:
|
|
220
|
+
// - It must return an `action-response` within 30 seconds
|
|
221
|
+
// - The serialized-output must be a valid JSON object serialized as a string
|
|
222
|
+
//
|
|
223
|
+
// Actions can perform various operations such as:
|
|
224
|
+
// - Making HTTP requests to external APIs
|
|
225
|
+
// - Processing and transforming data
|
|
226
|
+
// - Storing data for future use
|
|
227
|
+
// - Triggering other systems or workflows
|
|
228
|
+
//
|
|
229
|
+
// The action receives input data from the previous step and can return
|
|
230
|
+
// serialized output data to be passed to the next step in the workflow.
|
|
231
|
+
execute: func(context: action-context) -> result<action-response, app-error>;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
interface environment {
|
|
235
|
+
// Get all environment variables
|
|
236
|
+
env-vars: func() -> list<tuple<string, string>>;
|
|
237
|
+
// Get a specific environment variable by name
|
|
238
|
+
env-var: func(name: string) -> option<string>;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
interface http {
|
|
242
|
+
record response {
|
|
243
|
+
status: u16,
|
|
244
|
+
headers: headers,
|
|
245
|
+
body: string,
|
|
246
|
+
/// Raw response payload for binary responses.
|
|
247
|
+
body-bytes: option<list<u8>>,
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
record request {
|
|
251
|
+
method: method,
|
|
252
|
+
url: string,
|
|
253
|
+
headers: headers,
|
|
254
|
+
body: string,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
variant request-error {
|
|
258
|
+
other(string)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
type headers = list<tuple<string, string>>;
|
|
262
|
+
|
|
263
|
+
resource request-builder {
|
|
264
|
+
constructor();
|
|
265
|
+
|
|
266
|
+
method: func(method: method) -> request-builder;
|
|
267
|
+
url: func(url: string) -> request-builder;
|
|
268
|
+
|
|
269
|
+
// Add a header to the request
|
|
270
|
+
header: func(key: string, value: string) -> request-builder;
|
|
271
|
+
headers: func(headers: list<tuple<string, string>>) -> request-builder;
|
|
272
|
+
|
|
273
|
+
// Add a body to the request
|
|
274
|
+
body: func(body: string) -> request-builder;
|
|
275
|
+
// Add a binary body to the request
|
|
276
|
+
body-bytes: func(body: list<u8>) -> request-builder;
|
|
277
|
+
|
|
278
|
+
object: func() -> request;
|
|
279
|
+
|
|
280
|
+
// Send the request
|
|
281
|
+
send: func() -> result<response, request-error>;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
variant method {
|
|
285
|
+
get,
|
|
286
|
+
post,
|
|
287
|
+
put,
|
|
288
|
+
delete,
|
|
289
|
+
patch,
|
|
290
|
+
options,
|
|
291
|
+
head,
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
interface file {
|
|
296
|
+
// HTTP headers for file requests (same as http interface)
|
|
297
|
+
type headers = list<tuple<string, string>>;
|
|
298
|
+
|
|
299
|
+
// Normalized file data
|
|
300
|
+
record file-data {
|
|
301
|
+
// Base64-encoded file content
|
|
302
|
+
base64: string,
|
|
303
|
+
// MIME type (e.g., "application/pdf")
|
|
304
|
+
content-type: string,
|
|
305
|
+
// Filename
|
|
306
|
+
filename: string,
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
variant file-error {
|
|
310
|
+
// Failed to fetch file from URL
|
|
311
|
+
fetch-failed(string),
|
|
312
|
+
// Invalid input format (not a valid URL, data URI, or base64)
|
|
313
|
+
invalid-input(string),
|
|
314
|
+
// Request timed out
|
|
315
|
+
timeout(string),
|
|
316
|
+
// Any other error
|
|
317
|
+
other(string),
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Normalize any file source to FileData
|
|
321
|
+
//
|
|
322
|
+
// The source is automatically detected:
|
|
323
|
+
// - URL: "https://example.com/file.pdf" - fetched with optional headers
|
|
324
|
+
// - Data URI: "data:application/pdf;base64,JVBERi0..." - parsed and extracted
|
|
325
|
+
// - Base64: Any other string is treated as raw base64 - decoded to detect type
|
|
326
|
+
//
|
|
327
|
+
// Parameters:
|
|
328
|
+
// - source: URL, data URI, or base64-encoded content
|
|
329
|
+
// - headers: Optional HTTP headers for URL requests (e.g., Authorization)
|
|
330
|
+
// - filename: Optional filename override (auto-detected if not provided)
|
|
331
|
+
//
|
|
332
|
+
// Returns file-data which will be processed by the platform:
|
|
333
|
+
// 1. Fields with format: "file-output" in the output schema are identified
|
|
334
|
+
// 2. File data is uploaded using the configured file_uploader
|
|
335
|
+
// 3. The file-data is replaced with the blob ID in the response
|
|
336
|
+
normalize: func(source: string, headers: option<headers>, filename: option<string>) -> result<file-data, file-error>;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Connection configuration for app-defined connectors.
|
|
340
|
+
//
|
|
341
|
+
// The connector is stateless. It declares how users connect and what runtime
|
|
342
|
+
// data actions/triggers receive. The platform stores credentials and handles
|
|
343
|
+
// OAuth flows, token refresh, and building serialized-data for execution.
|
|
344
|
+
//
|
|
345
|
+
// connection-config returns a JSON string.
|
|
346
|
+
// It is not validated in WASM. app_bridge validates exported JSON against
|
|
347
|
+
// connection-config-schema.json (see ext/app_bridge/docs/) using AppBridge::ConnectionConfigValidator.
|
|
348
|
+
// Connector authors can run the same validator on JSON in CI without loading WASM.
|
|
349
|
+
// integrationer persists connection_config_data,
|
|
350
|
+
// and reads stored JSON at runtime — not a live WASM call.
|
|
351
|
+
interface connections {
|
|
352
|
+
// Returns the embedded connection-config JSON document as a string.
|
|
353
|
+
connection-config: func() -> string;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
world bridge {
|
|
357
|
+
import http;
|
|
358
|
+
import environment;
|
|
359
|
+
import file;
|
|
360
|
+
export triggers;
|
|
361
|
+
export actions;
|
|
362
|
+
export connections;
|
|
363
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
data/lib/app_bridge/app.rb
CHANGED
|
@@ -39,6 +39,16 @@ module AppBridge
|
|
|
39
39
|
30 # seconds
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
def connections_supported?
|
|
43
|
+
_rust_connections_supported?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def connection_config
|
|
47
|
+
json = _rust_connection_config
|
|
48
|
+
ConnectionConfigValidator.validate!(json)
|
|
49
|
+
json
|
|
50
|
+
end
|
|
51
|
+
|
|
42
52
|
private
|
|
43
53
|
|
|
44
54
|
def validate_number_of_events!(events)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "json_schemer"
|
|
5
|
+
|
|
6
|
+
module AppBridge
|
|
7
|
+
# Validates connection-config JSON against the bundled JSON Schema.
|
|
8
|
+
class ConnectionConfigValidator
|
|
9
|
+
SCHEMA_PATH = File.expand_path(
|
|
10
|
+
"../../ext/app_bridge/docs/connection-config-schema.json",
|
|
11
|
+
__dir__
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
def validate!(data)
|
|
16
|
+
new.validate!(data)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def validate(data)
|
|
20
|
+
new.validate(data)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def schema
|
|
24
|
+
@schema ||= JSON.parse(File.read(SCHEMA_PATH))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def schemer
|
|
28
|
+
@schemer ||= JSONSchemer.schema(schema)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def validate!(data)
|
|
33
|
+
errors = validate(data)
|
|
34
|
+
return if errors.empty?
|
|
35
|
+
|
|
36
|
+
raise AppBridge::ConnectionConfigError, format_errors(errors)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def validate(data)
|
|
40
|
+
parsed = parse_data(data)
|
|
41
|
+
self.class.schemer.validate(parsed).to_a
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def parse_data(data)
|
|
47
|
+
case data
|
|
48
|
+
when Hash
|
|
49
|
+
deep_stringify_keys(data)
|
|
50
|
+
when String
|
|
51
|
+
deep_stringify_keys(JSON.parse(data))
|
|
52
|
+
else
|
|
53
|
+
raise AppBridge::ConnectionConfigError, "connection-config must be a JSON object"
|
|
54
|
+
end
|
|
55
|
+
rescue JSON::ParserError => e
|
|
56
|
+
raise AppBridge::ConnectionConfigError, "Invalid connection-config JSON: #{e.message}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def deep_stringify_keys(value)
|
|
60
|
+
case value
|
|
61
|
+
when Hash
|
|
62
|
+
value.each_with_object({}) do |(key, nested), result|
|
|
63
|
+
result[key.to_s] = deep_stringify_keys(nested)
|
|
64
|
+
end
|
|
65
|
+
when Array
|
|
66
|
+
value.map { |item| deep_stringify_keys(item) }
|
|
67
|
+
else
|
|
68
|
+
value
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def format_errors(errors)
|
|
73
|
+
errors.map { |error| format_error(error) }.join("\n")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def format_error(error)
|
|
77
|
+
pointer = error["data_pointer"]
|
|
78
|
+
pointer = "/" if pointer.nil? || pointer.empty?
|
|
79
|
+
"#{pointer}: #{error_detail(error)}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def error_detail(error)
|
|
83
|
+
message = error["error"]
|
|
84
|
+
return message if message.is_a?(String) && !message.empty?
|
|
85
|
+
|
|
86
|
+
details = error["details"]
|
|
87
|
+
return schemer_details_message(details) if details.is_a?(Hash)
|
|
88
|
+
|
|
89
|
+
details || error["type"] || "validation failed"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def schemer_details_message(details)
|
|
93
|
+
if details["missing_keys"]
|
|
94
|
+
missing = Array(details["missing_keys"]).join(", ")
|
|
95
|
+
return "missing required properties: #{missing}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
details.map { |key, value| "#{key}: #{value}" }.join(", ")
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
data/lib/app_bridge/version.rb
CHANGED
data/lib/app_bridge.rb
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "app_bridge/version"
|
|
4
|
+
require_relative "app_bridge/connection_config_validator"
|
|
4
5
|
require_relative "app_bridge/app"
|
|
5
6
|
require_relative "app_bridge/file_processor"
|
|
6
7
|
|
|
7
8
|
# Communication layer for Standout integration apps using WebAssembly components.
|
|
8
9
|
module AppBridge
|
|
9
10
|
class Error < StandardError; end
|
|
11
|
+
class ConnectionConfigError < Error; end
|
|
10
12
|
class TimeoutError < Error; end
|
|
11
13
|
class TooManyEventsError < Error; end
|
|
12
14
|
class StoreTooLargeError < Error; end
|
data/tasks/fixtures.rake
CHANGED
|
@@ -22,13 +22,16 @@ namespace :fixtures do
|
|
|
22
22
|
desc "Compile the fixture apps"
|
|
23
23
|
task :compile_rust do
|
|
24
24
|
pwd = "spec/fixtures/components/rust_app"
|
|
25
|
-
compile_pid = Process.spawn(
|
|
26
|
-
|
|
25
|
+
compile_pid = Process.spawn(
|
|
26
|
+
"CARGO_TARGET_DIR=target cargo clean && CARGO_TARGET_DIR=target cargo build --release --target wasm32-wasip2",
|
|
27
|
+
chdir: pwd
|
|
28
|
+
)
|
|
27
29
|
Process.wait(compile_pid)
|
|
28
30
|
raise "Failed to build artifacts" unless $CHILD_STATUS.success?
|
|
29
31
|
|
|
30
|
-
move_pid = Process.spawn("
|
|
32
|
+
move_pid = Process.spawn("cp #{pwd}/target/wasm32-wasip2/release/rust_app.wasm #{pwd}/../rust_app.wasm")
|
|
31
33
|
Process.wait(move_pid)
|
|
34
|
+
raise "Failed to copy rust_app.wasm" unless $CHILD_STATUS.success?
|
|
32
35
|
end
|
|
33
36
|
|
|
34
37
|
task :compile_js do
|
|
@@ -51,8 +54,26 @@ namespace :fixtures do
|
|
|
51
54
|
move_pid = Process.spawn("mv #{pwd}/target/wasm32-wasip2/release/rust_app_v3.wasm #{pwd}/../rust_app_v3.wasm")
|
|
52
55
|
Process.wait(move_pid)
|
|
53
56
|
end
|
|
57
|
+
|
|
58
|
+
desc "Compile the v5 fixture app (connections interface)"
|
|
59
|
+
task :compile_rust_v5 do
|
|
60
|
+
pwd = "spec/fixtures/components/rust_app_v5"
|
|
61
|
+
next unless File.exist?(pwd)
|
|
62
|
+
|
|
63
|
+
compile_pid = Process.spawn(
|
|
64
|
+
"CARGO_TARGET_DIR=target cargo clean && CARGO_TARGET_DIR=target cargo build --release --target wasm32-wasip2",
|
|
65
|
+
chdir: pwd
|
|
66
|
+
)
|
|
67
|
+
Process.wait(compile_pid)
|
|
68
|
+
raise "Failed to build v5 artifacts" unless $CHILD_STATUS.success?
|
|
69
|
+
|
|
70
|
+
move_pid = Process.spawn("cp #{pwd}/target/wasm32-wasip2/release/rust_app_v5.wasm #{pwd}/../rust_app_v5.wasm")
|
|
71
|
+
Process.wait(move_pid)
|
|
72
|
+
raise "Failed to copy rust_app_v5.wasm" unless $CHILD_STATUS.success?
|
|
73
|
+
end
|
|
54
74
|
end
|
|
55
75
|
end
|
|
56
76
|
|
|
57
77
|
desc "Build all fixtures"
|
|
58
|
-
task fixtures: %i[fixtures:apps:clean fixtures:apps:compile_rust fixtures:apps:compile_js fixtures:apps:compile_rust_v3
|
|
78
|
+
task fixtures: %i[fixtures:apps:clean fixtures:apps:compile_rust fixtures:apps:compile_js fixtures:apps:compile_rust_v3
|
|
79
|
+
fixtures:apps:compile_rust_v5]
|
metadata
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: app_bridge
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 5.0.0
|
|
5
5
|
platform: aarch64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Alexander Ross
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
12
|
-
dependencies:
|
|
11
|
+
date: 2026-08-28 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: json_schemer
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.3'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '2.3'
|
|
13
27
|
description: The app_bridge gem is designed to enable seamless interaction with WebAssembly
|
|
14
28
|
components that adhere to the WIT specification `standout:app`. It is developed
|
|
15
29
|
for use in Standout's products.
|
|
@@ -26,13 +40,17 @@ files:
|
|
|
26
40
|
- CHANGELOG.md
|
|
27
41
|
- README.md
|
|
28
42
|
- Rakefile
|
|
43
|
+
- ext/app_bridge/docs/connection-config-schema.json
|
|
29
44
|
- ext/app_bridge/wit/v3/world.wit
|
|
30
45
|
- ext/app_bridge/wit/v4/world.wit
|
|
31
46
|
- ext/app_bridge/wit/v4_1/world.wit
|
|
47
|
+
- ext/app_bridge/wit/v5/world.wit
|
|
32
48
|
- lib/app_bridge.rb
|
|
33
|
-
- lib/app_bridge/3.
|
|
49
|
+
- lib/app_bridge/3.3/app_bridge.so
|
|
34
50
|
- lib/app_bridge/3.4/app_bridge.so
|
|
51
|
+
- lib/app_bridge/4.0/app_bridge.so
|
|
35
52
|
- lib/app_bridge/app.rb
|
|
53
|
+
- lib/app_bridge/connection_config_validator.rb
|
|
36
54
|
- lib/app_bridge/file_processor.rb
|
|
37
55
|
- lib/app_bridge/version.rb
|
|
38
56
|
- sig/app_bridge.rbs
|
|
@@ -55,10 +73,10 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
55
73
|
requirements:
|
|
56
74
|
- - ">="
|
|
57
75
|
- !ruby/object:Gem::Version
|
|
58
|
-
version: '3.
|
|
76
|
+
version: '3.3'
|
|
59
77
|
- - "<"
|
|
60
78
|
- !ruby/object:Gem::Version
|
|
61
|
-
version:
|
|
79
|
+
version: 4.1.dev
|
|
62
80
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
63
81
|
requirements:
|
|
64
82
|
- - ">="
|