@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,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
+ }