Files
ShadowsocksX-NG/ShadowsocksX-NG2/Domain/NodeIdentity.swift
T
Qiu Yuzhou 0cf28c97fe feat(ng2): config catalog domain core with credential refs (#25)
配置目录领域核心(spec #21 D3/D5,票 #25):

- Domain/ 新增配置目录树:单父、显子序、无环;手动/订阅来源严格分离;
  有效启用 = 节点及其全部祖先启用;手动节点携带子树移动与重排;
  非空手动组递归删除(返回被删条目供 Keychain 清理);空组持久保留;
  非法操作(跨来源、身份复用即共享父、成环、订阅节点结构改动、
  订阅服务器直挂根)全部带明确原因拒绝且状态不动。
- 凭据引用:密码/插件参数只在 Keychain 存权威副本,配置树只持
  CredentialReference;KeychainCredentialStore 写读一致、覆盖写、幂等删。
- CatalogFileStore:v2/catalog.json 目录 0700/文件 0600,临时文件原子替换;
  缺失 → 空目录,损坏/版本未知/结构不一致(悬空、共享父、成环含根外孤环、
  不可达)→ 命名明确的 corrupt,结构校验收敛在
  ConfigurationCatalog.validated 工厂。
- 测试 43 项覆盖全部验收项;project.yml 增加 Domain/ 源码目录。
- .swiftlint.yml 关闭 trailing_comma(与 .swift-format 尾逗号基线冲突,
  触发案例见文件内注释),README/AGENTS.md 同步 Domain/ 与工具说明。
2026-09-20 15:22:09 +08:00

50 lines
1.7 KiB
Swift

import Foundation
/// 节点来源:手动子树与订阅子树严格分离(CONTEXT.md「Relationships and invariants」)。
enum NodeSource: String, Codable, Equatable, Sendable {
case manual
case subscription
}
/// 节点持久身份:不透明字符串。手动节点每次新建生成全新 UUID,身份不随
/// 显示名、位置或父节点变化,也不复用于其他逻辑节点;订阅节点使用供应商
/// 稳定 ID(作用域限本订阅,由订阅数据保证唯一),本票仅供夹具使用。
struct NodeID: Hashable, Codable, Sendable {
let rawValue: String
init(rawValue: String) { self.rawValue = rawValue }
/// 手动新建节点身份。
static func fresh() -> NodeID { NodeID(rawValue: UUID().uuidString) }
init(from decoder: Decoder) throws {
rawValue = try decoder.singleValueContainer().decode(String.self)
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(rawValue)
}
}
/// 凭据引用:指向 Keychain 权威副本的非秘密关联(CONTEXT.md「Credential
/// reference」)。配置树只存引用,永不存秘密明文;秘密值按 D5 由
/// CredentialStoring 写读。
struct CredentialReference: Hashable, Codable, Sendable {
let rawValue: String
init(rawValue: String) { self.rawValue = rawValue }
/// 新建凭据引用。
static func fresh() -> CredentialReference { CredentialReference(rawValue: UUID().uuidString) }
init(from decoder: Decoder) throws {
rawValue = try decoder.singleValueContainer().decode(String.self)
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(rawValue)
}
}