remove unnecessary file and logics

Kim committed Apr 2, 2026 at 14:14 UTC 6f7cde7a5d784fcddb0d8afbd0bf0a05623f722c
13 files changed +73 -823
ROADMAP.md deleted
-452
@@ -1,452 +0,0 @@
1 -# Portal Ethereum Roadmap
2 -
3 -## Goal
4 -
5 -Turn Portal from a self-hosted relay product that already uses Ethereum identity internally into an Ethereum-native service publishing layer:
6 -
7 -- `wallet` owns the app or relay
8 -- `ENS` and `DNSSEC` verify which `address` currently controls a relay or app endpoint
9 -- `Portal` continues to provide transport, routing, and end-to-end TLS
10 -- `L2` handles rewards, payouts, and paid access
11 -
12 -This roadmap is intentionally incremental. It preserves the current transport model and avoids pushing dynamic lease state fully onchain.
13 -
14 -## Current Position
15 -
16 -Portal already has strong Ethereum alignment:
17 -
18 -- Lease registration is authenticated with SIWE.
19 -- Each app or relay identity already carries an `address` owner field.
20 -- Relay and tunnel transport are already separated from identity.
21 -- DNS + ACME + wildcard TLS already solve public HTTPS delivery.
22 -
23 -What is still missing:
24 -
25 -- user-facing ENS identity and discovery
26 -- a single public identity model across UI, registry, and rewards
27 -- an Ethereum-native rewards and payments layer
28 -- a clear split between stable relay identity and temporary app lease endpoints
29 -
30 -## Core Decisions
31 -
32 -### Keep these as-is
33 -
34 -- Keep the current raw TCP reverse transport and SNI routing model.
35 -- Keep current tenant TLS passthrough and keyless signing.
36 -- Keep DNS + ACME for browser-facing HTTPS.
37 -- Keep relay-hosted wildcard domains for actual service delivery.
38 -
39 -### Add these on top
40 -
41 -- Use `Identity` as the single product identity contract.
42 -- Use `address` as the canonical owner field inside that identity.
43 -- Use ENS first for verifiable relay identity and address binding, then later for richer profiles and discovery where it adds real value.
44 -- Use `L2 mainnet` as the default L2 for rewards and payments.
45 -- Use offchain accounting with onchain settlement, not onchain event-by-event metering.
46 -- Use x402 only where Portal can actually see HTTP, not across the raw passthrough data plane.
47 -- Keep app subdomains as temporary address-authenticated leases unless persistent name ownership becomes a real product requirement.
48 -
49 -## Product Positioning
50 -
51 -Portal should be positioned as:
52 -
53 -- wallet-owned infrastructure
54 -- ENS-addressable services
55 -- permissionless app publishing for Ethereum-native services
56 -- a bridge between offchain apps and Ethereum identity, naming, and economics
57 -
58 -This is not "ngrok but web3".
59 -It is closer to "Ethereum-native app publishing and service identity".
60 -
61 -## Architecture End State
62 -
63 -### Transport
64 -
65 -- Portal remains the transport and ingress layer.
66 -- Relay routing, reverse sessions, UDP relay, and E2EE remain unchanged.
67 -
68 -### HTTPS
69 -
70 -- Public app access continues to use DNS hostnames such as `app.portal.example.com`.
71 -- ACME continues to provision and renew relay root and wildcard certificates.
72 -- ENS does not replace ACME.
73 -
74 -### Identity
75 -
76 -- `Identity` becomes the only stable product identity contract.
77 -- `Identity.Name` is the service or relay identity label used by Portal.
78 -- `Identity.Address` is the canonical owner wallet for that identity.
79 -- UI, registry, rewards, and relay operator state all key off the same `Identity`, with `address` anchoring ownership.
80 -- Human-friendly owner names come from ENS primary names and ENS profile data.
81 -
82 -### Naming
83 -
84 -- `ENSIP-17` is used for stable relay DNS names, such as `portal.example.com`.
85 -- Canonical `.eth` relay names such as `relay.eth` are optional follow-on identity surfaces, not a requirement for initial ENS support.
86 -- Temporary app subdomains such as `app.portal.example.com` remain Portal leases by default; when published with ENSIP-17 they prove current controlling `address`, not permanent name ownership.
87 -- ENS-native app names such as `app.relay.eth` are deferred until Portal has a concrete need for persistent app identity beyond temporary lease hostnames.
88 -
89 -### Economics
90 -
91 -- Reward calculation stays offchain.
92 -- Reward claiming and paid access settle onchain on L2 mainnet.
93 -- Paid HTTP access can use x402.
94 -
95 -## Roadmap
96 -
97 -### Phase 0: Identity Cleanup
98 -
99 -Objective:
100 -Make `Identity` the single real identity contract across the product.
101 -
102 -Scope:
103 -
104 -- Keep `Identity` as the canonical contract for auth, registry, policy, rewards, claims, and grouping.
105 -- Keep `Identity.Address` as the canonical owner field for wallet ownership and future ENS resolution.
106 -- Keep `metadata.owner` as the user-facing owner label in frontend pages.
107 -- Do not expose `Address` directly in frontend page UI until ENS/address display policy is finalized.
108 -- Define a single normalization policy for `Identity.Name` and EVM `Address`.
109 -
110 -Success criteria:
111 -
112 -- Every public lease, relay operator, and future reward recipient can be mapped to one `Identity`.
113 -- Every `Identity` can be mapped to one canonical owner `address`.
114 -- No duplicate identity contracts remain in the main user path.
115 -
116 -### Phase 1: ENS Profile UX
117 -
118 -Objective:
119 -Make Portal identities legible inside the product.
120 -
121 -Scope:
122 -
123 -- Resolve primary ENS names for `Identity.Address`.
124 -- Resolve ENS avatars and basic profile records where available.
125 -- Display ENS name first, checksum address second, raw metadata fallback last.
126 -- Show relay operator identity and app identity using the same rendering rules.
127 -
128 -Success criteria:
129 -
130 -- Public app cards and relay/operator views show wallet-native identities instead of ad hoc labels.
131 -- Address ownership is still verifiable even when ENS data is absent.
132 -
133 -### Phase 2: DNS-Backed Relay ENS Presence
134 -
135 -Objective:
136 -Make Portal relays verifiably address-bound in the ENS ecosystem without requiring onchain ENS registration.
137 -
138 -Scope:
139 -
140 -- Enable DNSSEC on the relay root domain.
141 -- Add `ENSIP-17` TXT records for the stable relay DNS name.
142 -- Bind the relay root domain to the relay operator `address`.
143 -- Keep relay discovery and transport state separate from relay identity: ENS proves `who`, Portal discovery continues to publish live topology.
144 -- Do not require a canonical `.eth` name or resolver-managed ENS text records in this phase.
145 -
146 -Important:
147 -
148 -- ENSIP-17 is complementary to ACME, not a replacement.
149 -- Use ENSIP-17 first for stable relay names, not short-lived lease hostnames.
150 -- The primary value of ENS in this phase is independent verification of `domain -> address`, not browse/search UX by itself.
151 -- Dynamic app subdomains may publish the currently controlling `address`, but they remain temporary leases and are cleaned up when the lease ends.
152 -
153 -Success criteria:
154 -
155 -- A relay root domain can be independently verified as currently controlled by one wallet `address`.
156 -- ENS-aware clients can resolve `relay domain -> address` without trusting relay-local UI or API claims.
157 -- Relay identity stays stable even if live ingress or overlay endpoints later change.
158 -
159 -### Phase 3: Reward Engine v1
160 -
161 -Objective:
162 -Launch rewards without token and contract complexity.
163 -
164 -Scope:
165 -
166 -- Build an offchain reward index keyed by `Identity`, with rollups by owner `address`.
167 -- Score relay operators and app publishers by selected signals:
168 - - uptime
169 - - successful traffic handling
170 - - app activity
171 - - referrals
172 - - public relay participation
173 -- Add abuse controls:
174 - - self-traffic filtering
175 - - repeated low-value traffic suppression
176 - - operator/app collusion heuristics
177 - - minimum quality thresholds
178 -
179 -Output:
180 -
181 -- point balances
182 -- epochs
183 -- auditable reward reports
184 -
185 -Success criteria:
186 -
187 -- Rewards can run for multiple epochs without token issuance.
188 -- Abuse patterns are measurable before any onchain commitment.
189 -
190 -### Phase 4: Onchain Reward Claims on L2 Mainnet
191 -
192 -Objective:
193 -Turn rewards into Ethereum-native assets without moving dynamic scoring onchain.
194 -
195 -Scope:
196 -
197 -- Use `L2 mainnet` as the default settlement chain.
198 -- Publish epoch results as Merkle roots or signed claim vouchers.
199 -- Let users claim to their wallet address.
200 -- Start with simple fungible rewards, then expand to reputation assets.
201 -
202 -Recommended asset types:
203 -
204 -- `ERC-20` for claimable rewards
205 -- `ERC-1155` for seasonal badges or tiers
206 -- non-transferable reputation badges later if needed
207 -
208 -Success criteria:
209 -
210 -- Users can claim rewards directly with the same address that owns their Portal identity.
211 -- Reward distribution cost stays low enough for recurring epochs.
212 -
213 -### Phase 5: x402 Paid Access
214 -
215 -Objective:
216 -Add payment-native access for HTTP workloads.
217 -
218 -Scope:
219 -
220 -- Support x402 where Portal has an HTTP handler boundary:
221 - - app-side x402 integration
222 - - tunnel-side `RunHTTP` / `--http-route` paywall middleware
223 - - paid relay-side HTTP services
224 -- Use L2 mainnet and supported stable assets for default payment flows.
225 -- Add facilitator-backed verification and settlement rather than requiring every seller to run chain infrastructure.
226 -
227 -Non-goal:
228 -
229 -- Do not attempt to force x402 across the full raw TLS passthrough path at relay edge.
230 -- Raw passthrough traffic is not a global HTTP middleware boundary.
231 -
232 -Success criteria:
233 -
234 -- A Portal-published HTTP API can require payment without changing the relay transport model.
235 -- Repeated access can later compose with wallet/session identity.
236 -
237 -### Phase 6: ENS-Native App Naming
238 -
239 -Objective:
240 -Add ENS-native app identity only if temporary lease hostnames become insufficient.
241 -
242 -Scope:
243 -
244 -- Introduce `CCIP-Read`-backed resolution for dynamic names such as `app.relay.eth`.
245 -- Keep actual service delivery on DNS + ACME hostnames.
246 -- Use ENS names as an optional identity surface, with Portal providing the current target state.
247 -- Map ENS app names to current Portal lease state and metadata only when persistent app identity is worth the added complexity.
248 -
249 -Why later:
250 -
251 -- Lease state is dynamic and short-lived.
252 -- Today Portal app subdomains are intentionally temporary leases, not address-owned permanent namespaces.
253 -- Dynamic app naming is a poor fit for direct onchain storage and ownership updates unless the product deliberately moves toward persistent app identities.
254 -
255 -Success criteria:
256 -
257 -- ENS names can represent live Portal apps without rewriting the transport plane.
258 -- Dynamic service identity is possible without losing current HTTPS behavior.
259 -
260 -### Phase 7: Relay Staking and Reputation
261 -
262 -Objective:
263 -Align public relay participation with durable Ethereum incentives.
264 -
265 -Scope:
266 -
267 -- Add optional staking for public relay operators.
268 -- Add slashable rules for clear policy violations or persistent low-quality operation.
269 -- Tie reward multipliers to quality and stake, not only raw traffic.
270 -- Make operator reputation portable and wallet-bound.
271 -
272 -Success criteria:
273 -
274 -- Public relay operation has credible skin in the game.
275 -- Rewards and visibility can favor reliable operators without central review of every action.
276 -
277 -### Phase 8: Governance and Network Coordination
278 -
279 -Objective:
280 -Move from a product with crypto features to a real Ethereum-aligned network.
281 -
282 -Scope:
283 -
284 -- Formalize registry policy for public relays.
285 -- Define reward allocation policy and eligibility rules.
286 -- Introduce limited governance over network-level parameters only after incentives stabilize.
287 -
288 -Non-goal:
289 -
290 -- Do not put fast-moving runtime controls onchain too early.
291 -- Governance should come after identity, discovery, and economics are already working.
292 -
293 -Success criteria:
294 -
295 -- Portal can coordinate a public relay ecosystem without abandoning product quality or operational simplicity.
296 -
297 -## Recommended Execution Order
298 -
299 -Near-term:
300 -
301 -1. Phase 0
302 -2. Phase 1
303 -3. Phase 2
304 -
305 -Mid-term:
306 -
307 -4. Phase 3
308 -5. Phase 4
309 -6. Phase 5
310 -
311 -Long-term:
312 -
313 -7. Phase 6
314 -8. Phase 7
315 -9. Phase 8
316 -
317 -## Preferred Chain Strategy
318 -
319 -Default recommendation:
320 -
321 -- `L2 mainnet` for rewards, x402 payments, and user-facing economic activity
322 -- ENS as a hybrid model:
323 - - L1 anchor for resolution flow
324 - - L2-aware primary names for user identity
325 - - `CCIP-Read` for dynamic app naming
326 -
327 -Reasoning:
328 -
329 -- L2 economics fit frequent claims and low-value payments much better than L1.
330 -- ENS identity can still remain canonical while user activity happens on L2.
331 -
332 -## What This Roadmap Does Not Change
333 -
334 -- Portal does not become an onchain transport protocol.
335 -- Lease registration, renewal, and routing do not move to smart contracts.
336 -- ACME is still required for public browser HTTPS.
337 -- App subdomains do not become permanent address-owned namespaces by default.
338 -- Ethereum integration should extend the product, not replace the networking model that already works.
339 -
340 -## Summary
341 -
342 -The intended end state is:
343 -
344 -- Portal transport remains simple, fast, and mostly offchain.
345 -- Portal `Identity` becomes the native product identity contract.
346 -- Ethereum wallets become the canonical owners of those identities.
347 -- ENS first becomes the verification layer for `domain -> address`, then later expands into richer naming and discovery only where it improves the product.
348 -- L2 mainnet becomes the rewards and payments layer.
349 -- x402 enables paid HTTP access where Portal has an HTTP boundary.
350 -
351 -If executed in this order, Portal can become Ethereum-native without sacrificing its current simplicity or transport guarantees.
352 -
353 -그 문구는 현재 코드 설명이라기보다, 가능한 다음 아키텍처를 설명한 말입니다.
354 -지금 Portal에 그대로 적용된 상태는 아닙니다.
355 -
356 -지금 구조를 코드로 보면 relay가 여전히 권한자입니다.
357 -
358 -lease 등록을 relay가 승인하고 portal/api_server.go:534
359 -hostname을 relay가 정하고 portal/api_server.go:542
360 -hostname -> lease 매핑을 relay registry가 들고 있고 portal/lease.go:72
361 -실제 ingress에서 SNI를 보고 어느 lease로 보낼지 relay가 결정합니다 portal/server.go:479
362 -ENS gasless는 지금 여기에 검증 가능한 바깥 증거를 하나 더 붙인 겁니다.
363 -
364 -등록 후 relay가 hostname -> address TXT를 publish함 portal/api_server.go:605
365 -그래서 외부인은 “이 도메인이 현재 어느 address에 묶였는지”를 독립적으로 확인할 수 있음
366 -즉 지금 달라진 것은:
367 -
368 -relay가 말하는 owner를 외부에서 검증 가능
369 -relay root와 app subdomain이 address-bound endpoint가 됨
370 -지금 안 달라진 것은:
371 -
372 -누가 붙는지
373 -실제 어느 세션으로 라우팅할지
374 -승인/차단 정책
375 -이건 여전히 relay가 결정합니다.
376 -그래서 그 문구의 뜻을 정확히 풀면 이겁니다.
377 -
378 -현재 Portal:
379 -relay가 identity + live mapping + routing을 다 가짐
380 -ENS를 더 강하게 쓰는 미래 모델:
381 -identity/ownership은 ENS나 서명된 state가 담당
382 -relay는 live transport만 담당
383 -client는 relay 주장 대신 ENS/서명된 state를 검증
384 -하지만 중요한 보정이 있습니다.
385 -현재 public HTTPS ingress 모델에서는 relay를 완전 dumb pipe로 만들 수는 없습니다.
386 -relay는 여전히:
387 -
388 -active lease가 살아 있는지
389 -ready reverse session이 있는지
390 -어느 연결로 브리지할지
391 -를 알아야 하기 때문입니다.
392 -즉 그 문구를 Portal 현실에 맞게 바꾸면:
393 -
394 -완전 authority 제거는 아님
395 -ownership/identity에 대한 authority를 relay 밖으로 뺄 수 있음
396 -live session routing authority는 아직 relay 안에 남음
397 -한 줄로 줄이면:
398 -지금 ENS가 만든 변화는 “relay를 안 믿고도 owner identity를 검증할 수 있게 된 것”이고, 아직 “relay가 routing 권한을 잃은 것”은 아닙니다.
399 -
400 -원하면 다음 답변에서
401 -
402 -현재 구조
403 -ENS 강화 구조
404 -컨트랙트 source-of-truth 구조
405 -를 3단 비교로 그려드리겠습니다.
406 -
407 -그건 지금 단계에서 붙여도 사용성에 변화가 없어?
408 -
409 -
410 -거의 없습니다. 지금 단계에서 붙여도 내부 설계 의미는 있지만, 사용자 체감 사용성 변화는 작습니다.
411 -
412 -이유는 간단합니다.
413 -지금 사용자 플로우는 여전히:
414 -
415 -relay에 등록
416 -relay가 hostname 할당
417 -relay가 route 유지
418 -사용자는 그 URL로 접속
419 -이 구조라서, ENS가 있어도 접속 방식 자체는 안 바뀝니다.
420 -
421 -지금 바로 체감되는 변화는 이 정도뿐입니다.
422 -
423 -“이 도메인이 현재 어느 address에 묶였는지” 검증 가능
424 -relay/operator/app identity를 address 기준으로 설명 가능
425 -외부에서 더 신뢰성 있게 표시 가능
426 -하지만 대부분의 일반 사용자는:
427 -
428 -ENS-aware client를 쓰지 않으면 못 느낌
429 -접속 URL도 그대로
430 -publish 방식도 그대로
431 -relay 신뢰 모델도 대부분 그대로
432 -즉 지금 붙이면 생기는 건 주로:
433 -
434 -검증성
435 -설명 가능성
436 -미래 확장성
437 -이지,
438 -즉시 UX 개선
439 -은 아닙니다.
440 -사용성 변화가 커지려면 ENS가 아래 중 하나와 연결돼야 합니다.
441 -
442 -UI에서 verified owner/ENS name 표시
443 -catalog/search에서 owner/address 기준 묶기
444 -결제/정산에서 payee identity 표시
445 -relay 선택/평판 시스템과 연결
446 -persistent name claim
447 -그래서 냉정하게 말하면:
448 -
449 -backend에만 붙이면 체감 변화 작음
450 -UI/selection/payment와 연결되면 의미 커짐
451 -한 줄로 줄이면:
452 -지금 단계에서 ENS를 더 붙여도 “맞는 방향”일 수는 있지만, 그 자체만으로 사용성이 크게 달라지진 않습니다.
\ No newline at end of file
docs/architecture.md
+8 -8
@@ -82,13 +82,13 @@ Portal has three distinct network roles:
82 - `POST /sdk/unregister`
83 - `GET /sdk/domain`
84 - **Reverse session connection**
85 - - `GET /sdk/connect?name=...&address=...`
85 + - `GET /sdk/connect`
86 - HTTP/1.1 only
87 - hijacked into a long-lived raw TCP session
88 - starts idle in the per-lease stream ready queue, then becomes the tenant data path when claimed
89 - **Internal datagram tunnel**
90 - QUIC to the relay URL host:port with ALPN `portal-tunnel`
91 - - authenticated by a first-stream control message carrying `identity` + `access_token`
91 + - authenticated by a first-stream control message carrying `access_token`
92 - carries relay-to-SDK/tunnel datagram traffic only
93
94 That distinction matters because `/sdk/connect` stops being ordinary HTTP once hijacked, while the UDP backhaul is a separate internal QUIC carrier.
@@ -164,7 +164,7 @@ That distinction matters because `/sdk/connect` stops being ordinary HTTP once h
164 ### Raw reverse transport (TLS only)
165
166 1. SDK/tunnel registers one lease per relay through `POST /sdk/register/challenge` followed by `POST /sdk/register`.
167 -2. SDK opens one or more reverse sessions per registered lease with `GET /sdk/connect?name=...&address=...`.
167 +2. SDK opens one or more reverse sessions per registered lease with `GET /sdk/connect`.
168 3. Each relay hijacks `/sdk/connect` requests and places the connection in the per-lease stream ready queue.
169 4. While idle, the relay writes `0x00` keepalive markers.
170 5. A stream client connects to the relay SNI listener.
@@ -192,7 +192,7 @@ Result: this is a detect-only signal by default. It raises the cost of adaptive
192 2. Relay validates that the datagram plane is enabled (server has `UDP_PORT_COUNT > 0` and admin has enabled UDP), allocates a UDP port via `PortAllocator`, and creates a `transport.RelayDatagram` for the lease.
193 3. Registration response includes `udp_addr` (public UDP endpoint) and `access_token`. There is no separate `quic_addr`; the SDK dials QUIC to the relay URL host:port.
194 4. SDK `transport.ClientDatagram` opens a QUIC connection with ALPN `portal-tunnel` and QUIC DATAGRAM support enabled.
195 -5. Authentication: SDK sends `{identity, access_token}` JSON on the first QUIC stream; the relay validates that lease access token before calling `RelayDatagram.Register(conn)`.
195 +5. Authentication: SDK sends `{access_token}` JSON on the first QUIC stream; the relay validates that lease access token before calling `RelayDatagram.Register(conn)`.
196 6. External UDP client sends a packet to `udp_addr` -> `RelayDatagram.readLoop` -> `TouchFlow` (assigns flow ID) -> `SendDatagram` -> QUIC DATAGRAM frame.
197 7. SDK-side `datagramSession.receiveLoop` decodes frames -> `Listener.AcceptDatagram()` -> `Exposure.AcceptDatagram()` -> `proxyExposureDatagrams` -> local UDP target.
198 8. Return path: local response -> `Exposure.SendDatagram()` -> `Listener.SendDatagram()` -> `ClientDatagram.Send()` -> QUIC DATAGRAM -> `RelayDatagram.dispatch()` -> `conn.WriteToUDP` to the original client.
@@ -267,7 +267,7 @@ Wire format (`types/transport.go`): `[flowID uvarint][payload bytes]`
267
268 ### 2. Reverse Connect
269
270 -- `GET /sdk/connect?name=...&address=...`
270 +- `GET /sdk/connect`
271 - Requires HTTP/1.1
272 - Requires `X-Portal-Access-Token` header with the lease access token
273 - Relay validates:
@@ -279,13 +279,13 @@ Wire format (`types/transport.go`): `[flowID uvarint][payload bytes]`
279 ### 3. Renew
280
281 - `POST /sdk/renew`
282 -- Requires `identity` + `access_token`
282 +- Requires `access_token`
283 - Extends lease TTL and returns a refreshed `access_token`
284
285 ### 4. Unregister
286
287 - `POST /sdk/unregister`
288 -- Requires `identity` + `access_token`
288 +- Requires `access_token`
289 - Removes the lease, routes, and ready reverse sessions
290
291 ## Routing Behavior
@@ -373,7 +373,7 @@ Relay-local frontend asset filenames stay in `cmd/relay-server`, not `types/`.
373 - Lease-local stream and datagram ownership through per-lease transport runtimes
374 - Optional QUIC/UDP datagram transport coexisting with TCP on the same lease
375 - Per-lease UDP port allocation with sticky name-based reservation
376 -- QUIC tunnel authentication via control stream (`identity` + lease access token)
376 +- QUIC tunnel authentication via control stream (`access_token`)
377
378 ## ADRs
379
frontend/src/hooks/useAdmin.test.ts
+1
@@ -48,6 +48,7 @@ function buildLease(address: string, name: string = "relay-1"): AdminLeaseData {
48 ExpiresAt: "2026-03-03T01:00:00Z",
49 FirstSeenAt: "2026-03-02T00:00:00Z",
50 LastSeenAt: "2026-03-03T00:00:00Z",
51 + identity_key: `${name.toLowerCase()}:${address.toLowerCase()}`,
52 address,
53 name,
54 BPS: 1024,
frontend/src/hooks/useAdmin.ts
+5 -18
@@ -84,19 +84,11 @@ function toAdminErrorMessage(error: unknown, fallback: string): string {
84 return fallback;
85 }
86
87 -function buildIdentityKey(name: string, address: string): string {
88 - return `${name.trim().toLowerCase()}:${address.trim().toLowerCase()}`;
89 -}
90 -
87 function resolveLeaseIdentity(
88 rows: AdminLeaseData[],
89 identityKey: string
90 ): { name: string; address: string } {
95 - const match = rows.find((row) => {
96 - const name = (row.name || "").trim();
97 - const address = row.address.trim();
98 - return buildIdentityKey(name, address) === identityKey;
99 - });
91 + const match = rows.find((row) => row.identity_key.trim() === identityKey);
92 if (!match) {
93 throw new Error("Missing lease identity");
94 }
@@ -128,7 +120,7 @@ function toAdminServer(
120 link: hostname ? `https://${hostname}/` : "",
121 lastUpdated: row.LastSeenAt || undefined,
122 firstSeen: row.FirstSeenAt || undefined,
131 - identityKey: buildIdentityKey(serviceName, address),
123 + identityKey: row.identity_key.trim(),
124 address,
125 isBanned: row.IsBanned,
126 bps: row.BPS,
@@ -298,15 +290,11 @@ export function useAdmin() {
290 const identity = resolveLeaseIdentity(serverData, identityKey);
291 const normalizedBPS = Math.max(0, Math.trunc(bps));
292 const previousBPS =
301 - serverData.find((row) =>
302 - buildIdentityKey((row.name || "").trim(), row.address.trim()) ===
303 - identityKey
304 - )?.BPS ?? 0;
293 + serverData.find((row) => row.identity_key.trim() === identityKey)?.BPS ?? 0;
294
295 setServerData((prev) =>
296 prev.map((row) =>
308 - buildIdentityKey((row.name || "").trim(), row.address.trim()) ===
309 - identityKey
297 + row.identity_key.trim() === identityKey
298 ? { ...row, BPS: normalizedBPS }
299 : row
300 )
@@ -328,8 +316,7 @@ export function useAdmin() {
316 } catch (err) {
317 setServerData((prev) =>
318 prev.map((row) =>
331 - buildIdentityKey((row.name || "").trim(), row.address.trim()) ===
332 - identityKey
319 + row.identity_key.trim() === identityKey
320 ? { ...row, BPS: previousBPS }
321 : row
322 )
frontend/src/hooks/useSSRData.ts
+1
@@ -19,6 +19,7 @@ export interface PublicLeaseData {
19 }
20
21 export interface AdminLeaseData extends PublicLeaseData {
22 + identity_key: string;
23 address: string;
24 BPS: number;
25 ClientIP: string;
portal/api_server.go
+11 -16
@@ -309,7 +309,7 @@ func (s *Server) handleRenew(w http.ResponseWriter, r *http.Request) {
309 return
310 }
311
312 - claims, err := auth.VerifyLeaseAccessToken(req.AccessToken, s.identity.PublicKey, s.cfg.PortalURL, req.Identity, time.Now().UTC())
312 + claims, err := auth.VerifyLeaseAccessToken(req.AccessToken, s.identity.PublicKey, s.cfg.PortalURL, time.Now().UTC())
313 if err != nil {
314 utils.WriteAPIError(w, http.StatusForbidden, types.APIErrorCodeUnauthorized, errUnauthorized.Error())
315 return
@@ -331,7 +331,6 @@ func (s *Server) handleRenew(w http.ResponseWriter, r *http.Request) {
331 }
332
333 utils.WriteAPIData(w, http.StatusOK, types.RenewResponse{
334 - Identity: record.Copy(),
334 ExpiresAt: record.ExpiresAt,
335 AccessToken: nextAccessToken,
336 })
@@ -346,7 +345,7 @@ func (s *Server) handleUnregister(w http.ResponseWriter, r *http.Request) {
345 if !ok {
346 return
347 }
349 - claims, err := auth.VerifyLeaseAccessToken(req.AccessToken, s.identity.PublicKey, s.cfg.PortalURL, req.Identity, time.Now().UTC())
348 + claims, err := auth.VerifyLeaseAccessToken(req.AccessToken, s.identity.PublicKey, s.cfg.PortalURL, time.Now().UTC())
349 if err != nil {
350 utils.WriteAPIError(w, http.StatusForbidden, types.APIErrorCodeUnauthorized, errUnauthorized.Error())
351 return
@@ -382,17 +381,13 @@ func (s *Server) handleConnect(w http.ResponseWriter, r *http.Request) {
381 return
382 }
383
385 - identity := types.Identity{
386 - Name: r.URL.Query().Get("name"),
387 - Address: r.URL.Query().Get("address"),
388 - }
384 token := strings.TrimSpace(r.Header.Get(types.HeaderAccessToken))
385 clientIP, ok := s.extractAllowedClientIP(w, r)
386 if !ok {
387 return
388 }
389
395 - lease, err := s.admitLeaseByIdentity(identity, token, false)
390 + lease, err := s.admitLeaseByToken(token, false)
391 if err != nil {
392 switch {
393 case errors.Is(err, errLeaseNotFound):
@@ -467,13 +462,13 @@ func (s *Server) handleQUICTunnelConn(conn *quic.Conn) {
462 return
463 }
464 _ = stream.SetReadDeadline(time.Time{})
470 - if msg.Identity.Key() == "" || msg.AccessToken == "" {
465 + if strings.TrimSpace(msg.AccessToken) == "" {
466 _ = json.NewEncoder(stream).Encode(types.QUICControlResponse{OK: false, Error: "invalid_control_message"})
467 _ = conn.CloseWithError(1, "invalid control message")
468 return
469 }
470
476 - lease, err := s.admitLeaseByIdentity(msg.Identity, msg.AccessToken, true)
471 + lease, err := s.admitLeaseByToken(msg.AccessToken, true)
472 switch {
473 case err == nil:
474 case errors.Is(err, errLeaseNotFound):
@@ -514,17 +509,18 @@ func (s *Server) handleQUICTunnelConn(conn *quic.Conn) {
509 Msg("quic tunnel connected")
510 }
511
517 -func (s *Server) admitLeaseByIdentity(identity types.Identity, token string, requireDatagram bool) (*leaseRecord, error) {
518 - lease, err := s.registry.Find(identity)
512 +func (s *Server) admitLeaseByToken(token string, requireDatagram bool) (*leaseRecord, error) {
513 + claims, err := auth.VerifyLeaseAccessToken(token, s.identity.PublicKey, s.cfg.PortalURL, time.Now().UTC())
514 + if err != nil {
515 + return nil, errUnauthorized
516 + }
517 + lease, err := s.registry.Find(claims.Identity)
518 if err != nil {
519 return nil, err
520 }
521 if !s.registry.policy.IsIdentityRoutable(lease.Key()) {
522 return nil, errLeaseRejected
523 }
525 - if _, err := auth.VerifyLeaseAccessToken(token, s.identity.PublicKey, s.cfg.PortalURL, lease.Copy(), time.Now().UTC()); err != nil {
526 - return nil, errUnauthorized
527 - }
524 if lease.stream == nil || (requireDatagram && lease.datagram == nil) {
525 return nil, errTransportMismatch
526 }
@@ -611,7 +607,6 @@ func (s *Server) registerLease(req types.RegisterChallengeRequest, clientIP, rep
607 resp := types.RegisterResponse{
608 Identity: record.Copy(),
609 Hostname: hostname,
614 - Metadata: record.Metadata.Copy(),
610 ExpiresAt: expiresAt,
611 AccessToken: accessToken,
612 UDPEnabled: record.UDPEnabled,
portal/auth/auth.go
+1 -10
@@ -234,7 +234,7 @@ func IssueLeaseAccessToken(privateKeyHex, keyID, issuer string, identity types.I
234 return token, claims, nil
235 }
236
237 -func VerifyLeaseAccessToken(token, publicKeyHex, issuer string, identity types.Identity, now time.Time) (LeaseAccessTokenClaims, error) {
237 +func VerifyLeaseAccessToken(token, publicKeyHex, issuer string, now time.Time) (LeaseAccessTokenClaims, error) {
238 publicKey, err := utils.ParseSecp256k1PublicKeyHex(publicKeyHex)
239 if err != nil {
240 return LeaseAccessTokenClaims{}, err
@@ -257,15 +257,6 @@ func VerifyLeaseAccessToken(token, publicKeyHex, issuer string, identity types.I
257 return LeaseAccessTokenClaims{}, errors.New("lease access token identity does not match subject")
258 }
259 claims.Identity = normalizedClaimsIdentity
260 - if identity.Key() != "" {
261 - normalizedIdentity, err := utils.NormalizeIdentity(identity)
262 - if err != nil {
263 - return LeaseAccessTokenClaims{}, err
264 - }
265 - if claims.Subject != normalizedIdentity.Key() {
266 - return LeaseAccessTokenClaims{}, errors.New("lease access token identity does not match request")
267 - }
268 - }
260 if err := claims.ValidateWithLeeway(jwt.Expected{
261 Issuer: strings.TrimSpace(issuer),
262 AnyAudience: jwt.Audience{leaseAccessTokenAudience},
portal/lease.go
+10 -9
@@ -307,15 +307,16 @@ func (r *leaseRegistry) AdminSnapshot(record *leaseRecord) types.AdminLease {
307 clientIP := record.ClientIP
308 identityKey := record.Key()
309 return types.AdminLease{
310 - Lease: r.Snapshot(record),
311 - Address: record.Address,
312 - BPS: r.policy.BPSManager().IdentityBPS(identityKey),
313 - ClientIP: clientIP,
314 - ReportedIP: record.ReportedIP,
315 - IsApproved: r.policy.EffectiveApproval(identityKey),
316 - IsBanned: r.policy.IsIdentityBanned(identityKey),
317 - IsDenied: r.policy.IsIdentityDenied(identityKey),
318 - IsIPBanned: r.policy.IPFilter().IsIPBanned(clientIP),
310 + Lease: r.Snapshot(record),
311 + IdentityKey: identityKey,
312 + Address: record.Address,
313 + BPS: r.policy.BPSManager().IdentityBPS(identityKey),
314 + ClientIP: clientIP,
315 + ReportedIP: record.ReportedIP,
316 + IsApproved: r.policy.EffectiveApproval(identityKey),
317 + IsBanned: r.policy.IsIdentityBanned(identityKey),
318 + IsDenied: r.policy.IsIdentityDenied(identityKey),
319 + IsIPBanned: r.policy.IPFilter().IsIPBanned(clientIP),
320 }
321 }
322
sdk/api_client.go
+5 -21
@@ -186,7 +186,7 @@ func (a *apiClient) ensureCompatible(ctx context.Context, httpClient *http.Clien
186 return nil
187 }
188
189 -func (a *apiClient) renewLease(ctx context.Context, identity types.Identity, ttl time.Duration) error {
189 +func (a *apiClient) renewLease(ctx context.Context, ttl time.Duration) error {
190 if err := a.ensureHTTPClient(ctx); err != nil {
191 return err
192 }
@@ -200,7 +200,6 @@ func (a *apiClient) renewLease(ctx context.Context, identity types.Identity, ttl
200
201 var resp types.RenewResponse
202 if err := utils.HTTPDoAPIPath(ctx, a.httpClient, a.baseURL, http.MethodPost, types.PathSDKRenew, types.RenewRequest{
203 - Identity: identity.Copy(),
203 AccessToken: accessToken,
204 TTL: int(ttl / time.Second),
205 ReportedIP: a.reportedIP(ctx),
@@ -211,13 +210,6 @@ func (a *apiClient) renewLease(ctx context.Context, identity types.Identity, ttl
210 if resp.AccessToken == "" {
211 return errors.New("relay did not return renewed access token")
212 }
214 - renewedIdentity, err := utils.NormalizeIdentity(resp.Identity)
215 - if err != nil {
216 - return err
217 - }
218 - if renewedIdentity.Key() != a.identity.Key() {
219 - return errors.New("relay returned mismatched renewed lease identity")
220 - }
213
214 a.mu.Lock()
215 if a.accessToken == accessToken {
@@ -227,17 +219,16 @@ func (a *apiClient) renewLease(ctx context.Context, identity types.Identity, ttl
219 return nil
220 }
221
230 -func (a *apiClient) unregisterLease(ctx context.Context, identity types.Identity) error {
222 +func (a *apiClient) unregisterLease(ctx context.Context) error {
223 a.mu.RLock()
224 accessToken := a.accessToken
225 a.mu.RUnlock()
226 return utils.HTTPDoAPIPath(ctx, a.httpClient, a.baseURL, http.MethodPost, types.PathSDKUnregister, types.UnregisterRequest{
235 - Identity: identity.Copy(),
227 AccessToken: accessToken,
228 }, nil, nil)
229 }
230
240 -func (a *apiClient) openReverseSession(ctx context.Context, identity types.Identity) (net.Conn, error) {
231 +func (a *apiClient) openReverseSession(ctx context.Context) (net.Conn, error) {
232 if err := a.ensureHTTPClient(ctx); err != nil {
233 return nil, err
234 }
@@ -252,15 +243,9 @@ func (a *apiClient) openReverseSession(ctx context.Context, identity types.Ident
243 return nil, err
244 }
245
255 - connectURL := utils.ResolveAPIURL(a.baseURL, types.PathSDKConnect)
256 - query := connectURL.Query()
257 - query.Set("name", identity.Name)
258 - query.Set("address", identity.Address)
259 - connectURL.RawQuery = query.Encode()
260 -
246 req := &http.Request{
247 Method: http.MethodGet,
263 - URL: connectURL,
248 + URL: utils.ResolveAPIURL(a.baseURL, types.PathSDKConnect),
249 Host: a.baseURL.Host,
250 Header: make(http.Header),
251 }
@@ -316,7 +301,7 @@ func (c *bufferedConn) Read(p []byte) (int, error) {
301 }
302
303 // openQUICSession opens a QUIC connection to the relay for datagram transport.
319 -func (a *apiClient) openQUICSession(ctx context.Context, identity types.Identity, accessToken string) (*quic.Conn, error) {
304 +func (a *apiClient) openQUICSession(ctx context.Context, accessToken string) (*quic.Conn, error) {
305 if err := a.ensureHTTPClient(ctx); err != nil {
306 return nil, err
307 }
@@ -343,7 +328,6 @@ func (a *apiClient) openQUICSession(ctx context.Context, identity types.Identity
328 }
329
330 controlMsg := types.QUICControlMessage{
346 - Identity: identity.Copy(),
331 AccessToken: accessToken,
332 }
333 if err := json.NewEncoder(stream).Encode(controlMsg); err != nil {
sdk/listener.go
+8 -9
@@ -108,7 +108,7 @@ func NewListener(ctx context.Context, relayURL string, cfg ListenerConfig) (*Lis
108 Msg("quic datagram plane disconnected; waiting to reconnect")
109 })
110 go l.datagram.RunLoop(listenerCtx, l.currentDatagramState, func(ctx context.Context, state transport.ClientDatagramState) (*quic.Conn, error) {
111 - return l.api.openQUICSession(ctx, state.Identity, state.AccessToken)
111 + return l.api.openQUICSession(ctx, state.AccessToken)
112 })
113 }
114
@@ -127,7 +127,7 @@ func (l *Listener) runStartup(ctx context.Context, readyTarget int) {
127 go l.stream.RunLoop(
128 ctx,
129 func(ctx context.Context) (net.Conn, error) {
130 - return l.api.openReverseSession(ctx, l.identity.Copy())
130 + return l.api.openReverseSession(ctx)
131 },
132 func() *tls.Config {
133 l.mu.Lock()
@@ -206,7 +206,7 @@ func (l *Listener) Close() error {
206
207 if api != nil && registered && identity.Key() != "" {
208 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
209 - closeErr = errors.Join(closeErr, api.unregisterLease(ctx, identity))
209 + closeErr = errors.Join(closeErr, api.unregisterLease(ctx))
210 cancel()
211 }
212 if tlsCloser != nil {
@@ -423,7 +423,7 @@ func (l *Listener) runRenewLoop(ctx context.Context) {
423
424 func (l *Listener) renewLease(ctx context.Context) error {
425 requestCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
426 - err := l.api.renewLease(requestCtx, l.identity.Copy(), l.leaseTTL)
426 + err := l.api.renewLease(requestCtx, l.leaseTTL)
427 cancel()
428 if err == nil {
429 return nil
@@ -446,7 +446,7 @@ func (l *Listener) registerAndConfigure(ctx context.Context) error {
446 return err
447 }
448 if l.datagram != nil && !resp.UDPEnabled {
449 - _ = l.api.unregisterLease(context.Background(), resp.Identity)
449 + _ = l.api.unregisterLease(context.Background())
450 return &types.APIRequestError{
451 Code: types.APIErrorCodeFeatureUnavailable,
452 Message: "relay did not enable required udp support",
@@ -454,12 +454,12 @@ func (l *Listener) registerAndConfigure(ctx context.Context) error {
454 }
455 tlsConf, tlsCloser, err := keyless.BuildClientTLSConfig(l.api.baseURL.String(), []string{resp.Hostname})
456 if err != nil {
457 - _ = l.api.unregisterLease(context.Background(), resp.Identity)
457 + _ = l.api.unregisterLease(context.Background())
458 return err
459 }
460
461 if ctx.Err() != nil {
462 - _ = l.api.unregisterLease(context.Background(), resp.Identity)
462 + _ = l.api.unregisterLease(context.Background())
463 if tlsCloser != nil {
464 _ = tlsCloser.Close()
465 }
@@ -469,7 +469,7 @@ func (l *Listener) registerAndConfigure(ctx context.Context) error {
469 l.mu.Lock()
470 if ctx.Err() != nil {
471 l.mu.Unlock()
472 - _ = l.api.unregisterLease(context.Background(), resp.Identity)
472 + _ = l.api.unregisterLease(context.Background())
473 if tlsCloser != nil {
474 _ = tlsCloser.Close()
475 }
@@ -481,7 +481,6 @@ func (l *Listener) registerAndConfigure(ctx context.Context) error {
481 l.identity.Address = resp.Identity.Address
482 l.hostname = resp.Hostname
483 l.udpAddr = resp.UDPAddr
484 - l.metadata = resp.Metadata.Copy()
484 l.tlsConfig = tlsConf
485 l.tlsCloser = tlsCloser
486 l.mu.Unlock()
sdk/sdk_test.go
+3 -256
@@ -16,150 +16,6 @@ import (
16 "github.com/gosuda/portal/v2/utils"
17 )
18
19 -func TestNewListenerRegistersLeaseWithMainContract(t *testing.T) {
20 - challengeReqCh := make(chan types.RegisterChallengeRequest, 1)
21 - registerReqCh := make(chan types.RegisterRequest, 1)
22 - var mu sync.RWMutex
23 - var registeredIdentity types.Identity
24 - server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
25 - switch r.URL.Path {
26 - case types.PathSDKDomain:
27 - writeSDKTestEnvelope(w, http.StatusOK, types.APIEnvelope[types.DomainResponse]{
28 - OK: true,
29 - Data: types.DomainResponse{
30 - ProtocolVersion: types.ProtocolVersion,
31 - },
32 - })
33 - case types.PathSDKRegisterChallenge:
34 - var challengeReq types.RegisterChallengeRequest
35 - if err := json.NewDecoder(r.Body).Decode(&challengeReq); err != nil {
36 - t.Fatalf("decode register challenge request: %v", err)
37 - }
38 - mu.Lock()
39 - registeredIdentity = challengeReq.Identity.Copy()
40 - mu.Unlock()
41 - select {
42 - case challengeReqCh <- challengeReq:
43 - default:
44 - }
45 - writeSDKTestEnvelope(w, http.StatusCreated, types.APIEnvelope[types.RegisterChallengeResponse]{
46 - OK: true,
47 - Data: types.RegisterChallengeResponse{
48 - ChallengeID: "challenge-1",
49 - ExpiresAt: time.Now().Add(time.Minute).UTC(),
50 - SIWEMessage: mustSDKTestSIWEMessage(t, r, challengeReq.Identity.Address, "challenge-1"),
51 - },
52 - })
53 - case types.PathSDKRegister:
54 - var registerReq types.RegisterRequest
55 - if err := json.NewDecoder(r.Body).Decode(&registerReq); err != nil {
56 - t.Fatalf("decode register request: %v", err)
57 - }
58 - mu.RLock()
59 - identity := registeredIdentity.Copy()
60 - mu.RUnlock()
61 - select {
62 - case registerReqCh <- registerReq:
63 - default:
64 - }
65 - writeSDKTestEnvelope(w, http.StatusCreated, types.APIEnvelope[types.RegisterResponse]{
66 - OK: true,
67 - Data: types.RegisterResponse{
68 - Identity: identity,
69 - Hostname: "127.0.0.1",
70 - Metadata: types.LeaseMetadata{Description: "demo"},
71 - AccessToken: "jwt-register-1",
72 - },
73 - })
74 - case types.PathSDKConnect:
75 - writeSDKTestEnvelope(w, http.StatusForbidden, types.APIEnvelope[any]{
76 - OK: false,
77 - Error: &types.APIError{Code: types.APIErrorCodeUnauthorized, Message: "not used in test"},
78 - })
79 - case types.PathSDKRenew:
80 - mu.RLock()
81 - identity := registeredIdentity.Copy()
82 - mu.RUnlock()
83 - writeSDKTestEnvelope(w, http.StatusOK, types.APIEnvelope[types.RenewResponse]{
84 - OK: true,
85 - Data: types.RenewResponse{Identity: identity, AccessToken: "jwt-renew-1"},
86 - })
87 - case types.PathSDKUnregister:
88 - writeSDKTestEnvelope(w, http.StatusOK, types.APIEnvelope[any]{OK: true})
89 - default:
90 - http.NotFound(w, r)
91 - }
92 - }))
93 - defer server.Close()
94 -
95 - listener, err := NewListener(context.Background(), server.URL, ListenerConfig{
96 - Identity: types.Identity{Name: "Demo-App"},
97 - Metadata: types.LeaseMetadata{Description: "demo"},
98 - LeaseTTL: 42 * time.Second,
99 - })
100 - if err != nil {
101 - t.Fatalf("NewListener() error = %v", err)
102 - }
103 - defer listener.Close()
104 -
105 - var challengeReq types.RegisterChallengeRequest
106 - waitForSDKTest(t, func() bool {
107 - select {
108 - case challengeReq = <-challengeReqCh:
109 - return true
110 - default:
111 - return false
112 - }
113 - })
114 -
115 - var registerReq types.RegisterRequest
116 - waitForSDKTest(t, func() bool {
117 - select {
118 - case registerReq = <-registerReqCh:
119 - return true
120 - default:
121 - return false
122 - }
123 - })
124 - waitForSDKTest(t, func() bool {
125 - return listener.Hostname() == "127.0.0.1"
126 - })
127 -
128 - if challengeReq.TTL != 42 {
129 - t.Fatalf("register challenge TTL = %d, want 42", challengeReq.TTL)
130 - }
131 - if challengeReq.UDPEnabled {
132 - t.Fatal("register challenge UDPEnabled = true, want false")
133 - }
134 - if challengeReq.Identity.Name != "demo-app" {
135 - t.Fatalf("register challenge Identity.Name = %q, want %q", challengeReq.Identity.Name, "demo-app")
136 - }
137 - if challengeReq.Identity.Address == "" {
138 - t.Fatal("register challenge Identity.Address = empty, want derived address")
139 - }
140 - if registerReq.ChallengeID != "challenge-1" {
141 - t.Fatalf("register request ChallengeID = %q, want %q", registerReq.ChallengeID, "challenge-1")
142 - }
143 - if registerReq.SIWEMessage == "" {
144 - t.Fatal("register request SIWEMessage = empty, want signed challenge payload")
145 - }
146 - if registerReq.SIWESignature == "" {
147 - t.Fatal("register request SIWESignature = empty, want signature")
148 - }
149 - if listener.Address() != challengeReq.Identity.Address {
150 - t.Fatalf("Address() = %q, want %q", listener.Address(), challengeReq.Identity.Address)
151 - }
152 - if got := listener.Hostname(); got != "127.0.0.1" {
153 - t.Fatalf("Hostname() = %q, want %q", got, "127.0.0.1")
154 - }
155 - if got := listener.PublicURL(); got != server.URL {
156 - t.Fatalf("PublicURL() = %q, want %q", got, server.URL)
157 - }
158 - if got := listener.Metadata(); got.Description != "demo" {
159 - t.Fatalf("Metadata().Description = %q, want %q", got.Description, "demo")
160 - }
161 -}
162 -
19 func TestExposeNoRelayInputs(t *testing.T) {
20 exposure, err := Expose(context.Background(), ExposeConfig{Name: "demo"})
21 if err != nil {
@@ -234,7 +90,7 @@ func TestExposeLoadsPrivateKeyFromIdentityPath(t *testing.T) {
90 case types.PathSDKRenew:
91 writeSDKTestEnvelope(w, http.StatusOK, types.APIEnvelope[types.RenewResponse]{
92 OK: true,
237 - Data: types.RenewResponse{Identity: types.Identity{Name: identity.Name, Address: identity.Address}, AccessToken: "jwt-renew-2"},
93 + Data: types.RenewResponse{AccessToken: "jwt-renew-2"},
94 })
95 case types.PathSDKUnregister:
96 writeSDKTestEnvelope(w, http.StatusOK, types.APIEnvelope[any]{OK: true})
@@ -330,7 +186,7 @@ func TestExposeLoadsIdentityFromJSON(t *testing.T) {
186 case types.PathSDKRenew:
187 writeSDKTestEnvelope(w, http.StatusOK, types.APIEnvelope[types.RenewResponse]{
188 OK: true,
333 - Data: types.RenewResponse{Identity: types.Identity{Name: identity.Name, Address: identity.Address}, AccessToken: "jwt-renew-json"},
189 + Data: types.RenewResponse{AccessToken: "jwt-renew-json"},
190 })
191 case types.PathSDKUnregister:
192 writeSDKTestEnvelope(w, http.StatusOK, types.APIEnvelope[any]{OK: true})
@@ -422,12 +278,9 @@ func TestExposeGeneratesAddressWithoutPrivateKey(t *testing.T) {
278 Error: &types.APIError{Code: types.APIErrorCodeUnauthorized, Message: "not used in test"},
279 })
280 case types.PathSDKRenew:
425 - mu.RLock()
426 - identity := registeredIdentity.Copy()
427 - mu.RUnlock()
281 writeSDKTestEnvelope(w, http.StatusOK, types.APIEnvelope[types.RenewResponse]{
282 OK: true,
430 - Data: types.RenewResponse{Identity: identity, AccessToken: "jwt-renew-3"},
283 + Data: types.RenewResponse{AccessToken: "jwt-renew-3"},
284 })
285 case types.PathSDKUnregister:
286 writeSDKTestEnvelope(w, http.StatusOK, types.APIEnvelope[any]{OK: true})
@@ -464,112 +317,6 @@ func TestExposeGeneratesAddressWithoutPrivateKey(t *testing.T) {
317 }
318 }
319
467 -func TestExposeCreatesIdentityAtPath(t *testing.T) {
468 - challengeReqCh := make(chan types.RegisterChallengeRequest, 1)
469 - var mu sync.RWMutex
470 - var registeredIdentity types.Identity
471 - server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
472 - switch r.URL.Path {
473 - case types.PathSDKDomain:
474 - writeSDKTestEnvelope(w, http.StatusOK, types.APIEnvelope[types.DomainResponse]{
475 - OK: true,
476 - Data: types.DomainResponse{
477 - ProtocolVersion: types.ProtocolVersion,
478 - },
479 - })
480 - case types.PathSDKRegisterChallenge:
481 - var challengeReq types.RegisterChallengeRequest
482 - if err := json.NewDecoder(r.Body).Decode(&challengeReq); err != nil {
483 - t.Fatalf("decode register challenge request: %v", err)
484 - }
485 - mu.Lock()
486 - registeredIdentity = challengeReq.Identity.Copy()
487 - mu.Unlock()
488 - select {
489 - case challengeReqCh <- challengeReq:
490 - default:
491 - }
492 - writeSDKTestEnvelope(w, http.StatusCreated, types.APIEnvelope[types.RegisterChallengeResponse]{
493 - OK: true,
494 - Data: types.RegisterChallengeResponse{
495 - ChallengeID: "challenge-1",
496 - ExpiresAt: time.Now().Add(time.Minute).UTC(),
497 - SIWEMessage: mustSDKTestSIWEMessage(t, r, challengeReq.Identity.Address, "challenge-1"),
498 - },
499 - })
500 - case types.PathSDKRegister:
501 - mu.RLock()
502 - identity := registeredIdentity.Copy()
503 - mu.RUnlock()
504 - writeSDKTestEnvelope(w, http.StatusCreated, types.APIEnvelope[types.RegisterResponse]{
505 - OK: true,
506 - Data: types.RegisterResponse{
507 - Identity: identity,
508 - Hostname: "127.0.0.1",
509 - AccessToken: "jwt-register-4",
510 - },
511 - })
512 - case types.PathSDKConnect:
513 - writeSDKTestEnvelope(w, http.StatusForbidden, types.APIEnvelope[any]{
514 - OK: false,
515 - Error: &types.APIError{Code: types.APIErrorCodeUnauthorized, Message: "not used in test"},
516 - })
517 - case types.PathSDKRenew:
518 - mu.RLock()
519 - identity := registeredIdentity.Copy()
520 - mu.RUnlock()
521 - writeSDKTestEnvelope(w, http.StatusOK, types.APIEnvelope[types.RenewResponse]{
522 - OK: true,
523 - Data: types.RenewResponse{Identity: identity, AccessToken: "jwt-renew-4"},
524 - })
525 - case types.PathSDKUnregister:
526 - writeSDKTestEnvelope(w, http.StatusOK, types.APIEnvelope[any]{OK: true})
527 - default:
528 - http.NotFound(w, r)
529 - }
530 - }))
531 - defer server.Close()
532 -
533 - identityPath := t.TempDir() + "/identity.json"
534 - exposure, err := Expose(context.Background(), ExposeConfig{
535 - RelayURLs: []string{server.URL},
536 - IdentityPath: identityPath,
537 - Name: "demo-path",
538 - })
539 - if err != nil {
540 - t.Fatalf("Expose() error = %v", err)
541 - }
542 - defer exposure.Close()
543 -
544 - var challengeReq types.RegisterChallengeRequest
545 - waitForSDKTest(t, func() bool {
546 - select {
547 - case challengeReq = <-challengeReqCh:
548 - return true
549 - default:
550 - return false
551 - }
552 - })
553 -
554 - if challengeReq.Identity.Name != "demo-path" {
555 - t.Fatalf("register challenge Identity.Name = %q, want %q", challengeReq.Identity.Name, "demo-path")
556 - }
557 - if challengeReq.Identity.Address == "" {
558 - t.Fatal("register challenge Identity.Address = empty, want generated address")
559 - }
560 -
561 - storedIdentity, err := utils.LoadIdentity(identityPath)
562 - if err != nil {
563 - t.Fatalf("LoadIdentity() error = %v", err)
564 - }
565 - if storedIdentity.Name != "demo-path" {
566 - t.Fatalf("stored identity name = %q, want %q", storedIdentity.Name, "demo-path")
567 - }
568 - if storedIdentity.Address != challengeReq.Identity.Address {
569 - t.Fatalf("stored identity address = %q, want %q", storedIdentity.Address, challengeReq.Identity.Address)
570 - }
571 -}
572 -
320 func mustSDKTestSIWEMessage(t *testing.T, r *http.Request, address, challengeID string) string {
321 t.Helper()
322
types/api.go
+11 -16
@@ -76,13 +76,12 @@ type RegisterChallengeResponse struct {
76 }
77
78 type RegisterResponse struct {
79 - Identity Identity `json:"identity"`
80 - ExpiresAt time.Time `json:"expires_at"`
81 - Hostname string `json:"hostname"`
82 - Metadata LeaseMetadata `json:"metadata"`
83 - AccessToken string `json:"access_token"`
84 - UDPAddr string `json:"udp_addr,omitempty"`
85 - UDPEnabled bool `json:"udp_enabled,omitempty"`
79 + Identity Identity `json:"identity"`
80 + ExpiresAt time.Time `json:"expires_at"`
81 + Hostname string `json:"hostname"`
82 + AccessToken string `json:"access_token"`
83 + UDPAddr string `json:"udp_addr,omitempty"`
84 + UDPEnabled bool `json:"udp_enabled,omitempty"`
85 }
86
87 type DiscoveryResponse struct {
@@ -93,8 +92,7 @@ type DiscoveryResponse struct {
92 }
93
94 type QUICControlMessage struct {
96 - Identity Identity `json:"identity"`
97 - AccessToken string `json:"access_token"`
95 + AccessToken string `json:"access_token"`
96 }
97
98 type QUICControlResponse struct {
@@ -103,21 +101,18 @@ type QUICControlResponse struct {
101 }
102
103 type RenewRequest struct {
106 - Identity Identity `json:"identity"`
107 - AccessToken string `json:"access_token"`
108 - TTL int `json:"ttl,omitempty"`
109 - ReportedIP string `json:"reported_ip,omitempty"`
104 + AccessToken string `json:"access_token"`
105 + TTL int `json:"ttl,omitempty"`
106 + ReportedIP string `json:"reported_ip,omitempty"`
107 }
108
109 type RenewResponse struct {
113 - Identity Identity `json:"identity"`
110 ExpiresAt time.Time `json:"expires_at"`
111 AccessToken string `json:"access_token"`
112 }
113
114 type UnregisterRequest struct {
119 - Identity Identity `json:"identity"`
120 - AccessToken string `json:"access_token"`
115 + AccessToken string `json:"access_token"`
116 }
117
118 type DomainResponse struct {
types/identity.go
+9 -8
@@ -63,14 +63,15 @@ type Lease struct {
63
64 type AdminLease struct {
65 Lease
66 - Address string `json:"address,omitempty"`
67 - BPS int64
68 - ClientIP string
69 - ReportedIP string
70 - IsApproved bool
71 - IsBanned bool
72 - IsDenied bool
73 - IsIPBanned bool
66 + IdentityKey string `json:"identity_key,omitempty"`
67 + Address string `json:"address,omitempty"`
68 + BPS int64
69 + ClientIP string
70 + ReportedIP string
71 + IsApproved bool
72 + IsBanned bool
73 + IsDenied bool
74 + IsIPBanned bool
75 }
76
77 type RelayDescriptor struct {