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(®isterReq); 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 {