// @pdsjs/spaces/commit - permissioned repo commits. // // A commit names the repo, the space, the rev and the digest of the repo's set // hash. It carries no signature: the repo host authenticates it to each syncer // with a MAC (see mac.js). import { LtHash } from './lthash.js'; import { bytesEqual, COMMIT_VERSION, verifyCommitMac } from './mac.js'; import { formatSetHashElement } from './path.js'; /** @typedef {import('./mac.js').Commit} Commit */ export class RepoCommit { /** @param {LtHash} [setHash] */ constructor(setHash = new LtHash()) { this.setHash = setHash; } /** @param {Uint8Array|null|undefined} state */ static fromState(state) { return new RepoCommit(new LtHash(state ?? null)); } /** @param {Iterable<{collection: string, rkey: string, cid: string}>} records */ static fromRecords(records) { const commit = new RepoCommit(); for (const { collection, rkey, cid } of records) { commit.add(collection, rkey, cid); } return commit; } /** * @param {string} collection * @param {string} rkey * @param {string} cid * @returns {this} */ add(collection, rkey, cid) { this.setHash.add(formatSetHashElement(collection, rkey, cid)); return this; } /** * @param {string} collection * @param {string} rkey * @param {string} cid * @returns {this} */ remove(collection, rkey, cid) { this.setHash.remove(formatSetHashElement(collection, rkey, cid)); return this; } /** @param {{collection: string, rkey: string, cid: string|null, prev: string|null}} op */ applyOp(op) { if (op.prev) this.remove(op.collection, op.rkey, op.prev); if (op.cid) this.add(op.collection, op.rkey, op.cid); return this; } /** * @param {Iterable<{collection: string, rkey: string, cid: string|null, prev: string|null}>} ops * @returns {this} */ applyOps(ops) { for (const op of ops) this.applyOp(op); return this; } state() { return this.setHash.state(); } /** * Whether this repo's contents match a commit. Verify the commit first. On * its own this says nothing about authenticity. * @param {Commit} commit */ async matches(commit) { return bytesEqual(await this.setHash.digest(), commit.data); } /** * A commit over the current contents. * @param {{space: string, repo: string, rev: string}} ctx * @returns {Promise} */ async toCommit(ctx) { return { ver: COMMIT_VERSION, repo: ctx.repo, space: ctx.space, rev: ctx.rev, data: await this.setHash.digest(), }; } } /** * @typedef {Object} VerifyCommitParams * @property {string} space - the space that was requested * @property {string} repo - the repo that was requested * @property {Uint8Array} sharedSecret - the secret for the request * @property {Uint8Array} mac - from the response's Atproto-Space-Mac header */ /** * Verify a commit against the MAC its response carried. Once this passes, * `data` is trusted as the author's claim about their repo, which is what makes * RepoCommit#matches meaningful. * * @param {Commit} commit * @param {VerifyCommitParams} params * @returns {Promise} */ export async function verifyCommit(commit, params) { if (commit.ver !== COMMIT_VERSION) return false; if (commit.repo !== params.repo) return false; if (commit.space !== params.space) return false; return verifyCommitMac(params.sharedSecret, commit, params.mac); }