@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,334 @@
1
+ package message
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "errors"
7
+ "fmt"
8
+ "net/http"
9
+ "strings"
10
+
11
+ "github.com/agentconnect/awiki-cli/internal/authsdk"
12
+ appconfig "github.com/agentconnect/awiki-cli/internal/config"
13
+ )
14
+
15
+ type Transport interface {
16
+ SendDirect(context.Context, SendRequest) (*directSendResult, error)
17
+ SendGroup(context.Context, SendRequest) (*groupSendResult, error)
18
+ GetInbox(context.Context, InboxRequest) (map[string]any, error)
19
+ GetHistory(context.Context, HistoryRequest) (map[string]any, error)
20
+ MarkRead(context.Context, MarkReadRequest) (map[string]any, error)
21
+ CreateGroup(context.Context, GroupCreateRequest) (map[string]any, error)
22
+ GetGroupInfo(context.Context, GroupInfoRequest) (map[string]any, error)
23
+ JoinGroup(context.Context, GroupJoinRequest) (map[string]any, error)
24
+ AddGroupMember(context.Context, GroupMemberRequest) (map[string]any, error)
25
+ RemoveGroupMember(context.Context, GroupMemberRequest) (map[string]any, error)
26
+ LeaveGroup(context.Context, GroupLeaveRequest) (map[string]any, error)
27
+ GetGroup(context.Context, GroupGetRequest) (map[string]any, error)
28
+ ListGroupMembers(context.Context, GroupMembersRequest) (map[string]any, error)
29
+ ListGroupMessages(context.Context, GroupMessagesRequest) (map[string]any, error)
30
+ UpdateGroupProfile(context.Context, GroupGetRequest, map[string]any) (map[string]any, error)
31
+ UpdateGroupPolicy(context.Context, GroupGetRequest, map[string]any) (map[string]any, error)
32
+ }
33
+
34
+ type ServiceError struct {
35
+ StatusCode int
36
+ RPCCode int
37
+ Message string
38
+ Data any
39
+ }
40
+
41
+ func (e *ServiceError) Error() string {
42
+ if e == nil {
43
+ return ""
44
+ }
45
+ switch {
46
+ case e.RPCCode != 0:
47
+ return fmt.Sprintf("message service rpc error %d: %s", e.RPCCode, e.Message)
48
+ case e.StatusCode != 0:
49
+ return fmt.Sprintf("message service http error %d: %s", e.StatusCode, e.Message)
50
+ default:
51
+ return e.Message
52
+ }
53
+ }
54
+
55
+ type rpcResponse struct {
56
+ Result json.RawMessage `json:"result"`
57
+ Error *struct {
58
+ Code int `json:"code"`
59
+ Message string `json:"message"`
60
+ Data any `json:"data,omitempty"`
61
+ } `json:"error,omitempty"`
62
+ }
63
+
64
+ type HTTPTransport struct {
65
+ resolved *appconfig.Resolved
66
+ auth *authContext
67
+ httpClient *http.Client
68
+ baseMessageURL string
69
+ }
70
+
71
+ func NewHTTPTransport(resolved *appconfig.Resolved, auth *authContext, httpClient *http.Client) *HTTPTransport {
72
+ if httpClient == nil {
73
+ httpClient = http.DefaultClient
74
+ }
75
+ return &HTTPTransport{
76
+ resolved: resolved,
77
+ auth: auth,
78
+ httpClient: httpClient,
79
+ baseMessageURL: strings.TrimRight(resolved.MessageServiceURL, "/"),
80
+ }
81
+ }
82
+
83
+ func (t *HTTPTransport) SendDirect(ctx context.Context, request SendRequest) (*directSendResult, error) {
84
+ payload, err := buildDirectTextPayload(t.auth.record.DID, request.Target, request.Text, contentTypeForMessageType(request.MessageType))
85
+ if err != nil {
86
+ return nil, err
87
+ }
88
+ senderProof, err := buildSenderProof(t.auth, payload, request.Target)
89
+ if err != nil {
90
+ return nil, err
91
+ }
92
+ params := map[string]any{
93
+ "meta": payload.Meta,
94
+ "auth": map[string]any{
95
+ "scheme": OriginProofScheme,
96
+ "sender_proof": senderProof,
97
+ },
98
+ "body": payload.Body,
99
+ }
100
+ var result directSendResult
101
+ if err := t.rpcCall(ctx, payload.Method, params, &result); err != nil {
102
+ return nil, err
103
+ }
104
+ if result.MessageID == "" {
105
+ result.MessageID = stringFromAny(payload.Meta["message_id"])
106
+ }
107
+ if result.OperationID == "" {
108
+ result.OperationID = stringFromAny(payload.Meta["operation_id"])
109
+ }
110
+ if result.TargetDID == "" {
111
+ result.TargetDID = request.Target
112
+ }
113
+ return &result, nil
114
+ }
115
+
116
+ func (t *HTTPTransport) SendGroup(ctx context.Context, request SendRequest) (*groupSendResult, error) {
117
+ params, err := BuildGroupSendRPCParams(t.auth.record, nil, request.Group, request.Text, request.MessageType)
118
+ if err != nil {
119
+ return nil, err
120
+ }
121
+ var result groupSendResult
122
+ if err := t.rpcCall(ctx, "group.send", params, &result); err != nil {
123
+ return nil, err
124
+ }
125
+ if result.GroupDID == "" {
126
+ result.GroupDID = request.Group
127
+ }
128
+ return &result, nil
129
+ }
130
+
131
+ func (t *HTTPTransport) GetInbox(ctx context.Context, request InboxRequest) (map[string]any, error) {
132
+ params := map[string]any{
133
+ "meta": map[string]any{
134
+ "anp_version": "1.0",
135
+ "profile": "anp.inbox.local.v1",
136
+ "security_profile": "transport-protected",
137
+ "sender_did": t.auth.record.DID,
138
+ "operation_id": "op-" + generateOperationID(),
139
+ "created_at": nowRFC3339(),
140
+ },
141
+ "body": map[string]any{
142
+ "user_did": t.auth.record.DID,
143
+ "limit": request.Limit,
144
+ },
145
+ }
146
+ return t.rpcMapCall(ctx, "inbox.get", params)
147
+ }
148
+
149
+ func (t *HTTPTransport) GetHistory(ctx context.Context, request HistoryRequest) (map[string]any, error) {
150
+ body := map[string]any{
151
+ "user_did": t.auth.record.DID,
152
+ "peer_did": request.With,
153
+ "limit": request.Limit,
154
+ }
155
+ if strings.TrimSpace(request.Cursor) != "" {
156
+ body["since_seq"] = request.Cursor
157
+ }
158
+ params := map[string]any{
159
+ "meta": map[string]any{
160
+ "anp_version": "1.0",
161
+ "profile": "anp.direct.local.v1",
162
+ "security_profile": "transport-protected",
163
+ "sender_did": t.auth.record.DID,
164
+ "operation_id": "op-" + generateOperationID(),
165
+ "created_at": nowRFC3339(),
166
+ },
167
+ "body": body,
168
+ }
169
+ return t.rpcMapCall(ctx, "direct.get_history", params)
170
+ }
171
+
172
+ func (t *HTTPTransport) MarkRead(ctx context.Context, request MarkReadRequest) (map[string]any, error) {
173
+ params := map[string]any{
174
+ "meta": map[string]any{
175
+ "anp_version": "1.0",
176
+ "profile": "anp.inbox.local.v1",
177
+ "security_profile": "transport-protected",
178
+ "sender_did": t.auth.record.DID,
179
+ "operation_id": "op-" + generateOperationID(),
180
+ "created_at": nowRFC3339(),
181
+ },
182
+ "body": map[string]any{
183
+ "user_did": t.auth.record.DID,
184
+ "message_ids": request.MessageIDs,
185
+ },
186
+ }
187
+ return t.rpcMapCall(ctx, "inbox.mark_read", params)
188
+ }
189
+
190
+ func (t *HTTPTransport) CreateGroup(ctx context.Context, request GroupCreateRequest) (map[string]any, error) {
191
+ serviceDID, err := t.GetMessageServiceDID(ctx)
192
+ if err != nil {
193
+ return nil, err
194
+ }
195
+ params, err := BuildGroupCreateRPCParams(t.auth.record, nil, serviceDID, request)
196
+ if err != nil {
197
+ return nil, err
198
+ }
199
+ return t.rpcMapCall(ctx, "group.create", params)
200
+ }
201
+
202
+ func (t *HTTPTransport) GetGroupInfo(ctx context.Context, request GroupInfoRequest) (map[string]any, error) {
203
+ params, err := BuildGroupGetInfoRPCParams(t.auth.record, request)
204
+ if err != nil {
205
+ return nil, err
206
+ }
207
+ return t.rpcMapCall(ctx, "group.get_info", params)
208
+ }
209
+
210
+ func (t *HTTPTransport) JoinGroup(ctx context.Context, request GroupJoinRequest) (map[string]any, error) {
211
+ params, err := BuildGroupJoinRPCParams(t.auth.record, nil, request)
212
+ if err != nil {
213
+ return nil, err
214
+ }
215
+ return t.rpcMapCall(ctx, "group.join", params)
216
+ }
217
+
218
+ func (t *HTTPTransport) AddGroupMember(ctx context.Context, request GroupMemberRequest) (map[string]any, error) {
219
+ params, err := BuildGroupAddRPCParams(t.auth.record, nil, request)
220
+ if err != nil {
221
+ return nil, err
222
+ }
223
+ return t.rpcMapCall(ctx, "group.add", params)
224
+ }
225
+
226
+ func (t *HTTPTransport) RemoveGroupMember(ctx context.Context, request GroupMemberRequest) (map[string]any, error) {
227
+ params, err := BuildGroupRemoveRPCParams(t.auth.record, nil, request)
228
+ if err != nil {
229
+ return nil, err
230
+ }
231
+ return t.rpcMapCall(ctx, "group.remove", params)
232
+ }
233
+
234
+ func (t *HTTPTransport) LeaveGroup(ctx context.Context, request GroupLeaveRequest) (map[string]any, error) {
235
+ params, err := BuildGroupLeaveRPCParams(t.auth.record, nil, request)
236
+ if err != nil {
237
+ return nil, err
238
+ }
239
+ return t.rpcMapCall(ctx, "group.leave", params)
240
+ }
241
+
242
+ func (t *HTTPTransport) GetGroup(ctx context.Context, request GroupGetRequest) (map[string]any, error) {
243
+ params, err := BuildGroupGetRPCParams(t.auth.record, request)
244
+ if err != nil {
245
+ return nil, err
246
+ }
247
+ return t.rpcMapCall(ctx, "group.get", params)
248
+ }
249
+
250
+ func (t *HTTPTransport) ListGroupMembers(ctx context.Context, request GroupMembersRequest) (map[string]any, error) {
251
+ params, err := BuildGroupMembersRPCParams(t.auth.record, request)
252
+ if err != nil {
253
+ return nil, err
254
+ }
255
+ return t.rpcMapCall(ctx, "group.list_members", params)
256
+ }
257
+
258
+ func (t *HTTPTransport) ListGroupMessages(ctx context.Context, request GroupMessagesRequest) (map[string]any, error) {
259
+ params, err := BuildGroupMessagesRPCParams(t.auth.record, request)
260
+ if err != nil {
261
+ return nil, err
262
+ }
263
+ return t.rpcMapCall(ctx, "group.list_messages", params)
264
+ }
265
+
266
+ func (t *HTTPTransport) UpdateGroupProfile(ctx context.Context, request GroupGetRequest, patch map[string]any) (map[string]any, error) {
267
+ params, err := BuildGroupUpdateProfileRPCParams(t.auth.record, nil, request.Group, patch)
268
+ if err != nil {
269
+ return nil, err
270
+ }
271
+ return t.rpcMapCall(ctx, "group.update_profile", params)
272
+ }
273
+
274
+ func (t *HTTPTransport) UpdateGroupPolicy(ctx context.Context, request GroupGetRequest, patch map[string]any) (map[string]any, error) {
275
+ params, err := BuildGroupUpdatePolicyRPCParams(t.auth.record, nil, request.Group, patch)
276
+ if err != nil {
277
+ return nil, err
278
+ }
279
+ return t.rpcMapCall(ctx, "group.update_policy", params)
280
+ }
281
+
282
+ func (t *HTTPTransport) GetMessageServiceDID(ctx context.Context) (string, error) {
283
+ result, err := t.rpcMapCall(ctx, "anp.get_capabilities", map[string]any{
284
+ "meta": map[string]any{
285
+ "anp_version": "1.0",
286
+ "profile": "anp.core.binding.v1",
287
+ "security_profile": "transport-protected",
288
+ "sender_did": t.auth.record.DID,
289
+ "operation_id": "op-" + generateOperationID(),
290
+ "created_at": nowRFC3339(),
291
+ },
292
+ "body": map[string]any{},
293
+ "client": map[string]any{
294
+ "response_mode": "wait-final",
295
+ },
296
+ })
297
+ if err != nil {
298
+ return "", err
299
+ }
300
+ serviceDID := stringFromAny(result["service_did"])
301
+ if serviceDID == "" {
302
+ return "", fmt.Errorf("message service capabilities response is missing service_did")
303
+ }
304
+ return serviceDID, nil
305
+ }
306
+
307
+ func (t *HTTPTransport) rpcMapCall(ctx context.Context, method string, params map[string]any) (map[string]any, error) {
308
+ var result map[string]any
309
+ if err := t.rpcCall(ctx, method, params, &result); err != nil {
310
+ return nil, err
311
+ }
312
+ if result == nil {
313
+ return map[string]any{}, nil
314
+ }
315
+ return result, nil
316
+ }
317
+
318
+ func (t *HTTPTransport) rpcCall(ctx context.Context, method string, params map[string]any, out any) error {
319
+ requestURL := t.baseMessageURL + MessageRPCEndpoint
320
+ err := t.auth.session.DoJSONRPC(ctx, t.httpClient, requestURL, http.MethodPost, method, params, out)
321
+ if err == nil {
322
+ t.auth.record.JWTToken = t.auth.session.CurrentJWT()
323
+ return nil
324
+ }
325
+ var rpcErr *authsdk.RPCError
326
+ if errors.As(err, &rpcErr) {
327
+ return &ServiceError{RPCCode: rpcErr.Code, Message: rpcErr.Message, Data: rpcErr.Data}
328
+ }
329
+ var httpErr *authsdk.HTTPError
330
+ if errors.As(err, &httpErr) {
331
+ return &ServiceError{StatusCode: httpErr.StatusCode, Message: httpErr.Message}
332
+ }
333
+ return err
334
+ }
@@ -0,0 +1,156 @@
1
+ package message
2
+
3
+ import (
4
+ "crypto/ecdsa"
5
+ "crypto/rand"
6
+ "crypto/sha256"
7
+ "fmt"
8
+ "math/big"
9
+ "strings"
10
+ "time"
11
+
12
+ "github.com/agentconnect/awiki-cli/internal/anpsdk"
13
+ secp256k1 "github.com/decred/dcrd/dcrec/secp256k1/v4"
14
+ "github.com/google/uuid"
15
+ )
16
+
17
+ type directPayload struct {
18
+ Method string `json:"method"`
19
+ Meta map[string]any `json:"meta"`
20
+ Body map[string]any `json:"body"`
21
+ }
22
+
23
+ type signedPayload = directPayload
24
+
25
+ const OriginProofScheme = "anp-rfc9421-origin-proof-v1"
26
+
27
+ func buildDirectTextPayload(senderDID string, targetDID string, text string, contentType string) (directPayload, error) {
28
+ if senderDID == "" || targetDID == "" {
29
+ return directPayload{}, fmt.Errorf("sender and target did are required")
30
+ }
31
+ if text == "" {
32
+ return directPayload{}, ErrTextRequired
33
+ }
34
+ if contentType == "" {
35
+ contentType = "text/plain"
36
+ }
37
+ operationID := "op-" + uuid.NewString()
38
+ messageID := "msg-" + uuid.NewString()
39
+ meta := map[string]any{
40
+ "anp_version": "1.0",
41
+ "profile": "anp.direct.base.v1",
42
+ "security_profile": "transport-protected",
43
+ "sender_did": senderDID,
44
+ "target": map[string]any{
45
+ "kind": "agent",
46
+ "did": targetDID,
47
+ },
48
+ "operation_id": operationID,
49
+ "message_id": messageID,
50
+ "created_at": time.Now().UTC().Format(time.RFC3339),
51
+ "content_type": contentType,
52
+ }
53
+ body := map[string]any{"text": text}
54
+ return directPayload{Method: "direct.send", Meta: meta, Body: body}, nil
55
+ }
56
+
57
+ func buildSenderProof(auth *authContext, payload directPayload, targetDID string) (map[string]any, error) {
58
+ return buildActorProof(auth, signedPayload(payload), "anp://agent/"+strictPercentEncode(targetDID))
59
+ }
60
+
61
+ func buildActorProof(auth *authContext, payload signedPayload, logicalTargetURI string) (map[string]any, error) {
62
+ keyID := verificationMethodID(auth.record.DIDDocument)
63
+ if keyID == "" {
64
+ return nil, fmt.Errorf("identity %s is missing an authentication verification method", auth.record.IdentityName)
65
+ }
66
+ payloadMap := map[string]any{
67
+ "method": payload.Method,
68
+ "meta": payload.Meta,
69
+ "body": payload.Body,
70
+ }
71
+ canonicalPayload, err := canonicalJSON(payloadMap)
72
+ if err != nil {
73
+ return nil, fmt.Errorf("canonicalize direct payload: %w", err)
74
+ }
75
+ signatureInput, err := anpsdk.BuildIMSignatureInput(keyID, anpsdk.IMGenerationOptions{})
76
+ if err != nil {
77
+ return nil, fmt.Errorf("build actor proof signatureInput: %w", err)
78
+ }
79
+ parsed, err := anpsdk.ParseIMSignatureInput(signatureInput)
80
+ if err != nil {
81
+ return nil, fmt.Errorf("parse actor proof signatureInput: %w", err)
82
+ }
83
+ contentDigest := anpsdk.BuildIMContentDigest(canonicalPayload)
84
+ signatureBase, err := buildBusinessSignatureBase(payload.Method, logicalTargetURI, contentDigest, parsed)
85
+ if err != nil {
86
+ return nil, err
87
+ }
88
+ signatureBytes, err := signBusinessProof(auth.privateKey, []byte(signatureBase))
89
+ if err != nil {
90
+ return nil, fmt.Errorf("sign actor proof: %w", err)
91
+ }
92
+ return map[string]any{
93
+ "contentDigest": contentDigest,
94
+ "signatureInput": signatureInput,
95
+ "signature": anpsdk.EncodeIMSignature(signatureBytes, parsed.Label),
96
+ }, nil
97
+ }
98
+
99
+ func strictPercentEncode(value string) string {
100
+ var builder strings.Builder
101
+ builder.Grow(len(value) * 3)
102
+ for i := 0; i < len(value); i++ {
103
+ ch := value[i]
104
+ if (ch >= 'A' && ch <= 'Z') ||
105
+ (ch >= 'a' && ch <= 'z') ||
106
+ (ch >= '0' && ch <= '9') ||
107
+ ch == '-' || ch == '.' || ch == '_' || ch == '~' {
108
+ builder.WriteByte(ch)
109
+ continue
110
+ }
111
+ builder.WriteString(fmt.Sprintf("%%%02X", ch))
112
+ }
113
+ return builder.String()
114
+ }
115
+
116
+ func signBusinessProof(privateKey anpsdk.PrivateKeyMaterial, message []byte) ([]byte, error) {
117
+ if privateKey.Type != anpsdk.KeyTypeSecp256k1 {
118
+ return nil, fmt.Errorf("unsupported business proof key type: %s", privateKey.Type)
119
+ }
120
+ secpPrivateKey := secp256k1.PrivKeyFromBytes(privateKey.Bytes)
121
+ ecdsaPrivateKey := secpPrivateKey.ToECDSA()
122
+ digest := sha256.Sum256(message)
123
+ r, s, err := ecdsa.Sign(rand.Reader, ecdsaPrivateKey, digest[:])
124
+ if err != nil {
125
+ return nil, err
126
+ }
127
+ curveOrder := secp256k1.S256().Params().N
128
+ halfOrder := new(big.Int).Rsh(new(big.Int).Set(curveOrder), 1)
129
+ if s.Cmp(halfOrder) > 0 {
130
+ s.Sub(curveOrder, s)
131
+ }
132
+ signature := make([]byte, 64)
133
+ copy(signature[32-len(r.Bytes()):32], r.Bytes())
134
+ copy(signature[64-len(s.Bytes()):], s.Bytes())
135
+ return signature, nil
136
+ }
137
+
138
+ func buildBusinessSignatureBase(method string, logicalTargetURI string, contentDigest string, parsed anpsdk.ParsedIMSignatureInput) (string, error) {
139
+ lines := make([]string, 0, len(parsed.Components)+1)
140
+ for _, component := range parsed.Components {
141
+ var value string
142
+ switch component {
143
+ case "@method":
144
+ value = method
145
+ case "@target-uri":
146
+ value = logicalTargetURI
147
+ case "content-digest":
148
+ value = contentDigest
149
+ default:
150
+ return "", fmt.Errorf("unsupported business proof component: %s", component)
151
+ }
152
+ lines = append(lines, fmt.Sprintf("\"%s\": %s", component, value))
153
+ }
154
+ lines = append(lines, fmt.Sprintf("\"@signature-params\": %s", parsed.SignatureParams))
155
+ return strings.Join(lines, "\n"), nil
156
+ }
@@ -0,0 +1,61 @@
1
+ package message
2
+
3
+ import (
4
+ "testing"
5
+
6
+ "github.com/agentconnect/awiki-cli/internal/anpsdk"
7
+ "github.com/agentconnect/awiki-cli/internal/identity"
8
+ )
9
+
10
+ func TestBuildSenderProofRoundTrip(t *testing.T) {
11
+ t.Parallel()
12
+
13
+ generated, err := identity.GenerateIdentity(identity.GenerateOptions{
14
+ Hostname: "awiki.ai",
15
+ PathPrefix: []string{"user"},
16
+ ProofDomain: "awiki.ai",
17
+ })
18
+ if err != nil {
19
+ t.Fatalf("GenerateIdentity() error = %v", err)
20
+ }
21
+ record := &identity.StoredIdentity{
22
+ IdentityName: "default",
23
+ DID: generated.DID,
24
+ DIDDocument: generated.DIDDocument,
25
+ Key1PrivatePEM: generated.Key1PrivatePEM,
26
+ }
27
+ auth, err := newAuthContext(record, nil)
28
+ if err != nil {
29
+ t.Fatalf("newAuthContext() error = %v", err)
30
+ }
31
+ payload, err := buildDirectTextPayload(generated.DID, "did:wba:awiki.ai:user:bob", "hello", "text/plain")
32
+ if err != nil {
33
+ t.Fatalf("buildDirectTextPayload() error = %v", err)
34
+ }
35
+ proofMap, err := buildSenderProof(auth, payload, "did:wba:awiki.ai:user:bob")
36
+ if err != nil {
37
+ t.Fatalf("buildSenderProof() error = %v", err)
38
+ }
39
+ signatureInput, _ := proofMap["signatureInput"].(string)
40
+ parsed, err := anpsdk.ParseIMSignatureInput(signatureInput)
41
+ if err != nil {
42
+ t.Fatalf("ParseIMSignatureInput() error = %v", err)
43
+ }
44
+ payloadMap := map[string]any{"method": payload.Method, "meta": payload.Meta, "body": payload.Body}
45
+ canonicalPayload, err := canonicalJSON(payloadMap)
46
+ if err != nil {
47
+ t.Fatalf("canonicalJSON() error = %v", err)
48
+ }
49
+ signatureBase, err := buildBusinessSignatureBase(payload.Method, "anp://agent/"+strictPercentEncode("did:wba:awiki.ai:user:bob"), proofMap["contentDigest"].(string), parsed)
50
+ if err != nil {
51
+ t.Fatalf("buildBusinessSignatureBase() error = %v", err)
52
+ }
53
+ proof := anpsdk.IMProof{
54
+ ContentDigest: proofMap["contentDigest"].(string),
55
+ SignatureInput: signatureInput,
56
+ Signature: proofMap["signature"].(string),
57
+ }
58
+ if _, err := anpsdk.VerifyIMProofWithDocument(proof, canonicalPayload, []byte(signatureBase), generated.DIDDocument, generated.DID); err != nil {
59
+ t.Fatalf("VerifyIMProofWithDocument() error = %v", err)
60
+ }
61
+ }