chore: use sdk identity path

Kim committed Apr 2, 2026 at 12:35 UTC 938c4165463d2eb92e54cda8a7c93332a584ae78
6 files changed +171 -66
cmd/demo-app/main.go
+11 -25
@@ -146,19 +146,12 @@ func runHelpCommand(args []string) error {
146 }
147
148 func runTCPDemo(ctx context.Context, cfg demoConfig) error {
149 - identity, createdIdentity, err := utils.LoadOrCreateIdentity(cfg.identityPath, types.Identity{Name: cfg.name})
150 - if err != nil {
151 - return fmt.Errorf("load demo identity: %w", err)
152 - }
153 - if createdIdentity {
154 - log.Info().Str("identity_path", cfg.identityPath).Str("address", identity.Address).Msg("generated demo identity and saved it to disk")
155 - }
156 -
149 exposure, err := sdk.Expose(ctx, sdk.ExposeConfig{
158 - RelayURLs: utils.SplitCSV(cfg.relayURLs),
159 - Identity: identity,
160 - BanMITM: cfg.banMITM,
161 - Discovery: cfg.discovery,
150 + RelayURLs: utils.SplitCSV(cfg.relayURLs),
151 + IdentityPath: cfg.identityPath,
152 + Name: cfg.name,
153 + BanMITM: cfg.banMITM,
154 + Discovery: cfg.discovery,
155 Metadata: types.LeaseMetadata{
156 Description: cfg.desc,
157 Tags: utils.SplitCSV(cfg.tags),
@@ -194,20 +187,13 @@ func runTCPDemo(ctx context.Context, cfg demoConfig) error {
187 }
188
189 func runUDPDemo(ctx context.Context, cfg demoConfig) error {
197 - identity, createdIdentity, err := utils.LoadOrCreateIdentity(cfg.identityPath, types.Identity{Name: cfg.name})
198 - if err != nil {
199 - return fmt.Errorf("load demo identity: %w", err)
200 - }
201 - if createdIdentity {
202 - log.Info().Str("identity_path", cfg.identityPath).Str("address", identity.Address).Msg("generated demo identity and saved it to disk")
203 - }
204 -
190 exposure, err := sdk.Expose(ctx, sdk.ExposeConfig{
206 - RelayURLs: utils.SplitCSV(cfg.relayURLs),
207 - Identity: identity,
208 - UDPEnabled: true,
209 - BanMITM: cfg.banMITM,
210 - Discovery: cfg.discovery,
191 + RelayURLs: utils.SplitCSV(cfg.relayURLs),
192 + IdentityPath: cfg.identityPath,
193 + Name: cfg.name,
194 + UDPEnabled: true,
195 + BanMITM: cfg.banMITM,
196 + Discovery: cfg.discovery,
197 Metadata: types.LeaseMetadata{
198 Description: cfg.desc,
199 Tags: utils.SplitCSV(cfg.tags),
cmd/portal-tunnel/main.go
+8 -15
@@ -110,25 +110,18 @@ func runExposeCommand(args []string) error {
110 if err != nil {
111 return fmt.Errorf("invalid service name: %w", err)
112 }
113 - identity, createdIdentity, err := utils.LoadOrCreateIdentity(flags.identityPath, types.Identity{Name: flags.name})
114 - if err != nil {
115 - return fmt.Errorf("load tunnel identity: %w", err)
116 - }
117 - if createdIdentity {
118 - log.Info().Str("identity_path", flags.identityPath).Str("address", identity.Address).Msg("generated tunnel identity and saved it to disk")
119 - }
120 -
113 ctx, stop := utils.SignalContext()
114 defer stop()
115
116 exposure, err := sdk.Expose(ctx, sdk.ExposeConfig{
125 - RelayURLs: utils.SplitCSV(flags.relayCSV),
126 - Identity: identity,
127 - TargetAddr: flags.targetAddr,
128 - UDPAddr: flags.udpAddr,
129 - UDPEnabled: flags.udp,
130 - BanMITM: flags.banMITM,
131 - Discovery: flags.discovery,
117 + RelayURLs: utils.SplitCSV(flags.relayCSV),
118 + IdentityPath: flags.identityPath,
119 + Name: flags.name,
120 + TargetAddr: flags.targetAddr,
121 + UDPAddr: flags.udpAddr,
122 + UDPEnabled: flags.udp,
123 + BanMITM: flags.banMITM,
124 + Discovery: flags.discovery,
125 Metadata: types.LeaseMetadata{
126 Description: flags.desc,
127 Tags: utils.SplitCSV(flags.tags),
sdk/api_client.go
+1 -6
@@ -51,11 +51,6 @@ type apiClient struct {
51 }
52
53 func newApiClient(relayURL string, cfg ListenerConfig) (*apiClient, error) {
54 - identity, err := utils.ResolveLeaseIdentity(cfg.Identity)
55 - if err != nil {
56 - return nil, fmt.Errorf("resolve identity: %w", err)
57 - }
58 -
54 normalizedRelayURL, err := utils.NormalizeRelayURL(relayURL)
55 if err != nil {
56 return nil, err
@@ -74,7 +69,7 @@ func newApiClient(relayURL string, cfg ListenerConfig) (*apiClient, error) {
69 dialTimeout: dialTimeout,
70 requestTimeout: requestTimeout,
71 rootCAPEM: append([]byte(nil), cfg.RootCAPEM...),
77 - identity: identity,
72 + identity: cfg.Identity.Copy(),
73 metadata: cfg.Metadata.Copy(),
74 }, nil
75 }
sdk/expose.go
+17 -10
@@ -45,15 +45,16 @@ type Exposure struct {
45 }
46
47 type ExposeConfig struct {
48 - RelayURLs []string
49 - Identity types.Identity
50 - TargetAddr string
51 - UDPAddr string
52 - UDPEnabled bool
53 - BanMITM bool
54 - Discovery bool
55 - Metadata types.LeaseMetadata
56 - RootCAPEM []byte
48 + RelayURLs []string
49 + IdentityPath string
50 + Name string
51 + TargetAddr string
52 + UDPAddr string
53 + UDPEnabled bool
54 + BanMITM bool
55 + Discovery bool
56 + Metadata types.LeaseMetadata
57 + RootCAPEM []byte
58 }
59
60 // Expose creates relay listeners for each normalized relay URL and exposes a
@@ -64,10 +65,16 @@ func Expose(ctx context.Context, cfg ExposeConfig) (*Exposure, error) {
65 return nil, err
66 }
67
67 - identity, err := utils.ResolveLeaseIdentity(cfg.Identity)
68 + identity, createdIdentity, err := utils.ResolveListenerIdentity(types.Identity{Name: cfg.Name}, cfg.IdentityPath)
69 if err != nil {
70 return nil, fmt.Errorf("resolve identity: %w", err)
71 }
72 + if createdIdentity {
73 + log.Info().
74 + Str("identity_path", strings.TrimSpace(cfg.IdentityPath)).
75 + Str("address", identity.Address).
76 + Msg("generated tunnel identity and saved it to disk")
77 + }
78 targetAddr, err := utils.NormalizeLoopbackTarget(cfg.TargetAddr)
79 if err != nil {
80 return nil, fmt.Errorf("invalid target value %q: %w", cfg.TargetAddr, err)
sdk/sdk_test.go
+116 -10
@@ -161,7 +161,7 @@ func TestNewListenerRegistersLeaseWithMainContract(t *testing.T) {
161 }
162
163 func TestExposeNoRelayInputs(t *testing.T) {
164 - exposure, err := Expose(context.Background(), ExposeConfig{Identity: types.Identity{Name: "demo"}})
164 + exposure, err := Expose(context.Background(), ExposeConfig{Name: "demo"})
165 if err != nil {
166 t.Fatalf("Expose() error = %v", err)
167 }
@@ -174,12 +174,17 @@ func TestExposeNoRelayInputs(t *testing.T) {
174 }
175 }
176
177 -func TestExposeResolvesPrivateKey(t *testing.T) {
177 +func TestExposeLoadsPrivateKeyFromIdentityPath(t *testing.T) {
178 privateKey := strings.Repeat("11", 32)
179 identity, err := utils.ResolveSecp256k1Identity(privateKey)
180 if err != nil {
181 t.Fatalf("ResolveSecp256k1Identity() error = %v", err)
182 }
183 + identity.Name = "demo"
184 + identityPath := t.TempDir() + "/identity.json"
185 + if err := utils.SaveIdentity(identityPath, identity); err != nil {
186 + t.Fatalf("SaveIdentity() error = %v", err)
187 + }
188
189 challengeReqCh := make(chan types.RegisterChallengeRequest, 1)
190 server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -240,11 +245,8 @@ func TestExposeResolvesPrivateKey(t *testing.T) {
245 defer server.Close()
246
247 exposure, err := Expose(context.Background(), ExposeConfig{
243 - RelayURLs: []string{server.URL},
244 - Identity: types.Identity{
245 - Name: "demo",
246 - PrivateKey: privateKey,
247 - },
248 + RelayURLs: []string{server.URL},
249 + IdentityPath: identityPath,
250 })
251 if err != nil {
252 t.Fatalf("Expose() error = %v", err)
@@ -338,9 +340,7 @@ func TestExposeGeneratesAddressWithoutPrivateKey(t *testing.T) {
340
341 exposure, err := Expose(context.Background(), ExposeConfig{
342 RelayURLs: []string{server.URL},
341 - Identity: types.Identity{
342 - Name: "demo",
343 - },
343 + Name: "demo",
344 })
345 if err != nil {
346 t.Fatalf("Expose() error = %v", err)
@@ -365,6 +365,112 @@ func TestExposeGeneratesAddressWithoutPrivateKey(t *testing.T) {
365 }
366 }
367
368 +func TestExposeCreatesIdentityAtPath(t *testing.T) {
369 + challengeReqCh := make(chan types.RegisterChallengeRequest, 1)
370 + var mu sync.RWMutex
371 + var registeredIdentity types.Identity
372 + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
373 + switch r.URL.Path {
374 + case types.PathSDKDomain:
375 + writeSDKTestEnvelope(w, http.StatusOK, types.APIEnvelope[types.DomainResponse]{
376 + OK: true,
377 + Data: types.DomainResponse{
378 + ProtocolVersion: types.ProtocolVersion,
379 + },
380 + })
381 + case types.PathSDKRegisterChallenge:
382 + var challengeReq types.RegisterChallengeRequest
383 + if err := json.NewDecoder(r.Body).Decode(&challengeReq); err != nil {
384 + t.Fatalf("decode register challenge request: %v", err)
385 + }
386 + mu.Lock()
387 + registeredIdentity = challengeReq.Identity.Copy()
388 + mu.Unlock()
389 + select {
390 + case challengeReqCh <- challengeReq:
391 + default:
392 + }
393 + writeSDKTestEnvelope(w, http.StatusCreated, types.APIEnvelope[types.RegisterChallengeResponse]{
394 + OK: true,
395 + Data: types.RegisterChallengeResponse{
396 + ChallengeID: "challenge-1",
397 + ExpiresAt: time.Now().Add(time.Minute).UTC(),
398 + SIWEMessage: mustSDKTestSIWEMessage(t, r, challengeReq.Identity.Address, "challenge-1"),
399 + },
400 + })
401 + case types.PathSDKRegister:
402 + mu.RLock()
403 + identity := registeredIdentity.Copy()
404 + mu.RUnlock()
405 + writeSDKTestEnvelope(w, http.StatusCreated, types.APIEnvelope[types.RegisterResponse]{
406 + OK: true,
407 + Data: types.RegisterResponse{
408 + Identity: identity,
409 + Hostname: "127.0.0.1",
410 + AccessToken: "jwt-register-4",
411 + },
412 + })
413 + case types.PathSDKConnect:
414 + writeSDKTestEnvelope(w, http.StatusForbidden, types.APIEnvelope[any]{
415 + OK: false,
416 + Error: &types.APIError{Code: types.APIErrorCodeUnauthorized, Message: "not used in test"},
417 + })
418 + case types.PathSDKRenew:
419 + mu.RLock()
420 + identity := registeredIdentity.Copy()
421 + mu.RUnlock()
422 + writeSDKTestEnvelope(w, http.StatusOK, types.APIEnvelope[types.RenewResponse]{
423 + OK: true,
424 + Data: types.RenewResponse{Identity: identity, AccessToken: "jwt-renew-4"},
425 + })
426 + case types.PathSDKUnregister:
427 + writeSDKTestEnvelope(w, http.StatusOK, types.APIEnvelope[any]{OK: true})
428 + default:
429 + http.NotFound(w, r)
430 + }
431 + }))
432 + defer server.Close()
433 +
434 + identityPath := t.TempDir() + "/identity.json"
435 + exposure, err := Expose(context.Background(), ExposeConfig{
436 + RelayURLs: []string{server.URL},
437 + IdentityPath: identityPath,
438 + Name: "demo-path",
439 + })
440 + if err != nil {
441 + t.Fatalf("Expose() error = %v", err)
442 + }
443 + defer exposure.Close()
444 +
445 + var challengeReq types.RegisterChallengeRequest
446 + waitForSDKTest(t, func() bool {
447 + select {
448 + case challengeReq = <-challengeReqCh:
449 + return true
450 + default:
451 + return false
452 + }
453 + })
454 +
455 + if challengeReq.Identity.Name != "demo-path" {
456 + t.Fatalf("register challenge Identity.Name = %q, want %q", challengeReq.Identity.Name, "demo-path")
457 + }
458 + if challengeReq.Identity.Address == "" {
459 + t.Fatal("register challenge Identity.Address = empty, want generated address")
460 + }
461 +
462 + storedIdentity, err := utils.LoadIdentity(identityPath)
463 + if err != nil {
464 + t.Fatalf("LoadIdentity() error = %v", err)
465 + }
466 + if storedIdentity.Name != "demo-path" {
467 + t.Fatalf("stored identity name = %q, want %q", storedIdentity.Name, "demo-path")
468 + }
469 + if storedIdentity.Address != challengeReq.Identity.Address {
470 + t.Fatalf("stored identity address = %q, want %q", storedIdentity.Address, challengeReq.Identity.Address)
471 + }
472 +}
473 +
474 func mustSDKTestSIWEMessage(t *testing.T, r *http.Request, address, challengeID string) string {
475 t.Helper()
476
utils/identity.go
+18
@@ -188,6 +188,24 @@ func LoadOrCreateIdentity(path string, identity types.Identity) (types.Identity,
188 return loaded, true, nil
189 }
190
191 +func ResolveListenerIdentity(identity types.Identity, identityPath string) (types.Identity, bool, error) {
192 + identityPath = strings.TrimSpace(identityPath)
193 + if identityPath == "" {
194 + resolved, err := ResolveLeaseIdentity(identity)
195 + return resolved, false, err
196 + }
197 +
198 + loaded, created, err := LoadOrCreateIdentity(identityPath, identity)
199 + if err != nil {
200 + return types.Identity{}, false, err
201 + }
202 + resolved, err := ResolveLeaseIdentity(loaded)
203 + if err != nil {
204 + return types.Identity{}, false, err
205 + }
206 + return resolved, created, nil
207 +}
208 +
209 func NormalizeIdentityKey(raw string) string {
210 key := strings.ToLower(strings.TrimSpace(raw))
211 if key == "" {