Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions .agents/skills/validate-opensecret/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ OPENSECRET_DEV_POSTGRES=0 OPENSECRET_DEV_ENV=0 OPENSECRET_DEV_CONTAINERS=0 \
cargo test --locked --all-features
```

Report passed, failed, and ignored counts. Default CI has no PostgreSQL service
and does not execute ignored tests. Do not substitute an aggregate recipe
Report passed, failed, and ignored counts. The Rust unit-test CI job has no
PostgreSQL service and does not execute ignored tests. Do not substitute an aggregate recipe
unless its checked-in definition preserves the same targets, features,
lockfile, and warning policy.

Expand Down Expand Up @@ -93,8 +93,12 @@ OPENSECRET_DEV_POSTGRES=0 OPENSECRET_DEV_ENV=0 OPENSECRET_DEV_CONTAINERS=0 \
```

The helper proves an empty-database migration and the selected local synthetic
database suites. It does not prove an OAuth provider flow, encrypted client
transport, or a data conversion from representative old rows.
database suites, including OAuth settings preservation and callback selection
through the real V1 encryption middleware and V2 gateway. The SDK integration
workflow runs this helper too. The callback fixtures use synthetic local
credentials and perform no provider exchanges; they do not prove a live OAuth
provider flow, a released SDK or application, or a data conversion from
representative old rows.

For a data migration, separately build an upgrade-shaped disposable database
with representative pre-change rows and verify restart, rollback, and retry
Expand Down
26 changes: 24 additions & 2 deletions .agents/skills/validate-opensecret/scripts/disposable_db_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ cleanup() {
rm -rf -- "$workdir"

if [ "$status" -eq 0 ] && [ "$tests_passed" -eq 1 ]; then
printf 'Disposable-DB evidence: %s AEAD/database tests and %s OAuth database tests passed; no tests skipped; temporary cluster removed.\n' \
"$aead_count" "$oauth_count"
printf 'Disposable-DB evidence: %s AEAD/database tests, %s OAuth database tests, and %s callback settings/transport tests passed; no tests skipped; temporary cluster removed.\n' \
"$aead_count" "$oauth_count" "$callback_count"
fi
exit "$status"
}
Expand Down Expand Up @@ -200,6 +200,28 @@ fi
grep -Eq "test result: ok\\. ${oauth_count} passed; 0 failed; 0 ignored;" \
"$workdir/oauth-tests.log"

callback_count=0
for callback_filter in \
db::tests::db_oauth_settings_ \
transport_v2::gateway::tests::db_oauth_callback_selection_v1_v2; do
cargo test --locked --all-features "$callback_filter" \
-- --ignored --list >"$workdir/callback-tests.list"
selected_count="$(awk -v prefix="$callback_filter" \
'index($0, prefix) == 1 && /: test$/ { count++ }
END { print count + 0 }' "$workdir/callback-tests.list")"
test "$selected_count" -gt 0

cargo test --locked --all-features "$callback_filter" \
-- --ignored --test-threads=1 --nocapture 2>&1 | tee "$workdir/callback-tests.log"
if grep -qi 'skipping:' "$workdir/callback-tests.log"; then
printf 'OAuth callback test output contained a skip marker\n' >&2
exit 1
fi
grep -Eq "test result: ok\\. ${selected_count} passed; 0 failed; 0 ignored;" \
"$workdir/callback-tests.log"
callback_count=$((callback_count + selected_count))
done

assert_database_identity
assert_migration_count
tests_passed=1
8 changes: 8 additions & 0 deletions .github/workflows/sdk-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ jobs:
psql "${DATABASE_URL}" -X -v ON_ERROR_STOP=1 \
-f sdk/test/integration/bootstrap.sql

- name: Verify OAuth persistence and encrypted callback selection
run: |
nix develop --no-update-lock-file './services/opensecret?submodules=1' -c bash -lc '
set -euo pipefail
cd services/opensecret
bash ../../.agents/skills/validate-opensecret/scripts/disposable_db_tests.sh
'

- name: Start and verify OpenSecret
run: |
nix develop --no-update-lock-file './services/opensecret?submodules=1' -c \
Expand Down
1 change: 1 addition & 0 deletions scripts/ci/opensecret_change_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"sdk/.npmrc", "sdk/bun.lock", "sdk/bunfig.toml", "sdk/package.json",
"sdk/rust-toolchain.toml", "sdk/flake.nix", "sdk/flake.lock",
".github/workflows/sdk-integration.yml",
".agents/skills/validate-opensecret/scripts/disposable_db_tests.sh",
})
KNOWN_INDEPENDENT_PREFIXES = (
"apps/", "proxy/", "services/updates/", ".agents/", ".github/",
Expand Down
7 changes: 7 additions & 0 deletions scripts/ci/test_opensecret_change_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@ def test_independent_components_and_docs_skip_backend_checks(self):
with self.subTest(path=path):
self.assert_routes([path])

def test_disposable_database_harness_selects_integration_without_app_packaging(self):
path = ".agents/skills/validate-opensecret/scripts/disposable_db_tests.sh"
self.assert_routes([path], "integration")
self.assertEqual(research_routes(path), frozenset())
self.assertFalse(affects_agent(path))
self.assert_routes([".agents/skills/validate-opensecret/SKILL.md"])

def test_submodules_and_selector_changes_select_all_backend_checks(self):
for path in (".gitmodules", ".github/workflows/opensecret-change-detection.yml",
"scripts/ci/opensecret_change_detection.py"):
Expand Down
7 changes: 6 additions & 1 deletion services/opensecret/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ OpenAI-compatible wire endpoint. Use an OpenSecret SDK or Maple for
protected-route integration tests; plain `curl` is suitable only for public
health probes.

For per-project callback allowlists, optional callback selection, and
old-client compatibility, see [OAuth callback selection](docs/oauth-callbacks.md).

`GET /health-check` returns HTTP 200 with `{"status":"pass","version":"v1"}`
when the server can respond. It does not contact Tinfoil, other providers, or
PostgreSQL; provider outages must not remove responsive enclaves from
Expand Down Expand Up @@ -83,7 +86,9 @@ OPENSECRET_DEV_POSTGRES=0 OPENSECRET_DEV_ENV=0 OPENSECRET_DEV_CONTAINERS=0 \
cargo test --locked --all-features
```

Default CI does not run ignored database or live-provider tests. Use the
The Rust unit-test job does not run ignored tests. The SDK integration job
also runs the disposable-database helper, including OAuth settings and
encrypted V1/V2 callback-selection checks. Live-provider tests remain separate. Use the
[`validate-opensecret`](../../.agents/skills/validate-opensecret/SKILL.md) workflow
for disposable PostgreSQL tests, authorized provider checks, encrypted-client
smoke tests, Nix checks, and release-only EIF/PCR evidence. Report those layers
Expand Down
91 changes: 91 additions & 0 deletions services/opensecret/docs/oauth-callbacks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# OAuth callback selection

OAuth callbacks are configured per project and provider. This additive API
contract lets browser clients on different origins finish sign-in on the
origin that started it. It does not change the default callback or require
existing clients to send a new field.

## Provider settings

The encrypted platform settings API at
`/platform/orgs/:org_id/projects/:project_id/settings/oauth` retains its
existing GET and PUT methods and organization/project authorization. Updating
settings still requires an organization owner or administrator.

Each Google, GitHub, or Apple settings object keeps `redirect_url` and accepts
an optional `additional_redirect_urls` array. For example, a Google settings
object can contain:

```json
{
"client_id": "example-provider-client-id",
"redirect_url": "https://app.example.com/auth/google/callback",
"additional_redirect_urls": [
"https://auth.example.com/auth/google/callback"
]
}
```

This is a nested provider object, not a complete PUT request. The surrounding
enabled flags and other provider settings retain their existing semantics.

- At most 16 additional URLs are accepted per provider. Each must satisfy
the existing generic callback URL validation, including its length bound.
The platform does not restrict this list to one application's hostnames.
- On PUT, omitting the new field or sending `null` preserves that provider's
stored additional list. An explicit array replaces it; `[]` clears it.
- This preservation applies when the provider settings object is supplied.
Omitting or clearing the entire provider object retains the existing
whole-object PUT behavior; it is not a patch API for other fields.
- GET and the PUT response include `additional_redirect_urls` when a list is
stored, including `[]`. An unset or null stored value omits the field from
the response; both omission and `[]` mean no additional callbacks on read.
Existing rows without the field remain readable; no SQL schema migration
is required.
- URL-list preservation and the settings write are serialized per project,
so an older writer that omits the field cannot overwrite a concurrently
committed list with an earlier snapshot.

## Initiation and completion

The decrypted request body for `/auth/github`, `/auth/google`, and
`/auth/apple` accepts an optional `redirect_url` alongside the existing
`client_id`. The same contract applies through both
Transport V1 and Transport V2.

When absent or `null`, the provider's default `redirect_url` is used. When
present, the value must exactly match the default or one of that provider's
additional entries for the requested project. There is no wildcard, prefix,
or same-host matching. A rejected selection returns the existing bad-request
error before allocating OAuth state.

The chosen callback is recorded in the server-validated, one-use OAuth state.
The provider authorization request and token exchange use that same callback,
including Apple's token exchange. Changing the default or additional list
does not retarget an already-started flow. Removing a list entry stops new
flows from selecting it; it does not revoke a pending flow. Existing state
expiry, one-use checks, provider checks, and V2 session/PKCE/nonce bindings
continue to apply.

Clients must treat the returned `state` as opaque and return it unchanged.
The callback request does not accept a separate redirect override. Changing
the callback inside the returned state cannot change the server's stored
selection.

## Compatibility and adoption

Deploy backend support before a client selects a non-default callback.
Register each callback with its OAuth provider as well as in the project's
backend settings; these are separate requirements. Existing clients that
omit the request field continue to use the default.

An older backend ignores the new configuration and request fields and keeps
using its default callback. Therefore a client that depends on a non-default
callback must not remain active during a backend downgrade. Older backend
settings writes may also discard the additional list; preserve configuration
outside the downgraded writer and verify it before re-enabling consumers.
OAuth state is process-local, so restarting or replacing a backend can
invalidate pending sign-ins; the user must start a new attempt.

SDK publication, consumer upgrades, provider registration, and traffic changes
are separate from implementing this backend contract.
Loading
Loading