From 03c89e297256b84de4cb0e2f4ccf96d2d20e1acc Mon Sep 17 00:00:00 2001 From: David Ragot <35502263+Dav-14@users.noreply.github.com> Date: Wed, 28 Oct 2026 12:30:39 +0100 Subject: [PATCH 1/6] feat(sdk): include Connectivity in the stack API contract --- .../scripts/prepare-connectivity-openapi.sh | 46 ++++++++++++ .github/scripts/test-connectivity-openapi.sh | 33 +++++++++ .../scripts/validate-connectivity-openapi.sh | 61 ++++++++++++++++ .github/workflows/releases.yml | 1 + .speakeasy/workflow.lock | 4 +- .speakeasy/workflow.yaml | 2 + Justfile | 19 +++-- README.md | 5 ++ docs/sdk-contract-composition.md | 72 +++++++++++++++++++ flake.nix | 1 + releases/base.yaml | 8 ++- 11 files changed, 246 insertions(+), 6 deletions(-) create mode 100644 .github/scripts/prepare-connectivity-openapi.sh create mode 100644 .github/scripts/test-connectivity-openapi.sh create mode 100644 .github/scripts/validate-connectivity-openapi.sh create mode 100644 docs/sdk-contract-composition.md diff --git a/.github/scripts/prepare-connectivity-openapi.sh b/.github/scripts/prepare-connectivity-openapi.sh new file mode 100644 index 0000000000..4541d8cfb6 --- /dev/null +++ b/.github/scripts/prepare-connectivity-openapi.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +set -euo pipefail + +source_json=$(mktemp) +prepared_json=$(mktemp) +trap 'rm -f "$source_json" "$prepared_json"' EXIT +yq -o=json '.' "$1" > "$source_json" + +# Adapt existing operations only. Public probes keep security: []; read scope +# and write scope are alternatives. Speakeasy rejects OAuth2 alternatives for +# one scheme, so retain authentication and describe the server scope check. +# The pinned Connectivity schema uses the OpenAPI 3.0-compatible subset. Keep +# the composed dialect at 3.0.3 so existing nullable schemas retain their meaning. +jq ' + "Requires connectivity:read or connectivity:write. The SDK supplies the OAuth2 bearer token; the server enforces scopes." as $read_note + | .paths |= with_entries( + .key = "/api/connectivity" + (.key | sub("^/api/connectivity"; "")) + | .value |= with_entries( + .key as $method + | if ($method | IN("get", "post", "put", "patch", "delete", "options", "head", "trace")) then + .value |= ( + .operationId = "connectivity_" + (.operationId | sub("^connectivity_"; "")) + | .tags = ["connectivity.v1"] + | if .security == [] then . + elif ($method | IN("get", "head")) then + .security = [{"Authorization": []}] + | .["x-formance-required-scopes-any-of"] = ["connectivity:read", "connectivity:write"] + | .description = ((.description // "") | if endswith($read_note) then . elif . == "" then $read_note else . + "\n\n" + $read_note end) + else .security = [{"Authorization": ["connectivity:write"]}] + end + ) + else . end + ) + ) + # OpenAPI 3.0 ignores siblings of Reference Objects. Preserve the 3.1 + # schema annotations (notably connectivityRef.readOnly) on allOf wrappers. + | walk(if type == "object" and has("$ref") then + if (."$ref" | startswith("#/components/schemas/")) and (keys | length > 1) then + .allOf = [{"$ref": ."$ref"}] | del(."$ref") + else . end + else . end) + | .openapi = "3.0.3" + | del(.security, .components.securitySchemes.bearerAuth) + | .tags = [{"name": "connectivity.v1", "description": "Connectivity tenant API"}] +' "$source_json" > "$prepared_json" +yq -oy -P -p=json '.' "$prepared_json" > "$1" diff --git a/.github/scripts/test-connectivity-openapi.sh b/.github/scripts/test-connectivity-openapi.sh new file mode 100644 index 0000000000..599d385636 --- /dev/null +++ b/.github/scripts/test-connectivity-openapi.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euo pipefail + +component=$1 +composed=$2 +script_dir=$(cd "$(dirname "$0")" && pwd) +test_dir=$(mktemp -d) +trap 'rm -rf "$test_dir"' EXIT + +bash "$script_dir/validate-connectivity-openapi.sh" "$component" "$composed" +cp "$component" "$test_dir/component.yaml" +bash "$script_dir/prepare-connectivity-openapi.sh" "$test_dir/component.yaml" +yq -o=json -I=0 '.' "$component" > "$test_dir/before.json" +yq -o=json -I=0 '.' "$test_dir/component.yaml" > "$test_dir/after.json" +cmp "$test_dir/before.json" "$test_dir/after.json" + +# Each mutation remains parseable JSON but loses a supported API guarantee. +# The integration validator must reject it rather than merely count routes. +for mutation in \ + 'del(.paths["/api/connectivity/connectors"].get)' \ + '.paths["/api/connectivity/connectorinstances"].post.security = []' \ + 'del(.paths["/api/connectivity/connectors"].get.responses["200"].content)' \ + 'del(.components.parameters.connectivity_Cursor)' \ + '.components.schemas.connectivity_ConnectorInstanceSpec.properties.connectivityRef.readOnly = false' \ + '.components.schemas.connectivity_ConnectorInstancePatch.additionalProperties = false' \ + '.components.schemas.connectivity_ConnectorInstanceSpec.properties.initialCursor.type = "integer"' +do + jq "$mutation" "$composed" > "$test_dir/broken.json" + if bash "$script_dir/validate-connectivity-openapi.sh" "$component" "$test_dir/broken.json" 2> "$test_dir/error"; then + echo "Connectivity validation accepted a broken contract: $mutation" >&2 + exit 1 + fi +done diff --git a/.github/scripts/validate-connectivity-openapi.sh b/.github/scripts/validate-connectivity-openapi.sh new file mode 100644 index 0000000000..20a71a6f81 --- /dev/null +++ b/.github/scripts/validate-connectivity-openapi.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -euo pipefail + +source_json=$(mktemp) +trap 'rm -f "$source_json"' EXIT +yq -o=json '.' "$1" > "$source_json" + +# Check the actual composition against the prepared component rather than a +# hard-coded operation count, so newly added upstream operations are covered. +jq -e --slurpfile source "$source_json" ' + def namespace_refs: + walk(if type == "object" and has("$ref") then + ."$ref" |= sub("^#/components/(?[^/]+)/(?.*)$"; "#/components/\(.category)/connectivity_\(.name)") + else . end); + def without_composition_metadata: + walk(if type == "object" then del(.["x-speakeasy-name-override"], .["x-speakeasy-model-namespace"]) else . end); + def operations: + .paths | to_entries[] | .key as $path | .value | to_entries[] + | select(.key | IN("get", "post", "put", "patch", "delete", "options", "head", "trace")) + | {path: $path, method: .key, operation: .value}; + . as $merged + | [$source[0] | operations] as $expected + | [operations | select(.path | startswith("/api/connectivity/"))] as $actual + | (.openapi == "3.0.3") + and ($expected | length > 0) + and (($expected | length) == ($actual | length)) + and all($expected[]; + . as $entry + | $merged.paths[$entry.path][$entry.method] as $operation + | ($operation.operationId == $entry.operation.operationId) + and ($operation.tags == ["connectivity.v1"]) + and ($operation.security == $entry.operation.security) + and (($operation.responses | keys) == ($entry.operation.responses | keys)) + and (($operation.requestBody.content // {} | keys) == ($entry.operation.requestBody.content // {} | keys)) + ) + and all($source[0].paths | to_entries[]; + . as $entry + | ($merged.paths[$entry.key] | without_composition_metadata) == ($entry.value | namespace_refs | without_composition_metadata) + ) + and all($source[0].components | to_entries[]; + .key as $category + | all(.value | to_entries[]; + . as $entry + | ($merged.components[$category]["connectivity_" + $entry.key] | without_composition_metadata) == ($entry.value | namespace_refs | without_composition_metadata) + ) + ) + and ($merged.components.securitySchemes.Authorization.flows.clientCredentials.scopes | has("connectivity:read") and has("connectivity:write")) + and ($merged.paths["/api/connectivity/_healthcheck"].get.security == []) + and ($merged.paths["/api/connectivity/_info"].get.security == []) + and ($merged.components.securitySchemes.Authorization.type == "oauth2") + and ($merged.components.securitySchemes.bearerAuth == null) + and ($merged.components.schemas.connectivity_ConnectorInstanceSpec.properties.connectivityRef.readOnly == true) + and ($merged.components.schemas.connectivity_ConnectorInstanceSpec.properties.connectivityRef.allOf[0]."$ref" == "#/components/schemas/connectivity_LocalObjectRef") + and ($merged.components.schemas.connectivity_ConnectorInstanceSpec.properties.connectorRef.description | length > 0) + and ($merged.components.schemas.connectivity_ConnectorInstanceSpec.properties.initialCursor.type == "string") + and ($merged.components.schemas.connectivity_ConnectorInstanceSpec.properties.initialCursor.format == "byte") + and ($merged.paths["/api/connectivity/connectorinstances/{name}"].patch.requestBody.content["application/merge-patch+json"] != null) +' "$2" > /dev/null || { + echo 'Connectivity operations, security, errors or configuration contract changed during composition' >&2 + exit 1 +} diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index f907b6b120..af1972626e 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -17,6 +17,7 @@ jobs: - name: Build OpenAPI spec and events run: nix develop --impure --command just build ${{github.ref_name}} env: + GH_TOKEN: ${{ secrets.NUMARY_GITHUB_TOKEN }} SPEAKEASY_API_KEY: ${{ secrets.SPEAKEASY_API_KEY }} - name: Generate changelog run: bash .github/scripts/generate-changelog.sh diff --git a/.speakeasy/workflow.lock b/.speakeasy/workflow.lock index 95fc989711..fcb3c3c2a5 100644 --- a/.speakeasy/workflow.lock +++ b/.speakeasy/workflow.lock @@ -1,4 +1,4 @@ -speakeasyVersion: 1.797.0 +speakeasyVersion: 1.804.0 sources: stacks-source: sourceNamespace: stacks-source @@ -14,6 +14,8 @@ workflow: sources: stacks-source: inputs: + - location: ./components/connectivity.openapi.yaml + modelNamespace: connectivity - location: ./components/auth.openapi.yaml modelNamespace: auth - location: ./components/gateway.openapi.yaml diff --git a/.speakeasy/workflow.yaml b/.speakeasy/workflow.yaml index 3e6fe609ea..484205c572 100644 --- a/.speakeasy/workflow.yaml +++ b/.speakeasy/workflow.yaml @@ -3,6 +3,8 @@ speakeasyVersion: latest sources: stacks-source: inputs: + - location: ./components/connectivity.openapi.yaml + modelNamespace: connectivity - location: ./components/auth.openapi.yaml modelNamespace: auth - location: ./components/gateway.openapi.yaml diff --git a/Justfile b/Justfile index 9542fc61d4..c012b37f7b 100644 --- a/Justfile +++ b/Justfile @@ -9,10 +9,13 @@ SEARCH_VERSION := "v2.1.0" ORCHESTRATION_VERSION := "v2.6.4" RECONCILIATION_VERSION := "v2.5.0" GATEWAY_VERSION := "v2.3.2" +# Connectivity API releases do not yet publish an OpenAPI asset. Pin main by commit. +CONNECTIVITY_REF := "f1f091cb0db31db230932f4c58ddc20fd852583b" -# Download all component OpenAPI specs from GitHub releases +# Download versioned component OpenAPI specs from GitHub releases or pinned source download-specs: mkdir -p components + gh api -H "Accept: application/vnd.github.raw+json" "repos/formancehq/connectivity/contents/misc/connectivity-api/openapi.yaml?ref={{ CONNECTIVITY_REF }}" > components/connectivity.openapi.yaml wget -q https://github.com/formancehq/ledger/releases/download/{{ LEDGER_VERSION }}/openapi.yml -O components/ledger.openapi.yaml wget -q https://github.com/formancehq/ledger/releases/download/{{ LEDGER_V2_VERSION }}/openapi.yaml -O components/ledger-v2.openapi.yaml wget -q https://github.com/formancehq/payments/releases/download/{{ PAYMENTS_VERSION }}/openapi.yaml -O components/payments.openapi.yaml @@ -26,6 +29,7 @@ download-specs: # Prepend API path prefix to each component spec prepend-paths: download-specs + bash .github/scripts/prepare-connectivity-openapi.sh components/connectivity.openapi.yaml yq -i '.paths |= (to_entries | map(select(.key == "/*").key = "/api/auth" + .key) | from_entries)' components/auth.openapi.yaml yq -i '.paths |= (to_entries | map(select(.key == "/*").key = "/api/ledger" + .key) | from_entries) | (.paths[] | .. | select(has("tags")).tags) = ["ledger.v3"]' components/ledger.openapi.yaml yq -i '.paths |= (to_entries | map(select(.key == "/*").key = "/api/ledger" + .key) | from_entries) | del(.paths."/api/ledger/_info")' components/ledger-v2.openapi.yaml @@ -48,7 +52,7 @@ generate-composition-overlay: strip-servers # Build the merged OpenAPI spec using Speakeasy build-openapi version="v0.0.0": generate-composition-overlay mkdir -p releases/build - speakeasy run -s all + speakeasy run -s all --skip-upload-spec cd releases && sed -i'' -e 's/SDK_VERSION/{{ version }}/g' build/generate.json just validate-openapi @@ -70,7 +74,14 @@ validate-openapi: jq -e '.paths["/api/ledger/v2"] != null and .paths["/api/ledger/v3/"] != null' releases/build/generate.json >/dev/null # Composition cleanups must remain applied when component specs change. jq -e '.components.schemas.auth_Scope == null and .components.schemas.auth_ScopeOptions == null' releases/build/generate.json >/dev/null - jq -e '[.. | objects | select(.enum? != null) | .enum | select(length != (unique | length))] | length == 0' releases/build/generate.json >/dev/null + jq -e '[.. | objects | select(.enum? | type == "array") | .enum | select(length != (unique | length))] | length == 0' releases/build/generate.json >/dev/null + + # Connectivity must retain every upstream operation under the stack route. + bash .github/scripts/validate-connectivity-openapi.sh components/connectivity.openapi.yaml releases/build/generate.json + +# Exercise contract validation against representative regressions. +test-openapi: + bash .github/scripts/test-connectivity-openapi.sh components/connectivity.openapi.yaml releases/build/generate.json # Generate event schemas generate-events: @@ -85,4 +96,4 @@ publish-speakeasy version: generate-composition-overlay speakeasy run -s all --registry-tags {{ version }},LATEST_RELEASE # Pre-commit: build spec and generate events -pre-commit: build-openapi generate-events +pre-commit: build-openapi test-openapi generate-events diff --git a/README.md b/README.md index 0bf90f3f20..c9c2413a13 100644 --- a/README.md +++ b/README.md @@ -54,3 +54,8 @@ The Formance Stack is built on open-source, battle tested technologies including - **PostgreSQL** - Main storage backend - **Kafka/NATS** - Cross-services async communication - **Traefik** - Main HTTP gateway + +## SDK contract composition + +See [SDK contract composition](docs/sdk-contract-composition.md) for building the +unified OpenAPI contract, updating Connectivity and publishing SDK inputs. diff --git a/docs/sdk-contract-composition.md b/docs/sdk-contract-composition.md new file mode 100644 index 0000000000..475ec0219b --- /dev/null +++ b/docs/sdk-contract-composition.md @@ -0,0 +1,72 @@ +# SDK contract composition + +`.speakeasy/workflow.yaml` combines component OpenAPI contracts into +`releases/build/generate.json`. Component models are namespaced; routes address +services through the stack gateway. `releases/base.yaml` owns the global +metadata, OAuth2 scheme, servers and SDK tag groups. + +## Build and validate + +```sh +nix develop --command just build-openapi +nix develop --command just validate-openapi +nix develop --command just test-openapi +``` + +The build downloads and prepares contracts, generates the composition overlay, +merges the documents, and validates the result. It does not upload the contract +to the Speakeasy registry. `just pre-commit` also regenerates the event schemas. + +## Connectivity source and authentication + +`CONNECTIVITY_REF` in `Justfile` pins a commit from Connectivity main. The source +is `misc/connectivity-api/openapi.yaml`; no root-level copy is required. Update +the pin deliberately when adopting a newer contract. Connectivity API releases +do not currently include an OpenAPI asset. + +Connectivity is private: local builds require GitHub CLI authentication with +read access to `formancehq/connectivity` (`gh auth login`, or a `GH_TOKEN` with +that access). Anonymous contributors cannot reproduce this part of the build. +The release build uses the existing `NUMARY_GITHUB_TOKEN` secret. Before merging +this integration, verify that the release credential can read the pinned file; +a successful local build does not establish CI credential access. Never print +or commit tokens. + +Preparation adds `/api/connectivity` to routes, groups operations under +`connectivity.v1`, and keeps models under the `connectivity` namespace. The +stack OAuth2 scheme replaces the standalone bearer scheme. Public probes remain +public; writes declare `connectivity:write`. + +Speakeasy rejects two alternatives for one OAuth2 scheme with different scopes. +Reads therefore declare OAuth2 authentication without a formal scope list and +carry `x-formance-required-scopes-any-of` plus a description stating the server +requirement: `connectivity:read` **or** `connectivity:write`. This avoids excluding +write-only tokens. The server continues to enforce these scopes. + +Composition validation checks the Connectivity operations and their security, +response codes, parameters, request/response bodies, all component models, +merge-patch media type and base64 cursor representation. `test-openapi` also +checks preparation idempotence and rejects deliberately damaged contracts. + +The pinned Connectivity input declares OpenAPI 3.1.0 but uses schema constructs +compatible with 3.0.3 after wrapping schema references with sibling annotations +in `allOf`. This preserves `readOnly` and descriptions that 3.0 Reference Objects +would otherwise ignore. Preparation declares that component as 3.0.3, and the +merged contract is validated in that dialect. This preserves the meaning of +existing modules’ `nullable` fields. Reassess dialect compatibility whenever +updating the source pin; do not adopt 3.1-only schema constructs by changing +the version string alone. + +## Publication and downstream SDKs + +After review and merge, an authorized `vX.Y.Z` stack tag triggers +`.github/workflows/releases.yml`. It publishes `openapi.json` and `events.json`, +then publishes `stacks-source` to the Speakeasy registry with the version and +`LATEST_RELEASE` tags. This includes the API contract fetched from private +Connectivity in the public stack release assets and SDK input; review that +publication boundary before release. + +The SDK repositories consume `stacks-source@LATEST_RELEASE`. For example, +`formance-sdk-go` runs its Generate workflow on demand or on its daily schedule, +and opens a generation PR. Publishing that SDK and updating consumers such as +fctl are separate steps; changing Connectivity main alone performs none of them. diff --git a/flake.nix b/flake.nix index a9c7e4887a..c73cc302f4 100644 --- a/flake.nix +++ b/flake.nix @@ -68,6 +68,7 @@ default = pkgs.mkShell { packages = [ pkgs.jq + pkgs.gh pkgs.just pkgs.nodejs_22 pkgs.wget diff --git a/releases/base.yaml b/releases/base.yaml index 87c5ac8559..4e5a582b67 100644 --- a/releases/base.yaml +++ b/releases/base.yaml @@ -40,6 +40,7 @@ servers: - us-east-1 tags: + - name: connectivity.v1 - name: ledger.v1 - name: ledger.v2 - name: ledger.v3 @@ -61,9 +62,14 @@ components: clientCredentials: tokenUrl: "/api/auth/oauth/token" refreshUrl: "/api/auth/oauth/token" - scopes: {} + scopes: + connectivity:read: Read Connectivity catalog and connections + connectivity:write: Manage Connectivity connections x-tagGroups: + - name: Connectivity + tags: + - connectivity.v1 - name: Auth tags: - auth.v1 From d2c4e53b2566e0af7684d1d4b8292a7169576030 Mon Sep 17 00:00:00 2001 From: David Ragot <35502263+Dav-14@users.noreply.github.com> Date: Wed, 28 Oct 2026 12:52:44 +0100 Subject: [PATCH 2/6] refactor(sdk): simplify Connectivity contract composition --- .../scripts/prepare-connectivity-openapi.sh | 46 -------------- .github/scripts/test-connectivity-openapi.sh | 33 ---------- .../scripts/validate-connectivity-openapi.sh | 61 ------------------- Justfile | 21 +++---- docs/sdk-contract-composition.md | 30 ++++----- 5 files changed, 20 insertions(+), 171 deletions(-) delete mode 100644 .github/scripts/prepare-connectivity-openapi.sh delete mode 100644 .github/scripts/test-connectivity-openapi.sh delete mode 100644 .github/scripts/validate-connectivity-openapi.sh diff --git a/.github/scripts/prepare-connectivity-openapi.sh b/.github/scripts/prepare-connectivity-openapi.sh deleted file mode 100644 index 4541d8cfb6..0000000000 --- a/.github/scripts/prepare-connectivity-openapi.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -source_json=$(mktemp) -prepared_json=$(mktemp) -trap 'rm -f "$source_json" "$prepared_json"' EXIT -yq -o=json '.' "$1" > "$source_json" - -# Adapt existing operations only. Public probes keep security: []; read scope -# and write scope are alternatives. Speakeasy rejects OAuth2 alternatives for -# one scheme, so retain authentication and describe the server scope check. -# The pinned Connectivity schema uses the OpenAPI 3.0-compatible subset. Keep -# the composed dialect at 3.0.3 so existing nullable schemas retain their meaning. -jq ' - "Requires connectivity:read or connectivity:write. The SDK supplies the OAuth2 bearer token; the server enforces scopes." as $read_note - | .paths |= with_entries( - .key = "/api/connectivity" + (.key | sub("^/api/connectivity"; "")) - | .value |= with_entries( - .key as $method - | if ($method | IN("get", "post", "put", "patch", "delete", "options", "head", "trace")) then - .value |= ( - .operationId = "connectivity_" + (.operationId | sub("^connectivity_"; "")) - | .tags = ["connectivity.v1"] - | if .security == [] then . - elif ($method | IN("get", "head")) then - .security = [{"Authorization": []}] - | .["x-formance-required-scopes-any-of"] = ["connectivity:read", "connectivity:write"] - | .description = ((.description // "") | if endswith($read_note) then . elif . == "" then $read_note else . + "\n\n" + $read_note end) - else .security = [{"Authorization": ["connectivity:write"]}] - end - ) - else . end - ) - ) - # OpenAPI 3.0 ignores siblings of Reference Objects. Preserve the 3.1 - # schema annotations (notably connectivityRef.readOnly) on allOf wrappers. - | walk(if type == "object" and has("$ref") then - if (."$ref" | startswith("#/components/schemas/")) and (keys | length > 1) then - .allOf = [{"$ref": ."$ref"}] | del(."$ref") - else . end - else . end) - | .openapi = "3.0.3" - | del(.security, .components.securitySchemes.bearerAuth) - | .tags = [{"name": "connectivity.v1", "description": "Connectivity tenant API"}] -' "$source_json" > "$prepared_json" -yq -oy -P -p=json '.' "$prepared_json" > "$1" diff --git a/.github/scripts/test-connectivity-openapi.sh b/.github/scripts/test-connectivity-openapi.sh deleted file mode 100644 index 599d385636..0000000000 --- a/.github/scripts/test-connectivity-openapi.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -component=$1 -composed=$2 -script_dir=$(cd "$(dirname "$0")" && pwd) -test_dir=$(mktemp -d) -trap 'rm -rf "$test_dir"' EXIT - -bash "$script_dir/validate-connectivity-openapi.sh" "$component" "$composed" -cp "$component" "$test_dir/component.yaml" -bash "$script_dir/prepare-connectivity-openapi.sh" "$test_dir/component.yaml" -yq -o=json -I=0 '.' "$component" > "$test_dir/before.json" -yq -o=json -I=0 '.' "$test_dir/component.yaml" > "$test_dir/after.json" -cmp "$test_dir/before.json" "$test_dir/after.json" - -# Each mutation remains parseable JSON but loses a supported API guarantee. -# The integration validator must reject it rather than merely count routes. -for mutation in \ - 'del(.paths["/api/connectivity/connectors"].get)' \ - '.paths["/api/connectivity/connectorinstances"].post.security = []' \ - 'del(.paths["/api/connectivity/connectors"].get.responses["200"].content)' \ - 'del(.components.parameters.connectivity_Cursor)' \ - '.components.schemas.connectivity_ConnectorInstanceSpec.properties.connectivityRef.readOnly = false' \ - '.components.schemas.connectivity_ConnectorInstancePatch.additionalProperties = false' \ - '.components.schemas.connectivity_ConnectorInstanceSpec.properties.initialCursor.type = "integer"' -do - jq "$mutation" "$composed" > "$test_dir/broken.json" - if bash "$script_dir/validate-connectivity-openapi.sh" "$component" "$test_dir/broken.json" 2> "$test_dir/error"; then - echo "Connectivity validation accepted a broken contract: $mutation" >&2 - exit 1 - fi -done diff --git a/.github/scripts/validate-connectivity-openapi.sh b/.github/scripts/validate-connectivity-openapi.sh deleted file mode 100644 index 20a71a6f81..0000000000 --- a/.github/scripts/validate-connectivity-openapi.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -source_json=$(mktemp) -trap 'rm -f "$source_json"' EXIT -yq -o=json '.' "$1" > "$source_json" - -# Check the actual composition against the prepared component rather than a -# hard-coded operation count, so newly added upstream operations are covered. -jq -e --slurpfile source "$source_json" ' - def namespace_refs: - walk(if type == "object" and has("$ref") then - ."$ref" |= sub("^#/components/(?[^/]+)/(?.*)$"; "#/components/\(.category)/connectivity_\(.name)") - else . end); - def without_composition_metadata: - walk(if type == "object" then del(.["x-speakeasy-name-override"], .["x-speakeasy-model-namespace"]) else . end); - def operations: - .paths | to_entries[] | .key as $path | .value | to_entries[] - | select(.key | IN("get", "post", "put", "patch", "delete", "options", "head", "trace")) - | {path: $path, method: .key, operation: .value}; - . as $merged - | [$source[0] | operations] as $expected - | [operations | select(.path | startswith("/api/connectivity/"))] as $actual - | (.openapi == "3.0.3") - and ($expected | length > 0) - and (($expected | length) == ($actual | length)) - and all($expected[]; - . as $entry - | $merged.paths[$entry.path][$entry.method] as $operation - | ($operation.operationId == $entry.operation.operationId) - and ($operation.tags == ["connectivity.v1"]) - and ($operation.security == $entry.operation.security) - and (($operation.responses | keys) == ($entry.operation.responses | keys)) - and (($operation.requestBody.content // {} | keys) == ($entry.operation.requestBody.content // {} | keys)) - ) - and all($source[0].paths | to_entries[]; - . as $entry - | ($merged.paths[$entry.key] | without_composition_metadata) == ($entry.value | namespace_refs | without_composition_metadata) - ) - and all($source[0].components | to_entries[]; - .key as $category - | all(.value | to_entries[]; - . as $entry - | ($merged.components[$category]["connectivity_" + $entry.key] | without_composition_metadata) == ($entry.value | namespace_refs | without_composition_metadata) - ) - ) - and ($merged.components.securitySchemes.Authorization.flows.clientCredentials.scopes | has("connectivity:read") and has("connectivity:write")) - and ($merged.paths["/api/connectivity/_healthcheck"].get.security == []) - and ($merged.paths["/api/connectivity/_info"].get.security == []) - and ($merged.components.securitySchemes.Authorization.type == "oauth2") - and ($merged.components.securitySchemes.bearerAuth == null) - and ($merged.components.schemas.connectivity_ConnectorInstanceSpec.properties.connectivityRef.readOnly == true) - and ($merged.components.schemas.connectivity_ConnectorInstanceSpec.properties.connectivityRef.allOf[0]."$ref" == "#/components/schemas/connectivity_LocalObjectRef") - and ($merged.components.schemas.connectivity_ConnectorInstanceSpec.properties.connectorRef.description | length > 0) - and ($merged.components.schemas.connectivity_ConnectorInstanceSpec.properties.initialCursor.type == "string") - and ($merged.components.schemas.connectivity_ConnectorInstanceSpec.properties.initialCursor.format == "byte") - and ($merged.paths["/api/connectivity/connectorinstances/{name}"].patch.requestBody.content["application/merge-patch+json"] != null) -' "$2" > /dev/null || { - echo 'Connectivity operations, security, errors or configuration contract changed during composition' >&2 - exit 1 -} diff --git a/Justfile b/Justfile index c012b37f7b..87eb473586 100644 --- a/Justfile +++ b/Justfile @@ -9,10 +9,10 @@ SEARCH_VERSION := "v2.1.0" ORCHESTRATION_VERSION := "v2.6.4" RECONCILIATION_VERSION := "v2.5.0" GATEWAY_VERSION := "v2.3.2" -# Connectivity API releases do not yet publish an OpenAPI asset. Pin main by commit. -CONNECTIVITY_REF := "f1f091cb0db31db230932f4c58ddc20fd852583b" +# Connectivity API releases do not yet publish an OpenAPI asset. +CONNECTIVITY_REF := "main" -# Download versioned component OpenAPI specs from GitHub releases or pinned source +# Download component OpenAPI specs from GitHub releases or source download-specs: mkdir -p components gh api -H "Accept: application/vnd.github.raw+json" "repos/formancehq/connectivity/contents/misc/connectivity-api/openapi.yaml?ref={{ CONNECTIVITY_REF }}" > components/connectivity.openapi.yaml @@ -29,7 +29,9 @@ download-specs: # Prepend API path prefix to each component spec prepend-paths: download-specs - bash .github/scripts/prepare-connectivity-openapi.sh components/connectivity.openapi.yaml + yq -i '.paths |= (to_entries | map(.key = "/api/connectivity" + .key) | from_entries) | (.paths[] | .. | select(has("operationId"))) |= (.operationId = "connectivity_" + .operationId | .tags = ["connectivity.v1"])' components/connectivity.openapi.yaml + yq -i '(.paths[] | .. | select(has("operationId") and (.security == null or (.security | length) > 0))).security = [{"Authorization": ["connectivity:write"]}]' components/connectivity.openapi.yaml + yq -i '(.paths[].get | select(has("operationId") and (.security == null or (.security | length) > 0))) |= (.security = [{"Authorization": []}] | .x-formance-required-scopes-any-of = ["connectivity:read", "connectivity:write"] | .description = (.description + "\n\nRequires connectivity:read or connectivity:write. The server enforces scopes.")) | del(.security, .components.securitySchemes.bearerAuth)' components/connectivity.openapi.yaml yq -i '.paths |= (to_entries | map(select(.key == "/*").key = "/api/auth" + .key) | from_entries)' components/auth.openapi.yaml yq -i '.paths |= (to_entries | map(select(.key == "/*").key = "/api/ledger" + .key) | from_entries) | (.paths[] | .. | select(has("tags")).tags) = ["ledger.v3"]' components/ledger.openapi.yaml yq -i '.paths |= (to_entries | map(select(.key == "/*").key = "/api/ledger" + .key) | from_entries) | del(.paths."/api/ledger/_info")' components/ledger-v2.openapi.yaml @@ -58,6 +60,8 @@ build-openapi version="v0.0.0": generate-composition-overlay # Validate invariants introduced by composing namespaced OpenAPI documents. validate-openapi: + # Keep nullable semantics stable across the existing component contracts. + jq -e '.openapi == "3.0.3"' releases/build/generate.json >/dev/null # Global API metadata must remain exactly sourced from base.yaml. jq -e --argjson base_info "$(yq -o=json -I=0 '.info' releases/base.yaml)" '(.info | .version = $base_info.version) == $base_info' releases/build/generate.json >/dev/null # Every local discriminator mapping must resolve to an existing schema. @@ -76,13 +80,6 @@ validate-openapi: jq -e '.components.schemas.auth_Scope == null and .components.schemas.auth_ScopeOptions == null' releases/build/generate.json >/dev/null jq -e '[.. | objects | select(.enum? | type == "array") | .enum | select(length != (unique | length))] | length == 0' releases/build/generate.json >/dev/null - # Connectivity must retain every upstream operation under the stack route. - bash .github/scripts/validate-connectivity-openapi.sh components/connectivity.openapi.yaml releases/build/generate.json - -# Exercise contract validation against representative regressions. -test-openapi: - bash .github/scripts/test-connectivity-openapi.sh components/connectivity.openapi.yaml releases/build/generate.json - # Generate event schemas generate-events: cd events && npm install @@ -96,4 +93,4 @@ publish-speakeasy version: generate-composition-overlay speakeasy run -s all --registry-tags {{ version }},LATEST_RELEASE # Pre-commit: build spec and generate events -pre-commit: build-openapi test-openapi generate-events +pre-commit: build-openapi generate-events diff --git a/docs/sdk-contract-composition.md b/docs/sdk-contract-composition.md index 475ec0219b..572ca60c1c 100644 --- a/docs/sdk-contract-composition.md +++ b/docs/sdk-contract-composition.md @@ -10,7 +10,6 @@ metadata, OAuth2 scheme, servers and SDK tag groups. ```sh nix develop --command just build-openapi nix develop --command just validate-openapi -nix develop --command just test-openapi ``` The build downloads and prepares contracts, generates the composition overlay, @@ -19,16 +18,17 @@ to the Speakeasy registry. `just pre-commit` also regenerates the event schemas. ## Connectivity source and authentication -`CONNECTIVITY_REF` in `Justfile` pins a commit from Connectivity main. The source -is `misc/connectivity-api/openapi.yaml`; no root-level copy is required. Update -the pin deliberately when adopting a newer contract. Connectivity API releases -do not currently include an OpenAPI asset. +`CONNECTIVITY_REF` in `Justfile` follows Connectivity `main`. Each build fetches +`misc/connectivity-api/openapi.yaml` at the current branch head; no root-level +copy is required. Connectivity API releases do not currently include an OpenAPI +asset. A change on Connectivity main is included in the next stack composition, +so rebuilding an older stack revision can consume a newer Connectivity contract. Connectivity is private: local builds require GitHub CLI authentication with read access to `formancehq/connectivity` (`gh auth login`, or a `GH_TOKEN` with that access). Anonymous contributors cannot reproduce this part of the build. The release build uses the existing `NUMARY_GITHUB_TOKEN` secret. Before merging -this integration, verify that the release credential can read the pinned file; +this integration, verify that the release credential can read the source file; a successful local build does not establish CI credential access. Never print or commit tokens. @@ -43,19 +43,11 @@ carry `x-formance-required-scopes-any-of` plus a description stating the server requirement: `connectivity:read` **or** `connectivity:write`. This avoids excluding write-only tokens. The server continues to enforce these scopes. -Composition validation checks the Connectivity operations and their security, -response codes, parameters, request/response bodies, all component models, -merge-patch media type and base64 cursor representation. `test-openapi` also -checks preparation idempotence and rejects deliberately damaged contracts. - -The pinned Connectivity input declares OpenAPI 3.1.0 but uses schema constructs -compatible with 3.0.3 after wrapping schema references with sibling annotations -in `allOf`. This preserves `readOnly` and descriptions that 3.0 Reference Objects -would otherwise ignore. Preparation declares that component as 3.0.3, and the -merged contract is validated in that dialect. This preserves the meaning of -existing modules’ `nullable` fields. Reassess dialect compatibility whenever -updating the source pin; do not adopt 3.1-only schema constructs by changing -the version string alone. +Connectivity must provide an OpenAPI 3.0.3 contract, including `allOf` wrappers +for schema references with annotations such as `readOnly` and descriptions. +The stack does not convert its dialect. This keeps existing modules’ `nullable` +fields meaningful in the composed OpenAPI 3.0.3 document. Merge the Connectivity +compatibility PR before merging this integration. ## Publication and downstream SDKs From e6a39e44349ff458e36f746d1ae39377c46c6f5e Mon Sep 17 00:00:00 2001 From: David Ragot <35502263+Dav-14@users.noreply.github.com> Date: Wed, 28 Oct 2026 13:05:28 +0100 Subject: [PATCH 3/6] refactor(sdk): preserve native Connectivity bearer authentication --- .speakeasy/workflow.lock | 4 ++-- .speakeasy/workflow.yaml | 4 ++-- Justfile | 2 -- docs/sdk-contract-composition.md | 19 ++++++++----------- releases/base.yaml | 4 +--- 5 files changed, 13 insertions(+), 20 deletions(-) diff --git a/.speakeasy/workflow.lock b/.speakeasy/workflow.lock index fcb3c3c2a5..ed08ee4b6f 100644 --- a/.speakeasy/workflow.lock +++ b/.speakeasy/workflow.lock @@ -14,8 +14,6 @@ workflow: sources: stacks-source: inputs: - - location: ./components/connectivity.openapi.yaml - modelNamespace: connectivity - location: ./components/auth.openapi.yaml modelNamespace: auth - location: ./components/gateway.openapi.yaml @@ -36,6 +34,8 @@ workflow: modelNamespace: orchestration - location: ./components/reconciliation.openapi.yaml modelNamespace: reconciliation + - location: ./components/connectivity.openapi.yaml + modelNamespace: connectivity - location: ./releases/base.yaml overlays: - location: ./releases/overlays/shared.overlay.yaml diff --git a/.speakeasy/workflow.yaml b/.speakeasy/workflow.yaml index 484205c572..63ff5df2d3 100644 --- a/.speakeasy/workflow.yaml +++ b/.speakeasy/workflow.yaml @@ -3,8 +3,6 @@ speakeasyVersion: latest sources: stacks-source: inputs: - - location: ./components/connectivity.openapi.yaml - modelNamespace: connectivity - location: ./components/auth.openapi.yaml modelNamespace: auth - location: ./components/gateway.openapi.yaml @@ -25,6 +23,8 @@ sources: modelNamespace: orchestration - location: ./components/reconciliation.openapi.yaml modelNamespace: reconciliation + - location: ./components/connectivity.openapi.yaml + modelNamespace: connectivity - location: ./releases/base.yaml overlays: - location: ./releases/overlays/shared.overlay.yaml diff --git a/Justfile b/Justfile index 87eb473586..53742d764b 100644 --- a/Justfile +++ b/Justfile @@ -30,8 +30,6 @@ download-specs: # Prepend API path prefix to each component spec prepend-paths: download-specs yq -i '.paths |= (to_entries | map(.key = "/api/connectivity" + .key) | from_entries) | (.paths[] | .. | select(has("operationId"))) |= (.operationId = "connectivity_" + .operationId | .tags = ["connectivity.v1"])' components/connectivity.openapi.yaml - yq -i '(.paths[] | .. | select(has("operationId") and (.security == null or (.security | length) > 0))).security = [{"Authorization": ["connectivity:write"]}]' components/connectivity.openapi.yaml - yq -i '(.paths[].get | select(has("operationId") and (.security == null or (.security | length) > 0))) |= (.security = [{"Authorization": []}] | .x-formance-required-scopes-any-of = ["connectivity:read", "connectivity:write"] | .description = (.description + "\n\nRequires connectivity:read or connectivity:write. The server enforces scopes.")) | del(.security, .components.securitySchemes.bearerAuth)' components/connectivity.openapi.yaml yq -i '.paths |= (to_entries | map(select(.key == "/*").key = "/api/auth" + .key) | from_entries)' components/auth.openapi.yaml yq -i '.paths |= (to_entries | map(select(.key == "/*").key = "/api/ledger" + .key) | from_entries) | (.paths[] | .. | select(has("tags")).tags) = ["ledger.v3"]' components/ledger.openapi.yaml yq -i '.paths |= (to_entries | map(select(.key == "/*").key = "/api/ledger" + .key) | from_entries) | del(.paths."/api/ledger/_info")' components/ledger-v2.openapi.yaml diff --git a/docs/sdk-contract-composition.md b/docs/sdk-contract-composition.md index 572ca60c1c..ce7b706c25 100644 --- a/docs/sdk-contract-composition.md +++ b/docs/sdk-contract-composition.md @@ -33,21 +33,18 @@ a successful local build does not establish CI credential access. Never print or commit tokens. Preparation adds `/api/connectivity` to routes, groups operations under -`connectivity.v1`, and keeps models under the `connectivity` namespace. The -stack OAuth2 scheme replaces the standalone bearer scheme. Public probes remain -public; writes declare `connectivity:write`. - -Speakeasy rejects two alternatives for one OAuth2 scheme with different scopes. -Reads therefore declare OAuth2 authentication without a formal scope list and -carry `x-formance-required-scopes-any-of` plus a description stating the server -requirement: `connectivity:read` **or** `connectivity:write`. This avoids excluding -write-only tokens. The server continues to enforce these scopes. +`connectivity.v1`, and prefixes operation IDs to avoid collisions with other +components. Models use the `connectivity` namespace. As for Ledger, the source +bearer authentication and operation descriptions remain unchanged. The API +server enforces Connectivity scopes; the stack does not rewrite these into +OAuth2 scope requirements. Composition reuses the existing bearer scheme; tokens +still need the scopes accepted by the service being called. Connectivity must provide an OpenAPI 3.0.3 contract, including `allOf` wrappers for schema references with annotations such as `readOnly` and descriptions. The stack does not convert its dialect. This keeps existing modules’ `nullable` -fields meaningful in the composed OpenAPI 3.0.3 document. Merge the Connectivity -compatibility PR before merging this integration. +fields meaningful in the composed OpenAPI 3.0.3 document. The compatibility change was integrated through +[Connectivity #493](https://github.com/formancehq/connectivity/pull/493). ## Publication and downstream SDKs diff --git a/releases/base.yaml b/releases/base.yaml index 4e5a582b67..044cdc858b 100644 --- a/releases/base.yaml +++ b/releases/base.yaml @@ -62,9 +62,7 @@ components: clientCredentials: tokenUrl: "/api/auth/oauth/token" refreshUrl: "/api/auth/oauth/token" - scopes: - connectivity:read: Read Connectivity catalog and connections - connectivity:write: Manage Connectivity connections + scopes: {} x-tagGroups: - name: Connectivity From 82d7115a0e99f411f1c449d16d61a3769c6d65af Mon Sep 17 00:00:00 2001 From: David Ragot <35502263+Dav-14@users.noreply.github.com> Date: Wed, 28 Oct 2026 13:19:47 +0100 Subject: [PATCH 4/6] refactor(sdk): remove explicit OpenAPI dialect assertion --- Justfile | 2 -- 1 file changed, 2 deletions(-) diff --git a/Justfile b/Justfile index 53742d764b..ed9baee2e8 100644 --- a/Justfile +++ b/Justfile @@ -58,8 +58,6 @@ build-openapi version="v0.0.0": generate-composition-overlay # Validate invariants introduced by composing namespaced OpenAPI documents. validate-openapi: - # Keep nullable semantics stable across the existing component contracts. - jq -e '.openapi == "3.0.3"' releases/build/generate.json >/dev/null # Global API metadata must remain exactly sourced from base.yaml. jq -e --argjson base_info "$(yq -o=json -I=0 '.info' releases/base.yaml)" '(.info | .version = $base_info.version) == $base_info' releases/build/generate.json >/dev/null # Every local discriminator mapping must resolve to an existing schema. From ee613059779f6819bfd6a5d07a135b3ec0041def Mon Sep 17 00:00:00 2001 From: David Ragot <35502263+Dav-14@users.noreply.github.com> Date: Wed, 28 Oct 2026 13:37:12 +0100 Subject: [PATCH 5/6] refactor(sdk): restore the existing Speakeasy build command --- Justfile | 2 +- docs/sdk-contract-composition.md | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Justfile b/Justfile index ed9baee2e8..e7bd718e18 100644 --- a/Justfile +++ b/Justfile @@ -52,7 +52,7 @@ generate-composition-overlay: strip-servers # Build the merged OpenAPI spec using Speakeasy build-openapi version="v0.0.0": generate-composition-overlay mkdir -p releases/build - speakeasy run -s all --skip-upload-spec + speakeasy run -s all cd releases && sed -i'' -e 's/SDK_VERSION/{{ version }}/g' build/generate.json just validate-openapi diff --git a/docs/sdk-contract-composition.md b/docs/sdk-contract-composition.md index ce7b706c25..c8a3cb00af 100644 --- a/docs/sdk-contract-composition.md +++ b/docs/sdk-contract-composition.md @@ -13,8 +13,10 @@ nix develop --command just validate-openapi ``` The build downloads and prepares contracts, generates the composition overlay, -merges the documents, and validates the result. It does not upload the contract -to the Speakeasy registry. `just pre-commit` also regenerates the event schemas. +merges the documents, and validates the result. Speakeasy uses the registry +configured in its workflow. For local validation without uploading the contract, +run Speakeasy with `--skip-upload-spec`. `just pre-commit` also regenerates the +event schemas. ## Connectivity source and authentication From 8f3ef8ec6a9cbeb9411e8b6d1b6c3f1a0084fc03 Mon Sep 17 00:00:00 2001 From: David Ragot <35502263+Dav-14@users.noreply.github.com> Date: Wed, 28 Oct 2026 14:44:30 +0100 Subject: [PATCH 6/6] refactor(sdk): align Connectivity composition with existing components --- Justfile | 6 +-- README.md | 5 --- docs/sdk-contract-composition.md | 63 -------------------------------- 3 files changed, 3 insertions(+), 71 deletions(-) delete mode 100644 docs/sdk-contract-composition.md diff --git a/Justfile b/Justfile index e7bd718e18..4641997e9c 100644 --- a/Justfile +++ b/Justfile @@ -10,12 +10,12 @@ ORCHESTRATION_VERSION := "v2.6.4" RECONCILIATION_VERSION := "v2.5.0" GATEWAY_VERSION := "v2.3.2" # Connectivity API releases do not yet publish an OpenAPI asset. -CONNECTIVITY_REF := "main" +CONNECTIVITY_VERSION := "main" # Download component OpenAPI specs from GitHub releases or source download-specs: mkdir -p components - gh api -H "Accept: application/vnd.github.raw+json" "repos/formancehq/connectivity/contents/misc/connectivity-api/openapi.yaml?ref={{ CONNECTIVITY_REF }}" > components/connectivity.openapi.yaml + gh api -H "Accept: application/vnd.github.raw+json" "repos/formancehq/connectivity/contents/misc/connectivity-api/openapi.yaml?ref={{ CONNECTIVITY_VERSION }}" > components/connectivity.openapi.yaml wget -q https://github.com/formancehq/ledger/releases/download/{{ LEDGER_VERSION }}/openapi.yml -O components/ledger.openapi.yaml wget -q https://github.com/formancehq/ledger/releases/download/{{ LEDGER_V2_VERSION }}/openapi.yaml -O components/ledger-v2.openapi.yaml wget -q https://github.com/formancehq/payments/releases/download/{{ PAYMENTS_VERSION }}/openapi.yaml -O components/payments.openapi.yaml @@ -29,7 +29,7 @@ download-specs: # Prepend API path prefix to each component spec prepend-paths: download-specs - yq -i '.paths |= (to_entries | map(.key = "/api/connectivity" + .key) | from_entries) | (.paths[] | .. | select(has("operationId"))) |= (.operationId = "connectivity_" + .operationId | .tags = ["connectivity.v1"])' components/connectivity.openapi.yaml + yq -i '.paths |= (to_entries | map(select(.key == "/*").key = "/api/connectivity" + .key) | from_entries) | (.paths[] | .. | select(has("tags")).tags) = ["connectivity.v1"] | .paths."/api/connectivity/_info".get.operationId = "connectivityInfo"' components/connectivity.openapi.yaml yq -i '.paths |= (to_entries | map(select(.key == "/*").key = "/api/auth" + .key) | from_entries)' components/auth.openapi.yaml yq -i '.paths |= (to_entries | map(select(.key == "/*").key = "/api/ledger" + .key) | from_entries) | (.paths[] | .. | select(has("tags")).tags) = ["ledger.v3"]' components/ledger.openapi.yaml yq -i '.paths |= (to_entries | map(select(.key == "/*").key = "/api/ledger" + .key) | from_entries) | del(.paths."/api/ledger/_info")' components/ledger-v2.openapi.yaml diff --git a/README.md b/README.md index c9c2413a13..0bf90f3f20 100644 --- a/README.md +++ b/README.md @@ -54,8 +54,3 @@ The Formance Stack is built on open-source, battle tested technologies including - **PostgreSQL** - Main storage backend - **Kafka/NATS** - Cross-services async communication - **Traefik** - Main HTTP gateway - -## SDK contract composition - -See [SDK contract composition](docs/sdk-contract-composition.md) for building the -unified OpenAPI contract, updating Connectivity and publishing SDK inputs. diff --git a/docs/sdk-contract-composition.md b/docs/sdk-contract-composition.md deleted file mode 100644 index c8a3cb00af..0000000000 --- a/docs/sdk-contract-composition.md +++ /dev/null @@ -1,63 +0,0 @@ -# SDK contract composition - -`.speakeasy/workflow.yaml` combines component OpenAPI contracts into -`releases/build/generate.json`. Component models are namespaced; routes address -services through the stack gateway. `releases/base.yaml` owns the global -metadata, OAuth2 scheme, servers and SDK tag groups. - -## Build and validate - -```sh -nix develop --command just build-openapi -nix develop --command just validate-openapi -``` - -The build downloads and prepares contracts, generates the composition overlay, -merges the documents, and validates the result. Speakeasy uses the registry -configured in its workflow. For local validation without uploading the contract, -run Speakeasy with `--skip-upload-spec`. `just pre-commit` also regenerates the -event schemas. - -## Connectivity source and authentication - -`CONNECTIVITY_REF` in `Justfile` follows Connectivity `main`. Each build fetches -`misc/connectivity-api/openapi.yaml` at the current branch head; no root-level -copy is required. Connectivity API releases do not currently include an OpenAPI -asset. A change on Connectivity main is included in the next stack composition, -so rebuilding an older stack revision can consume a newer Connectivity contract. - -Connectivity is private: local builds require GitHub CLI authentication with -read access to `formancehq/connectivity` (`gh auth login`, or a `GH_TOKEN` with -that access). Anonymous contributors cannot reproduce this part of the build. -The release build uses the existing `NUMARY_GITHUB_TOKEN` secret. Before merging -this integration, verify that the release credential can read the source file; -a successful local build does not establish CI credential access. Never print -or commit tokens. - -Preparation adds `/api/connectivity` to routes, groups operations under -`connectivity.v1`, and prefixes operation IDs to avoid collisions with other -components. Models use the `connectivity` namespace. As for Ledger, the source -bearer authentication and operation descriptions remain unchanged. The API -server enforces Connectivity scopes; the stack does not rewrite these into -OAuth2 scope requirements. Composition reuses the existing bearer scheme; tokens -still need the scopes accepted by the service being called. - -Connectivity must provide an OpenAPI 3.0.3 contract, including `allOf` wrappers -for schema references with annotations such as `readOnly` and descriptions. -The stack does not convert its dialect. This keeps existing modules’ `nullable` -fields meaningful in the composed OpenAPI 3.0.3 document. The compatibility change was integrated through -[Connectivity #493](https://github.com/formancehq/connectivity/pull/493). - -## Publication and downstream SDKs - -After review and merge, an authorized `vX.Y.Z` stack tag triggers -`.github/workflows/releases.yml`. It publishes `openapi.json` and `events.json`, -then publishes `stacks-source` to the Speakeasy registry with the version and -`LATEST_RELEASE` tags. This includes the API contract fetched from private -Connectivity in the public stack release assets and SDK input; review that -publication boundary before release. - -The SDK repositories consume `stacks-source@LATEST_RELEASE`. For example, -`formance-sdk-go` runs its Generate workflow on demand or on its daily schedule, -and opens a generation PR. Publishing that SDK and updating consumers such as -fctl are separate steps; changing Connectivity main alone performs none of them.