diff --git a/CHANGELOG.md b/CHANGELOG.md index 29673d1..02ade45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ What changed in each release of SCN Chat, for the people who use it, run it, or ## [Unreleased] +### Changed + +- The server now works with PDSs on the 2026-10-22 spaces alpha, and no longer with older spaces PDSs. On an updated PDS, the chat list failed to load with `Missing required key "hash"`. Opening a shared chat also needs the owner's new `#atproto_kem` key in their DID document. An updated PDS publishes one for the did:plc accounts it manages, and a did:web account has to add its own. + ## [0.5.4] - 2026-10-23 ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index 38cf55e..cee6ea7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,7 +14,7 @@ An AI assistant chat app where each conversation is an atproto permissioned spac - `GLOSSARY.md` holds the project's terms, and `docs/adr/` the decisions behind its less obvious choices. Read the relevant ADR before changing what it decided. - `pnpm dev` runs the server, which serves the web app through Vite, as one process, and `pnpm dev --scan` also loads React Scan in the browser. `pnpm preview` builds the web app and runs the production build locally. `pnpm dev:remote` and `pnpm preview:remote` do the same behind a localhost.run tunnel over ssh, with the tunnel's HTTPS URL as `PUBLIC_URL`, for testing from a phone. `pnpm start` runs the production build for a deployment. `pnpm check` runs lint, type checking, and every test. `pnpm codegen` regenerates the lexicon code after a lexicon change, and `pnpm publish-lexicons ` publishes the lexicons from the lexicon authority account. `pnpm keys` prints fresh secrets, and `pnpm setup-env` writes them into `.env`. - Local setup: run `pnpm setup-env`, which creates `.env` from `.env.example` with fresh secrets, and put your DID in `ADMIN_DIDS`. Plugins, models, roles, and every other runtime setting are configured in the admin area at `/admin`. -- Server tests run on both SQLite and PGlite through `tests/helpers/db.ts`. `tests/helpers/fake-pds.ts` is a fake spaces PDS with real error codes, policies, and signed commits. +- Server tests run on both SQLite and PGlite through `tests/helpers/db.ts`. `tests/helpers/fake-pds.ts` is a fake spaces PDS with real error codes, policies, commits, and MACs. ## The founding principle diff --git a/README.md b/README.md index 4c08616..ef4d144 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ SCN Chat is an ATProto AI assistant chat interface allowing users to use their A > **Status:** early development. atproto spaces are in alpha, so expect breaking changes and do not store anything sensitive. +**Note:** If using a spaces enabled PDS it must have been built on the 2026-10-22 spaces alpha or later (currently optional). Earlier alpha PDSs are not compatible. + ## How it works - **Every chat is its own space.** Each conversation lives in a permissioned space on the user's PDS. diff --git a/apps/server/package.json b/apps/server/package.json index d4be6dc..726bcef 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -17,18 +17,18 @@ "site-config": "node src/site-config-cli.ts" }, "dependencies": { - "@atproto/common-web": "0.0.0-spaces-alpha-20261001173819", - "@atproto/crypto": "0.0.0-spaces-alpha-20261001173819", - "@atproto/identity": "0.0.0-spaces-alpha-20261001173819", - "@atproto/lex-cbor": "0.0.0-spaces-alpha-20261001173819", - "@atproto/lex-client": "0.0.0-spaces-alpha-20261001173819", - "@atproto/lex-data": "0.0.0-spaces-alpha-20261001173819", - "@atproto/lex-json": "0.0.0-spaces-alpha-20261001173819", - "@atproto/oauth-client-node": "0.0.0-spaces-alpha-20261001173819", - "@atproto/oauth-scopes": "0.0.0-spaces-alpha-20261001173819", - "@atproto/space": "0.0.0-spaces-alpha-20261001173819", - "@atproto/syntax": "0.0.0-spaces-alpha-20261001173819", - "@atproto/xrpc-server": "0.0.0-spaces-alpha-20261001173819", + "@atproto/common-web": "0.0.0-spaces-alpha-20261022141228", + "@atproto/crypto": "0.0.0-spaces-alpha-20261022141228", + "@atproto/identity": "0.0.0-spaces-alpha-20261022141228", + "@atproto/lex-cbor": "0.0.0-spaces-alpha-20261022141228", + "@atproto/lex-client": "0.0.0-spaces-alpha-20261022141228", + "@atproto/lex-data": "0.0.0-spaces-alpha-20261022141228", + "@atproto/lex-json": "0.0.0-spaces-alpha-20261022141228", + "@atproto/oauth-client-node": "0.0.0-spaces-alpha-20261022141228", + "@atproto/oauth-scopes": "0.0.0-spaces-alpha-20261022141228", + "@atproto/space": "0.0.0-spaces-alpha-20261022141228", + "@atproto/syntax": "0.0.0-spaces-alpha-20261022141228", + "@atproto/xrpc-server": "0.0.0-spaces-alpha-20261022141228", "@hono/node-server": "2.1.1", "@scn-chat/lexicons": "workspace:*", "@scn-chat/plugin-api": "workspace:*", @@ -43,7 +43,7 @@ "zod": "4.6.5" }, "devDependencies": { - "@atproto/common": "0.0.0-spaces-alpha-20261001173819", + "@atproto/common": "0.0.0-spaces-alpha-20261022141228", "@electric-sql/pglite": "0.5.8", "@types/better-sqlite3": "9.6.0", "@types/pg": "8.23.1", diff --git a/apps/server/src/auth/identity.ts b/apps/server/src/auth/identity.ts index 4d0be84..424cd93 100644 --- a/apps/server/src/auth/identity.ts +++ b/apps/server/src/auth/identity.ts @@ -1,13 +1,27 @@ -import { IdResolver } from '@atproto/identity' +import { getVerificationMaterial } from '@atproto/common-web' +import { + type DidDocument, + ensureAtpDocument, + getDidKeyFromMultibase, + IdResolver, +} from '@atproto/identity' import { assertPublicHost } from '../providers/guarded-fetch.ts' -export type ResolvedIdentity = { did: string; handle: string | null; pdsUrl: string } +export type ResolvedIdentity = { + did: string + handle: string | null + pdsUrl: string + /** The key a repo host MACs this DID's space commits and records against. */ + kemKey: string | null +} export interface IdentityResolver { resolve(did: string): Promise resolveHandle(handle: string): Promise /** The DID's atproto signing key. */ resolveSigningKey(did: string, forceRefresh?: boolean): Promise + /** The DID's #atproto_kem key, or null when it has none. */ + resolveKemKey(did: string): Promise } export class IdentityResolutionError extends Error { @@ -17,6 +31,11 @@ export class IdentityResolutionError extends Error { } } +const kemKeyOf = (doc: DidDocument) => { + const key = getVerificationMaterial(doc, 'atproto_kem') + return (key && getDidKeyFromMultibase(key)) ?? null +} + const webHost = (did: string) => did.startsWith('did:web:') ? decodeURIComponent(did.slice(8).split(':')[0] as string) : undefined @@ -39,11 +58,17 @@ export function createIdentityResolver( return { async resolve(did) { await guard(webHost(did)) - const data = await resolver.did.resolveAtprotoData(did) + const doc = await resolver.did.ensureResolve(did) + const data = ensureAtpDocument(doc) if (!data.pds) throw new IdentityResolutionError(did, 'no PDS in DID document') await guard(data.handle) const handleDid = data.handle ? await resolver.handle.resolve(data.handle) : undefined - return { did, handle: handleDid === did ? (data.handle ?? null) : null, pdsUrl: data.pds } + return { + did, + handle: handleDid === did ? (data.handle ?? null) : null, + pdsUrl: data.pds, + kemKey: kemKeyOf(doc), + } }, async resolveHandle(handle) { await guard(handle) @@ -53,5 +78,9 @@ export function createIdentityResolver( await guard(webHost(did)) return resolver.did.resolveAtprotoKey(did, forceRefresh) }, + async resolveKemKey(did) { + await guard(webHost(did)) + return kemKeyOf(await resolver.did.ensureResolve(did)) + }, } } diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index ffd6cc6..86825b6 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -76,6 +76,7 @@ export async function createServer(deps: ServerDeps) { const services = createChatServices({ db, getPdsClient, + resolveKemKey: (did) => identity.resolveKemKey(did), dataDir: config.dataDir, logger, events, @@ -160,7 +161,6 @@ export async function createServer(deps: ServerDeps) { events, recentWrites, credentials, - resolveSigningKey: (did) => identity.resolveSigningKey(did), publicUrl: config.publicUrl, logger, backfillWindowMs: () => settings.get('turns').backfillMinutes * 60_000, diff --git a/apps/server/src/sharing/service.ts b/apps/server/src/sharing/service.ts index 4d1ddad..2ee543b 100644 --- a/apps/server/src/sharing/service.ts +++ b/apps/server/src/sharing/service.ts @@ -3,6 +3,7 @@ import { PoorlyFormattedDidError, UnsupportedDidMethodError, } from '@atproto/identity' +import { RECORD_MAC_TYPE, verifyRecord } from '@atproto/space' import { isValidDid } from '@atproto/syntax' import { atproto, nsid } from '@scn-chat/lexicons' import type { Account } from '../auth/accounts.ts' @@ -13,6 +14,7 @@ import type { Logger } from '../logger.ts' import type { Loose } from '../loose.ts' import { SpaceNotFound } from '../storage/record-store.ts' import { type JsonRecord, spaceUri, toJson } from '../storage/records.ts' +import { requestMac } from '../storage/space-mac.ts' import { MEMBER_LIST, PUBLIC } from '../storage/space-policies.ts' import { assertValidStored, keepValid } from '../storage/validate.ts' import { CredentialError } from '../sync/credentials.ts' @@ -166,7 +168,12 @@ export class SharingService { this.deps.mintCredential(viewer.did, space), this.deps.identity.resolve(ownerDid), ]) - return { space, client: credential.client(owner.pdsUrl, ownerDid), ownerHandle: owner.handle } + return { + space, + client: credential.client(owner.pdsUrl, ownerDid), + ownerHandle: owner.handle, + ownerKemKey: owner.kemKey, + } } catch (err) { if ( (err instanceof CredentialError && DENIED.has(err.code)) || @@ -193,15 +200,26 @@ export class SharingService { /** A conversation shared with the viewer, without its system prompt or the owner's preferences. */ async view(viewer: Account, ownerDid: string, skey: string) { - const { space, client, ownerHandle } = await this.viewerClient(viewer, ownerDid, skey) - const info = await this.read(() => - client.call(atproto.space.getRecord, { - space, - repo: ownerDid, - collection: nsid.info, - rkey: 'self', + const { space, client, ownerHandle, ownerKemKey } = await this.viewerClient( + viewer, + ownerDid, + skey, + ) + if (!ownerKemKey) { + this.deps.logger.warn({ ownerDid }, 'shared conversation owner has no #atproto_kem key') + throw new SharedNotFound() + } + const mac = await requestMac(RECORD_MAC_TYPE, ownerKemKey) + const response = await this.read(() => + client.xrpc(atproto.space.getRecord, { + params: { space, repo: ownerDid, collection: nsid.info, rkey: 'self' }, + headers: mac.headers, }), ) + const info = response.body + const uri = `${space}/${ownerDid}/${nsid.info}/self` + if (!(await verifyRecord(info, { uri, ...mac.params(response.headers) }))) + throw new Error(`${uri} does not match the MAC its PDS sent`) const messages: { rkey: string; value: JsonRecord; cid: string }[] = [] let cursor: string | undefined do { diff --git a/apps/server/src/storage/record-store.ts b/apps/server/src/storage/record-store.ts index 524c530..0668671 100644 --- a/apps/server/src/storage/record-store.ts +++ b/apps/server/src/storage/record-store.ts @@ -11,12 +11,12 @@ export type Op = { prev?: string | null value?: JsonRecord } -export type SignedCommit = Record +export type HeadCommit = Record export type OpsPage = { ops: Op[] rev: string | null - /** The signed commit at the head, missing only when the account has no repo in the space. */ - commit?: SignedCommit + /** The commit at the head, missing only when the account has no repo in the space. */ + commit?: HeadCommit } export type SpacePolicy = { read: 'memberList' | 'public' } @@ -42,8 +42,11 @@ export interface RecordStore { value: JsonRecord, ): Promise<{ cid: string }> deleteRecord(space: string, collection: string, rkey: string): Promise - /** Every op after `since`, and the revision to pass next time. */ - listOps(space: string, since?: string | null): Promise + /** + * Every op after `since`, and the revision to pass next time. With `verify`, the PDS must + * vouch for the head commit with a MAC. + */ + listOps(space: string, since?: string | null, verify?: boolean): Promise /** The newest revision, without reading the op log. */ headRev(space: string): Promise listSpaces(type: string): Promise diff --git a/apps/server/src/storage/services.ts b/apps/server/src/storage/services.ts index 0aa3190..ffe4afe 100644 --- a/apps/server/src/storage/services.ts +++ b/apps/server/src/storage/services.ts @@ -22,6 +22,7 @@ export type ChatServices = { export function createChatServices(deps: { db: Db getPdsClient: PdsClientFactory + resolveKemKey: (did: string) => Promise /** Where fallback users' blobs are kept. */ dataDir: string logger: Logger @@ -30,7 +31,7 @@ export function createChatServices(deps: { }): ChatServices { const storeFor = (account: Account): RecordStore => account.storageMode === 'space' - ? new SpaceRecordStore(account.did, deps.getPdsClient) + ? new SpaceRecordStore(account.did, deps.getPdsClient, deps.resolveKemKey, deps.logger) : new LocalRecordStore(account.did, deps.db) const blobsFor = (account: Account): BlobStore => account.storageMode === 'space' diff --git a/apps/server/src/storage/space-mac.ts b/apps/server/src/storage/space-mac.ts new file mode 100644 index 0000000..0aaef49 --- /dev/null +++ b/apps/server/src/storage/space-mac.ts @@ -0,0 +1,21 @@ +import { + encapsulate, + formatWantMacHeader, + MAC_HEADER, + type MacType, + parseMacHeader, + WANT_MAC_HEADER, +} from '@atproto/space' + +/** Ask a repo host to MAC its response, keyed against the author's KEM key. Each request needs its own. */ +export async function requestMac(type: MacType, kemKey: string) { + const { keyShare, sharedSecret } = await encapsulate(kemKey) + return { + headers: { [WANT_MAC_HEADER]: formatWantMacHeader(type, keyShare) }, + /** The shared secret and the MAC the response carried, for verifyCommit or verifyRecord. */ + params: (headers: Headers) => ({ + sharedSecret, + mac: parseMacHeader(headers.get(MAC_HEADER) ?? undefined, type), + }), + } +} diff --git a/apps/server/src/storage/space-record-store.ts b/apps/server/src/storage/space-record-store.ts index 4d68c9e..acc0123 100644 --- a/apps/server/src/storage/space-record-store.ts +++ b/apps/server/src/storage/space-record-store.ts @@ -1,7 +1,9 @@ import type { Client } from '@atproto/lex-client' +import { COMMIT_MAC_TYPE, verifyCommit } from '@atproto/space' import { atproto } from '@scn-chat/lexicons' import type { PdsClientFactory } from '../auth/pds.ts' import { lexErrorCode } from '../lex-errors.ts' +import type { Logger } from '../logger.ts' import type { Loose } from '../loose.ts' import { type OpsPage, @@ -13,6 +15,7 @@ import { type StoredRecord, } from './record-store.ts' import { type JsonRecord, toJson, toLex } from './records.ts' +import { requestMac } from './space-mac.ts' import { MEMBER_LIST, OPEN } from './space-policies.ts' import { assertValidRecord } from './validate.ts' @@ -22,10 +25,19 @@ const PAGE = 100 export class SpaceRecordStore implements RecordStore { readonly did: string private readonly getClient: PdsClientFactory + private readonly resolveKemKey: (did: string) => Promise + private readonly logger: Logger - constructor(did: string, getClient: PdsClientFactory) { + constructor( + did: string, + getClient: PdsClientFactory, + resolveKemKey: (did: string) => Promise, + logger: Logger, + ) { this.did = did this.getClient = getClient + this.resolveKemKey = resolveKemKey + this.logger = logger } private async client(): Promise { @@ -38,12 +50,13 @@ export class SpaceRecordStore implements RecordStore { } private async call(method: Loose, input: Loose, space: string): Promise { - try { - return await this.rawCall(method, input) - } catch (err) { - if (lexErrorCode(err) === 'SpaceNotFound') throw new SpaceNotFound(space) - throw err - } + return inSpace(space, () => this.rawCall(method, input)) + } + + /** A call whose response headers are needed too. */ + private async xrpc(method: Loose, options: Loose, space: string): Promise { + const client: Loose = await this.client() + return inSpace(space, () => client.xrpc(method, options)) } async createSpace(type: string, skey: string): Promise { @@ -167,22 +180,31 @@ export class SpaceRecordStore implements RecordStore { await this.call(atproto.space.deleteRecord, { space, repo: this.did, collection, rkey }, space) } - async listOps(space: string, since?: string | null): Promise { + async listOps(space: string, since?: string | null, verify = false): Promise { const page: OpsPage = { ops: [], rev: since ?? null } + const kemKey = verify ? await this.resolveKemKey(this.did) : null + // Spaces alpha: accounts made before #atproto_kem keys may have none to MAC against. + // Remove this fallback once every spaces PDS publishes one. + if (verify && !kemKey) + this.logger.warn({ did: this.did, space }, 'no #atproto_kem key, head commit not MAC-checked') let cursor: string | undefined do { + const mac = kemKey ? await requestMac(COMMIT_MAC_TYPE, kemKey) : undefined let out: Loose try { - out = await this.call( + out = await this.xrpc( atproto.space.listRepoOps, - { space, repo: this.did, since: since ?? undefined, cursor, limit: PAGE }, + { + params: { space, repo: this.did, since: since ?? undefined, cursor, limit: PAGE }, + headers: mac?.headers, + }, space, ) } catch (err) { if (lexErrorCode(err) === 'RepoNotFound') return page throw err } - for (const op of out.ops) { + for (const op of out.body.ops) { page.ops.push({ rev: op.rev, collection: op.collection, @@ -193,10 +215,14 @@ export class SpaceRecordStore implements RecordStore { }) page.rev = op.rev } - cursor = out.cursor - if (!cursor) page.commit = out.commit + cursor = out.body.cursor + if (cursor) continue + const commit = out.body.commit + if (!commit) throw new Error(`listRepoOps reached the head of ${space} without a commit`) + if (mac && !verifyCommit(commit, { space, repo: this.did, ...mac.params(out.headers) })) + throw new Error(`The head commit of ${space} does not match the MAC its PDS sent`) + page.commit = commit } while (cursor) - if (!page.commit) throw new Error(`listRepoOps reached the head of ${space} without a commit`) return page } @@ -225,3 +251,12 @@ export class SpaceRecordStore implements RecordStore { return uris } } + +async function inSpace(space: string, fn: () => Promise): Promise { + try { + return await fn() + } catch (err) { + if (lexErrorCode(err) === 'SpaceNotFound') throw new SpaceNotFound(space) + throw err + } +} diff --git a/apps/server/src/sync/credentials.ts b/apps/server/src/sync/credentials.ts index cfa99d6..184a09e 100644 --- a/apps/server/src/sync/credentials.ts +++ b/apps/server/src/sync/credentials.ts @@ -39,7 +39,7 @@ export class SpaceCredential { ): Promise => { const request = new Request(input, { ...init, redirect: 'error' }) const headers = await createSpaceSigHeaders(this.key, { - authorization: `Atproto-Space ${this.token}`, + authorization: `Atproto-Space-Credential ${this.token}`, audience: audience as DidString, }) for (const [name, value] of Object.entries(headers)) request.headers.set(name, value) @@ -85,12 +85,8 @@ export async function mintSpaceCredential( const request = new Request(url, { method: 'POST', redirect: 'error', - headers: { - accept: 'application/json', - ...(await createSpaceSigHeaders(key, { authorization: `Bearer ${token}` })), - 'content-type': 'application/json', - }, - body: JSON.stringify({ space }), + headers: { accept: 'application/json', 'content-type': 'application/json' }, + body: JSON.stringify({ space, credentialKid: key.did(), delegationToken: token }), }) const response = await fetchImpl(request) const body = (await response.json().catch(() => ({}))) as { diff --git a/apps/server/src/sync/engine.ts b/apps/server/src/sync/engine.ts index e3e5b24..056e3c2 100644 --- a/apps/server/src/sync/engine.ts +++ b/apps/server/src/sync/engine.ts @@ -1,5 +1,5 @@ import { parseCid } from '@atproto/lex-data' -import { RepoCommit, verifyCommit } from '@atproto/space' +import { RepoCommit } from '@atproto/space' import { atproto, isRecordNsid, nsid, validateRecord } from '@scn-chat/lexicons' import { type Account, getAccount } from '../auth/accounts.ts' import { isLoopbackUrl } from '../auth/oauth-client.ts' @@ -22,7 +22,6 @@ export type SyncEngineDeps = { events: SyncEventBus recentWrites: RecentWrites credentials: CredentialCache - resolveSigningKey: (did: string) => Promise publicUrl: string logger: Logger /** How old a synced message asking for a reply can be, read on each use. */ @@ -134,7 +133,7 @@ export class SyncEngine { const space = ctx.chats.settingsUri const state = await this.state(space) if (!state?.last_rev) return this.firstIndexSync(ctx) - const page = await ctx.store.listOps(space, state.last_rev) + const page = await ctx.store.listOps(space, state.last_rev, verify) for (const op of page.ops) { if (op.collection !== nsid.conversationRef) continue if (!op.cid) { @@ -178,7 +177,7 @@ export class SyncEngine { const first = !state?.last_rev let page: OpsPage try { - page = await ctx.store.listOps(space, state?.last_rev) + page = await ctx.store.listOps(space, state?.last_rev, verify || first) } catch (err) { if (!(err instanceof SpaceNotFound)) throw err await this.conversationDeleted(did, skey) @@ -239,7 +238,7 @@ export class SyncEngine { }) } - /** Check the op log's signed commit against the set hash of the space's current records. */ + /** Check the op log's verified head commit against the set hash of the space's current records. */ async commitMatches(ctx: Context, space: string, page: OpsPage): Promise { const commit = page.commit as Loose // No repo in the space yet, nothing to verify. @@ -252,13 +251,7 @@ export class SyncEngine { cid: parseCid(key.cid), })), ) - if (!local.matches(commit)) return false - const didKey = await this.deps.resolveSigningKey(ctx.account.did) - return verifyCommit( - commit, - { space: space as Loose, author: ctx.account.did as Loose, rev: commit.rev }, - didKey, - ) + return local.matches(commit) } /** Register for write notifications on the user's settings space. */ diff --git a/apps/server/tests/auth/identity.test.ts b/apps/server/tests/auth/identity.test.ts index c6a7c6c..1c6de1a 100644 --- a/apps/server/tests/auth/identity.test.ts +++ b/apps/server/tests/auth/identity.test.ts @@ -1,19 +1,50 @@ +import { P256Keypair, Secp256k1Keypair } from '@atproto/crypto' import { beforeEach, describe, expect, it, vi } from 'vitest' import { createIdentityResolver } from '../../src/auth/identity.ts' import { PrivateNetworkError } from '../../src/providers/guarded-fetch.ts' -const resolveData = vi.fn() +const resolveDoc = vi.fn() const resolveHandle = vi.fn() -vi.mock('@atproto/identity', () => ({ +vi.mock('@atproto/identity', async (importOriginal) => ({ + ...(await importOriginal()), IdResolver: class { - did = { resolveAtprotoData: resolveData, resolveAtprotoKey: vi.fn() } + did = { ensureResolve: resolveDoc, resolveAtprotoKey: vi.fn() } handle = { resolve: resolveHandle } }, })) +const signingKey = (await Secp256k1Keypair.create()).did() +const kemKey = (await P256Keypair.create()).did() + +const multikey = (did: string, fragment: string, key: string) => ({ + id: `${did}#${fragment}`, + type: 'Multikey', + controller: did, + publicKeyMultibase: key.slice('did:key:'.length), +}) + +function didDoc(did: string, keys: { kem?: boolean } = {}) { + return { + '@context': ['https://www.w3.org/ns/did/v1'], + id: did, + alsoKnownAs: ['at://alice.test'], + verificationMethod: [ + multikey(did, 'atproto', signingKey), + ...(keys.kem === false ? [] : [multikey(did, 'atproto_kem', kemKey)]), + ], + service: [ + { + id: '#atproto_pds', + type: 'AtprotoPersonalDataServer', + serviceEndpoint: 'https://pds.test', + }, + ], + } +} + beforeEach(() => { - resolveData.mockReset().mockResolvedValue({ pds: 'https://pds.test', handle: 'alice.test' }) + resolveDoc.mockReset().mockImplementation(async (did: string) => didDoc(did)) resolveHandle.mockReset() }) @@ -24,6 +55,7 @@ describe('createIdentityResolver', () => { did: 'did:plc:alice', handle: 'alice.test', pdsUrl: 'https://pds.test', + kemKey, }) resolveHandle.mockResolvedValue('did:plc:someone-else') expect((await createIdentityResolver().resolve('did:plc:alice')).handle).toBeNull() @@ -33,7 +65,7 @@ describe('createIdentityResolver', () => { await expect(createIdentityResolver().resolve('did:web:10.0.0.5')).rejects.toBeInstanceOf( PrivateNetworkError, ) - expect(resolveData).not.toHaveBeenCalled() + expect(resolveDoc).not.toHaveBeenCalled() }) it('allows private addresses when configured to, for a local PDS', async () => { @@ -43,4 +75,21 @@ describe('createIdentityResolver', () => { pdsUrl: 'https://pds.test', }) }) + + it('leaves the KEM key empty for a DID whose PDS has not published one', async () => { + resolveHandle.mockResolvedValue('did:plc:alice') + resolveDoc.mockResolvedValue(didDoc('did:plc:alice', { kem: false })) + expect((await createIdentityResolver().resolve('did:plc:alice')).kemKey).toBeNull() + }) +}) + +describe('resolveKemKey', () => { + it("returns the DID's #atproto_kem key, not its signing key", async () => { + expect(await createIdentityResolver().resolveKemKey('did:plc:alice')).toBe(kemKey) + }) + + it('returns null when the DID has no KEM key', async () => { + resolveDoc.mockResolvedValue(didDoc('did:plc:alice', { kem: false })) + expect(await createIdentityResolver().resolveKemKey('did:plc:alice')).toBeNull() + }) }) diff --git a/apps/server/tests/helpers/auth.ts b/apps/server/tests/helpers/auth.ts index ee9b518..4575ead 100644 --- a/apps/server/tests/helpers/auth.ts +++ b/apps/server/tests/helpers/auth.ts @@ -40,9 +40,11 @@ export const fakeIdentity = (): IdentityResolver => ({ did, handle: 'alice.test', pdsUrl: 'https://pds.test', + kemKey: 'did:key:zDnaetest', })), resolveHandle: vi.fn(async () => 'did:plc:alice'), resolveSigningKey: vi.fn(async () => 'did:key:zQ3shtest'), + resolveKemKey: vi.fn(async () => 'did:key:zDnaetest'), }) export function authDeps(db: Db, overrides: Partial = {}): AuthDeps { diff --git a/apps/server/tests/helpers/fake-pds.ts b/apps/server/tests/helpers/fake-pds.ts index 9c89b56..debdad7 100644 --- a/apps/server/tests/helpers/fake-pds.ts +++ b/apps/server/tests/helpers/fake-pds.ts @@ -1,7 +1,21 @@ import { TID } from '@atproto/common-web' -import { type Keypair, Secp256k1Keypair } from '@atproto/crypto' +import { P256Keypair } from '@atproto/crypto' import { cidForRawBytes, LexError, parseCid } from '@atproto/lex-data' -import { RepoCommit } from '@atproto/space' +import { + COMMIT_MAC_TYPE, + type Commit, + computeCommitMac, + computeRecordMac, + decapsulate, + formatMacHeader, + MAC_HEADER, + type MacType, + parseWantMacHeader, + RECORD_MAC_TYPE, + RepoCommit, + SpaceMacError, + WANT_MAC_HEADER, +} from '@atproto/space' import { atproto } from '@scn-chat/lexicons' import type { Db } from '../../src/db/index.ts' import { LocalRecordStore } from '../../src/storage/local-record-store.ts' @@ -15,6 +29,12 @@ const TID_BASE = 1_700_000_000_000_000 const toTid = (rev: string) => TID.fromTime(TID_BASE + Number(rev), 0).toString() const fromTid = (tid: string) => String(TID.fromStr(tid).timestamp() - TID_BASE) +const MAC_TYPES: Record = { + [atproto.space.getRecord.$nsid]: RECORD_MAC_TYPE, + [atproto.space.listRepoOps.$nsid]: COMMIT_MAC_TYPE, + [atproto.space.getLatestCommit.$nsid]: COMMIT_MAC_TYPE, +} + type OutputSchema = { safeParse(value: unknown): { success: boolean; reason?: unknown } } /** Fail the test when the fake answers in a shape the method's lexicon does not allow. */ @@ -32,10 +52,12 @@ export class FakePds { readonly calls: Call[] = [] private readonly db: Db failNext: { nsid: string; error: string } | null = null - /** Signs commits, as a real PDS signs with the account key. */ - readonly keypair: Keypair - /** Sign commits over a set that includes a record the repo does not have. */ + /** The KEM key every account here MACs its commits and records against. */ + readonly kemKeypair: P256Keypair + /** Commit to a set that includes a record the repo does not have. */ tamperCommits = false + /** MAC responses with a key share other than the one the caller sent. */ + tamperMacs = false readonly blobs = new Map() readonly policies = new Map< string, @@ -58,16 +80,17 @@ export class FakePds { return policy.readPolicy.endsWith('#publicPolicy') || policy.members.get(viewer)?.read === true } - constructor(db: Db, keypair: Keypair) { + constructor(db: Db, kemKeypair: P256Keypair) { this.db = db - this.keypair = keypair + this.kemKeypair = kemKeypair } static async create(db: Db): Promise { - return new FakePds(db, await Secp256k1Keypair.create()) + return new FakePds(db, await P256Keypair.create()) } - client(did: string) { + /** A client for the account's own session, or for a space credential, which must ask for MACs. */ + client(did: string, { credential = false } = {}) { const store = new LocalRecordStore(did, this.db) const call = async ( method: { $nsid: string }, @@ -102,17 +125,49 @@ export class FakePds { } return { call, - /** Only getBlob, whose response encoding the server reads. */ - xrpc: async (method: { $nsid: string }, options: { params: Record }) => { - if (method.$nsid !== atproto.space.getBlob.$nsid) - throw new Error(`FakePds.xrpc does not implement ${method.$nsid}`) - const body = await call(method, options.params) - const blob = this.blobs.get(options.params.cid as string) - return { body, encoding: blob?.mimeType } + xrpc: async ( + method: { $nsid: string }, + options: { params: Record; headers?: Record }, + ) => { + if (method.$nsid === atproto.space.getBlob.$nsid) { + const body = await call(method, options.params) + const blob = this.blobs.get(options.params.cid as string) + return { body, encoding: blob?.mimeType } + } + const type = MAC_TYPES[method.$nsid] + if (!type) throw new Error(`FakePds.xrpc does not implement ${method.$nsid}`) + const wantMac = options.headers?.[WANT_MAC_HEADER] + const secret = + wantMac !== undefined || credential ? await this.macSecret(wantMac, type) : null + const body = (await call(method, options.params)) as { + uri: string + cid: string + commit?: Commit + } + const headers = new Headers() + const mac = + secret && + (type === RECORD_MAC_TYPE + ? computeRecordMac(secret, body.uri, parseCid(body.cid)) + : body.commit && computeCommitMac(secret, body.commit)) + if (mac) headers.set(MAC_HEADER, formatMacHeader(type, mac)) + return { body, headers } }, } } + private async macSecret(wantMac: string | undefined, type: MacType): Promise { + try { + const keyShare = this.tamperMacs + ? (await P256Keypair.create()).did() + : parseWantMacHeader(wantMac, type) + return decapsulate(this.kemKeypair, keyShare) + } catch (err) { + if (err instanceof SpaceMacError) throw new LexError('BadSpaceMac', err.message) + throw err + } + } + private async dispatch(store: LocalRecordStore, nsid: string, input: Record) { const space = input.space as string const uri = (collection: string, rkey: string) => `${space}/${store.did}/${collection}/${rkey}` @@ -168,12 +223,16 @@ export class FakePds { const since = input.since ? fromTid(input.since as string) : undefined const page = await store.listOps(space, since) const prevs = await this.prevCids(store, space) - const ops = page.ops.map((op) => ({ + // Like a PDS, a full page carries a cursor and no commit. + const start = Number(input.cursor ?? 0) + const limit = (input.limit as number | undefined) ?? 100 + const ops = page.ops.slice(start, start + limit).map((op) => ({ ...op, prev: prevs.get(`${op.rev}/${op.collection}/${op.rkey}`) ?? null, rev: toTid(op.rev), value: op.value ? toLex(op.value) : undefined, })) + if (ops.length === limit) return { ops, cursor: String(start + limit) } return { ops, commit: await this.commit(store, space, toTid(head)) } } case atproto.space.getLatestCommit.$nsid: { @@ -258,10 +317,11 @@ export class FakePds { cid: parseCid(keys[0]?.cid ?? ''), }) } - return RepoCommit.fromRecords(records as never).sign( - { space, author: store.did, rev } as never, - this.keypair, - ) + return RepoCommit.fromRecords(records as never).toCommit({ + space, + repo: store.did, + rev, + } as never) } ownerOf(space: string): string { diff --git a/apps/server/tests/helpers/spaces.ts b/apps/server/tests/helpers/spaces.ts index ea8af50..e7a62cc 100644 --- a/apps/server/tests/helpers/spaces.ts +++ b/apps/server/tests/helpers/spaces.ts @@ -48,6 +48,7 @@ export async function spacesHarness( const services = createChatServices({ db, getPdsClient: async (did) => pds.client(did) as never, + resolveKemKey: async () => pds.kemKeypair.did(), dataDir, logger, events, @@ -75,7 +76,6 @@ export async function spacesHarness( events, recentWrites, credentials, - resolveSigningKey: async () => pds.keypair.did(), publicUrl: 'https://chat.example.com', logger, backfillWindowMs: () => options.backfillWindowMs ?? 60 * 60_000, diff --git a/apps/server/tests/sharing/sharing.test.ts b/apps/server/tests/sharing/sharing.test.ts index 8884cda..98808de 100644 --- a/apps/server/tests/sharing/sharing.test.ts +++ b/apps/server/tests/sharing/sharing.test.ts @@ -40,14 +40,16 @@ async function setup() { did, handle: Object.keys(handles).find((h) => handles[h] === did) ?? 'alice.test', pdsUrl: 'https://pds.test', + kemKey: h.pds.kemKeypair.did(), })), resolveHandle: vi.fn(async (handle: string) => handles[handle] ?? null), resolveSigningKey: vi.fn(async () => 'did:key:x'), + resolveKemKey: vi.fn(async () => h.pds.kemKeypair.did()), } const mintCredential = vi.fn(async (viewer: string, space: string) => { if (!h.pds.mayRead(space, viewer)) throw new CredentialError('UserNotAuthorized', 'not a member') - return { client: () => h.pds.client(h.pds.ownerOf(space)) } + return { client: () => h.pds.client(h.pds.ownerOf(space), { credential: true }) } }) const sharing = new SharingService({ getPdsClient: async (did) => h.pds.client(did) as never, @@ -248,6 +250,27 @@ describe('shared view', () => { expect((await (await appFor(CAROL)).get(`/shared/did:plc:alice/${skey}`)).status).toBe(200) }) + it('refuses a conversation whose info record does not match the MAC its PDS sent', async () => { + const { appFor, skey, pds } = await setup() + await (await appFor('did:plc:alice')).put(`/conversations/${skey}/sharing`, { mode: 'public' }) + const carol = await appFor(CAROL) + pds.tamperMacs = true + expect((await carol.raw(`/shared/did:plc:alice/${skey}`)).status).toBe(500) + }) + + it('returns 404 when the owner has no KEM key to check their records against', async () => { + const { appFor, skey, identity } = await setup() + await (await appFor('did:plc:alice')).put(`/conversations/${skey}/sharing`, { mode: 'public' }) + const carol = await appFor(CAROL) + identity.resolve.mockResolvedValueOnce({ + did: 'did:plc:alice', + handle: 'alice.test', + pdsUrl: 'https://pds.test', + kemKey: null as never, + }) + expect((await carol.get(`/shared/did:plc:alice/${skey}`)).status).toBe(404) + }) + it('returns 404 for a missing conversation', async () => { const { appFor } = await setup() expect((await (await appFor(BOB)).get('/shared/did:plc:alice/3zzzzzzzzzzzz')).status).toBe(404) diff --git a/apps/server/tests/storage/space-record-store.test.ts b/apps/server/tests/storage/space-record-store.test.ts index 467e734..6d7868e 100644 --- a/apps/server/tests/storage/space-record-store.test.ts +++ b/apps/server/tests/storage/space-record-store.test.ts @@ -1,4 +1,5 @@ import { nsid } from '@scn-chat/lexicons' +import pino from 'pino' import { describe, expect, it } from 'vitest' import { migrateToLatest } from '../../src/db/migrate.ts' import { RecordExists, SpaceExists, SpaceNotFound } from '../../src/storage/record-store.ts' @@ -8,12 +9,18 @@ import { FakePds } from '../helpers/fake-pds.ts' const did = 'did:plc:alice' const info = { $type: nsid.info, createdAt: '2026-09-26T00:00:00.000Z' } +const logger = pino({ level: 'silent' }) -async function setup() { +async function setup(options: { kemKey?: boolean } = {}) { const db = createSqliteDb() await migrateToLatest(db) const pds = await FakePds.create(db) - const store = new SpaceRecordStore(did, async () => pds.client(did) as never) + const store = new SpaceRecordStore( + did, + async () => pds.client(did) as never, + async () => (options.kemKey === false ? null : pds.kemKeypair.did()), + logger, + ) return { pds, store } } @@ -86,9 +93,40 @@ describe('SpaceRecordStore', () => { expect(await store.headRev(space)).toBe(page.rev) }) - it('fails when the op log reaches its head without a signed commit', async () => { - const client = { call: async () => ({ ops: [] }) } - const store = new SpaceRecordStore(did, async () => client as never) + it('checks the head commit against the MAC its PDS sent when asked to verify', async () => { + const { pds, store } = await setup() + const space = await store.createSpace(nsid.conversation, '3aaaaaaaaaaaa') + await store.putRecord(space, nsid.info, 'self', info) + expect((await store.listOps(space, null, true)).commit).toBeDefined() + pds.tamperMacs = true + await expect(store.listOps(space, null, true)).rejects.toThrow(/MAC/) + }) + + it('checks the MAC on the head commit of an op log longer than a page', async () => { + const { store } = await setup() + const space = await store.createSpace(nsid.conversation, '3aaaaaaaaaaaa') + for (let i = 0; i < 150; i++) + await store.putRecord(space, nsid.info, 'self', { ...info, title: `t${i}` }) + const page = await store.listOps(space, null, true) + expect(page.ops).toHaveLength(150) + expect(page.commit).toBeDefined() + }) + + it('still returns the head commit to verify when the account has no KEM key yet', async () => { + const { store } = await setup({ kemKey: false }) + const space = await store.createSpace(nsid.conversation, '3aaaaaaaaaaaa') + await store.putRecord(space, nsid.info, 'self', info) + expect((await store.listOps(space, null, true)).commit).toBeDefined() + }) + + it('fails when the op log reaches its head without a commit', async () => { + const client = { xrpc: async () => ({ body: { ops: [] }, headers: new Headers() }) } + const store = new SpaceRecordStore( + did, + async () => client as never, + async () => 'did:key:x', + logger, + ) await expect( store.listOps(`at://${did}/space/${nsid.conversation}/3aaaaaaaaaaaa`), ).rejects.toThrow() diff --git a/apps/server/tests/sync/credentials.test.ts b/apps/server/tests/sync/credentials.test.ts index 229275b..af83b39 100644 --- a/apps/server/tests/sync/credentials.test.ts +++ b/apps/server/tests/sync/credentials.test.ts @@ -1,5 +1,5 @@ import { LexError } from '@atproto/lex-data' -import { verifySpaceSignature } from '@atproto/space' +import { verifyCredentialSig } from '@atproto/space' import { describe, expect, it, vi } from 'vitest' import { CredentialCache, @@ -11,7 +11,7 @@ const space = 'at://did:plc:alice/space/network.sharedcomputer.chat.settings/sel /** An unsigned credential JWT, enough for the client to read its expiry. */ function credentialToken(expSec = 3600) { - const header = { typ: 'atproto-space-credential+jwt', alg: 'ES256' } + const header = { typ: 'atproto-space-credential+jwt', alg: 'ES256', kid: '#atproto' } const payload = { iss: 'did:plc:alice', sub: space, @@ -38,17 +38,18 @@ function deps(response: Response, now = () => 1_000) { } describe('mintSpaceCredential', () => { - it('exchanges a signed delegation token at the authority PDS', async () => { + it('exchanges a delegation token at the authority PDS for a credential bound to a new key', async () => { const token = credentialToken() const { deps: d, fetchImpl } = deps(Response.json({ credential: token })) const credential = await mintSpaceCredential(d, 'did:plc:alice', space) expect(credential.token).toBe(token) const request = fetchImpl.mock.calls[0]?.[0] as Request expect(request.url).toBe('https://pds.test/xrpc/com.atproto.space.getSpaceCredential') - expect(request.headers.get('authorization')).toBe('Bearer delegation-token') - await expect(verifySpaceSignature(Object.fromEntries(request.headers))).resolves.toMatch( - /^did:key:/, - ) + expect(await request.json()).toEqual({ + space, + delegationToken: 'delegation-token', + credentialKid: expect.stringMatching(/^did:key:z/), + }) expect(d.resolvePds).toHaveBeenCalledWith('did:plc:alice') }) @@ -68,19 +69,20 @@ describe('mintSpaceCredential', () => { }) }) - it('signs later requests for the audience with the exchange key', async () => { + it('signs later requests for the audience with the key the credential is bound to', async () => { const token = credentialToken() const { deps: d, fetchImpl } = deps(Response.json({ credential: token })) const credential = await mintSpaceCredential(d, 'did:plc:alice', space) fetchImpl.mockResolvedValueOnce(Response.json({})) await credential.fetch('https://pds.test/xrpc/com.atproto.space.listRepos', 'did:plc:bob') - const [exchange = {}, headers = {}] = fetchImpl.mock.calls.map(([r]) => - Object.fromEntries(r.headers), - ) - const exchangeKey = await verifySpaceSignature(exchange) - expect(headers.authorization).toBe(`Atproto-Space ${token}`) + const [exchange, request] = fetchImpl.mock.calls.map(([r]) => r) + const { credentialKid } = (await (exchange as Request).json()) as { + credentialKid: `did:key:${string}` + } + const headers = Object.fromEntries((request as Request).headers) + expect(headers.authorization).toBe(`Atproto-Space-Credential ${token}`) expect(headers['atproto-space-audience']).toBe('did:plc:bob') - await expect(verifySpaceSignature(headers, exchangeKey)).resolves.toBe(exchangeKey) + await expect(verifyCredentialSig(headers, credentialKid)).resolves.toBeUndefined() }) }) diff --git a/apps/server/tests/sync/engine.test.ts b/apps/server/tests/sync/engine.test.ts index 0369615..581dd61 100644 --- a/apps/server/tests/sync/engine.test.ts +++ b/apps/server/tests/sync/engine.test.ts @@ -241,10 +241,8 @@ describe('SyncEngine registration on a loopback URL', () => { }) describe('SyncEngine gap detection', () => { - const signedHarness = spacesHarness - - it('keeps the cursor when the signed commit matches the index', async () => { - const h = await signedHarness() + it('keeps the cursor when the head commit matches the index', async () => { + const h = await spacesHarness() await h.chats.createConversation() await h.engine.syncIndex(ALICE) const before = (await stateOf(h, h.chats.settingsUri))?.last_rev @@ -252,8 +250,8 @@ describe('SyncEngine gap detection', () => { expect((await stateOf(h, h.chats.settingsUri))?.last_rev).toBe(before) }) - it('resets the cursor when the signed commit does not match the index', async () => { - const h = await signedHarness() + it('resets the cursor when the head commit does not match the index', async () => { + const h = await spacesHarness() const { skey } = await h.chats.createConversation() await h.engine.syncIndex(ALICE) await h.db @@ -264,4 +262,14 @@ describe('SyncEngine gap detection', () => { await h.engine.safetyNet(ALICE) expect(await stateOf(h, h.chats.conversationUri(skey))).toBeDefined() }) + + it('fails, keeping the cursor, when the head commit does not match the MAC its PDS sent', async () => { + const h = await spacesHarness() + await h.chats.createConversation() + await h.engine.syncIndex(ALICE) + const before = (await stateOf(h, h.chats.settingsUri))?.last_rev + h.pds.tamperMacs = true + await expect(h.engine.safetyNet(ALICE)).rejects.toThrow(/MAC/) + expect((await stateOf(h, h.chats.settingsUri))?.last_rev).toBe(before) + }) }) diff --git a/lexicons/upstream/UPSTREAM.md b/lexicons/upstream/UPSTREAM.md index 5aaf7b9..b94bb61 100644 --- a/lexicons/upstream/UPSTREAM.md +++ b/lexicons/upstream/UPSTREAM.md @@ -1,5 +1,5 @@ # Vendored atproto lexicons These are the subset of `com.atproto.*` lexicons the server calls, copied from the -`permissioned-data-alpha` branch of `bluesky-social/atproto` at commit `79d6307e19908f3af406838ea0e5fae42847f182`. +`permissioned-data-alpha` branch of `bluesky-social/atproto` at commit `ac0c9f15392f6f0298d71dc52aa88c05036150be`. They are not ours and are never published. Refresh them together with the alpha package versions. diff --git a/lexicons/upstream/com/atproto/simplespace/deleteSpace.json b/lexicons/upstream/com/atproto/simplespace/deleteSpace.json index 8c83915..a808e1a 100644 --- a/lexicons/upstream/com/atproto/simplespace/deleteSpace.json +++ b/lexicons/upstream/com/atproto/simplespace/deleteSpace.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "procedure", - "description": "Delete a space. The authenticated user must be the space owner. The authority's own repo in the space is deleted along with it, since the space host and the repo host are the same service here; other members' repos are flagged as belonging to a deleted space rather than erased. After deletion, all reads and writes against the space fail with SpaceNotFound, and getSpaceCredential answers SpaceDeleted so a syncer that missed the notification still learns to drop its copy. Idempotent. Requires auth, implemented by PDS.", + "description": "Delete a space. Requires auth as the space owner. Deletes the owner's own repo in the space; other members' repos are left in place. Afterwards, reads and writes fail with SpaceNotFound and getSpaceCredential returns SpaceDeleted. Idempotent. Implemented by PDS.", "input": { "encoding": "application/json", "schema": { diff --git a/lexicons/upstream/com/atproto/space/defs.json b/lexicons/upstream/com/atproto/space/defs.json index db62653..7cc0ae0 100644 --- a/lexicons/upstream/com/atproto/space/defs.json +++ b/lexicons/upstream/com/atproto/space/defs.json @@ -2,35 +2,33 @@ "lexicon": 1, "id": "com.atproto.space.defs", "defs": { - "signedCommit": { + "commit": { "type": "object", - "description": "A signed commit over the current state of a permissioned repo.", - "required": ["ver", "hash", "mac", "ikm", "sig", "rev"], + "description": "A commit over the current state of a permissioned repo. Carries no signature: the repo host authenticates it to a syncer with a MAC in the Atproto-Space-Mac response header.", + "required": ["ver", "repo", "space", "rev", "data"], "properties": { "ver": { "type": "integer", - "description": "Commit format version, currently 1. Corresponds to the version in the ctx protocol tag (atproto-space-v1)." + "description": "Commit format version, currently 1." }, - "hash": { - "type": "bytes", - "description": "sha256 digest of the LtHash state (32 bytes)." - }, - "ikm": { - "type": "bytes", - "description": "Per-signature input keying material (32 random bytes)" - }, - "sig": { - "type": "bytes", - "description": "Signature over ctx (space, author DID, rev, ikm) by the user's atproto signing key. Does not cover the repo hash." + "repo": { + "type": "string", + "format": "did", + "description": "The author DID of the repo." }, - "mac": { - "type": "bytes", - "description": "HMAC-SHA256 over hash, keyed by HKDF-SHA256(ikm, info=ctx). Binds the repo hash to this commit's context." + "space": { + "type": "string", + "format": "space-ref", + "description": "Reference to the space." }, "rev": { "type": "string", "format": "tid", - "description": "Commit revision (TID), also bound into ctx." + "description": "Commit revision (TID)." + }, + "data": { + "type": "bytes", + "description": "sha256 digest of the LtHash state (32 bytes)." } } } diff --git a/lexicons/upstream/com/atproto/space/getLatestCommit.json b/lexicons/upstream/com/atproto/space/getLatestCommit.json index 1cc491f..1151558 100644 --- a/lexicons/upstream/com/atproto/space/getLatestCommit.json +++ b/lexicons/upstream/com/atproto/space/getLatestCommit.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Get the current signed commit for an account's permissioned repo within a space. Served by a repo host. Throws RepoNotFound when the account holds no repo in the space, including the case of a member that has never written to it. Callable with either OAuth (for the authenticated user's own data) or a space credential (for syncing services).", + "description": "Get the current commit for an account's permissioned repo within a space. Served by a repo host. When called with a space credential, the caller must send an Atproto-Space-Want-Mac header, and the commit's MAC is returned in the Atproto-Space-Mac response header. Throws RepoNotFound when the account holds no repo in the space, including the case of a member that has never written to it. Callable with either OAuth (for the authenticated user's own data) or a space credential (for syncing services).", "parameters": { "type": "params", "required": ["space", "repo"], @@ -28,14 +28,18 @@ "properties": { "commit": { "type": "ref", - "ref": "com.atproto.space.defs#signedCommit", - "description": "The account's current signed commit." + "ref": "com.atproto.space.defs#commit", + "description": "The account's current commit." } }, "required": ["commit"] } }, "errors": [ + { + "name": "BadSpaceMac", + "description": "The Atproto-Space-Want-Mac header is missing or malformed, names an unsupported type or version, or carries an invalid key share." + }, { "name": "SpaceNotFound" }, diff --git a/lexicons/upstream/com/atproto/space/getRecord.json b/lexicons/upstream/com/atproto/space/getRecord.json index 144ec5b..7ef6f87 100644 --- a/lexicons/upstream/com/atproto/space/getRecord.json +++ b/lexicons/upstream/com/atproto/space/getRecord.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Get a single record from a permissioned space. Callable with either OAuth (for the authenticated user's own data) or a space credential (for syncing services). Throws RepoNotFound when the account holds no repo in the space; this does not distinguish a member that has never written from a non-member.", + "description": "Get a single record from a permissioned space. Callable with either OAuth (for the authenticated user's own data) or a space credential (for syncing services). When called with a space credential, the caller must send an Atproto-Space-Want-Mac header, and the record's MAC is returned in the Atproto-Space-Mac response header. Throws RepoNotFound when the account holds no repo in the space; this does not distinguish a member that has never written from a non-member.", "parameters": { "type": "params", "required": ["space", "repo", "collection", "rkey"], @@ -52,6 +52,10 @@ } }, "errors": [ + { + "name": "BadSpaceMac", + "description": "The Atproto-Space-Want-Mac header is missing or malformed, names an unsupported type or version, or carries an invalid key share." + }, { "name": "RecordNotFound" }, diff --git a/lexicons/upstream/com/atproto/space/getRepo.json b/lexicons/upstream/com/atproto/space/getRepo.json index 4ace8bb..f184698 100644 --- a/lexicons/upstream/com/atproto/space/getRepo.json +++ b/lexicons/upstream/com/atproto/space/getRepo.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Download an account's permissioned repo within a space as a CAR file, for full-state recovery. The CAR declares two roots in order: the signed commit, then a DRISL (DAG-CBOR) index mapping '{collection}/{rkey}' to record CID. Record blocks follow, in the same canonical DAG-CBOR key order as the index (length-first, then bytewise). Blobs are not included and are fetched separately via getBlob. Served by a repo host. Callable with either OAuth (for the authenticated user's own data) or a space credential (for syncing services).", + "description": "Download an account's permissioned repo within a space as a STAR file, for full-state recovery. The archive's metadata is the repo's commit; each key/value pair is a record path ('{collection}/{rkey}') and the record's DRISL encoding, in strict lexicographic order of the path. Blobs are not included and are fetched separately via getBlob. Served by a repo host. Callable with either OAuth (for the authenticated user's own data) or a space credential (for syncing services). When called with a space credential, the caller must send an Atproto-Space-Want-Mac header, and the commit's MAC is returned in the Atproto-Space-Mac response header.", "parameters": { "type": "params", "required": ["space", "repo"], @@ -18,18 +18,17 @@ "type": "string", "format": "did", "description": "The DID of the account whose repo to download." - }, - "excludeValues": { - "type": "boolean", - "default": false, - "description": "If true, omit the record blocks and return only the commit and index roots. The index is still fully authenticated by folding its entries into a set hash and comparing against the commit, so a syncer can diff it against a local copy and fetch just the records it lacks. Note the resulting CAR declares two roots and carries no non-root blocks." } } }, "output": { - "encoding": "application/vnd.ipld.car" + "encoding": "application/octet-stream" }, "errors": [ + { + "name": "BadSpaceMac", + "description": "The Atproto-Space-Want-Mac header is missing or malformed, names an unsupported type or version, or carries an invalid key share." + }, { "name": "SpaceNotFound" }, { "name": "RepoNotFound" }, { "name": "RepoTakendown" }, diff --git a/lexicons/upstream/com/atproto/space/getSpaceCredential.json b/lexicons/upstream/com/atproto/space/getSpaceCredential.json index c5fa9fa..cdc9505 100644 --- a/lexicons/upstream/com/atproto/space/getSpaceCredential.json +++ b/lexicons/upstream/com/atproto/space/getSpaceCredential.json @@ -4,18 +4,26 @@ "defs": { "main": { "type": "procedure", - "description": "Exchange a delegation token for a space credential. Called on the space authority, with the delegation token as the request's authorization token and an HTTP message signature over Authorization made by the key to bind the credential to. The resulting space credential reads repos across the space. Requires a delegation token and HTTP message signature, plus a client attestation when the space gates on app identity.", + "description": "Request a space credential. Called on the space authority. A delegation token is required when the space gates on the user. A client attestation is required when the space gates on app identity. A space that requires neither offers public access. The resulting space credential reads repos across the space.", "input": { "encoding": "application/json", "schema": { "type": "object", - "required": ["space"], + "required": ["space", "credentialKid"], "properties": { "space": { "type": "string", "format": "space-ref", "description": "Reference to the space." }, + "credentialKid": { + "type": "string", + "description": "P-256 did:key to bind the credential to. Each request using the credential is signed by this key." + }, + "delegationToken": { + "type": "string", + "description": "Optional delegation token establishing the user being acted for. Required only when the space gates on the user." + }, "clientAttestation": { "type": "string", "description": "Optional client attestation JWT establishing the app's identity. Required only when the space gates on app identity." diff --git a/lexicons/upstream/com/atproto/space/listRepoOps.json b/lexicons/upstream/com/atproto/space/listRepoOps.json index 886b573..9a96b1a 100644 --- a/lexicons/upstream/com/atproto/space/listRepoOps.json +++ b/lexicons/upstream/com/atproto/space/listRepoOps.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "List the operation log for an account's permissioned repo within a space, returning operations after a given revision. Primary incremental sync mechanism. By default each created or updated operation inlines the record's current value; set excludeValues for metadata-only entries. Note the oplog is a transport optimization with no history guarantee: a host may compact it or drop it, and it does not survive account migration, so omitting `since` returns the retained window rather than the repo's full history. Use getRepo for an initial sync or when a `since` revision is no longer available. Callable with either OAuth (for the authenticated user's own data) or a space credential (for syncing services).", + "description": "List the operation log for an account's permissioned repo within a space, returning operations after a given revision. Primary incremental sync mechanism. By default each created or updated operation inlines the record's current value; set excludeValues for metadata-only entries. Note the oplog is a transport optimization with no history guarantee: a host may compact it or drop it, and it does not survive account migration, so omitting `since` returns the retained window rather than the repo's full history. Use getRepo for an initial sync or when a `since` revision is no longer available. Callable with either OAuth (for the authenticated user's own data) or a space credential (for syncing services). When called with a space credential, the caller must send an Atproto-Space-Want-Mac header, and a response that includes the commit carries its MAC in the Atproto-Space-Mac response header.", "parameters": { "type": "params", "required": ["space", "repo"], @@ -57,8 +57,8 @@ }, "commit": { "type": "ref", - "ref": "com.atproto.space.defs#signedCommit", - "description": "The account's current signed commit. Included when the response reaches the head of the oplog; omitted on backfill responses." + "ref": "com.atproto.space.defs#commit", + "description": "The account's current commit. Included when the response reaches the head of the oplog; omitted on backfill responses." }, "cursor": { "type": "string", @@ -68,6 +68,10 @@ } }, "errors": [ + { + "name": "BadSpaceMac", + "description": "The Atproto-Space-Want-Mac header is missing or malformed, names an unsupported type or version, or carries an invalid key share." + }, { "name": "SpaceNotFound" }, { "name": "RepoNotFound" }, { "name": "RepoTakendown" }, diff --git a/lexicons/upstream/com/atproto/space/listRepos.json b/lexicons/upstream/com/atproto/space/listRepos.json index 07771a1..d7d53da 100644 --- a/lexicons/upstream/com/atproto/space/listRepos.json +++ b/lexicons/upstream/com/atproto/space/listRepos.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "List the known repos that hold data in a space (the writer set), with each repo's current repoRev and commit hash. Served by the space host. This is the sync boundary, not an access-control list: it enumerates only writers, never readers. The set is what the authority claims from write notifications and is not itself authoritative; a repo's host is the source of truth.", + "description": "List the known repos that hold data in a space (the writer set), with each repo's current repoRev and commit data. Served by the space host. This is the sync boundary, not an access-control list: it enumerates only writers, never readers. The set is what the authority claims from write notifications and is not itself authoritative; a repo's host is the source of truth.", "parameters": { "type": "params", "required": ["space"], @@ -56,7 +56,7 @@ }, "repo": { "type": "object", - "required": ["did", "repoRev", "hash", "spaceRev"], + "required": ["did", "repoRev", "data", "spaceRev"], "properties": { "did": { "type": "string", @@ -68,9 +68,9 @@ "description": "The repo's current revision (TID), as last reported to the authority. May lag the repo host, which is the source of truth.", "format": "tid" }, - "hash": { + "data": { "type": "bytes", - "description": "The repo's current commit hash (sha256 of the LtHash state), as last reported to the authority." + "description": "The repo's current commit data (sha256 of the LtHash state), as last reported to the authority." }, "spaceRev": { "type": "string", diff --git a/lexicons/upstream/com/atproto/space/notifySpaceDeleted.json b/lexicons/upstream/com/atproto/space/notifySpaceDeleted.json index cff248d..dc50a9c 100644 --- a/lexicons/upstream/com/atproto/space/notifySpaceDeleted.json +++ b/lexicons/upstream/com/atproto/space/notifySpaceDeleted.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "procedure", - "description": "Notify a syncing service that a space has been deleted, and that it should drop every copy of the space's data it holds. Sent by the space authority to the services registered for the space, best-effort. Authenticated with service auth addressed to the receiving service.", + "description": "Notify a syncing service that a space was deleted and its copy should be dropped. Sent by the space authority to registered services, retried until delivered or the registration expires. Requires service auth.", "input": { "encoding": "application/json", "schema": { diff --git a/lexicons/upstream/com/atproto/space/notifyWrite.json b/lexicons/upstream/com/atproto/space/notifyWrite.json index d869fe5..c93cb57 100644 --- a/lexicons/upstream/com/atproto/space/notifyWrite.json +++ b/lexicons/upstream/com/atproto/space/notifyWrite.json @@ -9,7 +9,7 @@ "encoding": "application/json", "schema": { "type": "object", - "required": ["space", "repo", "repoRev", "hash"], + "required": ["space", "repo", "repoRev", "data"], "properties": { "space": { "type": "string", @@ -26,9 +26,9 @@ "description": "The repo revision after the write.", "format": "tid" }, - "hash": { + "data": { "type": "bytes", - "description": "The repo's current commit hash (sha256 of the LtHash state) after the write. Lets the space host maintain each repo's hash for listRepos." + "description": "The repo's current commit data (sha256 of the LtHash state) after the write. Lets the space host maintain each repo's commit data for listRepos." }, "spaceRev": { "type": "string", diff --git a/packages/lexicons/package.json b/packages/lexicons/package.json index 097e76c..ce142bc 100644 --- a/packages/lexicons/package.json +++ b/packages/lexicons/package.json @@ -12,15 +12,15 @@ "publish-lexicons": "node src/publish-cli.ts" }, "dependencies": { - "@atproto/lex-json": "0.0.0-spaces-alpha-20261001173819", - "@atproto/lex-schema": "0.0.0-spaces-alpha-20261001173819" + "@atproto/lex-json": "0.0.0-spaces-alpha-20261022141228", + "@atproto/lex-schema": "0.0.0-spaces-alpha-20261022141228" }, "devDependencies": { - "@atproto/identity": "0.0.0-spaces-alpha-20261001173819", - "@atproto/lex": "0.0.0-spaces-alpha-20261001173819", - "@atproto/lex-client": "0.0.0-spaces-alpha-20261001173819", - "@atproto/lex-data": "0.0.0-spaces-alpha-20261001173819", - "@atproto/lex-document": "0.0.0-spaces-alpha-20261001173819", - "@atproto/lex-password-session": "0.0.0-spaces-alpha-20261001173819" + "@atproto/identity": "0.0.0-spaces-alpha-20261022141228", + "@atproto/lex": "0.0.0-spaces-alpha-20261022141228", + "@atproto/lex-client": "0.0.0-spaces-alpha-20261022141228", + "@atproto/lex-data": "0.0.0-spaces-alpha-20261022141228", + "@atproto/lex-document": "0.0.0-spaces-alpha-20261022141228", + "@atproto/lex-password-session": "0.0.0-spaces-alpha-20261022141228" } } diff --git a/packages/lexicons/src/generated/com/atproto/simplespace/deleteSpace.defs.ts b/packages/lexicons/src/generated/com/atproto/simplespace/deleteSpace.defs.ts index 24ba3a7..0fb6327 100644 --- a/packages/lexicons/src/generated/com/atproto/simplespace/deleteSpace.defs.ts +++ b/packages/lexicons/src/generated/com/atproto/simplespace/deleteSpace.defs.ts @@ -29,7 +29,7 @@ export type $OutputBody = l.InferPayloadBody< B > -/** Delete a space. The authenticated user must be the space owner. The authority's own repo in the space is deleted along with it, since the space host and the repo host are the same service here; other members' repos are flagged as belonging to a deleted space rather than erased. After deletion, all reads and writes against the space fail with SpaceNotFound, and getSpaceCredential answers SpaceDeleted so a syncer that missed the notification still learns to drop its copy. Idempotent. Requires auth, implemented by PDS. */ +/** Delete a space. Requires auth as the space owner. Deletes the owner's own repo in the space; other members' repos are left in place. Afterwards, reads and writes fail with SpaceNotFound and getSpaceCredential returns SpaceDeleted. Idempotent. Implemented by PDS. */ const main = /*#__PURE__*/ l.procedure($nsid, $params, $input, $output, [ 'SpaceNotFound', 'NotSpaceOwner', diff --git a/packages/lexicons/src/generated/com/atproto/space/defs.defs.ts b/packages/lexicons/src/generated/com/atproto/space/defs.defs.ts index f89e37c..3ab877b 100644 --- a/packages/lexicons/src/generated/com/atproto/space/defs.defs.ts +++ b/packages/lexicons/src/generated/com/atproto/space/defs.defs.ts @@ -10,55 +10,49 @@ type $nsid = typeof $nsid export { $nsid } -/** A signed commit over the current state of a permissioned repo. */ -type SignedCommit = { - $type?: 'com.atproto.space.defs#signedCommit' +/** A commit over the current state of a permissioned repo. Carries no signature: the repo host authenticates it to a syncer with a MAC in the Atproto-Space-Mac response header. */ +type Commit = { + $type?: 'com.atproto.space.defs#commit' /** - * Commit format version, currently 1. Corresponds to the version in the ctx protocol tag (atproto-space-v1). + * Commit format version, currently 1. */ ver: number /** - * sha256 digest of the LtHash state (32 bytes). - */ - hash: Uint8Array - - /** - * Per-signature input keying material (32 random bytes) + * The author DID of the repo. */ - ikm: Uint8Array + repo: l.DidString /** - * Signature over ctx (space, author DID, rev, ikm) by the user's atproto signing key. Does not cover the repo hash. + * Reference to the space. */ - sig: Uint8Array + space: l.SpaceRefString /** - * HMAC-SHA256 over hash, keyed by HKDF-SHA256(ikm, info=ctx). Binds the repo hash to this commit's context. + * Commit revision (TID). */ - mac: Uint8Array + rev: l.TidString /** - * Commit revision (TID), also bound into ctx. + * sha256 digest of the LtHash state (32 bytes). */ - rev: l.TidString + data: Uint8Array } -export type { SignedCommit } +export type { Commit } -/** A signed commit over the current state of a permissioned repo. */ -const signedCommit = /*#__PURE__*/ l.typedObject( +/** A commit over the current state of a permissioned repo. Carries no signature: the repo host authenticates it to a syncer with a MAC in the Atproto-Space-Mac response header. */ +const commit = /*#__PURE__*/ l.typedObject( $nsid, - 'signedCommit', + 'commit', /*#__PURE__*/ l.object({ ver: /*#__PURE__*/ l.integer(), - hash: /*#__PURE__*/ l.bytes(), - ikm: /*#__PURE__*/ l.bytes(), - sig: /*#__PURE__*/ l.bytes(), - mac: /*#__PURE__*/ l.bytes(), + repo: /*#__PURE__*/ l.string({ format: 'did' }), + space: /*#__PURE__*/ l.string({ format: 'space-ref' }), rev: /*#__PURE__*/ l.string({ format: 'tid' }), + data: /*#__PURE__*/ l.bytes(), }), ) -export { signedCommit } +export { commit } diff --git a/packages/lexicons/src/generated/com/atproto/space/getLatestCommit.defs.ts b/packages/lexicons/src/generated/com/atproto/space/getLatestCommit.defs.ts index 08873f4..2dd20c8 100644 --- a/packages/lexicons/src/generated/com/atproto/space/getLatestCommit.defs.ts +++ b/packages/lexicons/src/generated/com/atproto/space/getLatestCommit.defs.ts @@ -19,8 +19,8 @@ export const $params = /*#__PURE__*/ l.params({ export type $Params = l.InferOutput export const $output = /*#__PURE__*/ l.jsonPayload({ - commit: /*#__PURE__*/ l.ref( - (() => SpaceDefs.signedCommit) as any, + commit: /*#__PURE__*/ l.ref( + (() => SpaceDefs.commit) as any, ), }) @@ -30,8 +30,9 @@ export type $OutputBody = l.InferPayloadBody< B > -/** Get the current signed commit for an account's permissioned repo within a space. Served by a repo host. Throws RepoNotFound when the account holds no repo in the space, including the case of a member that has never written to it. Callable with either OAuth (for the authenticated user's own data) or a space credential (for syncing services). */ +/** Get the current commit for an account's permissioned repo within a space. Served by a repo host. When called with a space credential, the caller must send an Atproto-Space-Want-Mac header, and the commit's MAC is returned in the Atproto-Space-Mac response header. Throws RepoNotFound when the account holds no repo in the space, including the case of a member that has never written to it. Callable with either OAuth (for the authenticated user's own data) or a space credential (for syncing services). */ const main = /*#__PURE__*/ l.query($nsid, $params, $output, [ + 'BadSpaceMac', 'SpaceNotFound', 'RepoNotFound', 'RepoTakendown', diff --git a/packages/lexicons/src/generated/com/atproto/space/getRecord.defs.ts b/packages/lexicons/src/generated/com/atproto/space/getRecord.defs.ts index e3b7390..af35988 100644 --- a/packages/lexicons/src/generated/com/atproto/space/getRecord.defs.ts +++ b/packages/lexicons/src/generated/com/atproto/space/getRecord.defs.ts @@ -31,8 +31,9 @@ export type $OutputBody = l.InferPayloadBody< B > -/** Get a single record from a permissioned space. Callable with either OAuth (for the authenticated user's own data) or a space credential (for syncing services). Throws RepoNotFound when the account holds no repo in the space; this does not distinguish a member that has never written from a non-member. */ +/** Get a single record from a permissioned space. Callable with either OAuth (for the authenticated user's own data) or a space credential (for syncing services). When called with a space credential, the caller must send an Atproto-Space-Want-Mac header, and the record's MAC is returned in the Atproto-Space-Mac response header. Throws RepoNotFound when the account holds no repo in the space; this does not distinguish a member that has never written from a non-member. */ const main = /*#__PURE__*/ l.query($nsid, $params, $output, [ + 'BadSpaceMac', 'RecordNotFound', 'SpaceNotFound', 'RepoNotFound', diff --git a/packages/lexicons/src/generated/com/atproto/space/getRepo.defs.ts b/packages/lexicons/src/generated/com/atproto/space/getRepo.defs.ts index 6564046..51457f2 100644 --- a/packages/lexicons/src/generated/com/atproto/space/getRepo.defs.ts +++ b/packages/lexicons/src/generated/com/atproto/space/getRepo.defs.ts @@ -13,14 +13,11 @@ export { $nsid } export const $params = /*#__PURE__*/ l.params({ space: /*#__PURE__*/ l.string({ format: 'space-ref' }), repo: /*#__PURE__*/ l.string({ format: 'did' }), - excludeValues: /*#__PURE__*/ l.optional( - /*#__PURE__*/ l.withDefault(/*#__PURE__*/ l.boolean(), false), - ), }) export type $Params = l.InferOutput -export const $output = /*#__PURE__*/ l.payload('application/vnd.ipld.car') +export const $output = /*#__PURE__*/ l.payload('application/octet-stream') export type $Output = l.InferPayload export type $OutputBody = l.InferPayloadBody< @@ -28,8 +25,9 @@ export type $OutputBody = l.InferPayloadBody< B > -/** Download an account's permissioned repo within a space as a CAR file, for full-state recovery. The CAR declares two roots in order: the signed commit, then a DRISL (DAG-CBOR) index mapping '{collection}/{rkey}' to record CID. Record blocks follow, in the same canonical DAG-CBOR key order as the index (length-first, then bytewise). Blobs are not included and are fetched separately via getBlob. Served by a repo host. Callable with either OAuth (for the authenticated user's own data) or a space credential (for syncing services). */ +/** Download an account's permissioned repo within a space as a STAR file, for full-state recovery. The archive's metadata is the repo's commit; each key/value pair is a record path ('{collection}/{rkey}') and the record's DRISL encoding, in strict lexicographic order of the path. Blobs are not included and are fetched separately via getBlob. Served by a repo host. Callable with either OAuth (for the authenticated user's own data) or a space credential (for syncing services). When called with a space credential, the caller must send an Atproto-Space-Want-Mac header, and the commit's MAC is returned in the Atproto-Space-Mac response header. */ const main = /*#__PURE__*/ l.query($nsid, $params, $output, [ + 'BadSpaceMac', 'SpaceNotFound', 'RepoNotFound', 'RepoTakendown', diff --git a/packages/lexicons/src/generated/com/atproto/space/getSpaceCredential.defs.ts b/packages/lexicons/src/generated/com/atproto/space/getSpaceCredential.defs.ts index 6aba715..7b1616c 100644 --- a/packages/lexicons/src/generated/com/atproto/space/getSpaceCredential.defs.ts +++ b/packages/lexicons/src/generated/com/atproto/space/getSpaceCredential.defs.ts @@ -16,6 +16,8 @@ export type $Params = l.InferOutput export const $input = /*#__PURE__*/ l.jsonPayload({ space: /*#__PURE__*/ l.string({ format: 'space-ref' }), + credentialKid: /*#__PURE__*/ l.string(), + delegationToken: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.string()), clientAttestation: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.string()), }) @@ -32,7 +34,7 @@ export type $OutputBody = l.InferPayloadBody< B > -/** Exchange a delegation token for a space credential. Called on the space authority, with the delegation token as the request's authorization token and an HTTP message signature over Authorization made by the key to bind the credential to. The resulting space credential reads repos across the space. Requires a delegation token and HTTP message signature, plus a client attestation when the space gates on app identity. */ +/** Request a space credential. Called on the space authority. A delegation token is required when the space gates on the user. A client attestation is required when the space gates on app identity. A space that requires neither offers public access. The resulting space credential reads repos across the space. */ const main = /*#__PURE__*/ l.procedure($nsid, $params, $input, $output, [ 'SpaceNotFound', 'SpaceDeleted', diff --git a/packages/lexicons/src/generated/com/atproto/space/listRepoOps.defs.ts b/packages/lexicons/src/generated/com/atproto/space/listRepoOps.defs.ts index 749622a..88f7950 100644 --- a/packages/lexicons/src/generated/com/atproto/space/listRepoOps.defs.ts +++ b/packages/lexicons/src/generated/com/atproto/space/listRepoOps.defs.ts @@ -34,9 +34,7 @@ export const $output = /*#__PURE__*/ l.jsonPayload({ /*#__PURE__*/ l.ref((() => opEntry) as any), ), commit: /*#__PURE__*/ l.optional( - /*#__PURE__*/ l.ref( - (() => SpaceDefs.signedCommit) as any, - ), + /*#__PURE__*/ l.ref((() => SpaceDefs.commit) as any), ), cursor: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.string()), }) @@ -47,8 +45,9 @@ export type $OutputBody = l.InferPayloadBody< B > -/** List the operation log for an account's permissioned repo within a space, returning operations after a given revision. Primary incremental sync mechanism. By default each created or updated operation inlines the record's current value; set excludeValues for metadata-only entries. Note the oplog is a transport optimization with no history guarantee: a host may compact it or drop it, and it does not survive account migration, so omitting `since` returns the retained window rather than the repo's full history. Use getRepo for an initial sync or when a `since` revision is no longer available. Callable with either OAuth (for the authenticated user's own data) or a space credential (for syncing services). */ +/** List the operation log for an account's permissioned repo within a space, returning operations after a given revision. Primary incremental sync mechanism. By default each created or updated operation inlines the record's current value; set excludeValues for metadata-only entries. Note the oplog is a transport optimization with no history guarantee: a host may compact it or drop it, and it does not survive account migration, so omitting `since` returns the retained window rather than the repo's full history. Use getRepo for an initial sync or when a `since` revision is no longer available. Callable with either OAuth (for the authenticated user's own data) or a space credential (for syncing services). When called with a space credential, the caller must send an Atproto-Space-Want-Mac header, and a response that includes the commit carries its MAC in the Atproto-Space-Mac response header. */ const main = /*#__PURE__*/ l.query($nsid, $params, $output, [ + 'BadSpaceMac', 'SpaceNotFound', 'RepoNotFound', 'RepoTakendown', diff --git a/packages/lexicons/src/generated/com/atproto/space/listRepos.defs.ts b/packages/lexicons/src/generated/com/atproto/space/listRepos.defs.ts index b8e5eb6..3dcf0a3 100644 --- a/packages/lexicons/src/generated/com/atproto/space/listRepos.defs.ts +++ b/packages/lexicons/src/generated/com/atproto/space/listRepos.defs.ts @@ -34,7 +34,7 @@ export type $OutputBody = l.InferPayloadBody< B > -/** List the known repos that hold data in a space (the writer set), with each repo's current repoRev and commit hash. Served by the space host. This is the sync boundary, not an access-control list: it enumerates only writers, never readers. The set is what the authority claims from write notifications and is not itself authoritative; a repo's host is the source of truth. */ +/** List the known repos that hold data in a space (the writer set), with each repo's current repoRev and commit data. Served by the space host. This is the sync boundary, not an access-control list: it enumerates only writers, never readers. The set is what the authority claims from write notifications and is not itself authoritative; a repo's host is the source of truth. */ const main = /*#__PURE__*/ l.query($nsid, $params, $output, ['SpaceNotFound']) export { main } @@ -59,9 +59,9 @@ type Repo = { repoRev: l.TidString /** - * The repo's current commit hash (sha256 of the LtHash state), as last reported to the authority. + * The repo's current commit data (sha256 of the LtHash state), as last reported to the authority. */ - hash: Uint8Array + data: Uint8Array /** * The space revision at which this repo was last updated. @@ -77,7 +77,7 @@ const repo = /*#__PURE__*/ l.typedObject( /*#__PURE__*/ l.object({ did: /*#__PURE__*/ l.string({ format: 'did' }), repoRev: /*#__PURE__*/ l.string({ format: 'tid' }), - hash: /*#__PURE__*/ l.bytes(), + data: /*#__PURE__*/ l.bytes(), spaceRev: /*#__PURE__*/ l.string({ format: 'tid' }), }), ) diff --git a/packages/lexicons/src/generated/com/atproto/space/notifySpaceDeleted.defs.ts b/packages/lexicons/src/generated/com/atproto/space/notifySpaceDeleted.defs.ts index 7603cf0..fcc70f2 100644 --- a/packages/lexicons/src/generated/com/atproto/space/notifySpaceDeleted.defs.ts +++ b/packages/lexicons/src/generated/com/atproto/space/notifySpaceDeleted.defs.ts @@ -29,7 +29,7 @@ export type $OutputBody = l.InferPayloadBody< B > -/** Notify a syncing service that a space has been deleted, and that it should drop every copy of the space's data it holds. Sent by the space authority to the services registered for the space, best-effort. Authenticated with service auth addressed to the receiving service. */ +/** Notify a syncing service that a space was deleted and its copy should be dropped. Sent by the space authority to registered services, retried until delivered or the registration expires. Requires service auth. */ const main = /*#__PURE__*/ l.procedure($nsid, $params, $input, $output) export { main } diff --git a/packages/lexicons/src/generated/com/atproto/space/notifyWrite.defs.ts b/packages/lexicons/src/generated/com/atproto/space/notifyWrite.defs.ts index e8cb7b0..1b8a8cf 100644 --- a/packages/lexicons/src/generated/com/atproto/space/notifyWrite.defs.ts +++ b/packages/lexicons/src/generated/com/atproto/space/notifyWrite.defs.ts @@ -18,7 +18,7 @@ export const $input = /*#__PURE__*/ l.jsonPayload({ space: /*#__PURE__*/ l.string({ format: 'space-ref' }), repo: /*#__PURE__*/ l.string({ format: 'did' }), repoRev: /*#__PURE__*/ l.string({ format: 'tid' }), - hash: /*#__PURE__*/ l.bytes(), + data: /*#__PURE__*/ l.bytes(), spaceRev: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.string({ format: 'tid' })), prevSpaceRev: /*#__PURE__*/ l.optional( /*#__PURE__*/ l.string({ format: 'tid' }), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c3c465..e36191c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,51 +5,51 @@ settings: excludeLinksFromLockfile: false overrides: - '@atproto/api': 0.0.0-spaces-alpha-20261001173819 - '@atproto/aws': 0.0.0-spaces-alpha-20261001173819 - '@atproto/bsky': 0.0.0-spaces-alpha-20261001173819 - '@atproto/bsync': 0.0.0-spaces-alpha-20261001173819 - '@atproto/car': 0.0.0-spaces-alpha-20261001173819 - '@atproto/common': 0.0.0-spaces-alpha-20261001173819 - '@atproto/common-web': 0.0.0-spaces-alpha-20261001173819 - '@atproto/crypto': 0.0.0-spaces-alpha-20261001173819 - '@atproto/dev-env': 0.0.0-spaces-alpha-20261001173819 - '@atproto/did': 0.0.0-spaces-alpha-20261001173819 - '@atproto/identity': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-builder': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-cbor': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-cli': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-client': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-data': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-document': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-installer': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-json': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-password-session': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-resolver': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-schema': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-server': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lexicon': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lexicon-resolver': 0.0.0-spaces-alpha-20261001173819 - '@atproto/oauth-client': 0.0.0-spaces-alpha-20261001173819 - '@atproto/oauth-client-browser': 0.0.0-spaces-alpha-20261001173819 - '@atproto/oauth-client-browser-example': 0.0.0-spaces-alpha-20261001173819 - '@atproto/oauth-client-expo': 0.0.0-spaces-alpha-20261001173819 - '@atproto/oauth-client-node': 0.0.0-spaces-alpha-20261001173819 - '@atproto/oauth-provider': 0.0.0-spaces-alpha-20261001173819 - '@atproto/oauth-provider-api': 0.0.0-spaces-alpha-20261001173819 - '@atproto/oauth-provider-ui': 0.0.0-spaces-alpha-20261001173819 - '@atproto/oauth-scopes': 0.0.0-spaces-alpha-20261001173819 - '@atproto/oauth-types': 0.0.0-spaces-alpha-20261001173819 - '@atproto/ozone': 0.0.0-spaces-alpha-20261001173819 - '@atproto/pds': 0.0.0-spaces-alpha-20261001173819 - '@atproto/repo': 0.0.0-spaces-alpha-20261001173819 - '@atproto/space': 0.0.0-spaces-alpha-20261001173819 - '@atproto/sync': 0.0.0-spaces-alpha-20261001173819 - '@atproto/syntax': 0.0.0-spaces-alpha-20261001173819 - '@atproto/tap': 0.0.0-spaces-alpha-20261001173819 - '@atproto/xrpc': 0.0.0-spaces-alpha-20261001173819 - '@atproto/xrpc-server': 0.0.0-spaces-alpha-20261001173819 + '@atproto/api': 0.0.0-spaces-alpha-20261022141228 + '@atproto/aws': 0.0.0-spaces-alpha-20261022141228 + '@atproto/bsky': 0.0.0-spaces-alpha-20261022141228 + '@atproto/bsync': 0.0.0-spaces-alpha-20261022141228 + '@atproto/car': 0.0.0-spaces-alpha-20261022141228 + '@atproto/common': 0.0.0-spaces-alpha-20261022141228 + '@atproto/common-web': 0.0.0-spaces-alpha-20261022141228 + '@atproto/crypto': 0.0.0-spaces-alpha-20261022141228 + '@atproto/dev-env': 0.0.0-spaces-alpha-20261022141228 + '@atproto/did': 0.0.0-spaces-alpha-20261022141228 + '@atproto/identity': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-builder': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-cbor': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-cli': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-client': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-data': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-document': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-installer': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-json': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-password-session': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-resolver': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-schema': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-server': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lexicon': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lexicon-resolver': 0.0.0-spaces-alpha-20261022141228 + '@atproto/oauth-client': 0.0.0-spaces-alpha-20261022141228 + '@atproto/oauth-client-browser': 0.0.0-spaces-alpha-20261022141228 + '@atproto/oauth-client-browser-example': 0.0.0-spaces-alpha-20261022141228 + '@atproto/oauth-client-expo': 0.0.0-spaces-alpha-20261022141228 + '@atproto/oauth-client-node': 0.0.0-spaces-alpha-20261022141228 + '@atproto/oauth-provider': 0.0.0-spaces-alpha-20261022141228 + '@atproto/oauth-provider-api': 0.0.0-spaces-alpha-20261022141228 + '@atproto/oauth-provider-ui': 0.0.0-spaces-alpha-20261022141228 + '@atproto/oauth-scopes': 0.0.0-spaces-alpha-20261022141228 + '@atproto/oauth-types': 0.0.0-spaces-alpha-20261022141228 + '@atproto/ozone': 0.0.0-spaces-alpha-20261022141228 + '@atproto/pds': 0.0.0-spaces-alpha-20261022141228 + '@atproto/repo': 0.0.0-spaces-alpha-20261022141228 + '@atproto/space': 0.0.0-spaces-alpha-20261022141228 + '@atproto/sync': 0.0.0-spaces-alpha-20261022141228 + '@atproto/syntax': 0.0.0-spaces-alpha-20261022141228 + '@atproto/tap': 0.0.0-spaces-alpha-20261022141228 + '@atproto/xrpc': 0.0.0-spaces-alpha-20261022141228 + '@atproto/xrpc-server': 0.0.0-spaces-alpha-20261022141228 importers: @@ -102,41 +102,41 @@ importers: apps/server: dependencies: '@atproto/common-web': - specifier: 0.0.0-spaces-alpha-20261001173819 - version: 0.0.0-spaces-alpha-20261001173819 + specifier: 0.0.0-spaces-alpha-20261022141228 + version: 0.0.0-spaces-alpha-20261022141228 '@atproto/crypto': - specifier: 0.0.0-spaces-alpha-20261001173819 - version: 0.0.0-spaces-alpha-20261001173819 + specifier: 0.0.0-spaces-alpha-20261022141228 + version: 0.0.0-spaces-alpha-20261022141228 '@atproto/identity': - specifier: 0.0.0-spaces-alpha-20261001173819 - version: 0.0.0-spaces-alpha-20261001173819 + specifier: 0.0.0-spaces-alpha-20261022141228 + version: 0.0.0-spaces-alpha-20261022141228 '@atproto/lex-cbor': - specifier: 0.0.0-spaces-alpha-20261001173819 - version: 0.0.0-spaces-alpha-20261001173819 + specifier: 0.0.0-spaces-alpha-20261022141228 + version: 0.0.0-spaces-alpha-20261022141228 '@atproto/lex-client': - specifier: 0.0.0-spaces-alpha-20261001173819 - version: 0.0.0-spaces-alpha-20261001173819 + specifier: 0.0.0-spaces-alpha-20261022141228 + version: 0.0.0-spaces-alpha-20261022141228 '@atproto/lex-data': - specifier: 0.0.0-spaces-alpha-20261001173819 - version: 0.0.0-spaces-alpha-20261001173819 + specifier: 0.0.0-spaces-alpha-20261022141228 + version: 0.0.0-spaces-alpha-20261022141228 '@atproto/lex-json': - specifier: 0.0.0-spaces-alpha-20261001173819 - version: 0.0.0-spaces-alpha-20261001173819 + specifier: 0.0.0-spaces-alpha-20261022141228 + version: 0.0.0-spaces-alpha-20261022141228 '@atproto/oauth-client-node': - specifier: 0.0.0-spaces-alpha-20261001173819 - version: 0.0.0-spaces-alpha-20261001173819 + specifier: 0.0.0-spaces-alpha-20261022141228 + version: 0.0.0-spaces-alpha-20261022141228 '@atproto/oauth-scopes': - specifier: 0.0.0-spaces-alpha-20261001173819 - version: 0.0.0-spaces-alpha-20261001173819 + specifier: 0.0.0-spaces-alpha-20261022141228 + version: 0.0.0-spaces-alpha-20261022141228 '@atproto/space': - specifier: 0.0.0-spaces-alpha-20261001173819 - version: 0.0.0-spaces-alpha-20261001173819 + specifier: 0.0.0-spaces-alpha-20261022141228 + version: 0.0.0-spaces-alpha-20261022141228 '@atproto/syntax': - specifier: 0.0.0-spaces-alpha-20261001173819 - version: 0.0.0-spaces-alpha-20261001173819 + specifier: 0.0.0-spaces-alpha-20261022141228 + version: 0.0.0-spaces-alpha-20261022141228 '@atproto/xrpc-server': - specifier: 0.0.0-spaces-alpha-20261001173819 - version: 0.0.0-spaces-alpha-20261001173819 + specifier: 0.0.0-spaces-alpha-20261022141228 + version: 0.0.0-spaces-alpha-20261022141228 '@hono/node-server': specifier: 2.1.1 version: 2.1.1(hono@4.13.9) @@ -175,8 +175,8 @@ importers: version: 4.6.5 devDependencies: '@atproto/common': - specifier: 0.0.0-spaces-alpha-20261001173819 - version: 0.0.0-spaces-alpha-20261001173819 + specifier: 0.0.0-spaces-alpha-20261022141228 + version: 0.0.0-spaces-alpha-20261022141228 '@electric-sql/pglite': specifier: 0.5.8 version: 0.5.8 @@ -314,30 +314,30 @@ importers: packages/lexicons: dependencies: '@atproto/lex-json': - specifier: 0.0.0-spaces-alpha-20261001173819 - version: 0.0.0-spaces-alpha-20261001173819 + specifier: 0.0.0-spaces-alpha-20261022141228 + version: 0.0.0-spaces-alpha-20261022141228 '@atproto/lex-schema': - specifier: 0.0.0-spaces-alpha-20261001173819 - version: 0.0.0-spaces-alpha-20261001173819 + specifier: 0.0.0-spaces-alpha-20261022141228 + version: 0.0.0-spaces-alpha-20261022141228 devDependencies: '@atproto/identity': - specifier: 0.0.0-spaces-alpha-20261001173819 - version: 0.0.0-spaces-alpha-20261001173819 + specifier: 0.0.0-spaces-alpha-20261022141228 + version: 0.0.0-spaces-alpha-20261022141228 '@atproto/lex': - specifier: 0.0.0-spaces-alpha-20261001173819 - version: 0.0.0-spaces-alpha-20261001173819 + specifier: 0.0.0-spaces-alpha-20261022141228 + version: 0.0.0-spaces-alpha-20261022141228 '@atproto/lex-client': - specifier: 0.0.0-spaces-alpha-20261001173819 - version: 0.0.0-spaces-alpha-20261001173819 + specifier: 0.0.0-spaces-alpha-20261022141228 + version: 0.0.0-spaces-alpha-20261022141228 '@atproto/lex-data': - specifier: 0.0.0-spaces-alpha-20261001173819 - version: 0.0.0-spaces-alpha-20261001173819 + specifier: 0.0.0-spaces-alpha-20261022141228 + version: 0.0.0-spaces-alpha-20261022141228 '@atproto/lex-document': - specifier: 0.0.0-spaces-alpha-20261001173819 - version: 0.0.0-spaces-alpha-20261001173819 + specifier: 0.0.0-spaces-alpha-20261022141228 + version: 0.0.0-spaces-alpha-20261022141228 '@atproto/lex-password-session': - specifier: 0.0.0-spaces-alpha-20261001173819 - version: 0.0.0-spaces-alpha-20261001173819 + specifier: 0.0.0-spaces-alpha-20261022141228 + version: 0.0.0-spaces-alpha-20261022141228 packages/plugin-api: dependencies: @@ -521,28 +521,28 @@ packages: resolution: {integrity: sha512-lSWTBMjAcmu2xn5yEDU7jh6QDV+C8GEKtdJ4pIQhXh26RkKQ7S3FuQlt+zZkUbTdJ4d3XyV1kPI/G0zWXxqaqw==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} - '@atproto-labs/did-resolver@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-CQuOMIlmaYSImSxmWpc06GFTQwzDg1oGI5LIL2W7C6QJX+9QHWvxRrZgj+YpjbEqzcjcxt4jMEviAWXmePFrGw==} + '@atproto-labs/did-resolver@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-yp1YuHRuDUvdblK2rCg6baDfVU69hG1jaLqCd88IuEWLVUHrMRCmPKVpwU8ZLbuvA3YBUlFEr2CubFjoR5p+Cw==} engines: {node: '>=22'} - '@atproto-labs/fetch-node@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-q9/2bxBo0Dvti9dT4yRhBUjP+5avyIq2xYgGgVYdS+NJQFwQmVy53ST6VhzdRe5IHQuGpUpomASQnuvTbhpfNA==} + '@atproto-labs/fetch-node@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-vtar3q/div6iDgOa96Z/fDZ0Bs8GUzMAao/dG/jLekBDiGixlPrY7+K/afT8sMdeV6/3d6A9tiF1UN7sfU3R6g==} engines: {node: '>=22'} '@atproto-labs/fetch@0.3.6': resolution: {integrity: sha512-ro/J/cnqIp4bnSzDl8N3SkruvQcE3Pze+1H70mKdFfyFcdhPr+7O4lggl4VCbhI84Yqgz47c0xC6F/Yl39SxDA==} engines: {node: '>=22'} - '@atproto-labs/handle-resolver-node@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-tXImDOd0+aompK4k30Zx2PN/UEBUTpNLo8wUq5aiJPHlS9nGKSZ8jch8/XdGne9Z1ep3rb7/LVDTrQiyVuumBA==} + '@atproto-labs/handle-resolver-node@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-9Ff5q6YwnWpbGffCDL5gxiFku0LROiQdpr3bTOIob5ZEB3RQAXSI4uKAj8E9KdVPfxvcwCLEQEHQPn7M/EsdAA==} engines: {node: '>=22'} - '@atproto-labs/handle-resolver@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-ra+PPn1mJ96m39AnEaTHkIS7oI/HZFMwfsS8+MOPyFIiTXTmEp5b8t6eTU+mMHTvPHRPweQWnuVqhfhD7VQFsg==} + '@atproto-labs/handle-resolver@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-VyqHvGpX0gcNEHIliy4xyKoVEYkBOVmRYoFw/WmZ4yIY5fuyxrA2Dse3MozBb+g65B1MNuuB6te6qaXofkasXA==} engines: {node: '>=22'} - '@atproto-labs/identity-resolver@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-zlzCt4Ya3ol3GnMnJQmF9EziCJreW+n75g5wU/zncY2/Tayje68b6c4ktKkjwlwVo7qCOY5tzwz5rp2tayfnHg==} + '@atproto-labs/identity-resolver@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-+OEZpouRhDezdCv2FjQV+axoKXViZQsGT/EL2ufL7QUnWTZBdSfPM+vkI2lZfJ7VoKUtfQIrIlXBUloA/Ysxrg==} engines: {node: '>=22'} '@atproto-labs/pipe@0.2.4': @@ -557,28 +557,28 @@ packages: resolution: {integrity: sha512-vfvoDhu6ds6BT3Pqe+d2/LBU1WpDRtt69y6zltlfMCoucPm82m1j50/Wb6xTvv+oZ+2j0JyZVXsmXQ12SWYQJg==} engines: {node: '>=22'} - '@atproto/car@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-9uNMXCODU323AvoElPQdn69PEY9b955+z8PqfvWE292d5cQZULV62xLw9K9+BKvHEUVXRZC8xgFHvRSSMbEQRw==} + '@atproto/car@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-qkffc3gYOEwCrKJoeWZU9sQ8LoyDNSpieZj9YLBYsm7pOxXOHbiMqT9cheRov/neYkHSWD9RKUUGVpY8rWXGtQ==} engines: {node: '>=22'} - '@atproto/common-web@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-j77jkQFcGUQ5UWCIhbfgCunpjHo6Ob9LjeKfRuSV+HJvoKsWXqWn43x6lKNzmbw2volxEdRoBCcP88k+GbLlyA==} + '@atproto/common-web@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-rLk2ZO0P25ZBPwfWlg8/BPUPuUoBuSEBNTDbfLvcZ7jaNWFKhT/OrgUEfFP5kRhu52XF+4ElNlYkbLttOnYf+w==} engines: {node: '>=22'} - '@atproto/common@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-PfepeZ2VpHoVL4HXN0BF+G9gf4SwjW5Qjm+LHG5w1lAf2Qoc1Kpi30dfuz2NFh6hdgmRIAwTIZhqX05V3K82QQ==} + '@atproto/common@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-pabsbSpf9nUr5/skZh12mTEDM/hfh8ZQrKYA23NbUMLEnw/OQoeJuVE+HmO8AWnbOExwNb2OsZIpFOOx3jxMog==} engines: {node: '>=22'} - '@atproto/crypto@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-VlTQayUz1UYtfy1TLbc+I1/9CpeRt2hRuidPyifuIfRx9BLXgsju7bkwuAIF/LRg0XYo8enh+T+k0ASW248ZEQ==} + '@atproto/crypto@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-Tc9IUXbqKJ9Ew4S6ssPefRuwe/BpEBpfikeKjFTfSCHiKUPmzxnVPCVY2HuohcqfFc5pn8bt2bgbNPMYwnNJbA==} engines: {node: '>=22'} - '@atproto/did@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-vcjH/h5l0CDgXg1tHIhzLm2WLYMhF8si7i3V+KxDHPBPsYvoULvkcPfUiINifzyqiMHWHv4brGWeO1+0N6Ky+A==} + '@atproto/did@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-1da8hbahKnJ0pEDXKBhLxFLT/4JmQowm/i6PXgnqUlkSokG3/58Ic9UT9kf6yPQPtlcSL9BuVL8WwKiow8MXjQ==} engines: {node: '>=22'} - '@atproto/identity@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-2eyWcjeyY/bJdCyGz4XUsusytk1WrrDdg0mCFZDrJS30r0iGaPCfuUcdAuPwmGTlqZg8ylitq29yM30bVxoaTw==} + '@atproto/identity@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-pjzIzqH82VCPh8Kib18Js8dVuBEBC+kkzFdIxb3xtcgis0SZC53fkhPYtV9dg1V5MlcDML3bP9UOoEa4LmXWDg==} engines: {node: '>=22'} '@atproto/jwk-jose@0.2.4': @@ -593,93 +593,93 @@ packages: resolution: {integrity: sha512-tq7TUDmNfe1yDfpRgdGQMJdl9TUlJmREQNCag9yg5w8Evu+TOiFiLgiOCbo7X4ouRPSgd1DpOzXbUa8UyKKMZA==} engines: {node: '>=22'} - '@atproto/lex-builder@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-wXHhLXnNMXog+2NE1RW+JQiX1uSwV7bKEcpocSoaIjZLZ9qwaW35jF5YonUQxZG8HrlmieZ/TTSR7ZTdYGhWhA==} + '@atproto/lex-builder@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-0BZ443j+zKPtt26oS3WEcajNe1UYEh+Q7rq/P8yuc9+W/rHnYQGvazezTnz+Vxr8pemr4KZS0su8BNOqCLau+g==} engines: {node: '>=22'} - '@atproto/lex-cbor@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-+VermzWBi/zbT6ttimZKUdoojlgfuMHw394TB1lvt3PnIe4sdiYIxRgweSPquxYvrOxJVPTTDDtRRTg6X96V3A==} + '@atproto/lex-cbor@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-WIS9fH7KNsr3TnTL37YevE+0tbr23QtiWOs8r/fwpJcTRiagfRT9W2gRdSfDSJacqclIz8DF9g6B6sK0y5ImQQ==} engines: {node: '>=22'} - '@atproto/lex-client@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-zO6WdncwE674IGwcfIo0nMVHi9+Er5aOl4RBYvm5mQIL7GcYLrZuuI5KnqcLimRa+qletSUfxrMunGjooMMYhA==} + '@atproto/lex-client@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-H3HzHKOzNhqqPrpPrwPVInMgzCk6OEX5wUtvbr04DBB2kY27iGXrL52f2+sN5cCzRmI6DxiGNWgTb7z2lGbFbA==} engines: {node: '>=22'} - '@atproto/lex-data@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-DmK6vvSBtMGfjF5j0+ogY0hXRlud4TclU6IgDrG0vZ1VU/59N2sDu++b2ZThReyoFJ4erSf2nfCSm4EKQSu33Q==} + '@atproto/lex-data@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-rO6UILjV8B+Rp0TMc/OCqkHdKx2jMoD1Av0FRn7KarIzn9+lGt8kckdlL6aVFGQeidVTuPHcBVA/pS6/nqp+yA==} engines: {node: '>=22'} - '@atproto/lex-document@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-aGMnVckdVsA4M5kbgFgDQpi688HbvP5Dl8UyjzNlNaBR/tZMe/nfwsqNlX/SH+naIYL2Zc5RalrG3M8RC/nDdg==} + '@atproto/lex-document@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-HWQ2Q57hA+EDwfJbMdFBrhzdNJ/WzUEbRIU9Wuo/Mdwzks0jaMwr2Q2FI7s+wOB4juU1Td0XrmdBMCR3nO/SVg==} engines: {node: '>=22'} - '@atproto/lex-installer@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-adukmKxOLmP4WaHCasy90Gg3vogvIDJOHyuTBR+XMHcgcBk0MXthA7C3rxcfkiwRjaoxjGmXRK85gPaW/h9bEg==} + '@atproto/lex-installer@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-bDk16CgQg0GDx5AVN6m8dEI2eteizL1yDW5I5KwWatf58f+0W1n+uJPk7rPdPOnWvWpehe7M/esTLDdF/bHpyQ==} engines: {node: '>=22'} - '@atproto/lex-json@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-k991y6yiDsY/nb/U5iFmllY1Sj6WTjqFw1vT+XGLUEe9Zqf6JxQGixmJPzCR6p2lZLIu3RIWaQJtvfDkC8O7kw==} + '@atproto/lex-json@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-IFvP6fOEUG+PV5Zew02ZUpcU5F1/6AXjZa5RZfaCCI5j5T3Ja8Q6rmcYiZ71C0vL826XpOuToJdNdw6OHCmvhQ==} engines: {node: '>=22'} - '@atproto/lex-password-session@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-gMlRl9ERXoZxozP7ZwNowDn5uQc/9CNRmkDnzbafWSupvwNLBhsz9PJo8eZ1sp1a+q1lQDpMAbRKU3HLaiY/+Q==} + '@atproto/lex-password-session@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-4ugJyszYGbQ/sKv/Pd9uJ1IFPlhotdwFH1/Oxgdvzfzg1Ha4TeNcdagXtZbT7dGQJov3vJYtNKeE+IDjf6wX0A==} engines: {node: '>=22'} - '@atproto/lex-resolver@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-xeq6chcfqfON85uSbcTdf0USGSPXXZ9xSF1Z3pWXLpQI/oz19f8BkkKgC2dSFf3AVfPjp+fsFV106D+p1wrk7g==} + '@atproto/lex-resolver@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-vY28E0sQLnjywT8Zq1sQn48jyLJefCQHnHfSv77Khsr5aAM2x2FRcpCMXCjADxvMHB78kFSyshAZDQLX9uXEoA==} engines: {node: '>=22'} - '@atproto/lex-schema@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-fDKwbu0I9AmnLDuGopgFcbFFkjgUOBeUT20mkxwybImHiK6R5c1HouZ55G4OT1fC8W0M3FZYi6IArsAQTxOC2w==} + '@atproto/lex-schema@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-bBpk0Ebx45uWf9H706gGlLRaOV/g0Fnxy03rYuuZ5HsjDsp301uu+1hXYH3DtyPFmz/zlI3/h3/yqCPaXy7s3Q==} engines: {node: '>=22'} - '@atproto/lex@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-9S2kzWVDyBFkOKBeTxUG4ujiVSCrzZt4OUVthMLpZMRmoyb2npgYxubvxyf6HlEYhsaa0w2dEXEKMllqIncyMA==} + '@atproto/lex@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-vETbqMSLLdAKOjWN6B+S/UTQqEGi9SjRi6cRVCjI+xlMg0Fq2IWou2ewMUhoTxALQv5t2JIrjc2dZJIWG2KiPw==} engines: {node: '>=22'} hasBin: true - '@atproto/lexicon@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-8rG0JedIt6mxunXGNCqjVG76EH5NvJYQjW1rsKXUMRSmzy4GpsbNM8TEKC/qv0N6H3zJtOU8zzAJsd9DL16qYA==} + '@atproto/lexicon@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-o4GVOVWh3mkZdJ5nKr6UyS1BVb0UE6p88R3GTNqbLRerHzPL2L1PBdc3FWFNa0JyBGTNlBSS+Hc7GDAleiLESQ==} engines: {node: '>=22'} - '@atproto/oauth-client-node@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-l3T4UG01fxKH79B/fi8YAOQ3lq6kpMmXSzOqzICFl5VhfmxGblposT1J/4vgKX9WGMjeTd+8gIBQ1XA5y3+VXA==} + '@atproto/oauth-client-node@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-qlgdBhZv8AFlvlyJmuA7cjLpRgnc0VKDRtmivKOi/HXWLwP8eoKFY9+EIPu/keJYSkyh4Q6KGJak14f/2fXI9A==} engines: {node: '>=22'} - '@atproto/oauth-client@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-radDAiJQZB1Ps+biRhhzIXeHhItWJZfvWWKWmSAKi7toRlkBBqI6hREArF2CgmJFktRrG+4YbHzI+3OMIk9srw==} + '@atproto/oauth-client@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-zSOEF4n33U/bUboalhmBVCqqCHRvWocycWJ/vnODCrH4uMDLq30RG2ubcFd3T1XNbNx6huAKxFP40cMU4qOBdg==} engines: {node: '>=22'} - '@atproto/oauth-scopes@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-FftVZDVXfYqsCLbuKL+Q7CTpgS9jsPiajulMJs8liQ7cvSswr7P02YBExbZ1uDK6K1JVCmKppcUK+yTsluvFrw==} + '@atproto/oauth-scopes@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-T06geMvWa6zEwtoKdkTscyqEiz4ghiZXDLsy5FDas39KgChNgoY07gbLNjPLUerdivZVJJfUiSUddPVcQfqcCw==} engines: {node: '>=22'} - '@atproto/oauth-types@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-XR7oLEl9a4wvOCYObgdMo0lhsOqP0KR7hbJfe4p6/Gr+DIfCzjrWDbFuv9U/5kwgmMyTAJdlM6/7Y489c6RAzg==} + '@atproto/oauth-types@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-WT8N2YjrVDBfPMRTfGBIAqBoJMcjOfQ/N7nkXs5WKJpaCwBvuGk6qUAXEcRfol9wtIryFMLJORa9NnDeNyqfBw==} engines: {node: '>=22'} - '@atproto/repo@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-6nbH1QpZhLBs5KYklRWjyYyylN9AXnVWqo9DWmVpKnWnQXUKT3Z5fktO2jx7IrMJhNQ2xWK9vB5hDs+QrKHaJA==} + '@atproto/repo@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-TJ3rASRUgzJihHvw6/11FzX/UhGrOrXS+tVagRAWs3RZ/kGk/rVTVIfN70bShhZRujKsVLbs75PkeEewzObDaA==} engines: {node: '>=22'} - '@atproto/space@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-JDK/J0LeVk2qBNYFGF13lO7+8xci5fsuEzTY8nYyy1VZpVashOsNA4dH1ctqJZDG1IIQNZ2rvGiNwL9m7hghPQ==} + '@atproto/space@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-qoZAcZ3AUMBUguahNsyn4kmxv4p36D8xc9jL9vnbdp/MIF5Q/OxdIqt9zxpAhEHU6U+MDqJUZEHrEj01YN/kCw==} engines: {node: '>=22'} - '@atproto/syntax@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-RqH53yPZGM1iYDI4KinCNL4Nketi+4NkOaU04aRZz6XbywCYAS8r3giKnu1nU/UXKrKyJ9npqCJ31DA49/4Q5Q==} + '@atproto/syntax@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-Fx3tDa2AiKlT6nyQb2BJukHLGiVqTNNCeVPEXOrmXurvh/gRWGlMtkCwYSC+KRP82Rkni4uxw4v8jihVe67ImQ==} engines: {node: '>=22'} '@atproto/ws-client@0.2.1': resolution: {integrity: sha512-HCGkqH/Ksxpgg/xCyIvH5hK6o8Lg2PxcEA790GCEV9MspCZB/xIesHZVgJN0cEvklGe+6pNCOZE1B5kj9q1ONA==} engines: {node: '>=22'} - '@atproto/xrpc-server@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-UuR8ZzxKO2OgtlmKDynCKNDZ+9jyeG/a9ZI0mKYSmQSVBBk65g3Ume0YB0hMPueL1vNISFhrJJm++j5Z77eddw==} + '@atproto/xrpc-server@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-qhG/Hz4VWJFdk4xvp1w59s2/8EI+l9s6m8C/az18EmAUC9t4w+OQDKirc/LR+LYXSclxGIBkgcgkuvead+zdpQ==} engines: {node: '>=22'} - '@atproto/xrpc@0.0.0-spaces-alpha-20261001173819': - resolution: {integrity: sha512-/kl4oggyuFq7Me6B6zvj6hRwH20HDsuHiDW4bSHSdRhHsmDkmDpkFgQbn7iv/Ao85lWFUF1LNYwTlsoWQariFw==} + '@atproto/xrpc@0.0.0-spaces-alpha-20261022141228': + resolution: {integrity: sha512-raq+2fgnRni7qyKMwT/Lp9Cgho+AgDVau8KzpY1INyMYvQl9+9+rh+NiJgZBk1TYRSKla+QhVLkbUZUBbg3AWg==} engines: {node: '>=22'} '@babel/code-frame@7.29.7': @@ -3753,16 +3753,16 @@ snapshots: is-potential-custom-element-name: 1.0.1 lru-cache: 11.5.3 - '@atproto-labs/did-resolver@0.0.0-spaces-alpha-20261001173819': + '@atproto-labs/did-resolver@0.0.0-spaces-alpha-20261022141228': dependencies: '@atproto-labs/fetch': 0.3.6 '@atproto-labs/pipe': 0.2.4 '@atproto-labs/simple-store': 0.5.1 '@atproto-labs/simple-store-memory': 0.2.6 - '@atproto/did': 0.0.0-spaces-alpha-20261001173819 + '@atproto/did': 0.0.0-spaces-alpha-20261022141228 zod: 3.25.76 - '@atproto-labs/fetch-node@0.0.0-spaces-alpha-20261001173819': + '@atproto-labs/fetch-node@0.0.0-spaces-alpha-20261022141228': dependencies: '@atproto-labs/fetch': 0.3.6 '@atproto-labs/pipe': 0.2.4 @@ -3775,23 +3775,23 @@ snapshots: dependencies: '@atproto-labs/pipe': 0.2.4 - '@atproto-labs/handle-resolver-node@0.0.0-spaces-alpha-20261001173819': + '@atproto-labs/handle-resolver-node@0.0.0-spaces-alpha-20261022141228': dependencies: - '@atproto-labs/fetch-node': 0.0.0-spaces-alpha-20261001173819 - '@atproto-labs/handle-resolver': 0.0.0-spaces-alpha-20261001173819 - '@atproto/did': 0.0.0-spaces-alpha-20261001173819 + '@atproto-labs/fetch-node': 0.0.0-spaces-alpha-20261022141228 + '@atproto-labs/handle-resolver': 0.0.0-spaces-alpha-20261022141228 + '@atproto/did': 0.0.0-spaces-alpha-20261022141228 - '@atproto-labs/handle-resolver@0.0.0-spaces-alpha-20261001173819': + '@atproto-labs/handle-resolver@0.0.0-spaces-alpha-20261022141228': dependencies: '@atproto-labs/simple-store': 0.5.1 '@atproto-labs/simple-store-memory': 0.2.6 - '@atproto/did': 0.0.0-spaces-alpha-20261001173819 + '@atproto/did': 0.0.0-spaces-alpha-20261022141228 zod: 3.25.76 - '@atproto-labs/identity-resolver@0.0.0-spaces-alpha-20261001173819': + '@atproto-labs/identity-resolver@0.0.0-spaces-alpha-20261022141228': dependencies: - '@atproto-labs/did-resolver': 0.0.0-spaces-alpha-20261001173819 - '@atproto-labs/handle-resolver': 0.0.0-spaces-alpha-20261001173819 + '@atproto-labs/did-resolver': 0.0.0-spaces-alpha-20261022141228 + '@atproto-labs/handle-resolver': 0.0.0-spaces-alpha-20261022141228 '@atproto-labs/pipe@0.2.4': {} @@ -3802,42 +3802,42 @@ snapshots: '@atproto-labs/simple-store@0.5.1': {} - '@atproto/car@0.0.0-spaces-alpha-20261001173819': + '@atproto/car@0.0.0-spaces-alpha-20261022141228': dependencies: - '@atproto/lex-cbor': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-data': 0.0.0-spaces-alpha-20261001173819 + '@atproto/lex-cbor': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-data': 0.0.0-spaces-alpha-20261022141228 tslib: 2.8.1 varint: 6.0.0 - '@atproto/common-web@0.0.0-spaces-alpha-20261001173819': + '@atproto/common-web@0.0.0-spaces-alpha-20261022141228': dependencies: - '@atproto/lex-data': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-json': 0.0.0-spaces-alpha-20261001173819 - '@atproto/syntax': 0.0.0-spaces-alpha-20261001173819 + '@atproto/lex-data': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-json': 0.0.0-spaces-alpha-20261022141228 + '@atproto/syntax': 0.0.0-spaces-alpha-20261022141228 zod: 3.25.76 - '@atproto/common@0.0.0-spaces-alpha-20261001173819': + '@atproto/common@0.0.0-spaces-alpha-20261022141228': dependencies: - '@atproto/common-web': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-cbor': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-data': 0.0.0-spaces-alpha-20261001173819 + '@atproto/common-web': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-cbor': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-data': 0.0.0-spaces-alpha-20261022141228 multiformats: 13.4.2 pino: 10.3.1 - '@atproto/crypto@0.0.0-spaces-alpha-20261001173819': + '@atproto/crypto@0.0.0-spaces-alpha-20261022141228': dependencies: '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 uint8arrays: 5.1.1 - '@atproto/did@0.0.0-spaces-alpha-20261001173819': + '@atproto/did@0.0.0-spaces-alpha-20261022141228': dependencies: zod: 3.25.76 - '@atproto/identity@0.0.0-spaces-alpha-20261001173819': + '@atproto/identity@0.0.0-spaces-alpha-20261022141228': dependencies: - '@atproto/common-web': 0.0.0-spaces-alpha-20261001173819 - '@atproto/crypto': 0.0.0-spaces-alpha-20261001173819 + '@atproto/common-web': 0.0.0-spaces-alpha-20261022141228 + '@atproto/crypto': 0.0.0-spaces-alpha-20261022141228 '@atproto/jwk-jose@0.2.4': dependencies: @@ -3855,160 +3855,160 @@ snapshots: multiformats: 13.4.2 zod: 3.25.76 - '@atproto/lex-builder@0.0.0-spaces-alpha-20261001173819': + '@atproto/lex-builder@0.0.0-spaces-alpha-20261022141228': dependencies: - '@atproto/lex-document': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-schema': 0.0.0-spaces-alpha-20261001173819 + '@atproto/lex-document': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-schema': 0.0.0-spaces-alpha-20261022141228 prettier: 3.9.9 ts-morph: 27.0.2 tslib: 2.8.1 - '@atproto/lex-cbor@0.0.0-spaces-alpha-20261001173819': + '@atproto/lex-cbor@0.0.0-spaces-alpha-20261022141228': dependencies: - '@atproto/lex-data': 0.0.0-spaces-alpha-20261001173819 + '@atproto/lex-data': 0.0.0-spaces-alpha-20261022141228 cborg: 4.5.8 tslib: 2.8.1 - '@atproto/lex-client@0.0.0-spaces-alpha-20261001173819': + '@atproto/lex-client@0.0.0-spaces-alpha-20261022141228': dependencies: - '@atproto/lex-data': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-json': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-schema': 0.0.0-spaces-alpha-20261001173819 + '@atproto/lex-data': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-json': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-schema': 0.0.0-spaces-alpha-20261022141228 tslib: 2.8.1 - '@atproto/lex-data@0.0.0-spaces-alpha-20261001173819': + '@atproto/lex-data@0.0.0-spaces-alpha-20261022141228': dependencies: multiformats: 13.4.2 tslib: 2.8.1 unicode-segmenter: 0.14.5 - '@atproto/lex-document@0.0.0-spaces-alpha-20261001173819': + '@atproto/lex-document@0.0.0-spaces-alpha-20261022141228': dependencies: - '@atproto/lex-schema': 0.0.0-spaces-alpha-20261001173819 + '@atproto/lex-schema': 0.0.0-spaces-alpha-20261022141228 core-js: 3.50.0 tslib: 2.8.1 - '@atproto/lex-installer@0.0.0-spaces-alpha-20261001173819': + '@atproto/lex-installer@0.0.0-spaces-alpha-20261022141228': dependencies: - '@atproto/lex-builder': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-cbor': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-data': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-document': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-resolver': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-schema': 0.0.0-spaces-alpha-20261001173819 - '@atproto/syntax': 0.0.0-spaces-alpha-20261001173819 + '@atproto/lex-builder': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-cbor': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-data': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-document': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-resolver': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-schema': 0.0.0-spaces-alpha-20261022141228 + '@atproto/syntax': 0.0.0-spaces-alpha-20261022141228 tslib: 2.8.1 - '@atproto/lex-json@0.0.0-spaces-alpha-20261001173819': + '@atproto/lex-json@0.0.0-spaces-alpha-20261022141228': dependencies: - '@atproto/lex-data': 0.0.0-spaces-alpha-20261001173819 + '@atproto/lex-data': 0.0.0-spaces-alpha-20261022141228 tslib: 2.8.1 - '@atproto/lex-password-session@0.0.0-spaces-alpha-20261001173819': + '@atproto/lex-password-session@0.0.0-spaces-alpha-20261022141228': dependencies: - '@atproto/lex-client': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-schema': 0.0.0-spaces-alpha-20261001173819 + '@atproto/lex-client': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-schema': 0.0.0-spaces-alpha-20261022141228 tslib: 2.8.1 - '@atproto/lex-resolver@0.0.0-spaces-alpha-20261001173819': + '@atproto/lex-resolver@0.0.0-spaces-alpha-20261022141228': dependencies: - '@atproto-labs/did-resolver': 0.0.0-spaces-alpha-20261001173819 - '@atproto/crypto': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-client': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-data': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-document': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-schema': 0.0.0-spaces-alpha-20261001173819 - '@atproto/repo': 0.0.0-spaces-alpha-20261001173819 - '@atproto/syntax': 0.0.0-spaces-alpha-20261001173819 + '@atproto-labs/did-resolver': 0.0.0-spaces-alpha-20261022141228 + '@atproto/crypto': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-client': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-data': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-document': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-schema': 0.0.0-spaces-alpha-20261022141228 + '@atproto/repo': 0.0.0-spaces-alpha-20261022141228 + '@atproto/syntax': 0.0.0-spaces-alpha-20261022141228 tslib: 2.8.1 - '@atproto/lex-schema@0.0.0-spaces-alpha-20261001173819': + '@atproto/lex-schema@0.0.0-spaces-alpha-20261022141228': dependencies: - '@atproto/lex-data': 0.0.0-spaces-alpha-20261001173819 - '@atproto/syntax': 0.0.0-spaces-alpha-20261001173819 + '@atproto/lex-data': 0.0.0-spaces-alpha-20261022141228 + '@atproto/syntax': 0.0.0-spaces-alpha-20261022141228 '@standard-schema/spec': 1.1.0 tslib: 2.8.1 - '@atproto/lex@0.0.0-spaces-alpha-20261001173819': + '@atproto/lex@0.0.0-spaces-alpha-20261022141228': dependencies: - '@atproto/lex-builder': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-client': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-data': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-installer': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-json': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-schema': 0.0.0-spaces-alpha-20261001173819 + '@atproto/lex-builder': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-client': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-data': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-installer': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-json': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-schema': 0.0.0-spaces-alpha-20261022141228 tslib: 2.8.1 yargs: 18.2.0 - '@atproto/lexicon@0.0.0-spaces-alpha-20261001173819': + '@atproto/lexicon@0.0.0-spaces-alpha-20261022141228': dependencies: - '@atproto/common-web': 0.0.0-spaces-alpha-20261001173819 - '@atproto/syntax': 0.0.0-spaces-alpha-20261001173819 + '@atproto/common-web': 0.0.0-spaces-alpha-20261022141228 + '@atproto/syntax': 0.0.0-spaces-alpha-20261022141228 multiformats: 13.4.2 zod: 3.25.76 - '@atproto/oauth-client-node@0.0.0-spaces-alpha-20261001173819': + '@atproto/oauth-client-node@0.0.0-spaces-alpha-20261022141228': dependencies: - '@atproto-labs/did-resolver': 0.0.0-spaces-alpha-20261001173819 - '@atproto-labs/handle-resolver-node': 0.0.0-spaces-alpha-20261001173819 + '@atproto-labs/did-resolver': 0.0.0-spaces-alpha-20261022141228 + '@atproto-labs/handle-resolver-node': 0.0.0-spaces-alpha-20261022141228 '@atproto-labs/simple-store': 0.5.1 - '@atproto/did': 0.0.0-spaces-alpha-20261001173819 + '@atproto/did': 0.0.0-spaces-alpha-20261022141228 '@atproto/jwk': 0.7.4 '@atproto/jwk-jose': 0.2.4 '@atproto/jwk-webcrypto': 0.3.4 - '@atproto/oauth-client': 0.0.0-spaces-alpha-20261001173819 - '@atproto/oauth-types': 0.0.0-spaces-alpha-20261001173819 + '@atproto/oauth-client': 0.0.0-spaces-alpha-20261022141228 + '@atproto/oauth-types': 0.0.0-spaces-alpha-20261022141228 - '@atproto/oauth-client@0.0.0-spaces-alpha-20261001173819': + '@atproto/oauth-client@0.0.0-spaces-alpha-20261022141228': dependencies: - '@atproto-labs/did-resolver': 0.0.0-spaces-alpha-20261001173819 + '@atproto-labs/did-resolver': 0.0.0-spaces-alpha-20261022141228 '@atproto-labs/fetch': 0.3.6 - '@atproto-labs/handle-resolver': 0.0.0-spaces-alpha-20261001173819 - '@atproto-labs/identity-resolver': 0.0.0-spaces-alpha-20261001173819 + '@atproto-labs/handle-resolver': 0.0.0-spaces-alpha-20261022141228 + '@atproto-labs/identity-resolver': 0.0.0-spaces-alpha-20261022141228 '@atproto-labs/simple-store': 0.5.1 '@atproto-labs/simple-store-memory': 0.2.6 - '@atproto/did': 0.0.0-spaces-alpha-20261001173819 + '@atproto/did': 0.0.0-spaces-alpha-20261022141228 '@atproto/jwk': 0.7.4 - '@atproto/oauth-types': 0.0.0-spaces-alpha-20261001173819 - '@atproto/xrpc': 0.0.0-spaces-alpha-20261001173819 + '@atproto/oauth-types': 0.0.0-spaces-alpha-20261022141228 + '@atproto/xrpc': 0.0.0-spaces-alpha-20261022141228 core-js: 3.50.0 multiformats: 13.4.2 zod: 3.25.76 - '@atproto/oauth-scopes@0.0.0-spaces-alpha-20261001173819': + '@atproto/oauth-scopes@0.0.0-spaces-alpha-20261022141228': dependencies: - '@atproto/did': 0.0.0-spaces-alpha-20261001173819 - '@atproto/syntax': 0.0.0-spaces-alpha-20261001173819 + '@atproto/did': 0.0.0-spaces-alpha-20261022141228 + '@atproto/syntax': 0.0.0-spaces-alpha-20261022141228 - '@atproto/oauth-types@0.0.0-spaces-alpha-20261001173819': + '@atproto/oauth-types@0.0.0-spaces-alpha-20261022141228': dependencies: - '@atproto/did': 0.0.0-spaces-alpha-20261001173819 + '@atproto/did': 0.0.0-spaces-alpha-20261022141228 '@atproto/jwk': 0.7.4 zod: 3.25.76 - '@atproto/repo@0.0.0-spaces-alpha-20261001173819': + '@atproto/repo@0.0.0-spaces-alpha-20261022141228': dependencies: - '@atproto/car': 0.0.0-spaces-alpha-20261001173819 - '@atproto/common': 0.0.0-spaces-alpha-20261001173819 - '@atproto/common-web': 0.0.0-spaces-alpha-20261001173819 - '@atproto/crypto': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-cbor': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-data': 0.0.0-spaces-alpha-20261001173819 - '@atproto/syntax': 0.0.0-spaces-alpha-20261001173819 + '@atproto/car': 0.0.0-spaces-alpha-20261022141228 + '@atproto/common': 0.0.0-spaces-alpha-20261022141228 + '@atproto/common-web': 0.0.0-spaces-alpha-20261022141228 + '@atproto/crypto': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-cbor': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-data': 0.0.0-spaces-alpha-20261022141228 + '@atproto/syntax': 0.0.0-spaces-alpha-20261022141228 zod: 3.25.76 - '@atproto/space@0.0.0-spaces-alpha-20261001173819': + '@atproto/space@0.0.0-spaces-alpha-20261022141228': dependencies: - '@atproto/car': 0.0.0-spaces-alpha-20261001173819 - '@atproto/crypto': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-cbor': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-data': 0.0.0-spaces-alpha-20261001173819 - '@atproto/syntax': 0.0.0-spaces-alpha-20261001173819 + '@atproto/car': 0.0.0-spaces-alpha-20261022141228 + '@atproto/crypto': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-cbor': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-data': 0.0.0-spaces-alpha-20261022141228 + '@atproto/syntax': 0.0.0-spaces-alpha-20261022141228 '@noble/hashes': 1.8.0 structured-headers: 1.0.1 zod: 3.25.76 - '@atproto/syntax@0.0.0-spaces-alpha-20261001173819': + '@atproto/syntax@0.0.0-spaces-alpha-20261022141228': dependencies: iso-datestring-validator: 2.2.2 tslib: 2.8.1 @@ -4020,18 +4020,18 @@ snapshots: - bufferutil - utf-8-validate - '@atproto/xrpc-server@0.0.0-spaces-alpha-20261001173819': + '@atproto/xrpc-server@0.0.0-spaces-alpha-20261022141228': dependencies: - '@atproto/common': 0.0.0-spaces-alpha-20261001173819 - '@atproto/crypto': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-cbor': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-client': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-data': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-json': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-schema': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lexicon': 0.0.0-spaces-alpha-20261001173819 + '@atproto/common': 0.0.0-spaces-alpha-20261022141228 + '@atproto/crypto': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-cbor': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-client': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-data': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-json': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-schema': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lexicon': 0.0.0-spaces-alpha-20261022141228 '@atproto/ws-client': 0.2.1 - '@atproto/xrpc': 0.0.0-spaces-alpha-20261001173819 + '@atproto/xrpc': 0.0.0-spaces-alpha-20261022141228 express: 4.22.3 http-errors: 2.0.1 mime-types: 3.0.2 @@ -4042,9 +4042,9 @@ snapshots: - supports-color - utf-8-validate - '@atproto/xrpc@0.0.0-spaces-alpha-20261001173819': + '@atproto/xrpc@0.0.0-spaces-alpha-20261022141228': dependencies: - '@atproto/lexicon': 0.0.0-spaces-alpha-20261001173819 + '@atproto/lexicon': 0.0.0-spaces-alpha-20261022141228 zod: 3.25.76 '@babel/code-frame@7.29.7': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2aed24f..67db200 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -8,48 +8,48 @@ allowBuilds: esbuild: true overrides: - '@atproto/api': 0.0.0-spaces-alpha-20261001173819 - '@atproto/aws': 0.0.0-spaces-alpha-20261001173819 - '@atproto/bsky': 0.0.0-spaces-alpha-20261001173819 - '@atproto/bsync': 0.0.0-spaces-alpha-20261001173819 - '@atproto/car': 0.0.0-spaces-alpha-20261001173819 - '@atproto/common': 0.0.0-spaces-alpha-20261001173819 - '@atproto/common-web': 0.0.0-spaces-alpha-20261001173819 - '@atproto/crypto': 0.0.0-spaces-alpha-20261001173819 - '@atproto/dev-env': 0.0.0-spaces-alpha-20261001173819 - '@atproto/did': 0.0.0-spaces-alpha-20261001173819 - '@atproto/identity': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-builder': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-cbor': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-cli': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-client': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-data': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-document': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-installer': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-json': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-password-session': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-resolver': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-schema': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lex-server': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lexicon': 0.0.0-spaces-alpha-20261001173819 - '@atproto/lexicon-resolver': 0.0.0-spaces-alpha-20261001173819 - '@atproto/oauth-client': 0.0.0-spaces-alpha-20261001173819 - '@atproto/oauth-client-browser': 0.0.0-spaces-alpha-20261001173819 - '@atproto/oauth-client-browser-example': 0.0.0-spaces-alpha-20261001173819 - '@atproto/oauth-client-expo': 0.0.0-spaces-alpha-20261001173819 - '@atproto/oauth-client-node': 0.0.0-spaces-alpha-20261001173819 - '@atproto/oauth-provider': 0.0.0-spaces-alpha-20261001173819 - '@atproto/oauth-provider-api': 0.0.0-spaces-alpha-20261001173819 - '@atproto/oauth-provider-ui': 0.0.0-spaces-alpha-20261001173819 - '@atproto/oauth-scopes': 0.0.0-spaces-alpha-20261001173819 - '@atproto/oauth-types': 0.0.0-spaces-alpha-20261001173819 - '@atproto/ozone': 0.0.0-spaces-alpha-20261001173819 - '@atproto/pds': 0.0.0-spaces-alpha-20261001173819 - '@atproto/repo': 0.0.0-spaces-alpha-20261001173819 - '@atproto/space': 0.0.0-spaces-alpha-20261001173819 - '@atproto/sync': 0.0.0-spaces-alpha-20261001173819 - '@atproto/syntax': 0.0.0-spaces-alpha-20261001173819 - '@atproto/tap': 0.0.0-spaces-alpha-20261001173819 - '@atproto/xrpc': 0.0.0-spaces-alpha-20261001173819 - '@atproto/xrpc-server': 0.0.0-spaces-alpha-20261001173819 + '@atproto/api': 0.0.0-spaces-alpha-20261022141228 + '@atproto/aws': 0.0.0-spaces-alpha-20261022141228 + '@atproto/bsky': 0.0.0-spaces-alpha-20261022141228 + '@atproto/bsync': 0.0.0-spaces-alpha-20261022141228 + '@atproto/car': 0.0.0-spaces-alpha-20261022141228 + '@atproto/common': 0.0.0-spaces-alpha-20261022141228 + '@atproto/common-web': 0.0.0-spaces-alpha-20261022141228 + '@atproto/crypto': 0.0.0-spaces-alpha-20261022141228 + '@atproto/dev-env': 0.0.0-spaces-alpha-20261022141228 + '@atproto/did': 0.0.0-spaces-alpha-20261022141228 + '@atproto/identity': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-builder': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-cbor': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-cli': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-client': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-data': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-document': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-installer': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-json': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-password-session': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-resolver': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-schema': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lex-server': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lexicon': 0.0.0-spaces-alpha-20261022141228 + '@atproto/lexicon-resolver': 0.0.0-spaces-alpha-20261022141228 + '@atproto/oauth-client': 0.0.0-spaces-alpha-20261022141228 + '@atproto/oauth-client-browser': 0.0.0-spaces-alpha-20261022141228 + '@atproto/oauth-client-browser-example': 0.0.0-spaces-alpha-20261022141228 + '@atproto/oauth-client-expo': 0.0.0-spaces-alpha-20261022141228 + '@atproto/oauth-client-node': 0.0.0-spaces-alpha-20261022141228 + '@atproto/oauth-provider': 0.0.0-spaces-alpha-20261022141228 + '@atproto/oauth-provider-api': 0.0.0-spaces-alpha-20261022141228 + '@atproto/oauth-provider-ui': 0.0.0-spaces-alpha-20261022141228 + '@atproto/oauth-scopes': 0.0.0-spaces-alpha-20261022141228 + '@atproto/oauth-types': 0.0.0-spaces-alpha-20261022141228 + '@atproto/ozone': 0.0.0-spaces-alpha-20261022141228 + '@atproto/pds': 0.0.0-spaces-alpha-20261022141228 + '@atproto/repo': 0.0.0-spaces-alpha-20261022141228 + '@atproto/space': 0.0.0-spaces-alpha-20261022141228 + '@atproto/sync': 0.0.0-spaces-alpha-20261022141228 + '@atproto/syntax': 0.0.0-spaces-alpha-20261022141228 + '@atproto/tap': 0.0.0-spaces-alpha-20261022141228 + '@atproto/xrpc': 0.0.0-spaces-alpha-20261022141228 + '@atproto/xrpc-server': 0.0.0-spaces-alpha-20261022141228 diff --git a/specs/chat-storage.md b/specs/chat-storage.md index 62514e2..5c411f4 100644 --- a/specs/chat-storage.md +++ b/specs/chat-storage.md @@ -75,7 +75,7 @@ The server stores cursors and registration state in a `sync_state` table: space **Identity.** The server has a did:web identity derived from `PUBLIC_URL`, for example `did:web:chat.example.com`. It serves `/.well-known/did.json` with one service entry: ID `#scn_chat`, type `AtprotoSpaceService`, endpoint `PUBLIC_URL`. The service identifier is `#scn_chat`. -**Space credentials.** `registerNotify` only accepts a space credential, even from the space's owner. To get one, the server calls `com.atproto.space.getDelegationToken` on the user's PDS, generates a P-256 key with `P256Keypair.create` from `@atproto/crypto`, and posts to `com.atproto.space.getSpaceCredential` on the user's PDS, signed with `createSpaceSigHeaders` from `@atproto/space`. Each request made with the credential is signed with the same key for the DID of the repo it reads. Credentials are cached in memory until the expiry in their token. A call that fails with an auth error discards the credential and retries once with a fresh one. +**Space credentials.** `registerNotify` only accepts a space credential, even from the space's owner. To get one, the server calls `com.atproto.space.getDelegationToken` on the user's PDS, generates a P-256 key with `P256Keypair.create` from `@atproto/crypto`, and posts the delegation token and the key's did:key, as `credentialKid`, to `com.atproto.space.getSpaceCredential` on the user's PDS. Each request made with the credential presents it under the `Atproto-Space-Credential` scheme, signed with `createSpaceSigHeaders` from `@atproto/space` by the same key, for the DID of the repo it reads. Credentials are cached in memory until the expiry in their token. A call that fails with an auth error discards the credential and retries once with a fresh one. **Watching the index.** Each spaces user's settings space is registered with `com.atproto.space.registerNotify`, at login and then renewed by a job every five minutes when less than an hour remains. That is one registration per user, whatever their number of conversations. Conversations themselves are not registered. @@ -87,7 +87,7 @@ A user's first index sync, with no `last_rev`, does not sync every conversation. **Syncing a conversation.** It reads `listOps` on the conversation from its `last_rev`, advances `last_rev`, and emits a `message:changed` event for each message op. The event carries the conversation, the record, whether it was a create or an update, and whether it is live. It is live when the conversation already had a `last_rev`. It is a backfill when this is the first sync of the conversation, and then the ops cover its whole history. Chat-turns uses that distinction. A record that fails lexicon validation emits a `message:invalid` event instead, with the raw record and the validation error. When an info op carries a title that differs from the `conversationRef`'s copy, the entry is rewritten with the info record's title. -**Detecting gaps.** `listRepoOps` includes the repo's signed commit when a page reaches the head of the log, and the commit's hash is the set hash over every record's collection, key, and CID. The safety net, and the first sync of a conversation, check it. The server lists the space's records with `excludeValues`, which returns those triples without content, rebuilds the set hash with the `@atproto/space` helpers, and verifies the commit. On a mismatch, it resets that space's cursor. The index cursor moves to the head and the entries inside the backfill window are synced, as on a first sync. A conversation's cursor is cleared, so its next sync is a backfill. Either way a warning is logged. +**Detecting gaps.** `listRepoOps` includes the repo's commit when a page reaches the head of the log, and the commit's `data` is the set hash over every record's collection, key, and CID. The safety net, and the first sync of a conversation, check it. The server resolves the user's `#atproto_kem` key and sends each page's request with an `Atproto-Space-Want-Mac` header carrying a fresh key share, so the PDS MACs the commit, and checks it with `verifyCommit`. A commit that fails its MAC fails the sync and leaves the cursor where it was. While spaces are in alpha, an account whose DID document has no `#atproto_kem` key yet has its commit compared without a MAC, with a warning logged. Remove that fallback once every spaces PDS publishes the key. The server then lists the space's records with `excludeValues`, which returns those triples without content, rebuilds the set hash with the `@atproto/space` helpers, and compares it with the commit. On a mismatch, it resets that space's cursor. The index cursor moves to the head and the entries inside the backfill window are synced, as on a first sync. A conversation's cursor is cleared, so its next sync is a backfill. Either way a warning is logged. **What starts a sync.** diff --git a/specs/sharing.md b/specs/sharing.md index d08d81a..f680410 100644 --- a/specs/sharing.md +++ b/specs/sharing.md @@ -36,10 +36,10 @@ Both routes return 403 with a message saying sharing needs a spaces PDS when the 1. The server gets a delegation token from the viewer's PDS for the conversation space, through the viewer's session. The permission set's read access to conversation spaces of any authority allows this. 2. It exchanges the token for a space credential at the owner's PDS, the space authority, with the same signing code chat-storage uses for its own credentials. The owner's PDS applies the read policy and member list. -3. With the credential, it reads the owner's info record with `getRecord` and every message with `listRecords`, from the owner's PDS. +3. With the credential, it reads the owner's info record with `getRecord` and every message with `listRecords`, from the owner's PDS. A credential read of a record must ask for a MAC, so the `getRecord` request carries an `Atproto-Space-Want-Mac` header keyed against the owner's `#atproto_kem` key, and the server checks the record against the returned MAC with `verifyRecord`. 4. It returns the title and messages. The system prompt and the owner's preferences are left out. Tags live in the owner's private settings space, so viewers never see them. Any member can still read the system prompt through the protocol, since it is in the info record, so leaving it out is a presentation choice, not a privacy guarantee. -A refused credential (`UserNotAuthorized`), a missing space (`SpaceNotFound`), or a deleted one (`SpaceDeleted`) all return 404. A private conversation's existence is therefore not revealed. Viewer credentials are not cached, so revoking access takes effect on the viewer's next page load. +A refused credential (`UserNotAuthorized`), a missing space (`SpaceNotFound`), a deleted one (`SpaceDeleted`), or an owner whose DID document has no `#atproto_kem` key all return 404. An info record that fails its MAC returns 500. A private conversation's existence is therefore not revealed. Viewer credentials are not cached, so revoking access takes effect on the viewer's next page load. Attachments in a shared conversation are fetched the same way, with `getBlob` and the viewer's credential, through `GET /api/shared/:ownerDid/:skey/blobs/:cid`, with the same headers as the owner's blob route, so a viewer who loses access keeps only what their browser already cached.