@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.
- package/.github/workflows/release.yml +44 -0
- package/.goreleaser.yml +44 -0
- package/AGENTS.md +60 -0
- package/CLAUDE.md +192 -0
- package/README.md +2 -0
- package/docs/architecture/awiki-command-v2.md +955 -0
- package/docs/architecture/awiki-skill-architecture.md +475 -0
- package/docs/architecture/awiki-v2-architecture.md +1063 -0
- package/docs/architecture//345/217/202/350/200/203/346/226/207/346/241/243/cli-init.md +1008 -0
- package/docs/architecture//345/217/202/350/200/203/346/226/207/346/241/243/output-format.md +407 -0
- package/docs/architecture//345/217/202/350/200/203/346/226/207/346/241/243/overall-init.md +741 -0
- package/docs/harness/review-spec.md +474 -0
- package/docs/installation.md +372 -0
- package/docs/plan/awiki-v2-implementation-plan.md +903 -0
- package/docs/plan/phase-0/adr-index.md +56 -0
- package/docs/plan/phase-0/audit-findings.md +251 -0
- package/docs/plan/phase-0/capability-mapping.md +108 -0
- package/docs/plan/phase-0/implementation-constraints.md +363 -0
- package/docs/publish.md +169 -0
- package/go.mod +29 -0
- package/go.sum +73 -0
- package/internal/anpsdk/registry.go +63 -0
- package/internal/authsdk/session.go +351 -0
- package/internal/buildinfo/buildinfo.go +34 -0
- package/internal/cli/app.go +136 -0
- package/internal/cli/app_test.go +88 -0
- package/internal/cli/debug.go +104 -0
- package/internal/cli/group.go +263 -0
- package/internal/cli/id.go +473 -0
- package/internal/cli/init.go +134 -0
- package/internal/cli/msg.go +228 -0
- package/internal/cli/page.go +267 -0
- package/internal/cli/root.go +499 -0
- package/internal/cli/runtime.go +232 -0
- package/internal/cli/upgrade.go +60 -0
- package/internal/cmdmeta/catalog.go +203 -0
- package/internal/cmdmeta/catalog_test.go +21 -0
- package/internal/config/config.go +399 -0
- package/internal/config/config_test.go +104 -0
- package/internal/config/write.go +37 -0
- package/internal/content/service.go +314 -0
- package/internal/content/service_test.go +165 -0
- package/internal/content/types.go +44 -0
- package/internal/docs/topics.go +110 -0
- package/internal/doctor/doctor.go +306 -0
- package/internal/identity/client.go +267 -0
- package/internal/identity/did.go +85 -0
- package/internal/identity/did_test.go +50 -0
- package/internal/identity/layout.go +206 -0
- package/internal/identity/legacy.go +378 -0
- package/internal/identity/public.go +70 -0
- package/internal/identity/public_test.go +73 -0
- package/internal/identity/readiness.go +74 -0
- package/internal/identity/service.go +826 -0
- package/internal/identity/store.go +385 -0
- package/internal/identity/store_test.go +180 -0
- package/internal/identity/types.go +204 -0
- package/internal/message/auth.go +167 -0
- package/internal/message/group_service.go +838 -0
- package/internal/message/group_wire.go +350 -0
- package/internal/message/group_wire_test.go +67 -0
- package/internal/message/helpers.go +61 -0
- package/internal/message/http_client.go +334 -0
- package/internal/message/proof.go +156 -0
- package/internal/message/proof_test.go +61 -0
- package/internal/message/service.go +696 -0
- package/internal/message/service_test.go +97 -0
- package/internal/message/types.go +155 -0
- package/internal/message/wire.go +100 -0
- package/internal/message/wire_test.go +49 -0
- package/internal/message/ws_proxy_client.go +151 -0
- package/internal/output/output.go +350 -0
- package/internal/output/output_test.go +48 -0
- package/internal/runtime/config.go +117 -0
- package/internal/runtime/config_test.go +46 -0
- package/internal/runtime/listener/files.go +65 -0
- package/internal/runtime/listener/manager.go +142 -0
- package/internal/runtime/listener/server.go +983 -0
- package/internal/runtime/listener/server_test.go +319 -0
- package/internal/runtime/listener/sysproc_unix.go +17 -0
- package/internal/runtime/listener/sysproc_windows.go +13 -0
- package/internal/runtime/listener/types.go +21 -0
- package/internal/runtime/listener/wsclient.go +299 -0
- package/internal/runtime/listener/wsclient_test.go +41 -0
- package/internal/store/dao.go +632 -0
- package/internal/store/dao_test.go +87 -0
- package/internal/store/helpers.go +197 -0
- package/internal/store/import.go +499 -0
- package/internal/store/import_test.go +103 -0
- package/internal/store/open.go +71 -0
- package/internal/store/query.go +151 -0
- package/internal/store/schema.go +277 -0
- package/internal/store/schema_test.go +56 -0
- package/internal/store/types.go +177 -0
- package/internal/update/update.go +368 -0
- package/package.json +17 -0
- package/scripts/install.js +171 -0
- package/scripts/release/release-prerelease.sh +86 -0
- package/scripts/release/tag-release.sh +66 -0
- package/scripts/release/withdraw-release.sh +78 -0
- package/scripts/run.js +69 -0
- package/skills/README.md +32 -0
- package/skills/awiki-bundle/SKILL.md +76 -0
- package/skills/awiki-debug/SKILL.md +80 -0
- package/skills/awiki-group/SKILL.md +111 -0
- package/skills/awiki-id/SKILL.md +123 -0
- package/skills/awiki-msg/SKILL.md +131 -0
- package/skills/awiki-page/SKILL.md +93 -0
- package/skills/awiki-people/SKILL.md +66 -0
- package/skills/awiki-runtime/SKILL.md +137 -0
- package/skills/awiki-shared/SKILL.md +124 -0
- package/skills/awiki-workflow-discovery/SKILL.md +93 -0
- package/skills/awiki-workflow-onboarding/SKILL.md +119 -0
- package/skills/manifests/skills.yaml +260 -0
- package/skills/templates/bundle-skill-template.md +42 -0
- package/skills/templates/debug-skill-template.md +44 -0
- package/skills/templates/domain-skill-template.md +56 -0
- package/skills/templates/shared-skill-template.md +46 -0
- package/skills/templates/workflow-skill-template.md +46 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
package anpsdk
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
anp "github.com/agent-network-protocol/anp/golang"
|
|
5
|
+
anpauth "github.com/agent-network-protocol/anp/golang/authentication"
|
|
6
|
+
directe2ee "github.com/agent-network-protocol/anp/golang/direct_e2ee"
|
|
7
|
+
anpproof "github.com/agent-network-protocol/anp/golang/proof"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
const (
|
|
11
|
+
ModulePath = "github.com/agent-network-protocol/anp/golang"
|
|
12
|
+
ModuleVersion = "v0.7.2"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
type (
|
|
16
|
+
KeyType = anp.KeyType
|
|
17
|
+
PrivateKeyMaterial = anp.PrivateKeyMaterial
|
|
18
|
+
PublicKeyMaterial = anp.PublicKeyMaterial
|
|
19
|
+
GeneratedKeyPairPEM = anp.GeneratedKeyPairPEM
|
|
20
|
+
DidDocumentBundle = anpauth.DidDocumentBundle
|
|
21
|
+
DidDocumentOptions = anpauth.DidDocumentOptions
|
|
22
|
+
DIDWbaAuthHeader = anpauth.DIDWbaAuthHeader
|
|
23
|
+
DidWbaVerifierConfig = anpauth.DidWbaVerifierConfig
|
|
24
|
+
AuthMode = anpauth.AuthMode
|
|
25
|
+
HttpSignatureOptions = anpauth.HttpSignatureOptions
|
|
26
|
+
MessageServiceE2EEClient = directe2ee.MessageServiceDirectE2eeClient
|
|
27
|
+
PrekeyBundle = directe2ee.PrekeyBundle
|
|
28
|
+
DirectSessionState = directe2ee.DirectSessionState
|
|
29
|
+
IMProof = anpproof.IMProof
|
|
30
|
+
IMGenerationOptions = anpproof.IMGenerationOptions
|
|
31
|
+
ParsedIMSignatureInput = anpproof.ParsedIMSignatureInput
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
var (
|
|
35
|
+
KeyTypeSecp256k1 = anp.KeyTypeSecp256k1
|
|
36
|
+
KeyTypeSecp256r1 = anp.KeyTypeSecp256r1
|
|
37
|
+
KeyTypeEd25519 = anp.KeyTypeEd25519
|
|
38
|
+
KeyTypeX25519 = anp.KeyTypeX25519
|
|
39
|
+
AuthModeHTTPSignatures = anpauth.AuthModeHTTPSignatures
|
|
40
|
+
AuthModeLegacyDidWba = anpauth.AuthModeLegacyDidWba
|
|
41
|
+
AuthModeAuto = anpauth.AuthModeAuto
|
|
42
|
+
GenerateKeyPairPEM = anp.GenerateKeyPairPEM
|
|
43
|
+
PrivateKeyFromPEM = anp.PrivateKeyFromPEM
|
|
44
|
+
PublicKeyFromPEM = anp.PublicKeyFromPEM
|
|
45
|
+
CreateDidWBADocument = anpauth.CreateDidWBADocument
|
|
46
|
+
CreateDidWBADocumentWithKeyBinding = anpauth.CreateDidWBADocumentWithKeyBinding
|
|
47
|
+
ResolveDidDocument = anpauth.ResolveDidDocument
|
|
48
|
+
ResolveDidDocumentWithOptions = anpauth.ResolveDidDocumentWithOptions
|
|
49
|
+
GenerateAuthHeader = anpauth.GenerateAuthHeader
|
|
50
|
+
GenerateHTTPSignatureHeaders = anpauth.GenerateHTTPSignatureHeaders
|
|
51
|
+
NewDIDWbaAuthHeader = anpauth.NewDIDWbaAuthHeader
|
|
52
|
+
NewDidWbaVerifier = anpauth.NewDidWbaVerifier
|
|
53
|
+
NewFileSessionStore = directe2ee.NewFileSessionStore
|
|
54
|
+
NewFileSignedPrekeyStore = directe2ee.NewFileSignedPrekeyStore
|
|
55
|
+
NewFilePendingOutboundStore = directe2ee.NewFilePendingOutboundStore
|
|
56
|
+
NewMessageServiceDirectE2eeClient = directe2ee.NewMessageServiceDirectE2eeClient
|
|
57
|
+
BuildIMContentDigest = anpproof.BuildIMContentDigest
|
|
58
|
+
BuildIMSignatureInput = anpproof.BuildIMSignatureInput
|
|
59
|
+
ParseIMSignatureInput = anpproof.ParseIMSignatureInput
|
|
60
|
+
EncodeIMSignature = anpproof.EncodeIMSignature
|
|
61
|
+
GenerateIMProof = anpproof.GenerateIMProof
|
|
62
|
+
VerifyIMProofWithDocument = anpproof.VerifyIMProofWithDocument
|
|
63
|
+
)
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
package authsdk
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"context"
|
|
6
|
+
"encoding/asn1"
|
|
7
|
+
"encoding/json"
|
|
8
|
+
"encoding/pem"
|
|
9
|
+
"fmt"
|
|
10
|
+
"io"
|
|
11
|
+
"net/http"
|
|
12
|
+
"os"
|
|
13
|
+
"path/filepath"
|
|
14
|
+
"strings"
|
|
15
|
+
|
|
16
|
+
"github.com/agentconnect/awiki-cli/internal/anpsdk"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
type Session struct {
|
|
20
|
+
helper *anpsdk.DIDWbaAuthHeader
|
|
21
|
+
identityName string
|
|
22
|
+
did string
|
|
23
|
+
jwtToken string
|
|
24
|
+
persistToken func(string) error
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func NewSession(didDocumentPath string, privateKeyPath string, identityName string, did string, jwtToken string, persistToken func(string) error) *Session {
|
|
28
|
+
normalizedKeyPath := privateKeyPath
|
|
29
|
+
if rewrittenPath, err := normalizeSecp256k1PrivateKeyPath(privateKeyPath); err == nil && strings.TrimSpace(rewrittenPath) != "" {
|
|
30
|
+
normalizedKeyPath = rewrittenPath
|
|
31
|
+
}
|
|
32
|
+
helper := anpsdk.NewDIDWbaAuthHeader(didDocumentPath, normalizedKeyPath, anpsdk.AuthModeHTTPSignatures)
|
|
33
|
+
session := &Session{
|
|
34
|
+
helper: helper,
|
|
35
|
+
identityName: identityName,
|
|
36
|
+
did: did,
|
|
37
|
+
jwtToken: jwtToken,
|
|
38
|
+
persistToken: persistToken,
|
|
39
|
+
}
|
|
40
|
+
if strings.TrimSpace(jwtToken) != "" {
|
|
41
|
+
headers := map[string]string{"Authorization": "Bearer " + jwtToken}
|
|
42
|
+
helper.UpdateToken("https://awiki.ai", headers)
|
|
43
|
+
}
|
|
44
|
+
return session
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func (s *Session) SetBearer(serverURL string, token string) {
|
|
48
|
+
if s == nil || s.helper == nil || strings.TrimSpace(token) == "" {
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
s.helper.UpdateToken(serverURL, map[string]string{"Authorization": "Bearer " + token})
|
|
52
|
+
s.jwtToken = token
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func (s *Session) CurrentJWT() string {
|
|
56
|
+
if s == nil {
|
|
57
|
+
return ""
|
|
58
|
+
}
|
|
59
|
+
return s.jwtToken
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
func (s *Session) Headers(serverURL string, method string, body []byte, forceNew bool) (map[string]string, error) {
|
|
63
|
+
if s == nil {
|
|
64
|
+
return nil, fmt.Errorf("auth session is not configured")
|
|
65
|
+
}
|
|
66
|
+
if strings.TrimSpace(s.jwtToken) != "" && !forceNew {
|
|
67
|
+
return map[string]string{
|
|
68
|
+
"Content-Type": "application/json",
|
|
69
|
+
"Authorization": "Bearer " + s.jwtToken,
|
|
70
|
+
}, nil
|
|
71
|
+
}
|
|
72
|
+
if s.helper == nil {
|
|
73
|
+
return nil, fmt.Errorf("auth session is not configured")
|
|
74
|
+
}
|
|
75
|
+
headers := map[string]string{"Content-Type": "application/json"}
|
|
76
|
+
return s.helper.GetAuthHeader(serverURL, forceNew, method, headers, body)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
func (s *Session) ShouldRetryAfter401(headers http.Header) bool {
|
|
80
|
+
if s == nil || s.helper == nil {
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
return s.helper.ShouldRetryAfter401(flattenHeaders(headers))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
func (s *Session) ChallengeHeaders(serverURL string, headers http.Header, method string, body []byte) (map[string]string, error) {
|
|
87
|
+
if s == nil || s.helper == nil {
|
|
88
|
+
return nil, fmt.Errorf("auth session is not configured")
|
|
89
|
+
}
|
|
90
|
+
baseHeaders := map[string]string{"Content-Type": "application/json"}
|
|
91
|
+
return s.helper.GetChallengeAuthHeader(serverURL, flattenHeaders(headers), method, baseHeaders, body)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
func (s *Session) ClearToken(serverURL string) {
|
|
95
|
+
if s == nil || s.helper == nil {
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
s.helper.ClearToken(serverURL)
|
|
99
|
+
s.jwtToken = ""
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func (s *Session) CaptureToken(serverURL string, headers http.Header) string {
|
|
103
|
+
if s == nil || s.helper == nil {
|
|
104
|
+
return ""
|
|
105
|
+
}
|
|
106
|
+
token := s.helper.UpdateToken(serverURL, flattenHeaders(headers))
|
|
107
|
+
if token == "" {
|
|
108
|
+
return ""
|
|
109
|
+
}
|
|
110
|
+
s.jwtToken = token
|
|
111
|
+
if s.persistToken != nil {
|
|
112
|
+
_ = s.persistToken(token)
|
|
113
|
+
}
|
|
114
|
+
return token
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
func (s *Session) DoJSONRPC(ctx context.Context, client *http.Client, requestURL string, method string, rpcMethod string, params any, out any) error {
|
|
118
|
+
if client == nil {
|
|
119
|
+
client = http.DefaultClient
|
|
120
|
+
}
|
|
121
|
+
payload := map[string]any{"jsonrpc": "2.0", "id": "req-1", "method": rpcMethod, "params": params}
|
|
122
|
+
body, err := json.Marshal(payload)
|
|
123
|
+
if err != nil {
|
|
124
|
+
return err
|
|
125
|
+
}
|
|
126
|
+
response, err := s.doRequest(ctx, client, requestURL, method, body)
|
|
127
|
+
if err != nil {
|
|
128
|
+
return err
|
|
129
|
+
}
|
|
130
|
+
defer response.Body.Close()
|
|
131
|
+
raw, err := io.ReadAll(response.Body)
|
|
132
|
+
if err != nil {
|
|
133
|
+
return err
|
|
134
|
+
}
|
|
135
|
+
var decoded struct {
|
|
136
|
+
Result json.RawMessage `json:"result"`
|
|
137
|
+
Error *struct {
|
|
138
|
+
Code int `json:"code"`
|
|
139
|
+
Message string `json:"message"`
|
|
140
|
+
Data any `json:"data,omitempty"`
|
|
141
|
+
} `json:"error,omitempty"`
|
|
142
|
+
}
|
|
143
|
+
if err := json.Unmarshal(raw, &decoded); err != nil {
|
|
144
|
+
return err
|
|
145
|
+
}
|
|
146
|
+
if decoded.Error != nil {
|
|
147
|
+
return &RPCError{Code: decoded.Error.Code, Message: decoded.Error.Message, Data: decoded.Error.Data}
|
|
148
|
+
}
|
|
149
|
+
if out == nil {
|
|
150
|
+
return nil
|
|
151
|
+
}
|
|
152
|
+
return json.Unmarshal(decoded.Result, out)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
func (s *Session) EnsureJWT(ctx context.Context, client *http.Client, requestURL string) (string, error) {
|
|
156
|
+
var result map[string]any
|
|
157
|
+
if err := s.DoJSONRPC(ctx, client, requestURL, http.MethodPost, "get_me", map[string]any{}, &result); err != nil {
|
|
158
|
+
return "", err
|
|
159
|
+
}
|
|
160
|
+
return s.jwtToken, nil
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
func (s *Session) DoJSON(ctx context.Context, client *http.Client, method string, requestURL string, payload any, out any) error {
|
|
164
|
+
if client == nil {
|
|
165
|
+
client = http.DefaultClient
|
|
166
|
+
}
|
|
167
|
+
body, err := json.Marshal(payload)
|
|
168
|
+
if err != nil {
|
|
169
|
+
return err
|
|
170
|
+
}
|
|
171
|
+
response, err := s.doRequest(ctx, client, requestURL, method, body)
|
|
172
|
+
if err != nil {
|
|
173
|
+
return err
|
|
174
|
+
}
|
|
175
|
+
defer response.Body.Close()
|
|
176
|
+
if out == nil {
|
|
177
|
+
_, _ = io.Copy(io.Discard, response.Body)
|
|
178
|
+
return nil
|
|
179
|
+
}
|
|
180
|
+
return json.NewDecoder(response.Body).Decode(out)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
func (s *Session) doRequest(ctx context.Context, client *http.Client, requestURL string, method string, body []byte) (*http.Response, error) {
|
|
184
|
+
headers, err := s.Headers(requestURL, method, body, false)
|
|
185
|
+
if err != nil {
|
|
186
|
+
return nil, err
|
|
187
|
+
}
|
|
188
|
+
response, err := doHTTPRequest(ctx, client, method, requestURL, body, headers)
|
|
189
|
+
if err != nil {
|
|
190
|
+
return nil, err
|
|
191
|
+
}
|
|
192
|
+
if response.StatusCode == http.StatusUnauthorized {
|
|
193
|
+
defer response.Body.Close()
|
|
194
|
+
if s.ShouldRetryAfter401(response.Header) {
|
|
195
|
+
headers, err = s.ChallengeHeaders(requestURL, response.Header, method, body)
|
|
196
|
+
if err != nil {
|
|
197
|
+
return nil, err
|
|
198
|
+
}
|
|
199
|
+
} else if s.helper != nil {
|
|
200
|
+
s.ClearToken(requestURL)
|
|
201
|
+
headers, err = s.Headers(requestURL, method, body, true)
|
|
202
|
+
if err != nil {
|
|
203
|
+
return nil, err
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
raw, _ := io.ReadAll(response.Body)
|
|
207
|
+
return nil, &HTTPError{StatusCode: response.StatusCode, Message: strings.TrimSpace(string(raw))}
|
|
208
|
+
}
|
|
209
|
+
response, err = doHTTPRequest(ctx, client, method, requestURL, body, headers)
|
|
210
|
+
if err != nil {
|
|
211
|
+
return nil, err
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if response.StatusCode >= 400 {
|
|
215
|
+
defer response.Body.Close()
|
|
216
|
+
raw, _ := io.ReadAll(response.Body)
|
|
217
|
+
return nil, &HTTPError{StatusCode: response.StatusCode, Message: strings.TrimSpace(string(raw))}
|
|
218
|
+
}
|
|
219
|
+
s.CaptureToken(requestURL, response.Header)
|
|
220
|
+
return response, nil
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
func doHTTPRequest(ctx context.Context, client *http.Client, method string, requestURL string, body []byte, headers map[string]string) (*http.Response, error) {
|
|
224
|
+
request, err := http.NewRequestWithContext(ctx, method, requestURL, bytes.NewReader(body))
|
|
225
|
+
if err != nil {
|
|
226
|
+
return nil, err
|
|
227
|
+
}
|
|
228
|
+
for key, value := range headers {
|
|
229
|
+
request.Header.Set(key, value)
|
|
230
|
+
}
|
|
231
|
+
return client.Do(request)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
func flattenHeaders(headers http.Header) map[string]string {
|
|
235
|
+
values := make(map[string]string, len(headers))
|
|
236
|
+
for key, item := range headers {
|
|
237
|
+
if len(item) == 0 {
|
|
238
|
+
continue
|
|
239
|
+
}
|
|
240
|
+
values[key] = item[0]
|
|
241
|
+
}
|
|
242
|
+
return values
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
type pkcs8PrivateKeyInfo struct {
|
|
246
|
+
Version int
|
|
247
|
+
Algorithm pkixAlgorithmIdentifier
|
|
248
|
+
PrivateKey []byte
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
type pkixAlgorithmIdentifier struct {
|
|
252
|
+
Algorithm asn1.ObjectIdentifier
|
|
253
|
+
Parameters asn1.RawValue `asn1:"optional"`
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
type sec1ECPrivateKey struct {
|
|
257
|
+
Version int
|
|
258
|
+
PrivateKey []byte
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
func normalizeSecp256k1PrivateKeyPath(privateKeyPath string) (string, error) {
|
|
262
|
+
privateKeyPath = strings.TrimSpace(privateKeyPath)
|
|
263
|
+
if privateKeyPath == "" {
|
|
264
|
+
return "", nil
|
|
265
|
+
}
|
|
266
|
+
raw, err := os.ReadFile(filepath.Clean(privateKeyPath))
|
|
267
|
+
if err != nil {
|
|
268
|
+
return "", err
|
|
269
|
+
}
|
|
270
|
+
normalizedPEM, _, err := NormalizeSecp256k1PrivatePEM(string(raw))
|
|
271
|
+
if err != nil {
|
|
272
|
+
return "", err
|
|
273
|
+
}
|
|
274
|
+
if strings.TrimSpace(normalizedPEM) == strings.TrimSpace(string(raw)) {
|
|
275
|
+
return privateKeyPath, nil
|
|
276
|
+
}
|
|
277
|
+
dir := filepath.Dir(privateKeyPath)
|
|
278
|
+
target := filepath.Join(dir, ".awiki-cli-key-1-private-normalized.pem")
|
|
279
|
+
if writeErr := os.WriteFile(target, []byte(normalizedPEM), 0o600); writeErr != nil {
|
|
280
|
+
return "", writeErr
|
|
281
|
+
}
|
|
282
|
+
return target, nil
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
func NormalizeSecp256k1PrivatePEM(pemText string) (string, []byte, error) {
|
|
286
|
+
block, _ := pem.Decode([]byte(strings.TrimSpace(pemText)))
|
|
287
|
+
if block == nil {
|
|
288
|
+
return "", nil, fmt.Errorf("invalid key-1 private key pem")
|
|
289
|
+
}
|
|
290
|
+
switch block.Type {
|
|
291
|
+
case "ANP SECP256K1 PRIVATE KEY":
|
|
292
|
+
return strings.TrimSpace(pemText), append([]byte(nil), block.Bytes...), nil
|
|
293
|
+
case "PRIVATE KEY":
|
|
294
|
+
scalar, err := extractSecp256k1ScalarFromPKCS8(block.Bytes)
|
|
295
|
+
if err != nil {
|
|
296
|
+
return "", nil, err
|
|
297
|
+
}
|
|
298
|
+
normalized := pem.EncodeToMemory(&pem.Block{Type: "ANP SECP256K1 PRIVATE KEY", Bytes: scalar})
|
|
299
|
+
return string(normalized), scalar, nil
|
|
300
|
+
default:
|
|
301
|
+
return "", nil, fmt.Errorf("invalid key-1 private key label: %s", block.Type)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
func extractSecp256k1ScalarFromPKCS8(der []byte) ([]byte, error) {
|
|
306
|
+
var info pkcs8PrivateKeyInfo
|
|
307
|
+
if _, err := asn1.Unmarshal(der, &info); err != nil {
|
|
308
|
+
return nil, fmt.Errorf("parse pkcs8 private key: %w", err)
|
|
309
|
+
}
|
|
310
|
+
var sec1 sec1ECPrivateKey
|
|
311
|
+
if _, err := asn1.Unmarshal(info.PrivateKey, &sec1); err != nil {
|
|
312
|
+
return nil, fmt.Errorf("parse embedded sec1 private key: %w", err)
|
|
313
|
+
}
|
|
314
|
+
if len(sec1.PrivateKey) == 0 {
|
|
315
|
+
return nil, fmt.Errorf("embedded sec1 private key is empty")
|
|
316
|
+
}
|
|
317
|
+
if len(sec1.PrivateKey) > 32 {
|
|
318
|
+
sec1.PrivateKey = sec1.PrivateKey[len(sec1.PrivateKey)-32:]
|
|
319
|
+
}
|
|
320
|
+
if len(sec1.PrivateKey) < 32 {
|
|
321
|
+
padded := make([]byte, 32)
|
|
322
|
+
copy(padded[32-len(sec1.PrivateKey):], sec1.PrivateKey)
|
|
323
|
+
sec1.PrivateKey = padded
|
|
324
|
+
}
|
|
325
|
+
return append([]byte(nil), sec1.PrivateKey...), nil
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
type HTTPError struct {
|
|
329
|
+
StatusCode int
|
|
330
|
+
Message string
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
func (e *HTTPError) Error() string {
|
|
334
|
+
if e == nil {
|
|
335
|
+
return ""
|
|
336
|
+
}
|
|
337
|
+
return fmt.Sprintf("http error %d: %s", e.StatusCode, e.Message)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
type RPCError struct {
|
|
341
|
+
Code int
|
|
342
|
+
Message string
|
|
343
|
+
Data any
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
func (e *RPCError) Error() string {
|
|
347
|
+
if e == nil {
|
|
348
|
+
return ""
|
|
349
|
+
}
|
|
350
|
+
return fmt.Sprintf("rpc error %d: %s", e.Code, e.Message)
|
|
351
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
package buildinfo
|
|
2
|
+
|
|
3
|
+
import "runtime"
|
|
4
|
+
|
|
5
|
+
var (
|
|
6
|
+
Version = "dev"
|
|
7
|
+
Commit = "unknown"
|
|
8
|
+
BuildDate = "unknown"
|
|
9
|
+
CGOEnabled = "unknown"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
type Info struct {
|
|
13
|
+
Version string `json:"version"`
|
|
14
|
+
Commit string `json:"commit"`
|
|
15
|
+
BuildDate string `json:"build_date"`
|
|
16
|
+
GoVersion string `json:"go_version"`
|
|
17
|
+
GOOS string `json:"goos"`
|
|
18
|
+
GOARCH string `json:"goarch"`
|
|
19
|
+
Compiler string `json:"compiler"`
|
|
20
|
+
CGOEnabled string `json:"cgo_enabled"`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func Current() Info {
|
|
24
|
+
return Info{
|
|
25
|
+
Version: Version,
|
|
26
|
+
Commit: Commit,
|
|
27
|
+
BuildDate: BuildDate,
|
|
28
|
+
GoVersion: runtime.Version(),
|
|
29
|
+
GOOS: runtime.GOOS,
|
|
30
|
+
GOARCH: runtime.GOARCH,
|
|
31
|
+
Compiler: runtime.Compiler,
|
|
32
|
+
CGOEnabled: CGOEnabled,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
package cli
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"errors"
|
|
5
|
+
"fmt"
|
|
6
|
+
"os"
|
|
7
|
+
"strings"
|
|
8
|
+
|
|
9
|
+
"github.com/agentconnect/awiki-cli/internal/buildinfo"
|
|
10
|
+
"github.com/agentconnect/awiki-cli/internal/cmdmeta"
|
|
11
|
+
appconfig "github.com/agentconnect/awiki-cli/internal/config"
|
|
12
|
+
docindex "github.com/agentconnect/awiki-cli/internal/docs"
|
|
13
|
+
"github.com/agentconnect/awiki-cli/internal/identity"
|
|
14
|
+
"github.com/agentconnect/awiki-cli/internal/output"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
type GlobalOptions struct {
|
|
18
|
+
Format string
|
|
19
|
+
FormatChanged bool
|
|
20
|
+
JQ string
|
|
21
|
+
DryRun bool
|
|
22
|
+
Identity string
|
|
23
|
+
IdentityChanged bool
|
|
24
|
+
Verbose bool
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type App struct {
|
|
28
|
+
globals GlobalOptions
|
|
29
|
+
catalog *cmdmeta.Catalog
|
|
30
|
+
docs *docindex.Index
|
|
31
|
+
|
|
32
|
+
updateWarning string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func Execute() int {
|
|
36
|
+
app := &App{
|
|
37
|
+
globals: GlobalOptions{Format: string(output.FormatJSON)},
|
|
38
|
+
catalog: cmdmeta.NewCatalog(),
|
|
39
|
+
docs: docindex.NewIndex(),
|
|
40
|
+
}
|
|
41
|
+
rootCmd := newRootCommand(app)
|
|
42
|
+
if err := rootCmd.Execute(); err != nil {
|
|
43
|
+
return app.handleError(err)
|
|
44
|
+
}
|
|
45
|
+
return 0
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func (a *App) handleError(err error) int {
|
|
49
|
+
format := output.FormatJSON
|
|
50
|
+
if resolved, resolveErr := output.NormalizeFormat(a.globals.Format); resolveErr == nil {
|
|
51
|
+
format = resolved
|
|
52
|
+
}
|
|
53
|
+
detail := output.ErrorDetail{
|
|
54
|
+
Code: "internal_error",
|
|
55
|
+
Message: err.Error(),
|
|
56
|
+
Retryable: false,
|
|
57
|
+
}
|
|
58
|
+
exitCode := 1
|
|
59
|
+
var exitErr *output.ExitError
|
|
60
|
+
if errors.As(err, &exitErr) {
|
|
61
|
+
detail = exitErr.Detail
|
|
62
|
+
exitCode = exitErr.Code
|
|
63
|
+
}
|
|
64
|
+
detail.Details = identity.PublicValue(detail.Details)
|
|
65
|
+
envelope := output.ErrorEnvelope{
|
|
66
|
+
OK: false,
|
|
67
|
+
Error: detail,
|
|
68
|
+
Meta: output.Meta{
|
|
69
|
+
Version: buildinfo.Version,
|
|
70
|
+
DryRun: a.globals.DryRun,
|
|
71
|
+
Format: string(format),
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
if identity := a.identityMeta(); identity != nil {
|
|
75
|
+
envelope.Meta.Identity = identity
|
|
76
|
+
}
|
|
77
|
+
if renderErr := output.RenderError(os.Stderr, format, a.globals.JQ, envelope); renderErr != nil {
|
|
78
|
+
fmt.Fprintln(os.Stderr, err.Error())
|
|
79
|
+
}
|
|
80
|
+
return exitCode
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func (a *App) renderSuccess(command string, format output.Format, jqExpr string, data any, summary string, warnings []string, identityMeta *output.IdentityMeta) error {
|
|
84
|
+
mergedWarnings := warnings
|
|
85
|
+
if a.updateWarning != "" {
|
|
86
|
+
mergedWarnings = append([]string{a.updateWarning}, warnings...)
|
|
87
|
+
}
|
|
88
|
+
envelope := output.SuccessEnvelope{
|
|
89
|
+
OK: true,
|
|
90
|
+
Command: command,
|
|
91
|
+
Data: identity.PublicValue(data),
|
|
92
|
+
Warnings: mergedWarnings,
|
|
93
|
+
Summary: summary,
|
|
94
|
+
Meta: output.Meta{
|
|
95
|
+
Version: buildinfo.Version,
|
|
96
|
+
Identity: identityMeta,
|
|
97
|
+
DryRun: a.globals.DryRun,
|
|
98
|
+
Format: string(format),
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
return output.RenderSuccess(os.Stdout, format, jqExpr, envelope)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
func (a *App) resolveConfig() (*appconfig.Resolved, error) {
|
|
105
|
+
resolved, err := appconfig.Resolve(appconfig.Overrides{
|
|
106
|
+
Identity: a.globals.Identity,
|
|
107
|
+
IdentityChanged: a.globals.IdentityChanged,
|
|
108
|
+
Format: a.globals.Format,
|
|
109
|
+
FormatChanged: a.globals.FormatChanged,
|
|
110
|
+
})
|
|
111
|
+
if err != nil {
|
|
112
|
+
return nil, err
|
|
113
|
+
}
|
|
114
|
+
if strings.TrimSpace(resolved.ActiveIdentity) == "" {
|
|
115
|
+
manager := identity.NewManager(resolved.Paths)
|
|
116
|
+
current, currentErr := manager.Current()
|
|
117
|
+
if currentErr == nil && current != nil {
|
|
118
|
+
resolved.ActiveIdentity = current.IdentityName
|
|
119
|
+
if resolved.Sources == nil {
|
|
120
|
+
resolved.Sources = map[string]appconfig.ValueSource{}
|
|
121
|
+
}
|
|
122
|
+
resolved.Sources["active_identity"] = appconfig.ValueSource{
|
|
123
|
+
Source: "identity_index",
|
|
124
|
+
Value: current.IdentityName,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return resolved, nil
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
func (a *App) identityMeta() *output.IdentityMeta {
|
|
132
|
+
if a.globals.Identity == "" {
|
|
133
|
+
return nil
|
|
134
|
+
}
|
|
135
|
+
return &output.IdentityMeta{Name: a.globals.Identity}
|
|
136
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
package cli
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"errors"
|
|
5
|
+
"io"
|
|
6
|
+
"os"
|
|
7
|
+
"strings"
|
|
8
|
+
"testing"
|
|
9
|
+
|
|
10
|
+
"github.com/agentconnect/awiki-cli/internal/identity"
|
|
11
|
+
"github.com/agentconnect/awiki-cli/internal/output"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
func TestRenderSuccessSanitizesInternalIdentityFields(t *testing.T) {
|
|
15
|
+
app := &App{}
|
|
16
|
+
rendered, err := captureStdout(func() error {
|
|
17
|
+
return app.renderSuccess(
|
|
18
|
+
"awiki-cli config show",
|
|
19
|
+
output.FormatJSON,
|
|
20
|
+
"",
|
|
21
|
+
map[string]any{
|
|
22
|
+
"default_identity": map[string]any{
|
|
23
|
+
"handle": "alice",
|
|
24
|
+
"user_id": "user-123",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
"Resolved configuration",
|
|
28
|
+
nil,
|
|
29
|
+
nil,
|
|
30
|
+
)
|
|
31
|
+
})
|
|
32
|
+
if err != nil {
|
|
33
|
+
t.Fatalf("captureStdout(renderSuccess) error = %v", err)
|
|
34
|
+
}
|
|
35
|
+
if strings.Contains(rendered, "user_id") || strings.Contains(rendered, "user-123") {
|
|
36
|
+
t.Fatalf("rendered output %q still contains internal user_id fields", rendered)
|
|
37
|
+
}
|
|
38
|
+
if !strings.Contains(rendered, "alice") {
|
|
39
|
+
t.Fatalf("rendered output %q lost public fields", rendered)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func TestIdentityGatingUsesFrozenErrorCode(t *testing.T) {
|
|
44
|
+
t.Parallel()
|
|
45
|
+
|
|
46
|
+
app := &App{}
|
|
47
|
+
err := identity.UserRegistrationError("alice", identity.UserState{
|
|
48
|
+
RegistrationState: "local_identity",
|
|
49
|
+
ReadyForMessaging: false,
|
|
50
|
+
Missing: []string{"registration", "handle"},
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
for _, got := range []error{
|
|
54
|
+
app.messageExit(err, "hint"),
|
|
55
|
+
app.runtimeExit(err, "hint"),
|
|
56
|
+
} {
|
|
57
|
+
var exitErr *output.ExitError
|
|
58
|
+
if !errors.As(got, &exitErr) {
|
|
59
|
+
t.Fatalf("errors.As(%T, *output.ExitError) = false", got)
|
|
60
|
+
}
|
|
61
|
+
if exitErr.Detail.Code != "identity_required" {
|
|
62
|
+
t.Fatalf("exitErr.Detail.Code = %q, want %q", exitErr.Detail.Code, "identity_required")
|
|
63
|
+
}
|
|
64
|
+
if exitErr.Code != 3 {
|
|
65
|
+
t.Fatalf("exitErr.Code = %d, want 3", exitErr.Code)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
func captureStdout(run func() error) (string, error) {
|
|
71
|
+
reader, writer, err := os.Pipe()
|
|
72
|
+
if err != nil {
|
|
73
|
+
return "", err
|
|
74
|
+
}
|
|
75
|
+
defer reader.Close()
|
|
76
|
+
|
|
77
|
+
originalStdout := os.Stdout
|
|
78
|
+
os.Stdout = writer
|
|
79
|
+
runErr := run()
|
|
80
|
+
_ = writer.Close()
|
|
81
|
+
os.Stdout = originalStdout
|
|
82
|
+
|
|
83
|
+
output, readErr := io.ReadAll(reader)
|
|
84
|
+
if readErr != nil {
|
|
85
|
+
return "", readErr
|
|
86
|
+
}
|
|
87
|
+
return string(output), runErr
|
|
88
|
+
}
|