@awiki/cli 0.0.1-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/.github/workflows/release.yml +44 -0
  2. package/.goreleaser.yml +44 -0
  3. package/AGENTS.md +60 -0
  4. package/CLAUDE.md +192 -0
  5. package/README.md +2 -0
  6. package/docs/architecture/awiki-command-v2.md +955 -0
  7. package/docs/architecture/awiki-skill-architecture.md +475 -0
  8. package/docs/architecture/awiki-v2-architecture.md +1063 -0
  9. package/docs/architecture//345/217/202/350/200/203/346/226/207/346/241/243/cli-init.md +1008 -0
  10. package/docs/architecture//345/217/202/350/200/203/346/226/207/346/241/243/output-format.md +407 -0
  11. package/docs/architecture//345/217/202/350/200/203/346/226/207/346/241/243/overall-init.md +741 -0
  12. package/docs/harness/review-spec.md +474 -0
  13. package/docs/installation.md +372 -0
  14. package/docs/plan/awiki-v2-implementation-plan.md +903 -0
  15. package/docs/plan/phase-0/adr-index.md +56 -0
  16. package/docs/plan/phase-0/audit-findings.md +251 -0
  17. package/docs/plan/phase-0/capability-mapping.md +108 -0
  18. package/docs/plan/phase-0/implementation-constraints.md +363 -0
  19. package/docs/publish.md +169 -0
  20. package/go.mod +29 -0
  21. package/go.sum +73 -0
  22. package/internal/anpsdk/registry.go +63 -0
  23. package/internal/authsdk/session.go +351 -0
  24. package/internal/buildinfo/buildinfo.go +34 -0
  25. package/internal/cli/app.go +136 -0
  26. package/internal/cli/app_test.go +88 -0
  27. package/internal/cli/debug.go +104 -0
  28. package/internal/cli/group.go +263 -0
  29. package/internal/cli/id.go +473 -0
  30. package/internal/cli/init.go +134 -0
  31. package/internal/cli/msg.go +228 -0
  32. package/internal/cli/page.go +267 -0
  33. package/internal/cli/root.go +499 -0
  34. package/internal/cli/runtime.go +232 -0
  35. package/internal/cli/upgrade.go +60 -0
  36. package/internal/cmdmeta/catalog.go +203 -0
  37. package/internal/cmdmeta/catalog_test.go +21 -0
  38. package/internal/config/config.go +399 -0
  39. package/internal/config/config_test.go +104 -0
  40. package/internal/config/write.go +37 -0
  41. package/internal/content/service.go +314 -0
  42. package/internal/content/service_test.go +165 -0
  43. package/internal/content/types.go +44 -0
  44. package/internal/docs/topics.go +110 -0
  45. package/internal/doctor/doctor.go +306 -0
  46. package/internal/identity/client.go +267 -0
  47. package/internal/identity/did.go +85 -0
  48. package/internal/identity/did_test.go +50 -0
  49. package/internal/identity/layout.go +206 -0
  50. package/internal/identity/legacy.go +378 -0
  51. package/internal/identity/public.go +70 -0
  52. package/internal/identity/public_test.go +73 -0
  53. package/internal/identity/readiness.go +74 -0
  54. package/internal/identity/service.go +826 -0
  55. package/internal/identity/store.go +385 -0
  56. package/internal/identity/store_test.go +180 -0
  57. package/internal/identity/types.go +204 -0
  58. package/internal/message/auth.go +167 -0
  59. package/internal/message/group_service.go +838 -0
  60. package/internal/message/group_wire.go +350 -0
  61. package/internal/message/group_wire_test.go +67 -0
  62. package/internal/message/helpers.go +61 -0
  63. package/internal/message/http_client.go +334 -0
  64. package/internal/message/proof.go +156 -0
  65. package/internal/message/proof_test.go +61 -0
  66. package/internal/message/service.go +696 -0
  67. package/internal/message/service_test.go +97 -0
  68. package/internal/message/types.go +155 -0
  69. package/internal/message/wire.go +100 -0
  70. package/internal/message/wire_test.go +49 -0
  71. package/internal/message/ws_proxy_client.go +151 -0
  72. package/internal/output/output.go +350 -0
  73. package/internal/output/output_test.go +48 -0
  74. package/internal/runtime/config.go +117 -0
  75. package/internal/runtime/config_test.go +46 -0
  76. package/internal/runtime/listener/files.go +65 -0
  77. package/internal/runtime/listener/manager.go +142 -0
  78. package/internal/runtime/listener/server.go +983 -0
  79. package/internal/runtime/listener/server_test.go +319 -0
  80. package/internal/runtime/listener/sysproc_unix.go +17 -0
  81. package/internal/runtime/listener/sysproc_windows.go +13 -0
  82. package/internal/runtime/listener/types.go +21 -0
  83. package/internal/runtime/listener/wsclient.go +299 -0
  84. package/internal/runtime/listener/wsclient_test.go +41 -0
  85. package/internal/store/dao.go +632 -0
  86. package/internal/store/dao_test.go +87 -0
  87. package/internal/store/helpers.go +197 -0
  88. package/internal/store/import.go +499 -0
  89. package/internal/store/import_test.go +103 -0
  90. package/internal/store/open.go +71 -0
  91. package/internal/store/query.go +151 -0
  92. package/internal/store/schema.go +277 -0
  93. package/internal/store/schema_test.go +56 -0
  94. package/internal/store/types.go +177 -0
  95. package/internal/update/update.go +368 -0
  96. package/package.json +17 -0
  97. package/scripts/install.js +171 -0
  98. package/scripts/release/release-prerelease.sh +86 -0
  99. package/scripts/release/tag-release.sh +66 -0
  100. package/scripts/release/withdraw-release.sh +78 -0
  101. package/scripts/run.js +69 -0
  102. package/skills/README.md +32 -0
  103. package/skills/awiki-bundle/SKILL.md +76 -0
  104. package/skills/awiki-debug/SKILL.md +80 -0
  105. package/skills/awiki-group/SKILL.md +111 -0
  106. package/skills/awiki-id/SKILL.md +123 -0
  107. package/skills/awiki-msg/SKILL.md +131 -0
  108. package/skills/awiki-page/SKILL.md +93 -0
  109. package/skills/awiki-people/SKILL.md +66 -0
  110. package/skills/awiki-runtime/SKILL.md +137 -0
  111. package/skills/awiki-shared/SKILL.md +124 -0
  112. package/skills/awiki-workflow-discovery/SKILL.md +93 -0
  113. package/skills/awiki-workflow-onboarding/SKILL.md +119 -0
  114. package/skills/manifests/skills.yaml +260 -0
  115. package/skills/templates/bundle-skill-template.md +42 -0
  116. package/skills/templates/debug-skill-template.md +44 -0
  117. package/skills/templates/domain-skill-template.md +56 -0
  118. package/skills/templates/shared-skill-template.md +46 -0
  119. package/skills/templates/workflow-skill-template.md +46 -0
@@ -0,0 +1,306 @@
1
+ package doctor
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "os"
7
+ "path/filepath"
8
+ "strings"
9
+
10
+ "github.com/agentconnect/awiki-cli/internal/buildinfo"
11
+ "github.com/agentconnect/awiki-cli/internal/config"
12
+ "github.com/agentconnect/awiki-cli/internal/identity"
13
+ "github.com/agentconnect/awiki-cli/internal/store"
14
+ )
15
+
16
+ type Check struct {
17
+ Name string `json:"name"`
18
+ Status string `json:"status"`
19
+ Summary string `json:"summary"`
20
+ Details map[string]any `json:"details,omitempty"`
21
+ }
22
+
23
+ type Report struct {
24
+ Checks []Check `json:"checks"`
25
+ Summary string `json:"summary"`
26
+ Counts Counts `json:"counts"`
27
+ }
28
+
29
+ type Counts struct {
30
+ OK int `json:"ok"`
31
+ Warn int `json:"warn"`
32
+ Error int `json:"error"`
33
+ Info int `json:"info"`
34
+ }
35
+
36
+ func Run(resolved *config.Resolved) Report {
37
+ checks := []Check{
38
+ buildCheck(resolved),
39
+ configFileCheck(resolved),
40
+ envCheck(resolved),
41
+ runtimeCheck(resolved),
42
+ identityStoreCheck(resolved),
43
+ sqliteCheck(resolved),
44
+ legacyCheck(resolved),
45
+ }
46
+ counts := Counts{}
47
+ for _, check := range checks {
48
+ switch check.Status {
49
+ case "ok":
50
+ counts.OK++
51
+ case "warn":
52
+ counts.Warn++
53
+ case "error":
54
+ counts.Error++
55
+ default:
56
+ counts.Info++
57
+ }
58
+ }
59
+ summary := "Doctor completed successfully"
60
+ if counts.Error > 0 {
61
+ summary = "Doctor found blocking issues"
62
+ } else if counts.Warn > 0 {
63
+ summary = "Doctor found warnings"
64
+ }
65
+ return Report{Checks: checks, Summary: summary, Counts: counts}
66
+ }
67
+
68
+ func buildCheck(resolved *config.Resolved) Check {
69
+ info := buildinfo.Current()
70
+ status := "ok"
71
+ summary := "Pure-Go build target is aligned"
72
+ if strings.EqualFold(info.CGOEnabled, "1") || strings.EqualFold(info.CGOEnabled, "true") {
73
+ status = "warn"
74
+ summary = "Build metadata indicates CGO was enabled"
75
+ }
76
+ return Check{
77
+ Name: "build",
78
+ Status: status,
79
+ Summary: summary,
80
+ Details: map[string]any{
81
+ "go_version": info.GoVersion,
82
+ "goos": info.GOOS,
83
+ "goarch": info.GOARCH,
84
+ "compiler": info.Compiler,
85
+ "cgo_enabled": info.CGOEnabled,
86
+ },
87
+ }
88
+ }
89
+
90
+ func configFileCheck(resolved *config.Resolved) Check {
91
+ status := "warn"
92
+ summary := "No config file found yet"
93
+ if resolved.ConfigExists {
94
+ status = "ok"
95
+ summary = "Config file loaded"
96
+ }
97
+ if resolved.ConfigError != "" {
98
+ status = "error"
99
+ summary = "Config file exists but failed to parse"
100
+ }
101
+ return Check{
102
+ Name: "config_file",
103
+ Status: status,
104
+ Summary: summary,
105
+ Details: map[string]any{
106
+ "path": resolved.Paths.ConfigFile,
107
+ "exists": resolved.ConfigExists,
108
+ "error": resolved.ConfigError,
109
+ },
110
+ }
111
+ }
112
+
113
+ func envCheck(resolved *config.Resolved) Check {
114
+ status := "info"
115
+ summary := "No environment overrides detected"
116
+ aliasHits := 0
117
+ legacyHits := 0
118
+ for _, hit := range resolved.EnvHits {
119
+ if hit.Tier == "draft_alias_env" {
120
+ aliasHits++
121
+ }
122
+ if hit.Tier == "legacy_env" {
123
+ legacyHits++
124
+ }
125
+ }
126
+ if len(resolved.EnvHits) > 0 {
127
+ status = "ok"
128
+ summary = "Environment overrides detected"
129
+ }
130
+ if aliasHits > 0 || legacyHits > 0 {
131
+ status = "warn"
132
+ summary = "Compatibility environment variables are in use"
133
+ }
134
+ return Check{
135
+ Name: "environment",
136
+ Status: status,
137
+ Summary: summary,
138
+ Details: map[string]any{
139
+ "hits": resolved.EnvHits,
140
+ },
141
+ }
142
+ }
143
+
144
+ func runtimeCheck(resolved *config.Resolved) Check {
145
+ status := "ok"
146
+ summary := "Runtime mode resolved"
147
+ if strings.TrimSpace(resolved.RuntimeMode) == "websocket" {
148
+ summary = "Runtime mode is websocket"
149
+ } else {
150
+ summary = "Runtime mode is http"
151
+ }
152
+ return Check{
153
+ Name: "runtime",
154
+ Status: status,
155
+ Summary: summary,
156
+ Details: map[string]any{
157
+ "mode": resolved.RuntimeMode,
158
+ "socket_path": resolved.RuntimeSocketPath,
159
+ },
160
+ }
161
+ }
162
+
163
+ func identityStoreCheck(resolved *config.Resolved) Check {
164
+ manager := identity.NewManager(resolved.Paths)
165
+ indexPath := filepath.Join(resolved.Paths.IdentityDir, identity.IndexFileName)
166
+ identityDirExists := pathExists(resolved.Paths.IdentityDir)
167
+ indexExists := pathExists(indexPath)
168
+ status := "warn"
169
+ summary := "Identity store has not been initialized"
170
+ if identityDirExists || indexExists {
171
+ status = "ok"
172
+ summary = "Identity store path resolved"
173
+ }
174
+ index, indexErr := manager.LoadIndex()
175
+ if indexErr != nil {
176
+ status = "error"
177
+ summary = "Identity index exists but failed to parse"
178
+ }
179
+ current, currentErr := manager.Current()
180
+ if currentErr != nil && !errors.Is(currentErr, identity.ErrNoDefaultIdentity) && len(index.Credentials) > 0 {
181
+ status = "error"
182
+ summary = "Identity index is missing a valid default identity"
183
+ } else if current != nil && !current.UserState.ReadyForMessaging {
184
+ status = "warn"
185
+ summary = "Default identity is local-only and cannot be used for messaging yet"
186
+ }
187
+ return Check{
188
+ Name: "identity_store",
189
+ Status: status,
190
+ Summary: summary,
191
+ Details: map[string]any{
192
+ "identity_dir": resolved.Paths.IdentityDir,
193
+ "dir_exists": identityDirExists,
194
+ "index_path": indexPath,
195
+ "index_exists": indexExists,
196
+ "index_entries": len(index.Credentials),
197
+ "default_identity": current,
198
+ "user_state": defaultIdentityUserState(current),
199
+ "index_error": errorText(indexErr),
200
+ },
201
+ }
202
+ }
203
+
204
+ func defaultIdentityUserState(current *identity.IdentitySummary) any {
205
+ if current == nil {
206
+ return nil
207
+ }
208
+ return current.UserState
209
+ }
210
+
211
+ func sqliteCheck(resolved *config.Resolved) Check {
212
+ databaseExists := pathExists(resolved.Paths.DatabaseFile)
213
+ status := "info"
214
+ summary := "SQLite target path resolved"
215
+ if databaseExists {
216
+ status = "ok"
217
+ summary = "SQLite database file already exists"
218
+ }
219
+ schemaVersion := 0
220
+ schemaError := ""
221
+ if databaseExists {
222
+ db, err := store.OpenReadOnly(resolved.Paths.DatabaseFile)
223
+ if err != nil {
224
+ status = "error"
225
+ summary = "SQLite database file exists but cannot be opened"
226
+ schemaError = err.Error()
227
+ } else {
228
+ defer db.Close()
229
+ version, err := store.CurrentSchemaVersion(db)
230
+ if err != nil {
231
+ status = "error"
232
+ summary = "SQLite database is readable but schema version could not be inspected"
233
+ schemaError = err.Error()
234
+ } else {
235
+ schemaVersion = version
236
+ if version != store.SchemaVersion {
237
+ status = "warn"
238
+ summary = "SQLite database exists but schema version is not current"
239
+ }
240
+ }
241
+ }
242
+ }
243
+ return Check{
244
+ Name: "sqlite",
245
+ Status: status,
246
+ Summary: summary,
247
+ Details: map[string]any{
248
+ "database_file": resolved.Paths.DatabaseFile,
249
+ "exists": databaseExists,
250
+ "parent_dir": filepath.Dir(resolved.Paths.DatabaseFile),
251
+ "schema_version": schemaVersion,
252
+ "target_schema_version": store.SchemaVersion,
253
+ "schema_error": schemaError,
254
+ },
255
+ }
256
+ }
257
+
258
+ func legacyCheck(resolved *config.Resolved) Check {
259
+ manager := identity.NewManager(resolved.Paths)
260
+ scan, scanErr := manager.ScanLegacy()
261
+ credentialsExists := pathExists(resolved.Paths.LegacyCredentialsDir)
262
+ dataExists := pathExists(resolved.Paths.LegacyDataDir)
263
+ legacyDB, dbErr := store.ScanLegacyDatabase(context.Background(), resolved.Paths)
264
+ status := "info"
265
+ summary := "No legacy v1 paths detected"
266
+ if scanErr != nil {
267
+ status = "error"
268
+ summary = "Legacy credential scan failed"
269
+ } else if scan != nil && scan.HasLegacy {
270
+ status = "warn"
271
+ summary = "Legacy awiki-agent-id-message credential layout detected"
272
+ } else if (legacyDB != nil && legacyDB.Exists) || credentialsExists || dataExists {
273
+ status = "warn"
274
+ summary = "Legacy awiki-agent-id-message paths detected"
275
+ }
276
+ return Check{
277
+ Name: "legacy_paths",
278
+ Status: status,
279
+ Summary: summary,
280
+ Details: map[string]any{
281
+ "legacy_credentials_dir": resolved.Paths.LegacyCredentialsDir,
282
+ "credentials_exists": credentialsExists,
283
+ "legacy_data_dir": resolved.Paths.LegacyDataDir,
284
+ "data_exists": dataExists,
285
+ "legacy_scan": scan,
286
+ "scan_error": errorText(scanErr),
287
+ "legacy_database": legacyDB,
288
+ "legacy_database_error": errorText(dbErr),
289
+ },
290
+ }
291
+ }
292
+
293
+ func pathExists(path string) bool {
294
+ if strings.TrimSpace(path) == "" {
295
+ return false
296
+ }
297
+ _, err := os.Stat(path)
298
+ return err == nil
299
+ }
300
+
301
+ func errorText(err error) string {
302
+ if err == nil {
303
+ return ""
304
+ }
305
+ return err.Error()
306
+ }
@@ -0,0 +1,267 @@
1
+ package identity
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "crypto/tls"
7
+ "crypto/x509"
8
+ "encoding/json"
9
+ "errors"
10
+ "fmt"
11
+ "io"
12
+ "net/http"
13
+ "net/url"
14
+ "os"
15
+ "path/filepath"
16
+ "strings"
17
+
18
+ "github.com/agentconnect/awiki-cli/internal/authsdk"
19
+ appconfig "github.com/agentconnect/awiki-cli/internal/config"
20
+ )
21
+
22
+ const (
23
+ didAuthRPCEndpoint = "/user-service/did-auth/rpc"
24
+ handleRPCEndpoint = "/user-service/handle/rpc"
25
+ didProfileRPCEndpoint = "/user-service/did/profile/rpc"
26
+
27
+ emailSendEndpoint = "/user-service/auth/email-send"
28
+ emailStatusEndpoint = "/user-service/auth/email-status"
29
+ phoneBindSendEndpoint = "/user-service/auth/phone-bind-send"
30
+ phoneBindVerifyEndpoint = "/user-service/auth/phone-bind-verify"
31
+ )
32
+
33
+ type ServiceError struct {
34
+ StatusCode int
35
+ RPCCode int
36
+ Message string
37
+ Data any
38
+ }
39
+
40
+ func (e *ServiceError) Error() string {
41
+ if e == nil {
42
+ return ""
43
+ }
44
+ switch {
45
+ case e.RPCCode != 0:
46
+ return fmt.Sprintf("service rpc error %d: %s", e.RPCCode, e.Message)
47
+ case e.StatusCode != 0:
48
+ return fmt.Sprintf("service http error %d: %s", e.StatusCode, e.Message)
49
+ default:
50
+ return e.Message
51
+ }
52
+ }
53
+
54
+ type rpcResponse struct {
55
+ Result json.RawMessage `json:"result"`
56
+ Error *struct {
57
+ Code int `json:"code"`
58
+ Message string `json:"message"`
59
+ Data any `json:"data,omitempty"`
60
+ } `json:"error,omitempty"`
61
+ }
62
+
63
+ type RemoteClient struct {
64
+ baseURL string
65
+ client *http.Client
66
+ }
67
+
68
+ func NewRemoteClient(resolved *appconfig.Resolved) (*RemoteClient, error) {
69
+ if resolved == nil {
70
+ return nil, fmt.Errorf("%w: resolved config is required", ErrInvalidInput)
71
+ }
72
+ httpClient, err := newHTTPClient(resolved.CABundle)
73
+ if err != nil {
74
+ return nil, err
75
+ }
76
+ return &RemoteClient{
77
+ baseURL: strings.TrimRight(resolved.UserServiceURL, "/"),
78
+ client: httpClient,
79
+ }, nil
80
+ }
81
+
82
+ func (c *RemoteClient) Client() *http.Client {
83
+ if c == nil {
84
+ return nil
85
+ }
86
+ return c.client
87
+ }
88
+
89
+ func newHTTPClient(caBundle string) (*http.Client, error) {
90
+ transport := &http.Transport{}
91
+ if strings.TrimSpace(caBundle) != "" {
92
+ rootCAs, err := x509.SystemCertPool()
93
+ if err != nil || rootCAs == nil {
94
+ rootCAs = x509.NewCertPool()
95
+ }
96
+ bundle, err := os.ReadFile(filepath.Clean(caBundle))
97
+ if err != nil {
98
+ return nil, fmt.Errorf("read ca bundle: %w", err)
99
+ }
100
+ if ok := rootCAs.AppendCertsFromPEM(bundle); !ok {
101
+ return nil, fmt.Errorf("invalid ca bundle: %s", caBundle)
102
+ }
103
+ transport.TLSClientConfig = &tls.Config{RootCAs: rootCAs, MinVersion: tls.VersionTLS12}
104
+ }
105
+ return &http.Client{Transport: transport}, nil
106
+ }
107
+
108
+ func (c *RemoteClient) rpcCall(ctx context.Context, endpoint string, method string, params any, bearer string, out any) error {
109
+ payload := map[string]any{
110
+ "jsonrpc": "2.0",
111
+ "method": method,
112
+ "params": params,
113
+ "id": "req-1",
114
+ }
115
+ body, err := json.Marshal(payload)
116
+ if err != nil {
117
+ return err
118
+ }
119
+ request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+endpoint, bytes.NewReader(body))
120
+ if err != nil {
121
+ return err
122
+ }
123
+ request.Header.Set("Content-Type", "application/json")
124
+ if bearer != "" {
125
+ request.Header.Set("Authorization", "Bearer "+bearer)
126
+ }
127
+ response, err := c.client.Do(request)
128
+ if err != nil {
129
+ return err
130
+ }
131
+ defer response.Body.Close()
132
+ raw, err := io.ReadAll(response.Body)
133
+ if err != nil {
134
+ return err
135
+ }
136
+ if response.StatusCode >= 400 {
137
+ return &ServiceError{
138
+ StatusCode: response.StatusCode,
139
+ Message: strings.TrimSpace(string(raw)),
140
+ }
141
+ }
142
+ var decoded rpcResponse
143
+ if err := json.Unmarshal(raw, &decoded); err != nil {
144
+ return fmt.Errorf("parse rpc response: %w", err)
145
+ }
146
+ if decoded.Error != nil {
147
+ return &ServiceError{
148
+ RPCCode: decoded.Error.Code,
149
+ Message: decoded.Error.Message,
150
+ Data: decoded.Error.Data,
151
+ }
152
+ }
153
+ if out == nil {
154
+ return nil
155
+ }
156
+ return json.Unmarshal(decoded.Result, out)
157
+ }
158
+
159
+ func (c *RemoteClient) RPCCall(ctx context.Context, endpoint string, method string, params any, bearer string, out any) error {
160
+ return c.rpcCall(ctx, endpoint, method, params, bearer, out)
161
+ }
162
+
163
+ func (c *RemoteClient) AuthenticatedRPCCall(ctx context.Context, endpoint string, method string, params any, auth *authsdk.Session, out any) error {
164
+ return c.authenticatedRPCCall(ctx, endpoint, method, params, auth, out)
165
+ }
166
+
167
+ func (c *RemoteClient) restPost(ctx context.Context, endpoint string, requestPayload any, bearer string, out any) error {
168
+ body, err := json.Marshal(requestPayload)
169
+ if err != nil {
170
+ return err
171
+ }
172
+ request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+endpoint, bytes.NewReader(body))
173
+ if err != nil {
174
+ return err
175
+ }
176
+ request.Header.Set("Content-Type", "application/json")
177
+ if bearer != "" {
178
+ request.Header.Set("Authorization", "Bearer "+bearer)
179
+ }
180
+ response, err := c.client.Do(request)
181
+ if err != nil {
182
+ return err
183
+ }
184
+ defer response.Body.Close()
185
+ raw, err := io.ReadAll(response.Body)
186
+ if err != nil {
187
+ return err
188
+ }
189
+ if response.StatusCode >= 400 {
190
+ return &ServiceError{
191
+ StatusCode: response.StatusCode,
192
+ Message: strings.TrimSpace(string(raw)),
193
+ }
194
+ }
195
+ if out == nil {
196
+ return nil
197
+ }
198
+ return json.Unmarshal(raw, out)
199
+ }
200
+
201
+ func (c *RemoteClient) RestPost(ctx context.Context, endpoint string, requestPayload any, bearer string, out any) error {
202
+ return c.restPost(ctx, endpoint, requestPayload, bearer, out)
203
+ }
204
+
205
+ func (c *RemoteClient) AuthenticatedRestPost(ctx context.Context, endpoint string, requestPayload any, auth *authsdk.Session, out any) error {
206
+ body, err := json.Marshal(requestPayload)
207
+ if err != nil {
208
+ return err
209
+ }
210
+ requestURL := c.baseURL + endpoint
211
+ if err := auth.DoJSON(ctx, c.client, http.MethodPost, requestURL, requestPayload, out); err != nil {
212
+ var httpErr *authsdk.HTTPError
213
+ if errors.As(err, &httpErr) {
214
+ return &ServiceError{StatusCode: httpErr.StatusCode, Message: httpErr.Message}
215
+ }
216
+ return err
217
+ }
218
+ _ = body
219
+ return nil
220
+ }
221
+
222
+ func (c *RemoteClient) restGet(ctx context.Context, endpoint string, query url.Values, out any) error {
223
+ target := c.baseURL + endpoint
224
+ if len(query) > 0 {
225
+ target += "?" + query.Encode()
226
+ }
227
+ request, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
228
+ if err != nil {
229
+ return err
230
+ }
231
+ response, err := c.client.Do(request)
232
+ if err != nil {
233
+ return err
234
+ }
235
+ defer response.Body.Close()
236
+ raw, err := io.ReadAll(response.Body)
237
+ if err != nil {
238
+ return err
239
+ }
240
+ if response.StatusCode >= 400 {
241
+ return &ServiceError{
242
+ StatusCode: response.StatusCode,
243
+ Message: strings.TrimSpace(string(raw)),
244
+ }
245
+ }
246
+ return json.Unmarshal(raw, out)
247
+ }
248
+
249
+ func (c *RemoteClient) RestGet(ctx context.Context, endpoint string, query url.Values, out any) error {
250
+ return c.restGet(ctx, endpoint, query, out)
251
+ }
252
+
253
+ func (c *RemoteClient) authenticatedRPCCall(ctx context.Context, endpoint string, method string, params any, auth *authsdk.Session, out any) error {
254
+ requestURL := c.baseURL + endpoint
255
+ if err := auth.DoJSONRPC(ctx, c.client, requestURL, http.MethodPost, method, params, out); err != nil {
256
+ var rpcErr *authsdk.RPCError
257
+ if errors.As(err, &rpcErr) {
258
+ return &ServiceError{RPCCode: rpcErr.Code, Message: rpcErr.Message, Data: rpcErr.Data}
259
+ }
260
+ var httpErr *authsdk.HTTPError
261
+ if errors.As(err, &httpErr) {
262
+ return &ServiceError{StatusCode: httpErr.StatusCode, Message: httpErr.Message}
263
+ }
264
+ return err
265
+ }
266
+ return nil
267
+ }
@@ -0,0 +1,85 @@
1
+ package identity
2
+
3
+ import (
4
+ "crypto/rand"
5
+ "encoding/hex"
6
+ "fmt"
7
+ "strings"
8
+
9
+ "github.com/agentconnect/awiki-cli/internal/anpsdk"
10
+ )
11
+
12
+ func GenerateIdentity(options GenerateOptions) (*GeneratedIdentity, error) {
13
+ hostname := strings.TrimSpace(options.Hostname)
14
+ if hostname == "" {
15
+ return nil, fmt.Errorf("%w: hostname is required", ErrInvalidInput)
16
+ }
17
+ pathSegments := clonePathPrefix(options.PathPrefix)
18
+ if len(pathSegments) == 0 {
19
+ pathSegments = []string{"user"}
20
+ }
21
+ proofDomain := strings.TrimSpace(options.ProofDomain)
22
+ if proofDomain == "" {
23
+ proofDomain = hostname
24
+ }
25
+
26
+ bundle, err := anpsdk.CreateDidWBADocumentWithKeyBinding(hostname, anpsdk.DidDocumentOptions{
27
+ PathSegments: pathSegments,
28
+ Domain: proofDomain,
29
+ Challenge: randomHex(16),
30
+ })
31
+ if err != nil {
32
+ return nil, fmt.Errorf("generate did document: %w", err)
33
+ }
34
+
35
+ did := stringValue(bundle.DidDocument["id"], "")
36
+ if did == "" {
37
+ return nil, fmt.Errorf("generated did document is missing id")
38
+ }
39
+ key1, ok := bundle.Keys["key-1"]
40
+ if !ok {
41
+ return nil, fmt.Errorf("generated did document is missing key-1")
42
+ }
43
+
44
+ generated := &GeneratedIdentity{
45
+ DID: did,
46
+ UniqueID: didSuffix(did),
47
+ DIDDocument: bundle.DidDocument,
48
+ Key1PrivatePEM: key1.PrivateKeyPEM,
49
+ Key1PublicPEM: key1.PublicKeyPEM,
50
+ }
51
+ if key2, ok := bundle.Keys["key-2"]; ok {
52
+ generated.E2EESigningPrivatePEM = key2.PrivateKeyPEM
53
+ }
54
+ if key3, ok := bundle.Keys["key-3"]; ok {
55
+ generated.E2EEAgreementPrivatePEM = key3.PrivateKeyPEM
56
+ }
57
+ return generated, nil
58
+ }
59
+
60
+ func didSuffix(did string) string {
61
+ index := strings.LastIndex(did, ":")
62
+ if index == -1 || index == len(did)-1 {
63
+ return did
64
+ }
65
+ return did[index+1:]
66
+ }
67
+
68
+ func clonePathPrefix(pathPrefix []string) []string {
69
+ cloned := make([]string, 0, len(pathPrefix))
70
+ for _, segment := range pathPrefix {
71
+ trimmed := strings.TrimSpace(segment)
72
+ if trimmed != "" {
73
+ cloned = append(cloned, trimmed)
74
+ }
75
+ }
76
+ return cloned
77
+ }
78
+
79
+ func randomHex(numBytes int) string {
80
+ buffer := make([]byte, numBytes)
81
+ if _, err := rand.Read(buffer); err != nil {
82
+ return ""
83
+ }
84
+ return hex.EncodeToString(buffer)
85
+ }
@@ -0,0 +1,50 @@
1
+ package identity
2
+
3
+ import (
4
+ "testing"
5
+
6
+ anp "github.com/agent-network-protocol/anp/golang"
7
+ anpauth "github.com/agent-network-protocol/anp/golang/authentication"
8
+ anpproof "github.com/agent-network-protocol/anp/golang/proof"
9
+ )
10
+
11
+ func TestGenerateIdentity(t *testing.T) {
12
+ t.Parallel()
13
+
14
+ generated, err := GenerateIdentity(GenerateOptions{
15
+ Hostname: "awiki.ai",
16
+ PathPrefix: []string{"user"},
17
+ ProofDomain: "awiki.ai",
18
+ })
19
+ if err != nil {
20
+ t.Fatalf("GenerateIdentity() error = %v", err)
21
+ }
22
+ if generated.DID == "" || generated.UniqueID == "" {
23
+ t.Fatalf("generated identity is missing did/unique_id: %+v", generated)
24
+ }
25
+ if generated.DIDDocument == nil {
26
+ t.Fatal("generated identity is missing did_document")
27
+ }
28
+ if got := stringValue(generated.DIDDocument["id"], ""); got != generated.DID {
29
+ t.Fatalf("did document id mismatch: got %q want %q", got, generated.DID)
30
+ }
31
+ if !anpauth.ValidateDIDDocumentBinding(generated.DIDDocument, true) {
32
+ t.Fatal("generated did document failed did:wba binding validation")
33
+ }
34
+ publicKey, err := anp.PublicKeyFromPEM(generated.Key1PublicPEM)
35
+ if err != nil {
36
+ t.Fatalf("PublicKeyFromPEM() error = %v", err)
37
+ }
38
+ if !anpproof.VerifyW3CProof(generated.DIDDocument, publicKey, anpproof.VerificationOptions{
39
+ ExpectedPurpose: "assertionMethod",
40
+ ExpectedDomain: "awiki.ai",
41
+ }) {
42
+ t.Fatal("generated did proof verification failed")
43
+ }
44
+ if generated.E2EESigningPrivatePEM == "" {
45
+ t.Fatal("generated identity is missing e2ee signing private key")
46
+ }
47
+ if generated.E2EEAgreementPrivatePEM == "" {
48
+ t.Fatal("generated identity is missing e2ee agreement private key")
49
+ }
50
+ }