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
18 changes: 9 additions & 9 deletions CONFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1683,7 +1683,7 @@ Named by their `// =====` section markers.

**walletconnect-style**

- every route answers without a projectId (the gate is not wired)
- every route demands a projectId — body, query, or bearer all pass
- a wc: URI pairing round-trips its topic, relay protocol, and symKey
- an auto pairing mints a fresh topic and a 64-hex symKey
- proposing requires pairingTopic — and accepts one never paired
Expand Down Expand Up @@ -9029,19 +9029,19 @@ behavior notes live in each adapter's README.

- Pairing, session approve, and JSON-RPC requests are auto-approved (no wallet device)
- personal_sign and eth_sendTransaction return synthetic hashes; nothing is signed
- The projectId gate ships but is never wired — every route answers without a credential
- Any non-empty projectId passes the gate — no validation against a project registry
- Expiry fields are TTL constants, not absolute unix timestamps; extend persists nothing
- Unacknowledged sessions serve JSON-RPC requests immediately (no approval gate)

<details><summary>Derived behavior tags (static — from scripts/*.star, not SDK-verified)</summary>

- `POST` `/v1/pairings` — body, stateful, errors
- `POST` `/v1/sessions` — body, stateful, errors
- `GET` `/v1/sessions` — query, stateful, paginate, errors
- `POST` `/v1/sessions/{topic}/approve` — params, stateful, errors
- `POST` `/v1/sessions/{topic}/request` — body, params, stateful, errors
- `POST` `/v1/sessions/{topic}/extend` — params, stateful, errors
- `DELETE` `/v1/sessions/{topic}` — params, stateful, errors
- `POST` `/v1/pairings` — body, query, auth, stateful, errors
- `POST` `/v1/sessions` — body, query, auth, stateful, errors
- `GET` `/v1/sessions` — body, query, auth, stateful, paginate, errors
- `POST` `/v1/sessions/{topic}/approve` — body, query, params, auth, stateful, errors
- `POST` `/v1/sessions/{topic}/request` — body, query, params, auth, stateful, errors
- `POST` `/v1/sessions/{topic}/extend` — body, query, params, auth, stateful, errors
- `DELETE` `/v1/sessions/{topic}` — body, query, params, auth, stateful, errors

</details>

Expand Down
21 changes: 21 additions & 0 deletions adapters/walletconnect-style/scripts/relay.star
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
# on_create_pairing establishes a pairing from either a wc: URI or an
# explicit topic. Returns the pairing object.
def on_create_pairing(req):
if _require_project_id(req) == None:
return respond(401, {"error": "unauthorized", "message": "a valid projectId is required (body, query, or bearer)"})

body = req.get("body")
if body == None:
body = {}
Expand Down Expand Up @@ -61,6 +64,9 @@ def on_create_pairing(req):

# on_propose_session proposes a new session from a pairing.
def on_propose_session(req):
if _require_project_id(req) == None:
return respond(401, {"error": "unauthorized", "message": "a valid projectId is required (body, query, or bearer)"})

body = req.get("body")
if body == None:
body = {}
Expand Down Expand Up @@ -102,6 +108,9 @@ def on_propose_session(req):
# field for a continuation cursor, so cursor-based paging is not surfaced here;
# `limit` still caps the page size.
def on_list_sessions(req):
if _require_project_id(req) == None:
return respond(401, {"error": "unauthorized", "message": "a valid projectId is required (body, query, or bearer)"})

sc = store_collection("sessions")
result = []
for s in sc.list():
Expand All @@ -114,6 +123,9 @@ def on_list_sessions(req):
# on_approve_session acknowledges (approves) a session, simulating the
# wallet's approval response.
def on_approve_session(req):
if _require_project_id(req) == None:
return respond(401, {"error": "unauthorized", "message": "a valid projectId is required (body, query, or bearer)"})

topic = req["params"]["topic"]

sc = store_collection("sessions")
Expand Down Expand Up @@ -142,6 +154,9 @@ def on_approve_session(req):
# on_session_request handles a wallet JSON-RPC request (e.g.
# eth_requestAccounts, personal_sign, eth_sendTransaction).
def on_session_request(req):
if _require_project_id(req) == None:
return respond(401, {"error": "unauthorized", "message": "a valid projectId is required (body, query, or bearer)"})

topic = req["params"]["topic"]

sc = store_collection("sessions")
Expand Down Expand Up @@ -191,6 +206,9 @@ def on_session_request(req):

# on_extend_session refreshes the session expiry.
def on_extend_session(req):
if _require_project_id(req) == None:
return respond(401, {"error": "unauthorized", "message": "a valid projectId is required (body, query, or bearer)"})

topic = req["params"]["topic"]

sc = store_collection("sessions")
Expand All @@ -205,6 +223,9 @@ def on_extend_session(req):

# on_disconnect_session disconnects (deletes) a session.
def on_disconnect_session(req):
if _require_project_id(req) == None:
return respond(401, {"error": "unauthorized", "message": "a valid projectId is required (body, query, or bearer)"})

topic = req["params"]["topic"]

sc = store_collection("sessions")
Expand Down
20 changes: 11 additions & 9 deletions adapters/walletconnect_style_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ func newWalletconnectFixture(t *testing.T, start time.Time) *walletconnectFixtur
}

func (f *walletconnectFixture) call(handler, method, path string, params, query map[string]string, body map[string]any, auth string) starlark.Response {
if auth == "" {
auth = "Bearer wc-vm-test-project"
}
f.t.Helper()
headers := map[string]string{}
if auth != "" {
Expand Down Expand Up @@ -156,18 +159,17 @@ func wcHash(s string) bool {
func TestWalletconnectPairingAndSessionLifecycle(t *testing.T) {
f := newWalletconnectFixture(t, wcBase())

// ===== every route answers without a projectId (the gate is not wired) =====
// The manifest declares identity.token_scheme: bearer and lib.star ships
// _require_project_id, but no handler calls the helper — the relay answers
// with no credential at all. Asserted as-is; see the deviation report.
if r := f.call("on_create_pairing", "POST", "/v1/pairings", nil, nil, map[string]any{}, ""); r.Status != 200 {
t.Fatalf("pairing with no projectId -> %d, want 200 (gate unenforced): %v", r.Status, r.Body)
// ===== every route demands a projectId — body, query, or bearer all pass =====
// The relay gates like the real WC service: a projectId must ride the
// body (most SDKs), the query, or the Authorization bearer.
if r := f.call("on_create_pairing", "POST", "/v1/pairings", nil, nil, map[string]any{}, "none"); r.Status != 401 {
t.Fatalf("pairing with no projectId -> %d, want 401: %v", r.Status, r.Body)
}
if r := f.call("on_create_pairing", "POST", "/v1/pairings", nil, nil, map[string]any{}, "Bearer not-a-project-id"); r.Status != 200 {
t.Fatalf("pairing with a bogus bearer -> %d, want 200 (gate unenforced): %v", r.Status, r.Body)
t.Fatalf("pairing with a bearer projectId -> %d, want 200: %v", r.Status, r.Body)
}
if r := f.call("on_list_sessions", "GET", "/v1/sessions", nil, nil, nil, ""); r.Status != 200 {
t.Fatalf("list with no projectId -> %d, want 200 (gate unenforced)", r.Status)
if r := f.call("on_list_sessions", "GET", "/v1/sessions", nil, nil, nil, "none"); r.Status != 401 {
t.Fatalf("list with no projectId -> %d, want 401", r.Status)
}

// ===== a wc: URI pairing round-trips its topic, relay protocol, and symKey =====
Expand Down
21 changes: 19 additions & 2 deletions conformance/matrix.json
Original file line number Diff line number Diff line change
Expand Up @@ -206096,7 +206096,7 @@
"sdks": [],
"behaviors": [],
"vm_behaviors": [
"every route answers without a projectId (the gate is not wired)",
"every route demands a projectId — body, query, or bearer all pass",
"a wc: URI pairing round-trips its topic, relay protocol, and symKey",
"an auto pairing mints a fresh topic and a 64-hex symKey",
"proposing requires pairingTopic — and accepts one never paired",
Expand All @@ -206116,7 +206116,7 @@
"deviations": [
"Pairing, session approve, and JSON-RPC requests are auto-approved (no wallet device)",
"personal_sign and eth_sendTransaction return synthetic hashes; nothing is signed",
"The projectId gate ships but is never wired — every route answers without a credential",
"Any non-empty projectId passes the gate — no validation against a project registry",
"Expiry fields are TTL constants, not absolute unix timestamps; extend persists nothing",
"Unacknowledged sessions serve JSON-RPC requests immediately (no approval gate)"
],
Expand Down Expand Up @@ -206156,6 +206156,8 @@
"route": "/v1/pairings",
"tags": [
"body",
"query",
"auth",
"stateful",
"errors"
]
Expand All @@ -206165,6 +206167,8 @@
"route": "/v1/sessions",
"tags": [
"body",
"query",
"auth",
"stateful",
"errors"
]
Expand All @@ -206173,7 +206177,9 @@
"method": "GET",
"route": "/v1/sessions",
"tags": [
"body",
"query",
"auth",
"stateful",
"paginate",
"errors"
Expand All @@ -206183,7 +206189,10 @@
"method": "POST",
"route": "/v1/sessions/{topic}/approve",
"tags": [
"body",
"query",
"params",
"auth",
"stateful",
"errors"
]
Expand All @@ -206193,7 +206202,9 @@
"route": "/v1/sessions/{topic}/request",
"tags": [
"body",
"query",
"params",
"auth",
"stateful",
"errors"
]
Expand All @@ -206202,7 +206213,10 @@
"method": "POST",
"route": "/v1/sessions/{topic}/extend",
"tags": [
"body",
"query",
"params",
"auth",
"stateful",
"errors"
]
Expand All @@ -206211,7 +206225,10 @@
"method": "DELETE",
"route": "/v1/sessions/{topic}",
"tags": [
"body",
"query",
"params",
"auth",
"stateful",
"errors"
]
Expand Down
2 changes: 1 addition & 1 deletion conformance/matrix.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1093,7 +1093,7 @@ adapters:
deviations:
- "Pairing, session approve, and JSON-RPC requests are auto-approved (no wallet device)"
- "personal_sign and eth_sendTransaction return synthetic hashes; nothing is signed"
- "The projectId gate ships but is never wired — every route answers without a credential"
- "Any non-empty projectId passes the gate — no validation against a project registry"
- "Expiry fields are TTL constants, not absolute unix timestamps; extend persists nothing"
- "Unacknowledged sessions serve JSON-RPC requests immediately (no approval gate)"
missing:
Expand Down
3 changes: 3 additions & 0 deletions internal/engine/walletconnect_style_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ func wcPost(t *testing.T, base, path string, bodyObj map[string]any) (string, in
t.Fatal(err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer wc-engine-test-project")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
Expand All @@ -288,6 +289,7 @@ func wcPost(t *testing.T, base, path string, bodyObj map[string]any) (string, in
func wcGet(t *testing.T, base, path string) (string, int) {
t.Helper()
req, err := http.NewRequest("GET", base+path, nil)
req.Header.Set("Authorization", "Bearer wc-engine-test-project")
if err != nil {
t.Fatal(err)
}
Expand All @@ -303,6 +305,7 @@ func wcGet(t *testing.T, base, path string) (string, int) {
func wcDelete(t *testing.T, base, path string) (string, int) {
t.Helper()
req, err := http.NewRequest("DELETE", base+path, nil)
req.Header.Set("Authorization", "Bearer wc-engine-test-project")
if err != nil {
t.Fatal(err)
}
Expand Down
Loading