@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,206 @@
1
+ package identity
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "os"
7
+ "path/filepath"
8
+ "regexp"
9
+ "sort"
10
+ "strings"
11
+
12
+ appconfig "github.com/agentconnect/awiki-cli/internal/config"
13
+ )
14
+
15
+ var safePathComponent = regexp.MustCompile(`[^A-Za-z0-9._-]+`)
16
+
17
+ type Manager struct {
18
+ paths appconfig.Paths
19
+ }
20
+
21
+ func NewManager(paths appconfig.Paths) *Manager {
22
+ return &Manager{paths: paths}
23
+ }
24
+
25
+ func (m *Manager) RootDir() string {
26
+ return m.paths.IdentityDir
27
+ }
28
+
29
+ func (m *Manager) LegacyRootDir() string {
30
+ return m.paths.LegacyCredentialsDir
31
+ }
32
+
33
+ func (m *Manager) indexPath() string {
34
+ return filepath.Join(m.RootDir(), IndexFileName)
35
+ }
36
+
37
+ func (m *Manager) EnsureRoot() error {
38
+ if err := os.MkdirAll(m.RootDir(), 0o700); err != nil {
39
+ return fmt.Errorf("create identity root: %w", err)
40
+ }
41
+ return os.Chmod(m.RootDir(), 0o700)
42
+ }
43
+
44
+ func (m *Manager) BuildPaths(dirName string) Paths {
45
+ rootDir := m.RootDir()
46
+ identityDir := filepath.Join(rootDir, dirName)
47
+ return Paths{
48
+ RootDir: rootDir,
49
+ DirName: dirName,
50
+ IdentityDir: identityDir,
51
+ IdentityPath: filepath.Join(identityDir, IdentityFileName),
52
+ AuthPath: filepath.Join(identityDir, AuthFileName),
53
+ DIDDocumentPath: filepath.Join(identityDir, DIDDocumentFileName),
54
+ Key1PrivatePath: filepath.Join(identityDir, Key1PrivateFileName),
55
+ Key1PublicPath: filepath.Join(identityDir, Key1PublicFileName),
56
+ E2EESigningPrivatePath: filepath.Join(identityDir, E2EESigningPrivateFileName),
57
+ E2EEAgreementPrivatePath: filepath.Join(identityDir, E2EEAgreementPrivateFileName),
58
+ E2EEStatePath: filepath.Join(identityDir, E2EEStateFileName),
59
+ }
60
+ }
61
+
62
+ func sanitizeComponent(raw string) string {
63
+ sanitized := safePathComponent.ReplaceAllString(raw, "_")
64
+ return strings.Trim(sanitized, "._-")
65
+ }
66
+
67
+ func preferredDirName(uniqueID string) (string, error) {
68
+ sanitized := sanitizeComponent(uniqueID)
69
+ if sanitized == "" {
70
+ return "", fmt.Errorf("%w: unique_id is required", ErrInvalidInput)
71
+ }
72
+ return sanitized, nil
73
+ }
74
+
75
+ func sanitizeIdentityName(raw string) string {
76
+ return sanitizeComponent(strings.ToLower(strings.TrimSpace(raw)))
77
+ }
78
+
79
+ func defaultIndex() IndexPayload {
80
+ return IndexPayload{
81
+ SchemaVersion: IndexSchemaVersion,
82
+ DefaultCredentialName: "",
83
+ Credentials: map[string]IndexEntry{},
84
+ }
85
+ }
86
+
87
+ func normalizeIndexPayload(payload IndexPayload) IndexPayload {
88
+ if payload.SchemaVersion == 0 {
89
+ payload.SchemaVersion = IndexSchemaVersion
90
+ }
91
+ if payload.Credentials == nil {
92
+ payload.Credentials = map[string]IndexEntry{}
93
+ }
94
+ if payload.DefaultCredentialName == "" {
95
+ if _, ok := payload.Credentials["default"]; ok {
96
+ payload.DefaultCredentialName = "default"
97
+ }
98
+ }
99
+ keys := make([]string, 0, len(payload.Credentials))
100
+ for name := range payload.Credentials {
101
+ keys = append(keys, name)
102
+ }
103
+ sort.Strings(keys)
104
+ for _, name := range keys {
105
+ entry := payload.Credentials[name]
106
+ entry.CredentialName = name
107
+ entry.IsDefault = payload.DefaultCredentialName == name
108
+ payload.Credentials[name] = entry
109
+ }
110
+ return payload
111
+ }
112
+
113
+ func loadIndexFrom(path string) (IndexPayload, error) {
114
+ raw, err := os.ReadFile(path)
115
+ if err != nil {
116
+ if os.IsNotExist(err) {
117
+ return defaultIndex(), nil
118
+ }
119
+ return IndexPayload{}, err
120
+ }
121
+ var payload IndexPayload
122
+ if err := json.Unmarshal(raw, &payload); err != nil {
123
+ return IndexPayload{}, fmt.Errorf("parse identity index: %w", err)
124
+ }
125
+ if payload.SchemaVersion != 0 && payload.SchemaVersion != 2 && payload.SchemaVersion != 3 {
126
+ return IndexPayload{}, fmt.Errorf("unsupported identity index schema version: %d", payload.SchemaVersion)
127
+ }
128
+ payload = normalizeIndexPayload(payload)
129
+ return payload, nil
130
+ }
131
+
132
+ func saveIndexTo(path string, payload IndexPayload) error {
133
+ normalized := normalizeIndexPayload(payload)
134
+ if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
135
+ return err
136
+ }
137
+ raw, err := json.MarshalIndent(normalized, "", " ")
138
+ if err != nil {
139
+ return err
140
+ }
141
+ if err := os.WriteFile(path, raw, 0o600); err != nil {
142
+ return err
143
+ }
144
+ return os.Chmod(path, 0o600)
145
+ }
146
+
147
+ func (m *Manager) LoadIndex() (IndexPayload, error) {
148
+ return loadIndexFrom(m.indexPath())
149
+ }
150
+
151
+ func (m *Manager) SaveIndex(payload IndexPayload) error {
152
+ if err := m.EnsureRoot(); err != nil {
153
+ return err
154
+ }
155
+ return saveIndexTo(m.indexPath(), payload)
156
+ }
157
+
158
+ func (m *Manager) resolveEntryName(requested string, index IndexPayload) (string, IndexEntry, bool) {
159
+ if entry, ok := index.Credentials[requested]; ok {
160
+ return requested, entry, true
161
+ }
162
+ if requested == "" || requested == "default" {
163
+ if index.DefaultCredentialName != "" {
164
+ entry, ok := index.Credentials[index.DefaultCredentialName]
165
+ return index.DefaultCredentialName, entry, ok
166
+ }
167
+ }
168
+ return "", IndexEntry{}, false
169
+ }
170
+
171
+ func (m *Manager) PathsForIdentity(name string) (Paths, error) {
172
+ index, err := m.LoadIndex()
173
+ if err != nil {
174
+ return Paths{}, err
175
+ }
176
+ _, entry, ok := m.resolveEntryName(name, index)
177
+ if !ok {
178
+ return Paths{}, fmt.Errorf("%w: %s", ErrIdentityNotFound, name)
179
+ }
180
+ return m.BuildPaths(entry.DirName), nil
181
+ }
182
+
183
+ func writeSecureJSON(path string, payload any) error {
184
+ raw, err := json.MarshalIndent(payload, "", " ")
185
+ if err != nil {
186
+ return err
187
+ }
188
+ if err := os.WriteFile(path, raw, 0o600); err != nil {
189
+ return err
190
+ }
191
+ return os.Chmod(path, 0o600)
192
+ }
193
+
194
+ func writeSecureText(path string, content string) error {
195
+ if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
196
+ return err
197
+ }
198
+ return os.Chmod(path, 0o600)
199
+ }
200
+
201
+ func ensureDir(path string) error {
202
+ if err := os.MkdirAll(path, 0o700); err != nil {
203
+ return err
204
+ }
205
+ return os.Chmod(path, 0o700)
206
+ }
@@ -0,0 +1,378 @@
1
+ package identity
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "io"
7
+ "os"
8
+ "path/filepath"
9
+ "sort"
10
+ "strings"
11
+ )
12
+
13
+ func (m *Manager) ScanLegacy() (*LegacyScan, error) {
14
+ root := m.LegacyRootDir()
15
+ scan := &LegacyScan{
16
+ RootDir: root,
17
+ IndexedEntries: map[string]IndexEntry{},
18
+ LegacyCredentials: []LegacyFlatIdentity{},
19
+ InvalidJSONFiles: []map[string]string{},
20
+ OrphanE2EEFiles: []map[string]string{},
21
+ Hint: LegacyLayoutHint,
22
+ }
23
+ if strings.TrimSpace(root) == "" {
24
+ return scan, nil
25
+ }
26
+ info, err := os.Stat(root)
27
+ if err != nil {
28
+ if os.IsNotExist(err) {
29
+ return scan, nil
30
+ }
31
+ return nil, err
32
+ }
33
+ if !info.IsDir() {
34
+ return scan, nil
35
+ }
36
+
37
+ indexPath := filepath.Join(root, IndexFileName)
38
+ if _, err := os.Stat(indexPath); err == nil {
39
+ payload, err := loadIndexFrom(indexPath)
40
+ if err != nil {
41
+ return nil, err
42
+ }
43
+ scan.IndexedLayout = true
44
+ for name, entry := range payload.Credentials {
45
+ scan.IndexedEntries[name] = entry
46
+ }
47
+ }
48
+
49
+ e2eeCandidates := map[string]string{}
50
+ entries, err := os.ReadDir(root)
51
+ if err != nil {
52
+ return nil, err
53
+ }
54
+ for _, entry := range entries {
55
+ if entry.IsDir() {
56
+ continue
57
+ }
58
+ name := entry.Name()
59
+ if !strings.HasSuffix(name, ".json") {
60
+ continue
61
+ }
62
+ switch {
63
+ case name == IndexFileName:
64
+ continue
65
+ case strings.HasSuffix(name, "_did_document.json"):
66
+ continue
67
+ case strings.HasPrefix(name, LegacyE2EEPrefix):
68
+ e2eeCandidates[strings.TrimSuffix(strings.TrimPrefix(name, LegacyE2EEPrefix), ".json")] = filepath.Join(root, name)
69
+ continue
70
+ }
71
+
72
+ fullPath := filepath.Join(root, name)
73
+ raw, err := os.ReadFile(fullPath)
74
+ if err != nil {
75
+ return nil, err
76
+ }
77
+ var payload map[string]any
78
+ if err := json.Unmarshal(raw, &payload); err != nil {
79
+ scan.InvalidJSONFiles = append(scan.InvalidJSONFiles, map[string]string{
80
+ "file": name,
81
+ "reason": fmt.Sprintf("invalid_json: %v", err),
82
+ })
83
+ continue
84
+ }
85
+ did, didOK := payload["did"].(string)
86
+ privateKeyPEM, keyOK := payload["private_key_pem"].(string)
87
+ if !didOK || did == "" || !keyOK || privateKeyPEM == "" {
88
+ scan.InvalidJSONFiles = append(scan.InvalidJSONFiles, map[string]string{
89
+ "file": name,
90
+ "reason": "not_a_legacy_credential_payload",
91
+ })
92
+ continue
93
+ }
94
+ uniqueID := stringValue(payload["unique_id"], "")
95
+ if uniqueID == "" {
96
+ parts := strings.Split(did, ":")
97
+ uniqueID = parts[len(parts)-1]
98
+ }
99
+ scan.LegacyCredentials = append(scan.LegacyCredentials, LegacyFlatIdentity{
100
+ CredentialName: strings.TrimSuffix(name, ".json"),
101
+ Path: fullPath,
102
+ DID: did,
103
+ UniqueID: uniqueID,
104
+ Handle: stringValue(payload["handle"], ""),
105
+ })
106
+ }
107
+
108
+ legacyNames := map[string]struct{}{}
109
+ for _, entry := range scan.LegacyCredentials {
110
+ legacyNames[entry.CredentialName] = struct{}{}
111
+ }
112
+ for name, path := range e2eeCandidates {
113
+ if _, ok := legacyNames[name]; ok {
114
+ continue
115
+ }
116
+ scan.OrphanE2EEFiles = append(scan.OrphanE2EEFiles, map[string]string{
117
+ "credential_name": name,
118
+ "file": path,
119
+ })
120
+ }
121
+ sort.Slice(scan.LegacyCredentials, func(i, j int) bool {
122
+ return scan.LegacyCredentials[i].CredentialName < scan.LegacyCredentials[j].CredentialName
123
+ })
124
+ sort.Slice(scan.InvalidJSONFiles, func(i, j int) bool {
125
+ return scan.InvalidJSONFiles[i]["file"] < scan.InvalidJSONFiles[j]["file"]
126
+ })
127
+ sort.Slice(scan.OrphanE2EEFiles, func(i, j int) bool {
128
+ return scan.OrphanE2EEFiles[i]["credential_name"] < scan.OrphanE2EEFiles[j]["credential_name"]
129
+ })
130
+ scan.HasLegacy = scan.IndexedLayout || len(scan.LegacyCredentials) > 0 || len(scan.InvalidJSONFiles) > 0 || len(scan.OrphanE2EEFiles) > 0
131
+ return scan, nil
132
+ }
133
+
134
+ func (m *Manager) ImportLegacy(name string) (*ImportResult, error) {
135
+ scan, err := m.ScanLegacy()
136
+ if err != nil {
137
+ return nil, err
138
+ }
139
+ result := &ImportResult{}
140
+ if !scan.HasLegacy {
141
+ return nil, fmt.Errorf("%w: no legacy layout detected", ErrLegacyNotFound)
142
+ }
143
+ if name == "" {
144
+ if len(scan.IndexedEntries) == 1 {
145
+ for candidate := range scan.IndexedEntries {
146
+ name = candidate
147
+ }
148
+ } else if len(scan.LegacyCredentials) == 1 {
149
+ name = scan.LegacyCredentials[0].CredentialName
150
+ } else {
151
+ return nil, fmt.Errorf("%w: multiple legacy identities detected, specify --name or --all", ErrInvalidInput)
152
+ }
153
+ }
154
+
155
+ if entry, ok := scan.IndexedEntries[name]; ok {
156
+ summary, err := m.importIndexedEntry(name, entry, scan)
157
+ if err != nil {
158
+ return nil, err
159
+ }
160
+ result.Imported = append(result.Imported, *summary)
161
+ return result, nil
162
+ }
163
+ for _, legacy := range scan.LegacyCredentials {
164
+ if legacy.CredentialName != name {
165
+ continue
166
+ }
167
+ summary, err := m.importFlatLegacy(legacy)
168
+ if err != nil {
169
+ return nil, err
170
+ }
171
+ result.Imported = append(result.Imported, *summary)
172
+ return result, nil
173
+ }
174
+ return nil, fmt.Errorf("%w: %s", ErrLegacyNotFound, name)
175
+ }
176
+
177
+ func (m *Manager) ImportAllLegacy() (*ImportResult, error) {
178
+ scan, err := m.ScanLegacy()
179
+ if err != nil {
180
+ return nil, err
181
+ }
182
+ if !scan.HasLegacy {
183
+ return nil, fmt.Errorf("%w: no legacy layout detected", ErrLegacyNotFound)
184
+ }
185
+ result := &ImportResult{}
186
+ names := make([]string, 0, len(scan.IndexedEntries))
187
+ for name := range scan.IndexedEntries {
188
+ names = append(names, name)
189
+ }
190
+ sort.Strings(names)
191
+ for _, name := range names {
192
+ summary, err := m.importIndexedEntry(name, scan.IndexedEntries[name], scan)
193
+ if err != nil {
194
+ if strings.Contains(err.Error(), ErrIdentityConflict.Error()) {
195
+ result.Skipped = append(result.Skipped, name)
196
+ continue
197
+ }
198
+ return nil, err
199
+ }
200
+ result.Imported = append(result.Imported, *summary)
201
+ }
202
+ for _, legacy := range scan.LegacyCredentials {
203
+ summary, err := m.importFlatLegacy(legacy)
204
+ if err != nil {
205
+ if strings.Contains(err.Error(), ErrIdentityConflict.Error()) {
206
+ result.Skipped = append(result.Skipped, legacy.CredentialName)
207
+ continue
208
+ }
209
+ return nil, err
210
+ }
211
+ result.Imported = append(result.Imported, *summary)
212
+ }
213
+ return result, nil
214
+ }
215
+
216
+ func (m *Manager) importIndexedEntry(name string, entry IndexEntry, scan *LegacyScan) (*IdentitySummary, error) {
217
+ index, err := m.LoadIndex()
218
+ if err != nil {
219
+ return nil, err
220
+ }
221
+ if existing, ok := index.Credentials[name]; ok && existing.DID != entry.DID {
222
+ return nil, fmt.Errorf("%w: identity %s already exists", ErrIdentityConflict, name)
223
+ }
224
+ srcDir := filepath.Join(scan.RootDir, entry.DirName)
225
+ dst := m.BuildPaths(entry.DirName)
226
+ if err := ensureDir(m.RootDir()); err != nil {
227
+ return nil, err
228
+ }
229
+ if err := copyDir(srcDir, dst.IdentityDir); err != nil {
230
+ return nil, err
231
+ }
232
+ entry.CredentialName = name
233
+ if index.DefaultCredentialName == "" && scan.IndexedLayout {
234
+ if payload, err := loadIndexFrom(filepath.Join(scan.RootDir, IndexFileName)); err == nil && payload.DefaultCredentialName == name {
235
+ index.DefaultCredentialName = name
236
+ entry.IsDefault = true
237
+ }
238
+ }
239
+ index.Credentials[name] = entry
240
+ if err := m.SaveIndex(index); err != nil {
241
+ return nil, err
242
+ }
243
+ return m.summaryFor(entry, index.DefaultCredentialName)
244
+ }
245
+
246
+ func (m *Manager) importFlatLegacy(legacy LegacyFlatIdentity) (*IdentitySummary, error) {
247
+ index, err := m.LoadIndex()
248
+ if err != nil {
249
+ return nil, err
250
+ }
251
+ if existing, ok := index.Credentials[legacy.CredentialName]; ok && existing.DID != legacy.DID {
252
+ return nil, fmt.Errorf("%w: identity %s already exists", ErrIdentityConflict, legacy.CredentialName)
253
+ }
254
+ payload, err := readJSONMap(legacy.Path)
255
+ if err != nil {
256
+ return nil, err
257
+ }
258
+ dirName, err := preferredDirName(legacy.UniqueID)
259
+ if err != nil {
260
+ return nil, err
261
+ }
262
+ input := SaveInput{
263
+ IdentityName: legacy.CredentialName,
264
+ DID: legacy.DID,
265
+ UniqueID: legacy.UniqueID,
266
+ UserID: stringValue(payload["user_id"], ""),
267
+ DisplayName: stringValue(payload["name"], ""),
268
+ Handle: stringValue(payload["handle"], legacy.Handle),
269
+ JWTToken: stringValue(payload["jwt_token"], ""),
270
+ DIDDocument: mapValue(payload["did_document"]),
271
+ Key1PrivatePEM: stringValue(payload["private_key_pem"], ""),
272
+ Key1PublicPEM: stringValue(payload["public_key_pem"], ""),
273
+ E2EESigningPrivatePEM: stringValue(payload["e2ee_signing_private_pem"], ""),
274
+ E2EEAgreementPrivatePEM: stringValue(payload["e2ee_agreement_private_pem"], ""),
275
+ }
276
+ record, err := m.save(input)
277
+ if err != nil {
278
+ return nil, err
279
+ }
280
+ e2eeStatePath := filepath.Join(scanFlatRoot(legacy.Path), LegacyE2EEPrefix+legacy.CredentialName+".json")
281
+ if fileExists(e2eeStatePath) {
282
+ dst := m.BuildPaths(dirName)
283
+ raw, readErr := os.ReadFile(e2eeStatePath)
284
+ if readErr == nil {
285
+ _ = os.WriteFile(dst.E2EEStatePath, raw, 0o600)
286
+ _ = os.Chmod(dst.E2EEStatePath, 0o600)
287
+ }
288
+ }
289
+ return &IdentitySummary{
290
+ IdentityName: record.IdentityName,
291
+ DID: record.DID,
292
+ UniqueID: record.UniqueID,
293
+ UserID: record.UserID,
294
+ DisplayName: record.DisplayName,
295
+ Handle: record.Handle,
296
+ CreatedAt: record.CreatedAt,
297
+ DirName: record.DirName,
298
+ IsDefault: record.IsDefault,
299
+ HasJWT: record.JWTToken != "",
300
+ HasDIDDocument: record.DIDDocument != nil,
301
+ HasKey1Private: record.Key1PrivatePEM != "",
302
+ HasKey1Public: record.Key1PublicPEM != "",
303
+ HasE2EESigningPrivate: record.E2EESigningPrivatePEM != "",
304
+ HasE2EEAgreementPrivate: record.E2EEAgreementPrivatePEM != "",
305
+ }, nil
306
+ }
307
+
308
+ func scanFlatRoot(path string) string {
309
+ return filepath.Dir(path)
310
+ }
311
+
312
+ func mapValue(value any) map[string]any {
313
+ if value == nil {
314
+ return nil
315
+ }
316
+ payload, ok := value.(map[string]any)
317
+ if ok {
318
+ return payload
319
+ }
320
+ raw, err := json.Marshal(value)
321
+ if err != nil {
322
+ return nil
323
+ }
324
+ var decoded map[string]any
325
+ if err := json.Unmarshal(raw, &decoded); err != nil {
326
+ return nil
327
+ }
328
+ return decoded
329
+ }
330
+
331
+ func copyDir(src, dst string) error {
332
+ if err := ensureDir(dst); err != nil {
333
+ return err
334
+ }
335
+ return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
336
+ if err != nil {
337
+ return err
338
+ }
339
+ rel, err := filepath.Rel(src, path)
340
+ if err != nil {
341
+ return err
342
+ }
343
+ target := filepath.Join(dst, rel)
344
+ if info.IsDir() {
345
+ if rel == "." {
346
+ return nil
347
+ }
348
+ return ensureDir(target)
349
+ }
350
+ return copyFile(path, target, info.Mode())
351
+ })
352
+ }
353
+
354
+ func copyFile(src, dst string, mode os.FileMode) error {
355
+ if err := ensureDir(filepath.Dir(dst)); err != nil {
356
+ return err
357
+ }
358
+ from, err := os.Open(src)
359
+ if err != nil {
360
+ return err
361
+ }
362
+ defer from.Close()
363
+ to, err := os.Create(dst)
364
+ if err != nil {
365
+ return err
366
+ }
367
+ defer to.Close()
368
+ if _, err := io.Copy(to, from); err != nil {
369
+ return err
370
+ }
371
+ if err := to.Sync(); err != nil {
372
+ return err
373
+ }
374
+ if mode == 0 {
375
+ mode = 0o600
376
+ }
377
+ return os.Chmod(dst, mode)
378
+ }
@@ -0,0 +1,70 @@
1
+ package identity
2
+
3
+ import "encoding/json"
4
+
5
+ // PublicValue returns a sanitized copy of any public CLI payload.
6
+ // Internal linkage fields such as user_id must not be exposed through the
7
+ // public CLI contract.
8
+ func PublicValue(value any) any {
9
+ return sanitizePublicValue(value)
10
+ }
11
+
12
+ // PublicData returns a sanitized copy of identity-domain command output.
13
+ // Internal linkage fields such as user_id must not be exposed through the
14
+ // public CLI contract.
15
+ func PublicData(data map[string]any) map[string]any {
16
+ if data == nil {
17
+ return nil
18
+ }
19
+ sanitized, ok := PublicValue(data).(map[string]any)
20
+ if !ok {
21
+ return map[string]any{}
22
+ }
23
+ return sanitized
24
+ }
25
+
26
+ func sanitizePublicValue(value any) any {
27
+ if value == nil {
28
+ return nil
29
+ }
30
+ raw, err := json.Marshal(value)
31
+ if err != nil {
32
+ return value
33
+ }
34
+ var decoded any
35
+ if err := json.Unmarshal(raw, &decoded); err != nil {
36
+ return value
37
+ }
38
+ return sanitizePublicDecoded(decoded)
39
+ }
40
+
41
+ func sanitizePublicDecoded(value any) any {
42
+ switch typed := value.(type) {
43
+ case map[string]any:
44
+ sanitized := make(map[string]any, len(typed))
45
+ for key, item := range typed {
46
+ if isInternalIdentityKey(key) {
47
+ continue
48
+ }
49
+ sanitized[key] = sanitizePublicDecoded(item)
50
+ }
51
+ return sanitized
52
+ case []any:
53
+ items := make([]any, 0, len(typed))
54
+ for _, item := range typed {
55
+ items = append(items, sanitizePublicDecoded(item))
56
+ }
57
+ return items
58
+ default:
59
+ return value
60
+ }
61
+ }
62
+
63
+ func isInternalIdentityKey(key string) bool {
64
+ switch key {
65
+ case "user_id", "userId", "UserID":
66
+ return true
67
+ default:
68
+ return false
69
+ }
70
+ }