Changelog¶
All notable changes to Nexus are documented here. This project follows Semantic Versioning.
0.6.7 2026-10-12¶
Fixed
-require-encrypted-secretsno longer trusts anENC[prefix. A value SOPS left unencrypted (unencrypted_suffix,unencrypted_regex,encrypted_regex) is returned verbatim on decryption, so a literal that happened to start withENC[passed 0.6.6's check. A requested value now counts as unencrypted when the file as stored holds it exactly as it decrypts, which is what "never encrypted" means whatever the value looks like; an encrypted value can never equal its own ciphertext. The comparison is on parsed values rather than text, because SOPS re-emits the document after decrypting and may respell a scalar it left alone:TRUEandtrue, or1.50and1.5, are the same value, while the string"true"is not the boolean. Resolved values keep their exact text.- The SOPS guide's Workload Identity Federation condition now compares
assertion.sub, which needs no extra attribute mapping, in both its branch (repo:ORG/REPO:ref:refs/heads/main) and environment (repo:ORG/REPO:environment:NAME) forms, and notes the separaterepository,refandenvironmentclaims as the alternative.
0.6.6 2026-10-12¶
Added
nexus -require-encrypted-secrets(orNEXUS_REQUIRE_ENCRYPTED_SECRETS=true) makes plaintext a hard error onvalidate,planandapply, closing the three ways a secret could otherwise reach the Broker unencrypted at rest:-secretsmust besops:<path>orgcp-secret-manager:<project>(envis refused by name; a SOPS file is also inspected as stored, and a requested value left in plaintext by anunencrypted_suffix/unencrypted_regex/encrypted_regexrule is refused by key name), a placeholder the source does not hold is unresolved rather than taken from the environment, and aclient_secretthat is not exactly one${PLACEHOLDER}(a literal, or a literal with a placeholder mixed in) is reported with its file and provider, without the value. Onvalidatethe manifest rule runs with no source at all, so a pull request check can enforce it without any key; with-secretsthe source is checked too. Every message names the fix.paramsis not policed: which of its keys are credentials is provider-specific, andplanalready masks it.- Provider secrets with SOPS, end to end: a new guide (Provider Secrets with SOPS) covering the key,
.sops.yaml, writing and editing the file, placeholder naming, rotation, enforcing the policy in CI, multiple environments, key management, whether to commit the ciphertext, what the CLI guarantees about the plaintext, and a troubleshooting table built from the failures met while migrating a 149-provider deployment.
Changed
- An unresolved-placeholder message only says "then environment variable X" when the environment was in fact consulted.
0.6.5 2026-10-12¶
Added
- The CLI is now
nexus.go install github.com/Prescott-Data/nexus-framework/nexus-cli/cmd/nexus@latest(ormake cli) builds a binary namednexus, so the commands readnexus plan,nexus apply,nexus validateandnexus version. The code moved intonexus-cli/internal/cli; the module path is unchanged. - A directory of provider files is a manifest.
-filemay name a directory holding one provider per<name>.yaml, whichplan,applyandvalidateassemble in file-name order the way Terraform reads every.tfin a directory. A file whosenamedoes not match its file name, a name defined twice, or a file that is not a single provider mapping is a hard error before anything reaches the Broker. Hidden files and non-YAML files are ignored. - Secret sources.
-secrets(or$NEXUS_SECRETS) names where${PLACEHOLDER}values come from:env(the default, as before),sops:<file>(a SOPS-encrypted YAML, JSON or dotenv file mapping placeholder to value, decrypted in memory with the keys the process can reach, such as Google Cloud KMS through application default credentials) orgcp-secret-manager:<project>(one Secret Manager secret per placeholder, latest version, read concurrently; the id is the placeholder withoutNEXUS_PROVIDER_, lower-cased with-, behind-secret-prefix). Only the placeholders the manifest uses are fetched. A placeholder the source does not hold falls back to the environment, and anything still unresolved is listed by name. No wrapper script is needed to run the CLI against a hosted deployment. nexus validatechecks a manifest offline with the Broker's own registration rules: provider name format, the required fields foroauth2(including the discovery/issuer rules),samland the static auth types, unsupportedauth_typevalues and duplicate names, each attributed to its file. Placeholders count as present, so it runs in a pull request without secrets; with-secretsit resolves every placeholder and runs the same rules on the expanded manifest, so an empty secret or a malformed resolved value fails validate the way it would fail apply.planandapplyrun the same checks before contacting the Broker.- SAML providers can be managed from a manifest (
saml_idp_entity_id,saml_idp_sso_url,saml_idp_x509_cert,saml_sp_entity_id);validateapplies the Broker's URL and X.509 format rules to them. - Strict manifests. A manifest whose root is not a
providerslist (a typo such asprovider:, an empty file, a provider file passed as a manifest), a provider with a field the CLI does not know, or a file holding more than one YAML document is rejected with a line number instead of decoding to fewer providers than written, whichapply -prunewould have acted on. - Plan output masks
paramsalways, and every other field that held a placeholder, not onlyclient_idandclient_secret, so a secret underparamsis never written to a terminal or CI log on either side of the diff. The Broker'sprovider.updatedaudit event masks every value underparamsas well, keeping the keys, so a credential sent inparamsis not persisted in the audit table. - Numbers survive the round trip. Drift compares and sends numbers by their exact text, and the Broker's
PATCH /providers/{id}decodes them the same way, so a 16-digit id inparamsis not rounded through a float on either side. - Resolved values never appear in messages. A YAML type error or a validation finding that would quote a value which came from a secret source shows the
${PLACEHOLDER}instead. YAML aliases (*anchor) are followed when deciding which fields are sensitive, so a secret anchored on one field and aliased into another is masked under both. - The release workflow tags the CLI module (
nexus-cli/vX.Y.ZbesidevX.Y.Z), sogo install .../nexus-cli/cmd/nexus@vX.Y.Zresolves and the binary reports that version. descriptionandcategoryare manifest fields. The CLI's provider type did not have them, so a manifest that set them registered a provider without them andplannever noticed; with the unknown-field check they would have been rejected instead. They are now sent, compared and shown as drift like any other field; omitting one leaves the live value alone, an explicit""clears it.User-Agent: nexus-cli/<version>on every Broker request, so the audit log can tell CLI changes from manual API calls as the documentation already claimed.- Failures name what was looked for. An unresolved placeholder says which secret id and project, file key or environment variable was tried;
applyends with a list of every provider that failed and why.
Changed
- Orphans and updates are printed and applied in name order, so two runs of
planproduce the same text. - A provider with no
scopes:no longer drifts on every run: the manifest's missing list and the Broker's empty one are the same state, as an explicit[]against a missing value already was. - A manifest the Broker would reject now fails
planandapplyup front with every problem listed, instead of one400per provider during apply.
0.6.4 2026-10-12¶
Added
- Provider responses carry
client_secret_fingerprint. A truncated SHA-256 of the stored client secret, written on registration and rotation and backfilled the first time the Broker uses a secret it has no fingerprint for (migration21_add_provider_secret_fingerprint.sql). The secret itself is still never returned.nexus-cli plancompares the manifest value's fingerprint against it, so a rotated secret in a manifest is detected and sent while an unchanged one is left alone; the CLI previously could not see secret changes at all once it stopped re-sending them on every apply.
Fixed
- The startup log no longer prints a slice of the encryption key.
ENCRYPTION_KEYandSTATE_KEYfingerprints were the first 8 base64 characters of the key, 6 bytes of key material; they are now a hash prefix.
0.6.3 2026-10-12¶
Fixed
nexus-cliexpands placeholders in manifest values only. It expanded${VAR}across the raw file before parsing, so a$in a YAML comment was reported as an unset variable and abortedplan. The manifest is now parsed first and placeholders are resolved inside string values, nestedparamsincluded; comments and keys are untouched. All unset placeholders are listed by name. First test file for the CLI.nexus-clino longer rotates every OAuth secret on every apply. The Broker never returnsclient_secret, andplantreated that as drift, so eachapplyre-sent (and with an external backend re-versioned) all secrets. A secret the Broker does not return is no longer compared.nexus-cli apply -auto-approveskips the interactive confirmation for scripted applies from a trusted environment; the guide still advises against apply-on-merge.
0.6.2 2026-10-12¶
Changed
- Provider secret rotation is read-consistent. With an external secret backend, every write now returns an immutable version reference that the provider row stores in
client_secret_ref, written in the same statement as the metadata (migration20_add_provider_secret_ref.sql). Reads use that exact version, so an OAuth exchange during aPUT/PATCHrotation sees either the previous metadata with the previous secret or the new with the new, never one paired with the other. Earlier versions are retired only after the row commits; a reader that loaded the row just before that re-reads it once when its version is already retired. A rotation whose row update definitely fails discards the version it wrote. Rows written before this release have no reference; the first resolve backfills one under the provider lock, so their first rotation is serialized too. (#114) - Provider reads are metadata-only.
GET /providers/{id}, the consent spec and listings no longer touch the secret backend; the secret is resolved only on the paths that send it to the provider (exchange, refresh, revocation, health probe), throughProfileStorer.ResolveClientSecret. A backend outage therefore no longer breaks admin reads. (#114) providers.Backend:Setreturns the version reference;GetbecomesCurrentand returns the reference too; newGetRef,RetireandDiscardRef. The in-memory backend keeps numbered versions so rotation is testable.
0.6.1 2026-10-12¶
Changed
- The GCP Secret Manager backend is tested without cloud credentials. The backend now talks to a small interface over the Secret Manager client, and an in-memory fake with the service's real gRPC codes exercises every lifecycle path in
go test: create-or-reuse (AlreadyExists), rotation retiring only older versions, destroy races (NotFound, FailedPrecondition), a listing failure that leaves the new version live but is still reported, delete of a missing secret, and the startup probe including its cleanup on failure. The credentialed round-trip test remains for the real client. No behaviour change. (#115)
0.6.0 2026-10-10¶
Added
- The Gateway can require an API key from its callers. Until now the Gateway authenticated only itself to the Broker (
BROKER_API_KEY) and checked nothing on the way in, so anyone who could reach it could list providers and mint tokens; theREQUIRE_API_KEYthe Compose file handed it was never read. WithGATEWAY_REQUIRE_API_KEY=true, every route except/health,/auth/callback,/v1/capture-schemaand/v1/capture-credential(the ones a browser or a load balancer reaches) requiresX-API-Keyto match one ofGATEWAY_API_KEYSor a mountedGATEWAY_API_KEY_FILE/GATEWAY_API_KEYS_FILE, reloaded everyGATEWAY_API_KEY_RELOAD_INTERVAL(30s) so keys rotate without a restart. Missing key is 401, wrong key is 403; the gRPC server applies the same check onx-api-keymetadata. Off by default for existing deployments, with a loud startup warning. Enabling it with no keys configured refuses to start. - The Go SDK can send the key. New
WithAPIKey(key)option, the counterpart of the TypeScript and Python SDKs'apiKey; it is sent asX-API-Keyon every Gateway call.WithBrokeris unchanged and stays the Broker credential. The Sidecar passesGATEWAY_API_KEYthrough it. The Gateway OpenAPI contract declares theApiKeyAuthscheme on every operation.
Changed
docker-compose.ymlno longer passes the GatewayREQUIRE_API_KEY,REQUIRE_ALLOWLIST,ALLOWED_CIDRSandALLOWED_RETURN_DOMAINS, which it never read; it passesGATEWAY_REQUIRE_API_KEYandGATEWAY_API_KEYSinstead.
0.5.0 2026-10-10¶
Added
- Provider client secrets can live in GCP Secret Manager.
SECRET_BACKEND=gcp-secret-manageron the Broker stores each OAuth2 provider'sclient_secretas its own Secret Manager secret, named<GCP_SECRET_PREFIX>-<provider id>, and leavesprovider_profiles.client_secretNULL. Registration rolls back if the backend write fails,PATCHrotates the secret and destroys earlier versions, and deleting a provider deletes its secret. The defaultinternalbackend is unchanged, and rows that still carry a column value keep working after the switch. The Broker checks the backend at startup and refuses to boot if it cannot reach it. A Terraform module indeploy/terraform/gcp/secret-backendgrants the Broker's service account create on the project plus admin on prefixed secrets only. First backend for #63; thepkg/vault/providers.Backendinterface is where Vault and AWS go next.
Changed
- The token encryption helpers moved from
pkg/vaulttopkg/vault/tokens, so thatpkg/vaultcan hold both the token vault and the provider secret backends.vault.Encryptandvault.Decryptremain as deprecated wrappers. No behaviour change.
0.4.3 2026-10-10¶
Changed
- One version everywhere, enforced. The
VERSIONfile was still 0.3.1 while v0.4.0 and v0.4.1 had been tagged by hand through the GitHub Releases UI, so binaries, images, docs and SDK manifests disagreed about what version they were. This release reconciles them:VERSIONis bumped past the stray tags, andmake check-versionnow fails a pull request when any stamped file, the top changelog heading, or the newest git tag disagrees withVERSION. Stamping covers the TypeScript and Python SDK manifests as well as the OpenAPI specs and docs site. Main is protected: changes land by pull request with CI green, and a tag ruleset blocksv*tags from everyone but the release workflow, which pushes with a deploy key. The container image build readsVERSIONrather than the git ref and refuses a tag that disagrees with it. See the Versioning section ofCONTRIBUTING.md.
Fixed
- The changelog now has a section for every release since 0.2.0. The sections for 0.2.3 through 0.2.7, 0.3.0, 0.3.1 and 0.4.1 were missing or filed as "Unreleased".
0.4.1 2026-09-30¶
Fixed
- Providers that take a JSON token-exchange body are supported. Set
token_request_format: jsonin the provider'sparamsand the Broker sends the authorization-code exchange as a JSON object instead of a form, which Notion and other spec-deviating providers require. The parameter is kept out of the authorization URL. (#111)
Changed
- README rewritten: hero, architecture diagram, examples in all three SDK languages, and an FAQ.
0.4.0 2026-09-16¶
Added
-
Connections can be revoked.
DELETE /v1/connections/{id}on the Gateway (DELETE /connections/{id}on the Broker) permanently terminates a connection: it attempts an RFC 7009 revocation at the provider, deletes the encrypted credential, closes every open agent session bound to the connection, and moves it to the terminalrevokedstatus. Until now a stored credential had no off-switch — it lived in the database until the connection was superseded by a fresh consent — which left no answer for an offboarded user, a leaked token, or a deletion request.The local credential is destroyed even when the upstream call fails, so a provider outage can never leave a live token behind — local teardown runs on its own deadline, independent of the upstream call. The response reports
token_deletedandprovider_revokedseparately, because "Nexus can no longer use this token" and "this token is dead at the provider" are different guarantees and incident response needs to tell them apart. Providers that do not advertise a revocation endpoint, and staticapi_key/basic_authcredentials, are destroyed locally only and say so inprovider_revocation_error.Revocation is idempotent, and a retry reports the original outcome — the stored
revoked_atand upstream result — so a client retrying after a timeout gets a confirmable answer rather than a fresh-looking one. Passingworkspace_idscopes the request: a mismatch returns 404 rather than 403, so the endpoint does not confirm the existence of connection IDs to a caller that does not own them.reasonis recorded on the connection and in the newconnection.revokedaudit event.Available in all three SDKs as
RevokeConnection/revokeConnection/revoke_connection. -
make test-integrationruns tests against a real PostgreSQL instance. These live behind theintegrationbuild tag and are excluded frommake test, which has no database. PointNEXUS_TEST_DATABASE_URLat a migrated database to run them. Revocation is the first feature covered: transaction boundaries, the agent-session cascade and therevoked_atcolumns are all things sqlmock will accept but a real server can reject.
Changed
- Requesting a token or a refresh for a revoked connection returns
410 Gonewith codeconnection_revoked, rather than the generic400 connection_not_active. A revoked connection is never coming back, and a client must start a fresh connection flow instead of retrying. The Gateway now forwards the Broker's error body on/v1/token,/v1/refreshand/v1/connections/{id}, so the code actually reaches the caller instead of arriving as a bodyless status. - Revocation is terminal against concurrent writers. Credential writes take a
FOR SHARElock on the connection row and are gated onrevoked_at IS NULL, so a refresh, OAuth exchange or static capture that was in flight when a revocation committed is rejected rather than recreating the credential that was just destroyed. ConnectionSummarynow carriesrevoked_at.
0.3.1 2026-09-10¶
Fixed
- Explicit OAuth token endpoints are honored. When a provider profile sets
token_urlalongside anissuer, the Broker exchanges and refreshes against the configuredtoken_urlrather than the discovered one. (#106)
0.3.0 2026-09-07¶
Added
- The Broker applies its own migrations on boot. The image carries
migrations/, and the Broker applies whatever the database has not recorded in a newschema_migrationsledger, keyed by filename. Running twice is a no-op, and a partly-migrated database continues from where it stopped. SetAUTO_MIGRATE=falsewhere migrations are run as a separate step, andMIGRATIONS_DIRto relocate them. (#101, #102) - SAML 2.0 service-provider flow, agent identity sessions (
/v1/agents,/v1/agent-sessions), the nexus-sidecar proxy, bridge metrics and Broker API key rotation viaAPI_KEY_FILE/API_KEYS_FILE.
Fixed
- Migrations are replayable.
ADD COLUMN,CREATE TABLE,CREATE INDEXand thetokens_connection_id_uniqueconstraint are guarded, so a database adopting the ledger does not fail on migrations whose effect is already present. (#102) - Two migrations no longer share the prefix
11; they are now11a_add_provider_description.sqland11b_add_audit_created_at_index.sql. (#103) make stampworks on macOS; it relied on GNU-onlysedbehaviour and silently stamped nothing on BSD sed.- SAML
return_urlis validated before credentials are stored. The sidecar returns413 body_too_largefor oversized bodies. Bridge WebSocket duration is measured at exit. Dependency bumps forx/net,x/crypto,grpcandgoxmldsigsecurity advisories.
0.2.3 – 0.2.7 2026-05-14 to 2026-07-30¶
This section was filed as "Unreleased" at the time and was never split. It covers v0.2.3 (2026-05-14, the SDKs and release workflow below), v0.2.4 (2026-05-19, provider and connection health checks with --worker-only mode, bounded worker concurrency, GET /connections?workspace_id=, health_status on token responses), v0.2.5 (2026-07-14, versioning unified on the root VERSION file, refresh_token preserved on refresh, client_secret redacted from responses, expired OAuth2 tokens auto-refreshed, static credentials fail closed when unvalidatable), v0.2.6 (2026-07-29, self-hosted base URL and path-based credential validation, provider manifest for static-credential rollout, CodeQL fixes) and v0.2.7 (2026-07-30, credential validation never served from the response cache, non-public targets blocked for validation probes).
Added
- Python SDK (
nexus-sdk-python): full-feature-parity Python client —get_token_by_connection_id,resolve_token,request_connection,check_connection. Zero external dependencies. - TypeScript SDK (
@dromos/nexus-sdk): evolved fromnexus-mcp-adapter. MCP token resolution, in-memory caching, authenticated transport, andresolveTokenfor stateless MCP clients. Built todist/, ESM imports hardened. - Go SDK MCP integration:
ResolveTokenendpoint for stateless MCP clients — workspace and provider-scoped token resolution with TTL caching. - Multi-strategy credential support across all three SDKs: handles
oauth2,api_key,basic_auth,aws_sigv4,query_param, andhmac_payloadstrategies without caller-side branching. - Automated release workflow: GitHub Actions CI/CD pipeline, bumped
VERSIONto0.2.3. - Agent auth proposal (
AGENT_AUTH_PROPOSAL.md): full design document for agent identity, OBO sessions, scoped session TTLs, and custom scope enforcement. - SDK documentation: comprehensive reference pages for Go, TypeScript, and Python SDKs including install, method signatures, MCP integration examples, and error handling.
Fixed
- Broker OAuth endpoint selection: explicit provider
auth_urlandtoken_urlvalues are now authoritative whenenable_discovery=false. Previously, token exchange still attempted OIDC discovery and could replace a provider's configured endpoint; for Slack this exchanged the authorization code against the OpenID endpoint and stored an ID token without the bot access token. - Broker: static credentials are now validated before activation.
api_key/basic_authconnections fail closed (provider_not_validatable) when the provider has noapi_base_url+user_info_endpoint, instead of accepting any key and reportingactive. When configured, a401/403from the provider rejects the key withinvalid_credentials. - TypeScript SDK:
Bearertoken type normalized to RFC 6750 capitalization (wasbearer). - TypeScript SDK: package entry pointed at compiled
dist/, not.tssource. - Gateway:
resolveroute wired; ESM import errors resolved in adapter. - Adapter/Gateway: token TTL hardened, stdio safety improved, error handling tightened.
- MCP adapter smoke test added against live Gateway.
0.2.0 2026-05-05¶
Added
- Security-as-Code CLI (
nexus-cli): Terraform-styleplan → confirm → applyworkflow for declarative provider management via YAML manifest. PATCH-based reconciliation (no accidental overwrites), concurrent profile fetching with bounded worker pool, field-level diff output with secret masking, fail-fast on unresolved env vars, non-zero exit on partial apply failure. - Audit subsystem (
audit.Service): structured event logging toaudit_eventstable with IP validation, User-Agent capture, andaudit.Loggerinterface for test mocking. Events:provider.created,provider.updated,provider.deleted,connection.created,token.retrieved,token.refresh_fatal. GET /auditendpoint: queryable audit log withevent_type,resource_id,since,until,limit, andoffsetfilters.- Credential redaction:
PATCHaudit payloads redactclient_secretandclient_idbefore writing to the audit log. - Provider
categoryfield:categoryadded to provider profiles with migration. GatewayMetadataResponsepatched to includecategoryin the OpenAPI-generated response. capture-schemaandcapture-credentialendpoints: Gateway proxies for static credential capture flow, enabling API key and basic auth connections without OAuth redirects.
Fixed
- Gateway: manually patched
MetadataResponseto includecategoryfield, avoidingoapi-codegenversion mismatch. - Documentation: all examples standardized to
localhost:8090— internal Azure URLs removed. - OpenAPI:
descriptionandcategoryadded toMetadataResponseandProviderProfileschemas; gateway broker client regenerated.
0.1.5 2026-04-13¶
Changed
- Bridge: replaced
goto Retrywithfor-loop inMaintainGRPCConnection— cleaner control flow, no goto jumps. (@ashioyajotham) - Broker: replaced streaming
json.Encoderwith marshal-then-write pattern — eliminates partial-write race on slow connections. (@ashioyajotham) - Security documentation hardened: shared secrets, key rotation, and deployment guidance expanded.
Fixed
- Broker: handle SQL
NULLvalues for non-OAuth2 provider profiles —api_keyandbasic_authproviders no longer cause null pointer panics in the profile store.
0.1.4 2026-04-01¶
Added
- Broker:
skip_scope_on_authprovider parameter — bypasses strict scope validation on the authorization URL for providers that reject scope in the initial redirect (Salesforce).
0.1.3 2026-04-01¶
Added
- Broker: validate
api_keyandbasic_authcredentials before storing — rejects malformed or empty credentials at capture time rather than at retrieval.
Fixed
- Broker: enforce one token row per connection via upsert — eliminates duplicate token rows on reconnect. (@ashioyajotham)
- Security:
ENCRYPTION_KEYandSTATE_KEYare now required at startup — Broker and Gateway fatal-exit with a clear message if either is absent. (@ashioyajotham) - Tests:
TestMainused for binary lifecycle management; assertions refined. - Gateway:
gofmtformatting applied to main files.
0.1.2 2026-04-01¶
Fixed
- Docker: corrected image names to
nexus-brokerandnexus-gateway— was using incorrect names that brokedocker pulland Compose service references.
0.1.1 2026-04-01¶
Added
- Docker Hub publishing GitHub Actions workflow.
- Gateway:
capture-schemaandcapture-credentialproxy endpoints for static credential flows. (@Abdullahi254) - Open-core refactor: internal packages made public to support the OSS consumption model.
Fixed
- Go module paths updated to
github.com/Prescott-Data/nexus-frameworkthroughout. - Broken database migration corrected.
0.1.0 2026-02-19¶
Initial public release.
Added
- Nexus Broker: OAuth 2.0 and OIDC connection management — token storage (AES-GCM 256-bit at rest), background refresh loop, OIDC discovery with JWKS caching, nonce/id_token verification, Prometheus metrics.
- Nexus Gateway: public-facing API for agents and backends. Versioned at
/v1. gRPC-first communication to Broker with REST fallback. - Nexus Bridge: Go library for embedding in agent processes —
MaintainWebSocketandMaintainGRPCConnectionwith automatic credential injection, token refresh, exponential backoff reconnection, and Prometheus metrics. - Go SDK (
nexus-sdk): zero-dependency HTTP client for the Gateway API. - Provider support: Google (OIDC discovery), Azure AD (common tenant), GitHub, Salesforce, and arbitrary OAuth2 providers with manual endpoint configuration.
- Security guardrails: IP allowlisting (
ALLOWED_CIDRS), allowed return domain validation, API key enforcement. - Docker Compose: single
make upcommand runs Broker, Gateway, PostgreSQL, and Redis. - Bitbucket Pipelines: initial CI/CD configuration.



