物協(xié)議(Shared Artifact Protocol)的判別式 Subject 模型:candidates 與簽名 payload 的版本化演進(jìn))
pnpm 共享產(chǎn)物協(xié)議Shared Artifact Protocol的判別式 Subject 模型candidates 與簽名 payload 的版本化演進(jìn)【免費下載鏈接】pnpmFast, disk space efficient package manager項目地址: https://gitcode.com/gh_mirrors/pn/pnpm導(dǎo)讀pnpm 的共享產(chǎn)物協(xié)議Shared Artifact Protocol是一個實驗性協(xié)議構(gòu)建方把依賴副作用或工作區(qū)任務(wù)的構(gòu)建產(chǎn)物上傳到 pnpr 服務(wù)端其他機(jī)器可以按兼容性約束復(fù)用這些產(chǎn)物從而跳過重復(fù)的腳本執(zhí)行與構(gòu)建。本文基于 .changeset/general-artifact-subjects.md 的變更說明結(jié)合倉庫內(nèi) Rust 協(xié)議實現(xiàn)與 TypeScript 客戶端源碼深入講解協(xié)議中**判別式 Subjectdiscriminated subject**的設(shè)計candidates 與簽名 payload 如何通過dependency-side-effects與workspace-task兩類 subject 明確這份產(chǎn)物屬于誰以及該演進(jìn)對協(xié)議請求體、簽名載荷和版本兼容性帶來的影響。讀完本文你將理解共享產(chǎn)物協(xié)議的線格式、subject 校驗規(guī)則與升級注意事項。一、變更背景為什么需要 Subject 概念在共享產(chǎn)物協(xié)議中一次復(fù)用的核心是回答一個問題某一構(gòu)建輸入input的產(chǎn)物是否已經(jīng)由可信構(gòu)建方產(chǎn)出并存放在 pnpr 服務(wù)端在 subject 模型落地之前candidates 與簽名 payload 對產(chǎn)物歸屬的刻畫不夠結(jié)構(gòu)化。本次變更general-artifact-subjects將這一刻畫統(tǒng)一抽象為判別式 subjectGeneralized the experimental shared-artifact protocol so candidates and signed payloads identify a discriminated subject. Dependency side effects use package and source-integrity subjects, while workspace tasks use project and task subjects.依賴副作用dependency side effects的產(chǎn)物用package包名 版本與 source-integrity源碼完整性作為 subject工作區(qū)任務(wù)workspace tasks的產(chǎn)物用project項目標(biāo)識與 task任務(wù)名作為 subject。這一改動同時觸及協(xié)議的兩類關(guān)鍵數(shù)據(jù)resolve 請求中的 candidate客戶端詢問我要什么和發(fā)布請求中的簽名 payload服務(wù)端存儲我信什么因此屬于破壞性變更客戶端包pnpm/pnpr.client為此打上了major版本號。二、Subject 的線格式一個帶判別標(biāo)簽的枚舉2.1 Rust 側(cè)定義Subject 在協(xié)議 crate 中被建模為帶 Serde 標(biāo)簽的枚舉線格式上通過kind字段判別。核心定義位于 pnpm/crates/shared-artifact-protocol/src/lib.rs#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(tag kind)] pub enum ArtifactSubject { #[serde(rename dependency-side-effects)] DependencySideEffects { package: PackageIdentity, #[serde(rename sourceIntegrity)] source_integrity: String, }, #[serde(rename workspace-task)] WorkspaceTask { project: String, task: String }, }其中PackageIdentity由包名與版本構(gòu)成lib.rspub struct PackageIdentity { pub name: String, pub version: String, }對應(yīng)的 JSON 線格式示例// 依賴副作用 subject { kind: dependency-side-effects, package: { name: esbuild, version: 0.21.5 }, sourceIntegrity: sha512-xxxx } // 工作區(qū)任務(wù) subject { kind: workspace-task, project: apps/web, task: build }注意sourceIntegrity字段在 Rust 側(cè)通過#[serde(rename)]映射為 camelCase與協(xié)議線格式保持一致而kind字段的標(biāo)簽值使用 kebab-casedependency-side-effects/workspace-task。2.2 TypeScript 客戶端側(cè)的鏡像定義pnpr 的 Node.js 客戶端pnpr/client/src/sharedSideEffects.ts維護(hù)了完全同構(gòu)的類型定義并通過subjectArtifactIdentity函數(shù)把 subject 映射回對應(yīng)的 artifact kind 與 input key 前綴export interface DependencySideEffectsSubject { kind: dependency-side-effects package: PackageIdentity sourceIntegrity: string } export interface WorkspaceTaskSubject { kind: workspace-task project: string task: string } export type ArtifactSubject DependencySideEffectsSubject | WorkspaceTaskSubjectsubject 與產(chǎn)物類型artifact kind之間存在一一對應(yīng)的派生關(guān)系由 sharedSideEffects.ts 集中維護(hù)Subject kindArtifact kindInput key 前綴dependency-side-effectsdependency-side-effects:v1dependency-side-effects:v1:workspace-taskworkspace-task:v1workspace-task:v1:這些常量在 Rust 側(cè)同樣定義lib.rspub const DEPENDENCY_SIDE_EFFECTS_ARTIFACT_KIND: str dependency-side-effects:v1; pub const DEPENDENCY_SIDE_EFFECTS_INPUT_KEY_PREFIX: str dependency-side-effects:v1:; pub const WORKSPACE_TASK_ARTIFACT_KIND: str workspace-task:v1; pub const WORKSPACE_TASK_INPUT_KEY_PREFIX: str workspace-task:v1:;也就是說只要拿到一個 subject協(xié)議就能推導(dǎo)出它屬于哪一類產(chǎn)物、其 input key 必須以哪個前綴開頭——這正是判別式discriminated的含義。三、candidates客戶端如何聲明我要這份產(chǎn)物resolve 請求中客戶端發(fā)送一個 candidate 列表服務(wù)端據(jù)此返回已存儲的產(chǎn)物變體。candidate 由三要素組成lib.rspub struct ArtifactCandidate { pub key: String, pub subject: ArtifactSubject, pub owner: OwnerScope, }keyinput key必須以 subject 對應(yīng)的前綴開頭subject判別式 subject聲明這份產(chǎn)物屬于哪個包/源碼或哪個項目/任務(wù)owner所有權(quán)范圍Organization或Publisher見 lib.rs。在客戶端側(cè)resolveSharedSideEffectssharedSideEffects.ts會先按策略過濾候選再對每個 candidate 執(zhí)行validateCandidatesharedSideEffects.ts校驗key前綴、owner 與 subject 本身。發(fā)送的請求體形如POST /-/pnpr/v0/artifacts/resolve { candidates: [ { key: dependency-side-effects:v1:..., subject: { kind: dependency-side-effects, package: { name: ..., version: ... }, sourceIntegrity: ... }, owner: { type: publisher, package: ... } } ] }服務(wù)端返回每個 key 對應(yīng)的ResolvedArtifact其中variants是若干已簽名信封ArtifactVariantlib.rs。四、簽名 payloadsubject 如何進(jìn)入受信任的載荷4.1 Payload 結(jié)構(gòu)簽名 payloadArtifactPayload中同樣攜帶 subjectlib.rspub struct ArtifactPayload { pub kind: String, pub subject: ArtifactSubject, pub input_key: String, pub owner: OwnerScope, pub builder_id: String, pub builder_profile: BuilderProfile, pub compatibility: CompatibilityConstraints, pub manifest: ArtifactManifest, }也就是說subject 是簽名內(nèi)容的一部分。當(dāng)服務(wù)端/客戶端驗證信封簽名后得到的 payload 同時包含了產(chǎn)物歸屬subject這一承諾任何篡改 subject例如把 A 包的產(chǎn)物偽稱為 B 包的都會導(dǎo)致簽名驗證失敗或 subject 校驗失敗。4.2 簽名信封信封結(jié)構(gòu)定義于 lib.rspub struct SignedArtifactEnvelope { pub algorithm: String, // ecdsa-p256-sha256 pub key_id: String, pub payload: String, // base64 編碼的 JSON 字節(jié) pub signature: String, // base64 編碼的 DER P-256 簽名 }簽名與驗證的完整實現(xiàn)位于 pnpm/crates/shared-artifact-protocol/src/signatures.rs簽名前先對 payload 執(zhí)行payload.validate()再對serde_json::to_vec(payload)得到的精確字節(jié)做 ECDSA P-256 簽名驗證時先解碼 payload 字節(jié)、校驗 base64 與 DER 的 canonical 形式再用公鑰驗簽最后再次validate()。由于簽名覆蓋的是 payload 的原始字節(jié)而非某種規(guī)范化 JSON不同實現(xiàn)之間無需約定 JSON 規(guī)范化算法即可互操作??蛻舳藗?cè)對應(yīng)實現(xiàn)為createSignedArtifactEnvelope與verifySignedArtifactEnvelopesharedSideEffects.ts 與 sharedSideEffects.ts并在 sharedSideEffects.ts 強制要求密鑰必須是 P-256 EC 私鑰。4.3 Payload 與 candidate 的交叉校驗無論是服務(wù)端發(fā)布校驗還是客戶端 resolve 選擇都會把 payload 與 candidate 做一致性比對。Rust 側(cè)的artifact_matches_candidatepnpr/crates/shared-artifacts/src/artifact_identity.rs要求三者全部相等pub(super) fn artifact_matches_candidate(payload: ArtifactPayload, candidate: ArtifactCandidate) - bool { let ArtifactCandidate { key: input_key, subject, owner } candidate; payload.input_key *input_key payload.subject *subject payload.owner *owner }客戶端側(cè)在 sharedSideEffects.ts 用subjectsEqual對 payload.subject 與 candidate.subject 做逐字段比對sharedSideEffects.ts。這保證了請求的產(chǎn)物與簽名承諾的產(chǎn)物必須指向同一個 subject。五、Subject 的校驗規(guī)則兩個變體的不同約束subject 校驗位于 Rust 側(cè) pnpm/crates/shared-artifact-protocol/src/validation.rs客戶端側(cè)鏡像位于 sharedSideEffects.ts。兩類 subject 的約束差異如下dependency-side-effectspackage必須是合法的包標(biāo)識包名、版本長度均不超過 256 字節(jié)且不含控制字符PackageIdentity::validatevalidation.rssourceIntegrity長度不超過 1024 字節(jié)若 owner 為Publisher則owner 中的包名必須與 subject 中的包名一致validate_publisher_packagevalidation.rs——防止一個發(fā)布者替別的包署名產(chǎn)物。workspace-taskproject長度不超過 4096 字節(jié)task長度不超過 256 字節(jié)owner 必須是Organizationworkspace-task產(chǎn)物不允許Publisher所有權(quán)validation.rs錯誤信息為workspace task artifacts require an organization owner。此外payload 校驗還會檢查kind與 subject 派生出的 artifact kind 一致、input_key以對應(yīng)前綴開頭validation.rs從而把 kind、input key、subject 三者綁定為一個自洽的整體。六、Subject 如何參與存儲身份entry digest在 pnpr 服務(wù)端存儲條目entry的身份由input key subject共同哈希而來pnpr/crates/shared-artifacts/src/artifact_identity.rspub(super) fn entry_digest(key: str, subject: ArtifactSubject) - String { let mut hasher Sha256::new(); hasher.update(bpnpm-shared-artifact-entry-v1\0); hasher.update(key.as_bytes()); hasher.update([0]); hasher.update(serde_json::to_vec(subject).expect(artifact subjects serialize)); hex(hasher.finalize()) }這意味著subject 不同產(chǎn)物就存到不同的 entry 下。例如同一 input key 下dependency-side-effects與workspace-task兩個 subject 不會互相干擾同一包不同sourceIntegrity源碼被修改也會得到不同的 entry。這種key subject的復(fù)合尋址正是本次變更把 subject 引入 candidates 與簽名 payload 后存儲層得以區(qū)分產(chǎn)物歸屬的關(guān)鍵。七、版本兼容性為什么必須服務(wù)端與客戶端版本匹配變更說明最后一條明確強調(diào)This changes shared-artifact request bodies and signed payloads. A pnpr server and its clients have to be on matching versions.由于本次變更同時修改了resolve 請求體candidates 攜帶 subject與簽名 payloadsubject 字段舊版服務(wù)端無法理解新版客戶端的請求結(jié)構(gòu)新版客戶端也無法接受舊版服務(wù)端返回/存儲的載荷結(jié)構(gòu)。因此pnpm/pnpr.client打major版本請求體與簽名載荷的線格式破壞性變更pnpm/pnpr、pacquet、pnpm打minor版本作為消費方跟隨協(xié)議演進(jìn)。升級實踐上應(yīng)先升級 pnpr 服務(wù)端再升級使用共享產(chǎn)物協(xié)議的客戶端pnpm / pacquet / pnpr.client并確保兩端處于同一協(xié)議代際??蛻舳诉€提供了能力探測入口pnprSupportsSharedSideEffectssharedSideEffects.ts通過GET /-/pnpr檢查服務(wù)端是否聲明支持 artifacts 協(xié)議版本可用于發(fā)布/解析前的兼容性判斷。八、進(jìn)一步閱讀與驗證想深入驗證 subject 模型的完整行為可以從以下倉庫位置入手協(xié)議線格式與常量pnpm/crates/shared-artifact-protocol/src/lib.rs簽名與驗簽pnpm/crates/shared-artifact-protocol/src/signatures.rssubject/payload/candidate 校驗規(guī)則pnpm/crates/shared-artifact-protocol/src/validation.rs服務(wù)端存儲身份entry digestpnpr/crates/shared-artifacts/src/artifact_identity.rs服務(wù)端并發(fā)與作用域scope管理pnpr/crates/shared-artifacts/src/scopes.rsTypeScript 客戶端實現(xiàn)與類型定義pnpr/client/src/sharedSideEffects.ts客戶端行為測試pnpr/client/test/sharedSideEffects.test.ts 與 pnpr/crates/shared-artifacts/src/tests/behavior.rs小結(jié)general-artifact-subjects是共享產(chǎn)物協(xié)議在身份建模上的一次關(guān)鍵演進(jìn)通過dependency-side-effectspackage sourceIntegrity與workspace-taskproject task兩個判別式 subjectcandidates 與簽名 payload 得以精確、可校驗地聲明產(chǎn)物歸屬kind、input key 前綴與 subject 三者互相綁定entry 存儲身份也由 key subject 共同決定。由于線格式與簽名載荷同步變更升級時務(wù)必保證 pnpr 服務(wù)端與客戶端版本匹配。【免費下載鏈接】pnpmFast, disk space efficient package manager項目地址: https://gitcode.com/gh_mirrors/pn/pnpm創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考