@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,385 @@
|
|
|
1
|
+
package identity
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"fmt"
|
|
6
|
+
"os"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
"sort"
|
|
9
|
+
"strings"
|
|
10
|
+
"time"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
func (m *Manager) save(input SaveInput) (*StoredIdentity, error) {
|
|
14
|
+
if strings.TrimSpace(input.IdentityName) == "" {
|
|
15
|
+
return nil, fmt.Errorf("%w: identity name is required", ErrInvalidInput)
|
|
16
|
+
}
|
|
17
|
+
if strings.TrimSpace(input.DID) == "" || strings.TrimSpace(input.UniqueID) == "" {
|
|
18
|
+
return nil, fmt.Errorf("%w: did and unique_id are required", ErrInvalidInput)
|
|
19
|
+
}
|
|
20
|
+
if err := m.EnsureRoot(); err != nil {
|
|
21
|
+
return nil, err
|
|
22
|
+
}
|
|
23
|
+
index, err := m.LoadIndex()
|
|
24
|
+
if err != nil {
|
|
25
|
+
return nil, err
|
|
26
|
+
}
|
|
27
|
+
identityName := input.IdentityName
|
|
28
|
+
if existing, ok := index.Credentials[identityName]; ok {
|
|
29
|
+
if existing.DID != input.DID && !input.ReplaceExisting {
|
|
30
|
+
return nil, fmt.Errorf("%w: identity %s already exists for did %s", ErrIdentityConflict, identityName, existing.DID)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
dirName, err := preferredDirName(input.UniqueID)
|
|
35
|
+
if err != nil {
|
|
36
|
+
return nil, err
|
|
37
|
+
}
|
|
38
|
+
for name, entry := range index.Credentials {
|
|
39
|
+
if name == identityName {
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
if entry.DirName == dirName && entry.DID != input.DID {
|
|
43
|
+
return nil, fmt.Errorf("%w: dir %s already used by identity %s", ErrIdentityConflict, dirName, name)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
paths := m.BuildPaths(dirName)
|
|
48
|
+
if err := ensureDir(paths.IdentityDir); err != nil {
|
|
49
|
+
return nil, fmt.Errorf("create identity directory: %w", err)
|
|
50
|
+
}
|
|
51
|
+
createdAt := time.Now().UTC().Format(time.RFC3339)
|
|
52
|
+
if existing, err := m.Load(identityName); err == nil && existing != nil && existing.CreatedAt != "" {
|
|
53
|
+
createdAt = existing.CreatedAt
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
identityPayload := map[string]any{
|
|
57
|
+
"did": input.DID,
|
|
58
|
+
"unique_id": input.UniqueID,
|
|
59
|
+
"created_at": createdAt,
|
|
60
|
+
}
|
|
61
|
+
if input.UserID != "" {
|
|
62
|
+
identityPayload["user_id"] = input.UserID
|
|
63
|
+
}
|
|
64
|
+
if input.DisplayName != "" {
|
|
65
|
+
identityPayload["name"] = input.DisplayName
|
|
66
|
+
}
|
|
67
|
+
if input.Handle != "" {
|
|
68
|
+
identityPayload["handle"] = input.Handle
|
|
69
|
+
}
|
|
70
|
+
if err := writeSecureJSON(paths.IdentityPath, identityPayload); err != nil {
|
|
71
|
+
return nil, fmt.Errorf("write identity payload: %w", err)
|
|
72
|
+
}
|
|
73
|
+
if err := writeSecureJSON(paths.AuthPath, map[string]any{"jwt_token": nullableString(input.JWTToken)}); err != nil {
|
|
74
|
+
return nil, fmt.Errorf("write auth payload: %w", err)
|
|
75
|
+
}
|
|
76
|
+
if input.DIDDocument != nil {
|
|
77
|
+
if err := writeSecureJSON(paths.DIDDocumentPath, input.DIDDocument); err != nil {
|
|
78
|
+
return nil, fmt.Errorf("write did document: %w", err)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if input.Key1PrivatePEM != "" {
|
|
82
|
+
if err := writeSecureText(paths.Key1PrivatePath, input.Key1PrivatePEM); err != nil {
|
|
83
|
+
return nil, fmt.Errorf("write key-1 private key: %w", err)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if input.Key1PublicPEM != "" {
|
|
87
|
+
if err := writeSecureText(paths.Key1PublicPath, input.Key1PublicPEM); err != nil {
|
|
88
|
+
return nil, fmt.Errorf("write key-1 public key: %w", err)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if input.E2EESigningPrivatePEM != "" {
|
|
92
|
+
if err := writeSecureText(paths.E2EESigningPrivatePath, input.E2EESigningPrivatePEM); err != nil {
|
|
93
|
+
return nil, fmt.Errorf("write e2ee signing private key: %w", err)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if input.E2EEAgreementPrivatePEM != "" {
|
|
97
|
+
if err := writeSecureText(paths.E2EEAgreementPrivatePath, input.E2EEAgreementPrivatePEM); err != nil {
|
|
98
|
+
return nil, fmt.Errorf("write e2ee agreement private key: %w", err)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
entry := IndexEntry{
|
|
103
|
+
CredentialName: identityName,
|
|
104
|
+
DirName: dirName,
|
|
105
|
+
DID: input.DID,
|
|
106
|
+
UniqueID: input.UniqueID,
|
|
107
|
+
UserID: input.UserID,
|
|
108
|
+
Name: input.DisplayName,
|
|
109
|
+
Handle: input.Handle,
|
|
110
|
+
CreatedAt: createdAt,
|
|
111
|
+
IsDefault: index.DefaultCredentialName == identityName || index.DefaultCredentialName == "",
|
|
112
|
+
}
|
|
113
|
+
if index.DefaultCredentialName == "" {
|
|
114
|
+
index.DefaultCredentialName = identityName
|
|
115
|
+
}
|
|
116
|
+
index.Credentials[identityName] = entry
|
|
117
|
+
if err := m.SaveIndex(index); err != nil {
|
|
118
|
+
return nil, err
|
|
119
|
+
}
|
|
120
|
+
return m.Load(identityName)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
func (m *Manager) Save(input SaveInput) (*StoredIdentity, error) {
|
|
124
|
+
return m.save(input)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
func nullableString(value string) any {
|
|
128
|
+
if value == "" {
|
|
129
|
+
return nil
|
|
130
|
+
}
|
|
131
|
+
return value
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func (m *Manager) Load(name string) (*StoredIdentity, error) {
|
|
135
|
+
index, err := m.LoadIndex()
|
|
136
|
+
if err != nil {
|
|
137
|
+
return nil, err
|
|
138
|
+
}
|
|
139
|
+
resolvedName, entry, ok := m.resolveEntryName(name, index)
|
|
140
|
+
if !ok {
|
|
141
|
+
return nil, fmt.Errorf("%w: %s", ErrIdentityNotFound, name)
|
|
142
|
+
}
|
|
143
|
+
paths := m.BuildPaths(entry.DirName)
|
|
144
|
+
identityPayload, err := readJSONMap(paths.IdentityPath)
|
|
145
|
+
if err != nil {
|
|
146
|
+
return nil, err
|
|
147
|
+
}
|
|
148
|
+
authPayload, err := readJSONMap(paths.AuthPath)
|
|
149
|
+
if err != nil && !os.IsNotExist(err) {
|
|
150
|
+
return nil, err
|
|
151
|
+
}
|
|
152
|
+
record := &StoredIdentity{
|
|
153
|
+
IdentityName: resolvedName,
|
|
154
|
+
DirName: entry.DirName,
|
|
155
|
+
DID: stringValue(identityPayload["did"], entry.DID),
|
|
156
|
+
UniqueID: stringValue(identityPayload["unique_id"], entry.UniqueID),
|
|
157
|
+
UserID: stringValue(identityPayload["user_id"], entry.UserID),
|
|
158
|
+
DisplayName: stringValue(identityPayload["name"], entry.Name),
|
|
159
|
+
Handle: stringValue(identityPayload["handle"], entry.Handle),
|
|
160
|
+
CreatedAt: stringValue(identityPayload["created_at"], entry.CreatedAt),
|
|
161
|
+
IsDefault: index.DefaultCredentialName == resolvedName,
|
|
162
|
+
}
|
|
163
|
+
record.JWTToken = stringValue(authPayload["jwt_token"], "")
|
|
164
|
+
record.DIDDocument, _ = readJSONMap(paths.DIDDocumentPath)
|
|
165
|
+
record.Key1PrivatePEM = readText(paths.Key1PrivatePath)
|
|
166
|
+
record.Key1PublicPEM = readText(paths.Key1PublicPath)
|
|
167
|
+
record.E2EESigningPrivatePEM = readText(paths.E2EESigningPrivatePath)
|
|
168
|
+
record.E2EEAgreementPrivatePEM = readText(paths.E2EEAgreementPrivatePath)
|
|
169
|
+
return record, nil
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
func (m *Manager) UpdateJWT(name string, jwtToken string) error {
|
|
173
|
+
record, err := m.Load(name)
|
|
174
|
+
if err != nil {
|
|
175
|
+
return err
|
|
176
|
+
}
|
|
177
|
+
paths := m.BuildPaths(record.DirName)
|
|
178
|
+
return writeSecureJSON(paths.AuthPath, map[string]any{"jwt_token": nullableString(jwtToken)})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
func (m *Manager) UpdateDisplayName(name string, displayName string) error {
|
|
182
|
+
record, err := m.Load(name)
|
|
183
|
+
if err != nil {
|
|
184
|
+
return err
|
|
185
|
+
}
|
|
186
|
+
paths := m.BuildPaths(record.DirName)
|
|
187
|
+
payload, err := readJSONMap(paths.IdentityPath)
|
|
188
|
+
if err != nil {
|
|
189
|
+
return err
|
|
190
|
+
}
|
|
191
|
+
payload["name"] = displayName
|
|
192
|
+
if err := writeSecureJSON(paths.IdentityPath, payload); err != nil {
|
|
193
|
+
return err
|
|
194
|
+
}
|
|
195
|
+
index, err := m.LoadIndex()
|
|
196
|
+
if err != nil {
|
|
197
|
+
return err
|
|
198
|
+
}
|
|
199
|
+
entry, ok := index.Credentials[record.IdentityName]
|
|
200
|
+
if !ok {
|
|
201
|
+
return fmt.Errorf("%w: %s", ErrIdentityNotFound, record.IdentityName)
|
|
202
|
+
}
|
|
203
|
+
entry.Name = displayName
|
|
204
|
+
index.Credentials[record.IdentityName] = entry
|
|
205
|
+
return m.SaveIndex(index)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
func (m *Manager) List() ([]IdentitySummary, error) {
|
|
209
|
+
index, err := m.LoadIndex()
|
|
210
|
+
if err != nil {
|
|
211
|
+
return nil, err
|
|
212
|
+
}
|
|
213
|
+
names := make([]string, 0, len(index.Credentials))
|
|
214
|
+
for name := range index.Credentials {
|
|
215
|
+
names = append(names, name)
|
|
216
|
+
}
|
|
217
|
+
sort.Strings(names)
|
|
218
|
+
summaries := make([]IdentitySummary, 0, len(names))
|
|
219
|
+
for _, name := range names {
|
|
220
|
+
summary, err := m.summaryFor(index.Credentials[name], index.DefaultCredentialName)
|
|
221
|
+
if err != nil {
|
|
222
|
+
return nil, err
|
|
223
|
+
}
|
|
224
|
+
summaries = append(summaries, *summary)
|
|
225
|
+
}
|
|
226
|
+
return summaries, nil
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
func (m *Manager) summaryFor(entry IndexEntry, defaultName string) (*IdentitySummary, error) {
|
|
230
|
+
paths := m.BuildPaths(entry.DirName)
|
|
231
|
+
authPayload, err := readJSONMap(paths.AuthPath)
|
|
232
|
+
if err != nil && !os.IsNotExist(err) {
|
|
233
|
+
return nil, err
|
|
234
|
+
}
|
|
235
|
+
summary := &IdentitySummary{
|
|
236
|
+
IdentityName: entry.CredentialName,
|
|
237
|
+
DID: entry.DID,
|
|
238
|
+
UniqueID: entry.UniqueID,
|
|
239
|
+
UserID: entry.UserID,
|
|
240
|
+
DisplayName: entry.Name,
|
|
241
|
+
Handle: entry.Handle,
|
|
242
|
+
CreatedAt: entry.CreatedAt,
|
|
243
|
+
DirName: entry.DirName,
|
|
244
|
+
IsDefault: defaultName == entry.CredentialName,
|
|
245
|
+
HasJWT: stringValue(authPayload["jwt_token"], "") != "",
|
|
246
|
+
HasDIDDocument: fileExists(paths.DIDDocumentPath),
|
|
247
|
+
HasKey1Private: fileExists(paths.Key1PrivatePath),
|
|
248
|
+
HasKey1Public: fileExists(paths.Key1PublicPath),
|
|
249
|
+
HasE2EESigningPrivate: fileExists(paths.E2EESigningPrivatePath),
|
|
250
|
+
HasE2EEAgreementPrivate: fileExists(paths.E2EEAgreementPrivatePath),
|
|
251
|
+
}
|
|
252
|
+
summary.UserState = EvaluateIdentitySummaryUserState(summary)
|
|
253
|
+
return summary, nil
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
func (m *Manager) Current() (*IdentitySummary, error) {
|
|
257
|
+
index, err := m.LoadIndex()
|
|
258
|
+
if err != nil {
|
|
259
|
+
return nil, err
|
|
260
|
+
}
|
|
261
|
+
if index.DefaultCredentialName == "" {
|
|
262
|
+
if _, ok := index.Credentials["default"]; !ok {
|
|
263
|
+
return nil, fmt.Errorf("%w", ErrNoDefaultIdentity)
|
|
264
|
+
}
|
|
265
|
+
index.DefaultCredentialName = "default"
|
|
266
|
+
}
|
|
267
|
+
entry, ok := index.Credentials[index.DefaultCredentialName]
|
|
268
|
+
if !ok {
|
|
269
|
+
return nil, fmt.Errorf("%w: %s", ErrNoDefaultIdentity, index.DefaultCredentialName)
|
|
270
|
+
}
|
|
271
|
+
return m.summaryFor(entry, index.DefaultCredentialName)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
func (m *Manager) SetDefault(name string) (*IdentitySummary, error) {
|
|
275
|
+
index, err := m.LoadIndex()
|
|
276
|
+
if err != nil {
|
|
277
|
+
return nil, err
|
|
278
|
+
}
|
|
279
|
+
entry, ok := index.Credentials[name]
|
|
280
|
+
if !ok {
|
|
281
|
+
return nil, fmt.Errorf("%w: %s", ErrIdentityNotFound, name)
|
|
282
|
+
}
|
|
283
|
+
index.DefaultCredentialName = name
|
|
284
|
+
index.Credentials[name] = entry
|
|
285
|
+
if err := m.SaveIndex(index); err != nil {
|
|
286
|
+
return nil, err
|
|
287
|
+
}
|
|
288
|
+
return m.summaryFor(entry, name)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
func readJSONMap(path string) (map[string]any, error) {
|
|
292
|
+
raw, err := os.ReadFile(path)
|
|
293
|
+
if err != nil {
|
|
294
|
+
return nil, err
|
|
295
|
+
}
|
|
296
|
+
var payload map[string]any
|
|
297
|
+
if err := json.Unmarshal(raw, &payload); err != nil {
|
|
298
|
+
return nil, fmt.Errorf("parse json %s: %w", path, err)
|
|
299
|
+
}
|
|
300
|
+
return payload, nil
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
func readText(path string) string {
|
|
304
|
+
raw, err := os.ReadFile(path)
|
|
305
|
+
if err != nil {
|
|
306
|
+
return ""
|
|
307
|
+
}
|
|
308
|
+
return string(raw)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
func stringValue(value any, fallback string) string {
|
|
312
|
+
text, ok := value.(string)
|
|
313
|
+
if ok {
|
|
314
|
+
return text
|
|
315
|
+
}
|
|
316
|
+
return fallback
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
func fileExists(path string) bool {
|
|
320
|
+
_, err := os.Stat(path)
|
|
321
|
+
return err == nil
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
func chooseDefaultIdentityName(requested string, existing []IdentitySummary, fallback string) string {
|
|
325
|
+
if sanitized := sanitizeIdentityName(requested); sanitized != "" {
|
|
326
|
+
return sanitized
|
|
327
|
+
}
|
|
328
|
+
if len(existing) == 0 {
|
|
329
|
+
return "default"
|
|
330
|
+
}
|
|
331
|
+
base := sanitizeIdentityName(fallback)
|
|
332
|
+
if base == "" {
|
|
333
|
+
base = "identity"
|
|
334
|
+
}
|
|
335
|
+
used := map[string]struct{}{}
|
|
336
|
+
for _, summary := range existing {
|
|
337
|
+
used[summary.IdentityName] = struct{}{}
|
|
338
|
+
}
|
|
339
|
+
if _, ok := used[base]; !ok {
|
|
340
|
+
return base
|
|
341
|
+
}
|
|
342
|
+
for idx := 2; idx < 1000; idx++ {
|
|
343
|
+
candidate := fmt.Sprintf("%s-%d", base, idx)
|
|
344
|
+
if _, ok := used[candidate]; !ok {
|
|
345
|
+
return candidate
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return fmt.Sprintf("%s-%d", base, time.Now().Unix())
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
func chooseNamedIdentity(requested string, existing []IdentitySummary, fallback string) string {
|
|
352
|
+
if sanitized := sanitizeIdentityName(requested); sanitized != "" {
|
|
353
|
+
return sanitized
|
|
354
|
+
}
|
|
355
|
+
base := sanitizeIdentityName(fallback)
|
|
356
|
+
if base == "" {
|
|
357
|
+
base = "identity"
|
|
358
|
+
}
|
|
359
|
+
used := map[string]struct{}{}
|
|
360
|
+
for _, summary := range existing {
|
|
361
|
+
used[summary.IdentityName] = struct{}{}
|
|
362
|
+
}
|
|
363
|
+
if _, ok := used[base]; !ok {
|
|
364
|
+
return base
|
|
365
|
+
}
|
|
366
|
+
for idx := 2; idx < 1000; idx++ {
|
|
367
|
+
candidate := fmt.Sprintf("%s-%d", base, idx)
|
|
368
|
+
if _, ok := used[candidate]; !ok {
|
|
369
|
+
return candidate
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return fmt.Sprintf("%s-%d", base, time.Now().Unix())
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
func PreviewDefaultIdentityName(requested string, existing []IdentitySummary, fallback string) string {
|
|
376
|
+
return chooseDefaultIdentityName(requested, existing, fallback)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
func PreviewNamedIdentity(requested string, existing []IdentitySummary, fallback string) string {
|
|
380
|
+
return chooseNamedIdentity(requested, existing, fallback)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
func (m *Manager) legacyBackupRoot() string {
|
|
384
|
+
return filepath.Join(m.RootDir(), LegacyBackupDirName)
|
|
385
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
package identity
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"os"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"testing"
|
|
8
|
+
|
|
9
|
+
appconfig "github.com/agentconnect/awiki-cli/internal/config"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
func TestManagerSaveLoadAndCurrent(t *testing.T) {
|
|
13
|
+
t.Parallel()
|
|
14
|
+
|
|
15
|
+
root := t.TempDir()
|
|
16
|
+
manager := NewManager(appconfig.Paths{
|
|
17
|
+
IdentityDir: filepath.Join(root, "identities"),
|
|
18
|
+
LegacyCredentialsDir: filepath.Join(root, "legacy"),
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
generated, err := GenerateIdentity(GenerateOptions{
|
|
22
|
+
Hostname: "awiki.ai",
|
|
23
|
+
PathPrefix: []string{"user"},
|
|
24
|
+
ProofDomain: "awiki.ai",
|
|
25
|
+
})
|
|
26
|
+
if err != nil {
|
|
27
|
+
t.Fatalf("GenerateIdentity() error = %v", err)
|
|
28
|
+
}
|
|
29
|
+
record, err := manager.save(SaveInput{
|
|
30
|
+
IdentityName: "default",
|
|
31
|
+
DID: generated.DID,
|
|
32
|
+
UniqueID: generated.UniqueID,
|
|
33
|
+
DisplayName: "Alice",
|
|
34
|
+
DIDDocument: generated.DIDDocument,
|
|
35
|
+
Key1PrivatePEM: generated.Key1PrivatePEM,
|
|
36
|
+
Key1PublicPEM: generated.Key1PublicPEM,
|
|
37
|
+
E2EESigningPrivatePEM: generated.E2EESigningPrivatePEM,
|
|
38
|
+
E2EEAgreementPrivatePEM: generated.E2EEAgreementPrivatePEM,
|
|
39
|
+
})
|
|
40
|
+
if err != nil {
|
|
41
|
+
t.Fatalf("save() error = %v", err)
|
|
42
|
+
}
|
|
43
|
+
if record.IdentityName != "default" {
|
|
44
|
+
t.Fatalf("unexpected identity name: %+v", record)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
loaded, err := manager.Load("default")
|
|
48
|
+
if err != nil {
|
|
49
|
+
t.Fatalf("Load() error = %v", err)
|
|
50
|
+
}
|
|
51
|
+
if loaded.DID != generated.DID {
|
|
52
|
+
t.Fatalf("loaded DID mismatch: got=%s want=%s", loaded.DID, generated.DID)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
listed, err := manager.List()
|
|
56
|
+
if err != nil {
|
|
57
|
+
t.Fatalf("List() error = %v", err)
|
|
58
|
+
}
|
|
59
|
+
if len(listed) != 1 || listed[0].IdentityName != "default" {
|
|
60
|
+
t.Fatalf("unexpected list result: %#v", listed)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
current, err := manager.Current()
|
|
64
|
+
if err != nil {
|
|
65
|
+
t.Fatalf("Current() error = %v", err)
|
|
66
|
+
}
|
|
67
|
+
if current.IdentityName != "default" || !current.IsDefault {
|
|
68
|
+
t.Fatalf("unexpected current identity: %#v", current)
|
|
69
|
+
}
|
|
70
|
+
if current.UserState.RegistrationState != "local_identity" || current.UserState.ReadyForMessaging {
|
|
71
|
+
t.Fatalf("unexpected user state for local identity: %#v", current.UserState)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func TestImportLegacyFlatIdentity(t *testing.T) {
|
|
76
|
+
t.Parallel()
|
|
77
|
+
|
|
78
|
+
root := t.TempDir()
|
|
79
|
+
legacyRoot := filepath.Join(root, "legacy")
|
|
80
|
+
if err := os.MkdirAll(legacyRoot, 0o700); err != nil {
|
|
81
|
+
t.Fatalf("MkdirAll() error = %v", err)
|
|
82
|
+
}
|
|
83
|
+
manager := NewManager(appconfig.Paths{
|
|
84
|
+
IdentityDir: filepath.Join(root, "identities"),
|
|
85
|
+
LegacyCredentialsDir: legacyRoot,
|
|
86
|
+
})
|
|
87
|
+
generated, err := GenerateIdentity(GenerateOptions{
|
|
88
|
+
Hostname: "awiki.ai",
|
|
89
|
+
PathPrefix: []string{"user"},
|
|
90
|
+
ProofDomain: "awiki.ai",
|
|
91
|
+
})
|
|
92
|
+
if err != nil {
|
|
93
|
+
t.Fatalf("GenerateIdentity() error = %v", err)
|
|
94
|
+
}
|
|
95
|
+
legacyPayload := map[string]any{
|
|
96
|
+
"did": generated.DID,
|
|
97
|
+
"unique_id": generated.UniqueID,
|
|
98
|
+
"name": "Legacy Alice",
|
|
99
|
+
"handle": "legacy-alice",
|
|
100
|
+
"jwt_token": "legacy-token",
|
|
101
|
+
"private_key_pem": generated.Key1PrivatePEM,
|
|
102
|
+
"public_key_pem": generated.Key1PublicPEM,
|
|
103
|
+
"e2ee_signing_private_pem": generated.E2EESigningPrivatePEM,
|
|
104
|
+
"e2ee_agreement_private_pem": generated.E2EEAgreementPrivatePEM,
|
|
105
|
+
"did_document": generated.DIDDocument,
|
|
106
|
+
}
|
|
107
|
+
raw, err := json.MarshalIndent(legacyPayload, "", " ")
|
|
108
|
+
if err != nil {
|
|
109
|
+
t.Fatalf("MarshalIndent() error = %v", err)
|
|
110
|
+
}
|
|
111
|
+
if err := os.WriteFile(filepath.Join(legacyRoot, "default.json"), raw, 0o600); err != nil {
|
|
112
|
+
t.Fatalf("WriteFile() error = %v", err)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
scan, err := manager.ScanLegacy()
|
|
116
|
+
if err != nil {
|
|
117
|
+
t.Fatalf("ScanLegacy() error = %v", err)
|
|
118
|
+
}
|
|
119
|
+
if !scan.HasLegacy || len(scan.LegacyCredentials) != 1 {
|
|
120
|
+
t.Fatalf("unexpected legacy scan: %#v", scan)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
imported, err := manager.ImportLegacy("default")
|
|
124
|
+
if err != nil {
|
|
125
|
+
t.Fatalf("ImportLegacy() error = %v", err)
|
|
126
|
+
}
|
|
127
|
+
if len(imported.Imported) != 1 {
|
|
128
|
+
t.Fatalf("unexpected import result: %#v", imported)
|
|
129
|
+
}
|
|
130
|
+
current, err := manager.Current()
|
|
131
|
+
if err != nil {
|
|
132
|
+
t.Fatalf("Current() error = %v", err)
|
|
133
|
+
}
|
|
134
|
+
if current.IdentityName != "default" || current.DisplayName != "Legacy Alice" {
|
|
135
|
+
t.Fatalf("unexpected current identity after import: %#v", current)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
func TestManagerSummaryShowsRegisteredUserState(t *testing.T) {
|
|
140
|
+
t.Parallel()
|
|
141
|
+
|
|
142
|
+
root := t.TempDir()
|
|
143
|
+
manager := NewManager(appconfig.Paths{
|
|
144
|
+
IdentityDir: filepath.Join(root, "identities"),
|
|
145
|
+
LegacyCredentialsDir: filepath.Join(root, "legacy"),
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
generated, err := GenerateIdentity(GenerateOptions{
|
|
149
|
+
Hostname: "awiki.ai",
|
|
150
|
+
PathPrefix: []string{"alice"},
|
|
151
|
+
ProofDomain: "awiki.ai",
|
|
152
|
+
})
|
|
153
|
+
if err != nil {
|
|
154
|
+
t.Fatalf("GenerateIdentity() error = %v", err)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if _, err := manager.save(SaveInput{
|
|
158
|
+
IdentityName: "alice",
|
|
159
|
+
DID: generated.DID,
|
|
160
|
+
UniqueID: generated.UniqueID,
|
|
161
|
+
UserID: "user-123",
|
|
162
|
+
DisplayName: "Alice",
|
|
163
|
+
Handle: "alice",
|
|
164
|
+
DIDDocument: generated.DIDDocument,
|
|
165
|
+
Key1PrivatePEM: generated.Key1PrivatePEM,
|
|
166
|
+
Key1PublicPEM: generated.Key1PublicPEM,
|
|
167
|
+
E2EESigningPrivatePEM: generated.E2EESigningPrivatePEM,
|
|
168
|
+
E2EEAgreementPrivatePEM: generated.E2EEAgreementPrivatePEM,
|
|
169
|
+
}); err != nil {
|
|
170
|
+
t.Fatalf("save() error = %v", err)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
current, err := manager.Current()
|
|
174
|
+
if err != nil {
|
|
175
|
+
t.Fatalf("Current() error = %v", err)
|
|
176
|
+
}
|
|
177
|
+
if current.UserState.RegistrationState != "registered_user" || !current.UserState.ReadyForMessaging {
|
|
178
|
+
t.Fatalf("unexpected registered user state: %#v", current.UserState)
|
|
179
|
+
}
|
|
180
|
+
}
|