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