diff --git a/docs/src/content/docs/architecture/data-model.mdx b/docs/src/content/docs/architecture/data-model.mdx index c8b9b19cd..8c5e73c73 100644 --- a/docs/src/content/docs/architecture/data-model.mdx +++ b/docs/src/content/docs/architecture/data-model.mdx @@ -112,13 +112,18 @@ to it; if it's missing, the repo DID is treated as the actor. ## Attestations -`x-tngl-editor` is only as trustworthy as the knot. After you write to a -knot, web adds an `org.tangled.graph.attestation` to your own PDS: a -strong reference to the knot record, plus `change`, the git object ID of -the change the knot made (read through -`sh.tangled.repo.listCobVersions`). A record backed by an attestation is -one you claimed in your own repository, not only one the knot attributes -to you. +`x-tngl-editor` is only as trustworthy as the knot. After you create or +edit a record on a knot, web adds an `org.tangled.graph.attestation` to +your own PDS: a strong reference to that version of the knot record, plus +`change`, the git object ID of the change the knot made. The knot names it +as `x-tngl-change` in the answer to the write; for an answer without one, +web finds it in `sh.tangled.repo.listCobVersions`. Every create or edit +web sends gets its own attestation, so a maintainer's edit to someone +else's issue is attested by the maintainer, and closing and reopening an +issue is attested each time. Changes the knot makes by itself, such as +completing a pull's ticket when it merges, aren't attested. A record +backed by an attestation is one you claimed in your own repository, not +only one the knot attributes to you. Web queues attestations in the browser's local storage and retries until they're written, so a PDS outage delays them rather than losing them. diff --git a/docs/src/content/docs/reference/records.md b/docs/src/content/docs/reference/records.md index e069fc451..967f24d6e 100644 --- a/docs/src/content/docs/reference/records.md +++ b/docs/src/content/docs/reference/records.md @@ -57,7 +57,7 @@ Location key: | ------------------------------- | ------------ | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `org.tangled.feed.comment` | Repo account | TID (minted by the knot) | A comment on a ticket, patch or string, with optional `replyTo`. Images and video attach through the markdown `blobs`. | | `org.tangled.feed.reaction` | Repo account | TID (minted by the knot) | An emoji reaction on a record. Create and delete only. | -| `org.tangled.graph.attestation` | User PDS | TID | The author's claim that a knot-written record is really theirs: `{subject, change}` where `change` is the COB change ID from `sh.tangled.repo.listCobVersions`. | +| `org.tangled.graph.attestation` | User PDS | TID | The author's claim that a knot-written record is really theirs: `{subject, change}` where `change` is the COB change ID, from the write's `x-tngl-change`. | ## Social graph diff --git a/web/src/lib/api/attestation.test.ts b/web/src/lib/api/attestation.test.ts index 29f6c4811..c66031b98 100644 --- a/web/src/lib/api/attestation.test.ts +++ b/web/src/lib/api/attestation.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import type { AttestationSubject } from "$lib/api/attestation"; -import { atUriOf, cidOf, didOf, knotOf } from "$lib/api/syntax"; +import { atUriOf, cidOf, didOf, gitOidOf, knotOf } from "$lib/api/syntax"; vi.mock("$app/environment", () => ({ browser: true })); @@ -103,6 +103,26 @@ describe("attest and flushAttestations", () => { expect([...store.keys()].filter((key) => key.startsWith(settledPrefix))).toHaveLength(1); }); + it("attests a repeated cid under a new change, and still dedupes the same change and changeless replays", async () => { + const first = gitOidOf("1".repeat(40)); + const reopened = gitOidOf("2".repeat(40)); + const { attestation, writes, queued } = await loadWith(succeed); + + attestation.attest(agent, subject, undefined, first); + await queued(0); + attestation.attest(agent, subject, undefined, reopened); + await queued(0); + attestation.attest(agent, subject, undefined, reopened); + await queued(0); + expect(writes.map((write) => write.record.change)).toEqual([first, reopened]); + + attestation.attest(agent, other); + await queued(0); + attestation.attest(agent, other); + await queued(0); + expect(writes).toHaveLength(3); + }); + it("reports each failure message, keeps failed entries queued, and survives corrupt rows and full quotas", async () => { store.set("tangled:attestations:did:plc:writer:3lacorrupt", "{not json"); const { attestation, queued } = await loadWith(fail); @@ -121,22 +141,32 @@ describe("attest and flushAttestations", () => { expect(() => attestation.attest(agent, subject)).not.toThrow(); }); - it("attaches change git object IDs by paging listCobVersions, and writes without them when knot version listings fail", async () => { + it("takes the newest change listCobVersions has for a cid, and writes without one when the listing fails", async () => { const pages = [ - { versions: [{ cid: "bafyreirecent", changeId: "1c0ffee" }], cursor: "500" }, { versions: [ + { cid: subject.cid, changeId: "1c0ffee1c0ffee1c0ffee1c0ffee1c0ffee1c0ff" } + ], + cursor: "500" + }, + { + versions: [ + { cid: subject.cid, changeId: "2".repeat(40) }, { cid: subject.cid, changeId: "5d1f0a3c9b8e47d2a1c6f4e8b3d7a2c5e9f1b6d8" } ] } ]; const asked: string[] = []; + const orders: (string | null)[] = []; + const limits: (string | null)[] = []; vi.stubGlobal( "fetch", vi.fn(async (input: URL) => { if (String(input).includes("knot.other")) return { ok: false } as unknown as Response; asked.push(`${new URL(String(input)).searchParams.get("cursor")}`); + orders.push(new URL(String(input)).searchParams.get("order")); + limits.push(new URL(String(input)).searchParams.get("limit")); return { ok: true, json: async () => pages[asked.length - 1] @@ -151,6 +181,61 @@ describe("attest and flushAttestations", () => { expect(writes[0]?.record.change).toBe("5d1f0a3c9b8e47d2a1c6f4e8b3d7a2c5e9f1b6d8"); expect(writes[1]?.record.change).toBeUndefined(); expect(asked).toEqual(["0", "500"]); + expect(orders).toEqual([null, null]); + expect(limits).toEqual(["500", "500"]); + }); + + it("writes no change when the walk ends before the newest match could be seen", async () => { + const answers = [ + { + versions: [ + { cid: subject.cid, changeId: "1c0ffee1c0ffee1c0ffee1c0ffee1c0ffee1c0ff" } + ], + cursor: "500" + }, + undefined + ]; + vi.stubGlobal( + "fetch", + vi.fn(async () => { + const answer = answers.shift(); + return { + ok: answer !== undefined, + json: async () => answer + } as unknown as Response; + }) + ); + const { attestation, writes, queued } = await loadWith(succeed); + + attestation.attest(agent, subject, knotOf("knot.test")); + await queued(0); + + expect(writes[0]?.record.change).toBeUndefined(); + }); + + it("doesn't write again for a replay that names no change", async () => { + const change = gitOidOf("1".repeat(40)); + const { attestation, writes, queued } = await loadWith(succeed); + + attestation.attest(agent, subject, knotOf("knot.test"), change); + await queued(0); + attestation.attest(agent, subject, knotOf("knot.test")); + await queued(0); + + expect(writes).toHaveLength(1); + }); + + it("writes the change the knot handed over without asking listCobVersions", async () => { + const asked = vi.fn(); + vi.stubGlobal("fetch", asked); + const change = gitOidOf("5d1f0a3c9b8e47d2a1c6f4e8b3d7a2c5e9f1b6d8"); + const { attestation, writes, queued } = await loadWith(succeed); + + attestation.attest(agent, subject, knotOf("knot.test"), change); + await queued(0); + + expect(writes[0]?.record.change).toBe(change); + expect(asked).not.toHaveBeenCalled(); }); it("dismissal mid-flush drops queued entries and lets in-flight writes finish", async () => { diff --git a/web/src/lib/api/attestation.ts b/web/src/lib/api/attestation.ts index d3212de0c..f5eb2eb62 100644 --- a/web/src/lib/api/attestation.ts +++ b/web/src/lib/api/attestation.ts @@ -100,11 +100,19 @@ const readSettled = (did: Did): PendingAttestation[] => entriesUnder(under(SETTL export const pendingAttestations = (did: Did): readonly PendingAttestation[] => read(did); -const isQueued = (entries: readonly PendingAttestation[], subject: AttestationSubject): boolean => - entries.some((entry) => entry.subject.uri === subject.uri && entry.subject.cid === subject.cid); +type Claim = Pick; -const isSettled = (did: Did, subject: AttestationSubject): boolean => - isQueued(readSettled(did), subject); +// a cid is only the content, so reopening an issue repeats an old cid under a new change, +// while an answer with no change is a replay of a version that is already claimed +const isQueued = (entries: readonly Claim[], claim: Claim): boolean => + entries.some( + (entry) => + entry.subject.uri === claim.subject.uri && + entry.subject.cid === claim.subject.cid && + (claim.change === undefined || entry.change === claim.change) + ); + +const isSettled = (did: Did, claim: Claim): boolean => isQueued(readSettled(did), claim); const announce = (): void => guarded(() => window.dispatchEvent(new CustomEvent(ATTESTATIONS_CHANGED))); @@ -136,11 +144,13 @@ const settle = (did: Did, entry: PendingAttestation): void => announce(); }); +// only knots that don't send the change get here, and they list oldest first const changeIdOf = async ( knot: KnotOrigin, subject: AttestationSubject ): Promise => { const deadline = Date.now() + ENRICHMENT_DEADLINE_MS; + let found: string | undefined; for (let offset = 0; Date.now() < deadline;) { const url = new URL(`https://${knot}/xrpc/${LIST_COB_VERSIONS}`); url.searchParams.set("uri", subject.uri); @@ -155,10 +165,12 @@ const changeIdOf = async ( versions?: { cid?: string; changeId?: string }[]; cursor?: string; }; - const match = page.versions?.find((version) => version.cid === subject.cid); - if (match?.changeId !== undefined) return gitOidOf(match.changeId); + // a cid can come back under a newer change, so the last match wins and the walk has to finish + const match = page.versions?.filter((version) => version.cid === subject.cid).at(-1); + if (match?.changeId !== undefined) found = match.changeId; const next = page.cursor === undefined ? undefined : Number(page.cursor); - if (next === undefined || Number.isNaN(next) || next <= offset) return undefined; + if (next === undefined) return found === undefined ? undefined : gitOidOf(found); + if (Number.isNaN(next) || next <= offset) return undefined; offset = next; } return undefined; @@ -180,7 +192,7 @@ export const flushAttestations = async (agent: OAuthUserAgent): Promise live.rkey === entry.rkey)) continue; - if (isSettled(did, entry.subject)) { + if (isSettled(did, entry)) { guarded(() => { localStorage.removeItem(`${under(STORAGE_PREFIX, did)}${entry.rkey}`); announce(); @@ -214,16 +226,19 @@ export const flushAttestations = async (agent: OAuthUserAgent): Promise { const did = didOf(agent.sub); if (!browser) return; - if (isSettled(did, subject)) return; - if (isQueued(read(did), subject)) return; + const claim: Claim = { subject, ...(change ? { change } : {}) }; + if (isSettled(did, claim)) return; + if (isQueued(read(did), claim)) return; storePending(did, { rkey: tidOf(tidNow()), subject, ...(knot ? { knot } : {}), + ...(change ? { change } : {}), createdAt: datetimeOf(new Date().toISOString()) }); void flushAttestations(agent).catch(() => undefined); diff --git a/web/src/lib/api/cob.test.ts b/web/src/lib/api/cob.test.ts index eea474242..6c99d6954 100644 --- a/web/src/lib/api/cob.test.ts +++ b/web/src/lib/api/cob.test.ts @@ -16,11 +16,17 @@ const readCid = "bafyreiread" + "a".repeat(48); const storedCid = "bafyreistored" + "a".repeat(46); const written = { uri, cid: "bafyreiknot" + "a".repeat(48) }; const from = { uri, cid: cidOf(readCid) }; +const change = "5d1f0a3c9b8e47d2a1c6f4e8b3d7a2c5e9f1b6d8"; const load = async (...responses: (() => Response)[]) => { vi.resetModules(); const minted: { aud: string; lxm: string }[] = []; + const attested: { agent: unknown; subject: unknown; knot: unknown; change: unknown }[] = []; vi.doMock("$app/environment", () => ({ browser: true })); + vi.doMock("$lib/api/attestation", () => ({ + attest: (agent: unknown, subject: unknown, knot: unknown, change: unknown) => + attested.push({ agent, subject, knot, change }) + })); vi.doMock("$lib/auth/agent", () => ({ mintServiceAuth: async (_agent: unknown, { aud, lxm }: { aud: string; lxm: string }) => { minted.push({ aud, lxm }); @@ -35,7 +41,7 @@ const load = async (...responses: (() => Response)[]) => { return (responses.shift() ?? (() => Response.json(written)))(); }); const sent = (index = 0) => JSON.parse(String(calls[index].init.body)); - return { cob, cache, minted, calls, sent }; + return { cob, cache, minted, calls, sent, attested }; }; afterEach(() => { @@ -96,6 +102,45 @@ describe("cob writes go to the repo's knot", () => { expect(minted[0].lxm).toBe("com.atproto.repo.deleteRecord"); }); + it("attests the version the knot made, for creates and edits, not erases", async () => { + const edited = { uri, cid: cidOf("bafyreiedit" + "a".repeat(48)) }; + const { cob, attested } = await load( + () => Response.json({ ...written, "x-tngl-change": change }), + () => Response.json(edited), + () => Response.json({ commit: {} }) + ); + + await cob.createCob(agent, knot, repo, collection, record); + await cob.editCob(agent, knot, from, record); + await cob.eraseCob(agent, knot, from); + + expect(attested).toStrictEqual([ + { agent, subject: written, knot, change }, + { agent, subject: edited, knot, change: undefined } + ]); + }); + + it("still returns the write when the knot's change id is malformed", async () => { + const { cob, attested } = await load(() => + Response.json({ ...written, "x-tngl-change": "not-an-oid" }) + ); + + const result = await cob.createCob(agent, knot, repo, collection, record); + + expect(result).toEqual(written); + expect(attested).toStrictEqual([{ agent, subject: written, knot, change: undefined }]); + }); + + it("attests nothing when the knot refuses the write", async () => { + const { cob, attested } = await load(() => + Response.json({ error: "Forbidden" }, { status: 403 }) + ); + + await cob.createCob(agent, knot, repo, collection, record).catch(() => {}); + + expect(attested).toEqual([]); + }); + it("drops cached reads after a write lands", async () => { const { cob, cache } = await load(); const read = vi.fn().mockResolvedValue("stale"); diff --git a/web/src/lib/api/cob.ts b/web/src/lib/api/cob.ts index e337a1134..78ddfe043 100644 --- a/web/src/lib/api/cob.ts +++ b/web/src/lib/api/cob.ts @@ -1,12 +1,14 @@ import { parseCanonicalResourceUri } from "@atcute/lexicons/syntax"; import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import { mintServiceAuth } from "$lib/auth/agent"; +import { attest } from "$lib/api/attestation"; import { clear } from "$lib/api/cache"; import { buildUrl, toResponseError } from "$lib/api/_request"; import { atUriOf, cidOf, didOf, + gitOidOf, nsidOf, recordKeyOf, type AtUri, @@ -41,6 +43,13 @@ const DELETE_NSID = nsidOf("com.atproto.repo.deleteRecord"); const GET_NSID = nsidOf("com.atproto.repo.getRecord"); const UPLOAD_BLOB_NSID = nsidOf("com.atproto.repo.uploadBlob"); const EDITOR_FIELD = "x-tngl-editor"; +const CHANGE_FIELD = "x-tngl-change"; + +interface KnotWrite { + uri: string; + cid: string; + [CHANGE_FIELD]?: string; +} export const cobRefOf = (view: { uri: string; cid?: string | null }): CobRef => { if (!view.cid) throw new Error(`${view.uri} is missing its cid, so it can't be changed`); @@ -91,10 +100,21 @@ export const knotProcedure = async ( init ); -const refFrom = (saved: { uri: string; cid: string }): CobRef => ({ - uri: atUriOf(saved.uri), - cid: cidOf(saved.cid) -}); +// the write already landed, so a bad id falls back to the lookup instead of failing it +const changeOf = (saved: KnotWrite) => { + try { + return saved[CHANGE_FIELD] ? gitOidOf(saved[CHANGE_FIELD]) : undefined; + } catch { + return undefined; + } +}; + +// every create and edit comes through here, so call sites never attest themselves +const attested = (agent: OAuthUserAgent, knot: KnotOrigin, saved: KnotWrite): CobRef => { + const ref = { uri: atUriOf(saved.uri), cid: cidOf(saved.cid) }; + attest(agent, ref, knot, changeOf(saved)); + return ref; +}; export const createCob = async ( agent: OAuthUserAgent, @@ -104,8 +124,10 @@ export const createCob = async ( record: T, rkey?: Rkey ): Promise => - refFrom( - await knotProcedure<{ uri: string; cid: string }>(agent, knot, repo, CREATE_NSID, { + attested( + agent, + knot, + await knotProcedure(agent, knot, repo, CREATE_NSID, { collection, ...(rkey ? { rkey } : {}), record @@ -125,8 +147,10 @@ export const editCob = async ( swapRecord: from.cid, record }; - return refFrom( - await knotProcedure<{ uri: string; cid: string }>(agent, knot, didOf(repo), PUT_NSID, input) + return attested( + agent, + knot, + await knotProcedure(agent, knot, didOf(repo), PUT_NSID, input) ); }; diff --git a/web/src/lib/api/comment.test.ts b/web/src/lib/api/comment.test.ts index 932b3a921..b5564f4e6 100644 --- a/web/src/lib/api/comment.test.ts +++ b/web/src/lib/api/comment.test.ts @@ -1,7 +1,6 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import { atUriOf, cidOf, datetimeOf, didOf, gitOidOf, knotOf, recordKeyOf } from "$lib/api/syntax"; -import type { AttestationSubject } from "$lib/api/attestation"; vi.mock("$app/environment", () => ({ browser: false })); @@ -25,7 +24,7 @@ const load = async () => { vi.resetModules(); const legacy: { collection: string; rkey: string; record: Record }[] = []; const knotWrites: { collection?: string; record?: unknown; uri?: string }[] = []; - const attested: AttestationSubject[] = []; + const attested: unknown[] = []; const awaited: string[] = []; vi.doMock("$lib/api/repo", () => ({ resolveRepoKnot: vi.fn(async () => knot) })); vi.doMock("$lib/api/write", () => ({ @@ -39,8 +38,8 @@ const load = async () => { deleteRecord: vi.fn(async () => {}) })); vi.doMock("$lib/api/attestation", () => ({ - attest: vi.fn((_a: unknown, written: AttestationSubject) => { - attested.push({ uri: written.uri, cid: written.cid }); + attest: vi.fn((_a: unknown, subject: unknown) => { + attested.push(subject); }) })); vi.doMock("$lib/api/awaitRecord", () => ({ @@ -119,9 +118,9 @@ describe("comment write targets", () => { expect(attested).toEqual([]); }); - it("drops embeds on knot writes, attests creates, and restates image embeds on edits", async () => { + it("drops embeds on knot writes and restates image embeds on edits", async () => { capabilities(["knot-social-writes"]); - const { comment, knotWrites, attested } = await load(); + const { comment, knotWrites } = await load(); const written = await comment.openComment(agent, "https://bobbin.test", repoDid, { subject, text: "look", @@ -131,7 +130,6 @@ describe("comment write targets", () => { expect(written.rkey).toBe("3lnew"); expect(knotWrites[0]?.collection).toBe("org.tangled.feed.comment"); expect((knotWrites[0]?.record as Record).embed).toBeUndefined(); - expect(attested).toEqual([{ uri: written.uri, cid: written.cid }]); await comment.editCommentOn(agent, "https://bobbin.test", repoDid, { subject, @@ -149,6 +147,5 @@ describe("comment write targets", () => { { image: { ref: { $link: "bafyreinew" + "a".repeat(49) } } } ] }); - expect(attested).toHaveLength(1); }); }); diff --git a/web/src/lib/api/comment.ts b/web/src/lib/api/comment.ts index bbb11a550..1c8865c3d 100644 --- a/web/src/lib/api/comment.ts +++ b/web/src/lib/api/comment.ts @@ -18,7 +18,6 @@ import { now as tidNow } from "@atcute/tid"; import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import type { BobbinContext, XrpcRequestInit } from "$lib/api/client"; import { jsonGet } from "$lib/api/_request"; -import { attest } from "$lib/api/attestation"; import { awaitReadable, awaitRemoved } from "$lib/api/awaitRecord"; import { writeTargetFor } from "$lib/api/knot"; import { @@ -121,11 +120,7 @@ export const openComment = async ( const target = await writeTargetFor(bobbinUrl, repoDid); if (target.kind === "knot") { const record = commentRecord(input, input.createdAt); - const written = withRkey( - await createCob(agent, target.knot, repoDid, COMMENT_COLLECTION, record) - ); - attest(agent, written, target.knot); - return written; + return withRkey(await createCob(agent, target.knot, repoDid, COMMENT_COLLECTION, record)); } const saved = await putRecord( agent, diff --git a/web/src/lib/api/issue.test.ts b/web/src/lib/api/issue.test.ts index 23a2ebe45..2419f837b 100644 --- a/web/src/lib/api/issue.test.ts +++ b/web/src/lib/api/issue.test.ts @@ -1,7 +1,6 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import { cidOf, didOf, knotOf, recordKeyOf } from "$lib/api/syntax"; -import type { AttestationSubject } from "$lib/api/attestation"; vi.mock("$app/environment", () => ({ browser: false })); @@ -15,14 +14,14 @@ const load = async () => { vi.resetModules(); const knotWrites: { record?: Record }[] = []; const legacy: { record: Record; swap?: string }[] = []; - const attested: AttestationSubject[] = []; + const attested: unknown[] = []; const awaited: string[] = []; vi.doMock("$lib/api/repo", () => ({ resolveRepoKnot: vi.fn(async () => knot) })); vi.doMock("$lib/auth/agent", () => ({ mintServiceAuth: vi.fn(async () => "jwt") })); vi.doMock("$lib/api/cache", () => ({ clear: () => {} })); vi.doMock("$lib/api/attestation", () => ({ - attest: vi.fn((_a: unknown, written: AttestationSubject) => { - attested.push({ uri: written.uri, cid: written.cid }); + attest: vi.fn((_a: unknown, subject: unknown) => { + attested.push(subject); }) })); vi.doMock("$lib/api/awaitRecord", () => ({ @@ -94,10 +93,10 @@ afterEach(() => { }); describe("ticket write targets", () => { - it("writes lexicon ticket records on knots, attests creates only, and restates live labels and branch on edits", async () => { + it("writes lexicon ticket records on knots and restates live labels and branch on edits", async () => { capabilities(["knot-social-writes"]); - const { issue, knotWrites, attested } = await load(); - const created = await issue.openTicket(agent, "https://bobbin.test", repoDid, { + const { issue, knotWrites } = await load(); + await issue.openTicket(agent, "https://bobbin.test", repoDid, { title: "new", body: "prose" }); @@ -107,7 +106,6 @@ describe("ticket write targets", () => { body: { $type: "org.tangled.markup.markdown", text: "prose" }, state: "open" }); - expect(attested).toEqual([{ uri: created.uri, cid: created.cid }]); await issue.editTicket(agent, "https://bobbin.test", repoDid, { rkey, @@ -120,14 +118,14 @@ describe("ticket write targets", () => { tangledLabels: [{ key: "reviewer" }], targetBranch: "release" }); - expect(attested).toHaveLength(1); }); it("writes legacy issue collections when knots don't advertise social writes", async () => { capabilities(["knot-acl"]); - const { issue, knotWrites, legacy, awaited } = await load(); + const { issue, knotWrites, legacy, attested, awaited } = await load(); await issue.openTicket(agent, "https://bobbin.test", repoDid, { title: "new" }); expect(knotWrites).toEqual([]); + expect(attested).toEqual([]); expect(legacy[0]?.record).toEqual({ $type: "sh.tangled.repo.issue", repo: repoDid, diff --git a/web/src/lib/api/issue.ts b/web/src/lib/api/issue.ts index 4523a6433..d802bc5ca 100644 --- a/web/src/lib/api/issue.ts +++ b/web/src/lib/api/issue.ts @@ -11,7 +11,6 @@ import { import { now as tidNow } from "@atcute/tid"; import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import { deleteRecord, putRecord } from "$lib/api/write"; -import { attest } from "$lib/api/attestation"; import { awaitReadable, awaitRemoved } from "$lib/api/awaitRecord"; import { writeTargetFor } from "$lib/api/knot"; import { eraseCob, withRkey, type Written } from "$lib/api/cob"; @@ -62,11 +61,8 @@ export const openTicket = async ( input: TicketInput ): Promise => { const target = await writeTargetFor(bobbinUrl, repoDid); - if (target.kind === "knot") { - const written = withRkey(await createTicket(agent, target.knot, repoDid, proseOf(input))); - attest(agent, written, target.knot); - return written; - } + if (target.kind === "knot") + return withRkey(await createTicket(agent, target.knot, repoDid, proseOf(input))); const saved = await putRecord( agent, ISSUE_COLLECTION, diff --git a/web/src/lib/api/pull.ts b/web/src/lib/api/pull.ts index ce3224fd9..7a1a4125d 100644 --- a/web/src/lib/api/pull.ts +++ b/web/src/lib/api/pull.ts @@ -3,7 +3,6 @@ import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import { mintServiceAuth, serviceDidForHost } from "$lib/auth/agent"; import { createCob, knotProcedure, type CobRef } from "$lib/api/cob"; import { createTicket, eraseTicket, type TicketProse } from "$lib/api/ticket"; -import { attest } from "$lib/api/attestation"; import { datetimeOf, didOf, @@ -39,9 +38,7 @@ export const createPatch = async ( head: { oid: head }, createdAt: datetimeOf(new Date().toISOString()) }; - const written = await createCob(agent, knot, didOf(repo), PATCH_COLLECTION, record); - attest(agent, written, knot); - return written; + return createCob(agent, knot, didOf(repo), PATCH_COLLECTION, record); }; // if the knot refuses the patch, erase the ticket again so there's no pull without a diff diff --git a/web/src/lib/api/reaction.ts b/web/src/lib/api/reaction.ts index 4889e0b92..81adc9348 100644 --- a/web/src/lib/api/reaction.ts +++ b/web/src/lib/api/reaction.ts @@ -11,7 +11,6 @@ import { now as tidNow } from "@atcute/tid"; import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import type { BobbinContext, XrpcRequestInit } from "$lib/api/client"; import { jsonGet } from "$lib/api/_request"; -import { attest } from "$lib/api/attestation"; import type { RecordView } from "$lib/api/records"; import { cobRefOf, createCob, eraseCob, type CobRef } from "$lib/api/cob"; import { rkeyFromUri } from "$lib/api/uri"; @@ -41,9 +40,7 @@ export const react = async ( }; if (!knot) return putRecord(agent, REACTION_COLLECTION, recordKeyOf(tidNow()), record); const { repo } = parseCanonicalResourceUri(subject.uri); - const written = await createCob(agent, knot, didOf(repo), REACTION_COLLECTION, record); - attest(agent, written, knot); - return written; + return createCob(agent, knot, didOf(repo), REACTION_COLLECTION, record); }; export const unreact = async (