Skip to content

Commit 90ef7d3

Browse files
committed
Harden Studio Next wallet and API routing
1 parent eb9d17f commit 90ef7d3

10 files changed

Lines changed: 100 additions & 103 deletions

‎backend/service_api.py‎

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,12 @@
4444
"state_basis",
4545
)
4646

47-
_VERCEL_ROUTE_CAPTURE_KEY = "1"
47+
_VERCEL_ROUTE_CAPTURE_KEYS = frozenset(
48+
(
49+
"1",
50+
"path",
51+
)
52+
)
4853

4954

5055
class SnapshotStore(
@@ -87,11 +92,12 @@ def _invalid_query_response():
8792
def _public_query_items(
8893
request: Request,
8994
) -> list[tuple[str, str]]:
90-
# Vercel's regex route contributes numbered capture-group metadata to the
91-
# function query string. Capture key "1" is transport metadata, never a
92-
# COMMIT public input. Remove every instance before enforcing the exact
93-
# semantic query allowlist below. Unknown non-reserved keys and duplicate
94-
# semantic keys still fail closed in _has_exact_query_shape.
95+
# Vercel contributes the rewrite wildcard as query metadata to the
96+
# function. The legacy regex route used key "1"; the current named
97+
# rewrite uses key "path". Neither is a COMMIT public input. Remove every
98+
# instance before enforcing the exact semantic query allowlist below.
99+
# Unknown non-reserved keys and duplicate semantic keys still fail closed
100+
# in _has_exact_query_shape.
95101
return [
96102
(
97103
key,
@@ -101,7 +107,7 @@ def _public_query_items(
101107
request.query_params.multi_items()
102108
)
103109
if key
104-
!= _VERCEL_ROUTE_CAPTURE_KEY
110+
not in _VERCEL_ROUTE_CAPTURE_KEYS
105111
]
106112

107113

‎components/application/application-shell.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import {
4040
} from "@/components/application/claim-mission-flow";
4141
import {
4242
preflightCommitWallet,
43-
STUDIO_DEV_CHAIN_ID,
43+
STUDIO_NEXT_CHAIN_ID,
4444
type ConnectedCommitWallet,
4545
type WalletPreflight,
4646
} from "@/lib/genlayer-browser";
@@ -255,7 +255,7 @@ export function ApplicationShell() {
255255
</p>
256256
<div className="commit-app-anchor">
257257
<span>
258-
Chain {STUDIO_DEV_CHAIN_ID}
258+
Chain {STUDIO_NEXT_CHAIN_ID}
259259
</span>
260260
<span>
261261
{shorten(

‎docs/ARCHITECTURE.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,10 @@ claiming on behalf of a beneficiary through the public claim method.
8484

8585
The `/verify` backend reads transaction truth through supported hosted-Studio
8686
transaction/status surfaces and keeps finality distinct from execution success.
87-
Vercel rewrite metadata is filtered only when it exactly matches the internal
88-
route capture expected for the request.
87+
The Vercel rewrite passes its current named wildcard as `path` (legacy routes
88+
used `1`). The service removes only those documented transport keys before
89+
enforcing the exact public query allowlist; unknown non-reserved and duplicate
90+
semantic keys still fail closed.
8991

9092
## Candidate source identities
9193

‎eslint.config.mjs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const eslintConfig = defineConfig([
77
...nextTypeScript,
88
globalIgnores([
99
".next/**",
10+
".vercel/**",
1011
".venv/**",
1112
"out/**",
1213
"build/**",

‎lib/commit-wallet-connection.ts‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {
22
createClient,
33
} from "genlayer-js";
44
import {
5-
STUDIO_DEV_CHAIN_HEX,
5+
STUDIO_NEXT_CHAIN_HEX,
66
STUDIO_NETWORK_LABEL,
77
STUDIO_NEXT_CHAIN,
88
STUDIO_NEXT_RPC_URL,
@@ -192,7 +192,7 @@ function assertWalletAddress(
192192
}
193193
}
194194

195-
export async function ensureStudioDevWalletNetwork(
195+
export async function ensureStudioNextWalletNetwork(
196196
provider: BrowserProvider,
197197
): Promise<void> {
198198
const current = await provider.request({
@@ -202,7 +202,7 @@ export async function ensureStudioDevWalletNetwork(
202202
if (
203203
typeof current === "string"
204204
&& current.toLowerCase()
205-
=== STUDIO_DEV_CHAIN_HEX
205+
=== STUDIO_NEXT_CHAIN_HEX
206206
) {
207207
return;
208208
}
@@ -213,7 +213,7 @@ export async function ensureStudioDevWalletNetwork(
213213
params: [
214214
{
215215
chainId:
216-
STUDIO_DEV_CHAIN_HEX,
216+
STUDIO_NEXT_CHAIN_HEX,
217217
},
218218
],
219219
});
@@ -227,7 +227,7 @@ export async function ensureStudioDevWalletNetwork(
227227
params: [
228228
{
229229
chainId:
230-
STUDIO_DEV_CHAIN_HEX,
230+
STUDIO_NEXT_CHAIN_HEX,
231231
chainName:
232232
STUDIO_NETWORK_LABEL,
233233
rpcUrls: [
@@ -245,7 +245,7 @@ export async function ensureStudioDevWalletNetwork(
245245
params: [
246246
{
247247
chainId:
248-
STUDIO_DEV_CHAIN_HEX,
248+
STUDIO_NEXT_CHAIN_HEX,
249249
},
250250
],
251251
});
@@ -259,7 +259,7 @@ export async function ensureStudioDevWalletNetwork(
259259
if (
260260
typeof selected !== "string"
261261
|| selected.toLowerCase()
262-
!== STUDIO_DEV_CHAIN_HEX
262+
!== STUDIO_NEXT_CHAIN_HEX
263263
) {
264264
throw new Error(
265265
`MetaMask did not switch to ${STUDIO_NETWORK_LABEL} (61997).`,
@@ -343,7 +343,7 @@ export async function connectCommitWallet(): Promise<ConnectedCommitWallet> {
343343
address,
344344
);
345345

346-
await ensureStudioDevWalletNetwork(
346+
await ensureStudioNextWalletNetwork(
347347
provider,
348348
);
349349

‎lib/genlayer-browser.ts‎

Lines changed: 5 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import {
1717
export const COMMIT_POLICY_DIGEST =
1818
"983307fac383ac4a92be6c0c361ea8f3c9d9efa20ad5e6e8bc8dee932f2a6103";
1919

20-
export const STUDIO_DEV_CHAIN_ID = 61997;
21-
export const STUDIO_DEV_CHAIN_HEX = "0xf22d";
20+
export const STUDIO_NEXT_CHAIN_ID = 61997;
21+
export const STUDIO_NEXT_CHAIN_HEX = "0xf22d";
2222
export const STUDIO_NEXT_RPC_URL = "https://studio-next.genlayer.com/api";
2323
export const STUDIO_NETWORK_LABEL = "GenLayer Studio Next";
2424
export const GEN_DECIMALS = 18;
@@ -239,26 +239,6 @@ function assertAddress(
239239
}
240240
}
241241

242-
function browserProvider(): BrowserProvider {
243-
if (typeof window === "undefined") {
244-
throw new Error("Wallet connection is available only in the browser.");
245-
}
246-
247-
const candidate = (
248-
window as typeof window & {
249-
ethereum?: BrowserProvider;
250-
}
251-
).ethereum;
252-
253-
if (candidate === undefined) {
254-
throw new Error(
255-
"No MetaMask-compatible browser wallet was detected. Install or enable a wallet that supports the GenLayer Snap.",
256-
);
257-
}
258-
259-
return candidate;
260-
}
261-
262242
function receiptText(
263243
value: unknown,
264244
key: string,
@@ -417,58 +397,6 @@ export function validateMissionDraft(
417397
return errors;
418398
}
419399

420-
export async function connectCommitWallet(): Promise<ConnectedCommitWallet> {
421-
const provider = browserProvider();
422-
423-
const accounts = await provider.request({
424-
method: "eth_requestAccounts",
425-
});
426-
427-
if (
428-
!Array.isArray(accounts)
429-
|| typeof accounts[0] !== "string"
430-
) {
431-
throw new Error(
432-
"The wallet did not return an account.",
433-
);
434-
}
435-
436-
const address = accounts[0];
437-
assertAddress(
438-
address,
439-
"Wallet account",
440-
);
441-
442-
const client = createClient({
443-
chain: STUDIO_NEXT_CHAIN,
444-
account: address,
445-
provider: provider as never,
446-
});
447-
448-
await client.connect(
449-
"studioDevnet",
450-
);
451-
452-
const chainId = await provider.request({
453-
method: "eth_chainId",
454-
});
455-
456-
if (
457-
typeof chainId !== "string"
458-
|| chainId.toLowerCase() !== STUDIO_DEV_CHAIN_HEX
459-
) {
460-
throw new Error(
461-
`Wallet must be connected to ${STUDIO_NETWORK_LABEL} (${STUDIO_DEV_CHAIN_ID}).`,
462-
);
463-
}
464-
465-
return {
466-
address,
467-
provider,
468-
client,
469-
};
470-
}
471-
472400
function valueRecord(
473401
value: unknown,
474402
label: string,
@@ -591,10 +519,10 @@ export async function preflightCommitWallet(
591519

592520
if (
593521
typeof chainId !== "string"
594-
|| chainId.toLowerCase() !== STUDIO_DEV_CHAIN_HEX
522+
|| chainId.toLowerCase() !== STUDIO_NEXT_CHAIN_HEX
595523
) {
596524
throw new Error(
597-
`Wallet is not on ${STUDIO_NETWORK_LABEL} (${STUDIO_DEV_CHAIN_ID}).`,
525+
`Wallet is not on ${STUDIO_NETWORK_LABEL} (${STUDIO_NEXT_CHAIN_ID}).`,
598526
);
599527
}
600528

@@ -640,7 +568,7 @@ export async function preflightCommitWallet(
640568

641569
return {
642570
account: wallet.address,
643-
chainId: STUDIO_DEV_CHAIN_ID,
571+
chainId: STUDIO_NEXT_CHAIN_ID,
644572
balanceWei,
645573
balanceGen: formatGenAmount(
646574
balanceWei,

‎tests/frontend/commit-wallet-connection.test.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
it,
55
} from "vitest";
66
import {
7-
ensureStudioDevWalletNetwork,
7+
ensureStudioNextWalletNetwork,
88
tryEnableOptionalGenLayerSnap,
99
walletConnectionErrorMessage,
1010
} from "@/lib/commit-wallet-connection";
@@ -39,7 +39,7 @@ describe(
3939
},
4040
};
4141

42-
await ensureStudioDevWalletNetwork(
42+
await ensureStudioNextWalletNetwork(
4343
provider,
4444
);
4545

@@ -91,7 +91,7 @@ describe(
9191
},
9292
};
9393

94-
await ensureStudioDevWalletNetwork(
94+
await ensureStudioNextWalletNetwork(
9595
provider,
9696
);
9797

@@ -153,7 +153,7 @@ describe(
153153
},
154154
};
155155

156-
await ensureStudioDevWalletNetwork(
156+
await ensureStudioNextWalletNetwork(
157157
provider,
158158
);
159159

‎tests/frontend/genlayer-browser.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
formatGenAmount,
99
parseGenAmount,
1010
preflightCommitWallet,
11-
STUDIO_DEV_CHAIN_ID,
11+
STUDIO_NEXT_CHAIN_ID,
1212
validateFundingEligibility,
1313
validateEvidenceAttestationEligibility,
1414
validateEvidenceRegistrationEligibility,
@@ -26,7 +26,7 @@ import {
2626

2727
describe("COMMIT GenLayer browser integration", () => {
2828
it("binds the deployed policy and Studio Next chain", () => {
29-
expect(STUDIO_DEV_CHAIN_ID).toBe(61997);
29+
expect(STUDIO_NEXT_CHAIN_ID).toBe(61997);
3030
expect(COMMIT_POLICY_DIGEST).toBe(
3131
"983307fac383ac4a92be6c0c361ea8f3c9d9efa20ad5e6e8bc8dee932f2a6103",
3232
);

‎tests/test_backend_service_api_integrity.py‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,63 @@ def test_matching_vercel_transaction_capture_is_not_public_query_input(
219219
1,
220220
)
221221

222+
def test_matching_vercel_named_rewrite_capture_is_not_public_query_input(
223+
self,
224+
):
225+
api = _api()
226+
227+
store = FIXTURE.FakeStore(
228+
FIXTURE._index_snapshot()
229+
)
230+
231+
app = api.create_app(
232+
store=store
233+
)
234+
235+
query = urlencode(
236+
[
237+
(
238+
"chain_id",
239+
str(
240+
FIXTURE.CHAIN_ID
241+
),
242+
),
243+
(
244+
"contract_address",
245+
FIXTURE.CONTRACT_ADDRESS,
246+
),
247+
(
248+
"state_basis",
249+
"FINALIZED",
250+
),
251+
(
252+
"path",
253+
"index",
254+
),
255+
]
256+
)
257+
258+
status, body = FIXTURE._request(
259+
app,
260+
path="/api/v1/index",
261+
query=query,
262+
)
263+
264+
self.assertEqual(
265+
status,
266+
200,
267+
)
268+
self.assertIs(
269+
body[
270+
"found"
271+
],
272+
True,
273+
)
274+
self.assertEqual(
275+
store.loads,
276+
1,
277+
)
278+
222279
def test_reserved_vercel_capture_value_is_ignored_before_public_shape_validation(
223280
self,
224281
):

0 commit comments

Comments
 (0)