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 == "" {