~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
492 lines · 17.2 KB Raw History
/**
 * Lexicon for git repositories stored natively in an atproto repo.
 *
 * One record per git repository (rkey = repository name). The record holds
 * the ref list and an ordered chain of git bundles. Bundle bytes live in PDS
 * blob storage, chunked so each part stays under the PDS blob upload limit.
 * Every part is referenced from the record, which keeps the blobs out of
 * orphan cleanup; dropping a bundle from the record (repack) is how its
 * chunks get reaped.
 */

export const GIT_REPO_COLLECTION = 'dev.pdsjs.git.repo';

export const GIT_IDENTITY_COLLECTION = 'dev.pdsjs.git.identity';

export const GIT_SPACE_TYPE = 'dev.pdsjs.git.space';

export const gitSpaceLexicon = {
  lexicon: 1,
  id: GIT_SPACE_TYPE,
  defs: {
    main: {
      type: 'space',
      key: 'any',
      name: 'Git repository',
      collections: ['dev.pdsjs.git.repo', 'dev.pdsjs.git.config'],
    },
  },
};

/**
 * Lexicon for the git identities an account claims as its own.
 *
 * A commit records an author as a name and an email address. Neither is an
 * atproto identity and neither is signed, so nothing in a commit can prove
 * who wrote it. This record is the account's own statement about which
 * addresses belong to it, in its own repo under its own DID. A reader trusts
 * it exactly as far as it trusts that account, and nobody can write it for
 * somebody else.
 *
 * One record per account, rkey `self`, so the claim is made once rather than
 * once per repository.
 */
export const gitIdentityLexicon = {
  lexicon: 1,
  id: GIT_IDENTITY_COLLECTION,
  defs: {
    main: {
      type: 'record',
      description:
        'The git author identities an account claims as its own, for readers that want to show a commit under an atproto identity.',
      key: 'literal:self',
      record: {
        type: 'object',
        required: ['idents', 'createdAt'],
        properties: {
          idents: {
            type: 'array',
            description: 'Author identities this account claims.',
            items: { type: 'ref', ref: '#ident' },
          },
          createdAt: { type: 'string', format: 'datetime' },
          updatedAt: { type: 'string', format: 'datetime' },
        },
      },
    },
    ident: {
      type: 'object',
      description:
        'One claimed identity. An email alone matches any commit using that address. A name alone matches commits by that author name, which is weaker and meant for histories written without an address.',
      properties: {
        email: {
          type: 'string',
          maxLength: 512,
          description: 'Commit author email, compared case-insensitively.',
        },
        name: {
          type: 'string',
          maxLength: 512,
          description: 'Commit author name, compared exactly.',
        },
        label: {
          type: 'string',
          maxLength: 128,
          description:
            'What to call this identity where it has no atproto account to name it: a tool, a bot, or a person who keeps none.',
        },
        avatar: {
          type: 'blob',
          accept: ['image/*'],
          maxSize: 262144,
          description:
            'The face to show for a labelled identity. Ignored where a did names an account, whose own profile picture is the truer answer.',
        },
        did: {
          type: 'string',
          format: 'did',
          description:
            'Whose identity this is. Absent means the account holding the record, which is the claim an account makes about itself. Naming another DID vouches for somebody else, and is worth only as much as the reader trusts this account.',
        },
      },
    },
  },
};

export const GIT_ISSUE_COLLECTION = 'dev.pdsjs.git.issue';

/**
 * Lexicon for an issue against a repository, and everything said in its
 * thread.
 *
 * One collection carries the whole conversation: a record with a title opens
 * an issue, a record with `replyTo` answers one, and the repository owner's
 * record with a `disposition` is what a reader shows as the issue's state.
 * Nothing here is a mutable field on a shared object. GitHub's closed issue
 * is a permission exercised; this is a statement attributed, and the filer's
 * disagreement can sit beside it rather than reopening a war over one bit.
 *
 * Every record is in its author's own repo. The owner has no inbound
 * address, so the runner that already watches a firehose indexes what
 * strangers file, the same way it indexes pull requests.
 */
export const gitIssueLexicon = {
  lexicon: 1,
  id: GIT_ISSUE_COLLECTION,
  defs: {
    main: {
      type: 'record',
      description:
        "An issue against a repository, or a word in one's thread, in the author's own repo.",
      key: 'tid',
      record: {
        type: 'object',
        required: ['subject', 'createdAt'],
        properties: {
          subject: {
            type: 'ref',
            ref: '#subject',
            description: 'The repository this speaks about.',
          },
          title: {
            type: 'string',
            maxLength: 300,
            description:
              'The issue in one line. Present on the record that opens a thread; a reply carries none.',
          },
          body: { type: 'string', maxLength: 20000 },
          replyTo: {
            type: 'ref',
            ref: '#ref',
            description:
              'The issue or comment this answers, by its exact version.',
          },
          disposition: {
            type: 'string',
            knownValues: ['resolved', 'declined', 'reopened'],
            description:
              "A word on the whole thread. A reader shows the repository owner's latest as the issue's state; anybody else's is a comment with an opinion. Reopened is how the owner takes back either of the others.",
          },
          createdAt: { type: 'string', format: 'datetime' },
        },
      },
    },
    subject: {
      type: 'object',
      required: ['uri'],
      properties: {
        uri: { type: 'string', format: 'at-uri' },
        cid: { type: 'string', format: 'cid' },
      },
    },
    ref: {
      type: 'object',
      required: ['uri', 'cid'],
      properties: {
        uri: { type: 'string', format: 'at-uri' },
        cid: { type: 'string', format: 'cid' },
      },
    },
  },
};

export const GIT_PULL_COLLECTION = 'dev.pdsjs.git.pull';

/**
 * Lexicon for a pull request: work in one account's copy, offered to another
 * account's repository.
 *
 * A pull request is identified by the account that holds the work and the ref
 * that holds it, so the branch alone is a whole proposal and this record is
 * what the author says about it. The title, the body and the draft or
 * withdrawn word are the author's own and appear nowhere in the graph. A push
 * with no record still reads as a pull request, titled by its tip commit.
 *
 * The record is in the author's own repo, so opening one needs no write access
 * to the repository it is aimed at, and nothing about it obliges that
 * repository's owner.
 *
 * The owner has no inbound address, so the firehose carries this. A consumer
 * filtered to this one collection sees every pull request on the network for
 * the cost of a socket, which is what makes the index cheap enough to keep per
 * repository rather than for everyone at once.
 */
export const gitPullLexicon = {
  lexicon: 1,
  id: GIT_PULL_COLLECTION,
  defs: {
    main: {
      type: 'record',
      description:
        "A pull request against somebody else's repository, held in a branch of the requesting account's own copy.",
      key: 'tid',
      record: {
        type: 'object',
        required: ['subject', 'repo', 'ref', 'sha', 'createdAt'],
        properties: {
          subject: {
            type: 'ref',
            ref: '#subject',
            description:
              'The repository this is aimed at, and the version of its record the work was written against.',
          },
          repo: {
            type: 'string',
            maxLength: 512,
            description:
              "Record key of the dev.pdsjs.git.repo record holding the work, in the requesting account's own repo.",
          },
          ref: {
            type: 'string',
            maxLength: 512,
            description:
              'Full ref name of the branch holding the work. With the record owner it identifies the pull request.',
          },
          sha: {
            type: 'string',
            minLength: 40,
            maxLength: 64,
            description: 'The tip this record was written against.',
          },
          title: {
            type: 'string',
            maxLength: 512,
            description:
              "The author's name for the work. A reader without one falls back to the tip commit's subject.",
          },
          status: {
            type: 'string',
            knownValues: ['draft', 'withdrawn'],
            description:
              "The author's own word on the work. draft asks for eyes and not a merge; withdrawn asks for neither, and the record stays so the word is carried. Absent means open.",
          },
          note: { type: 'string', maxLength: 10000 },
          createdAt: { type: 'string', format: 'datetime' },
        },
      },
    },
    subject: {
      type: 'object',
      required: ['uri'],
      properties: {
        uri: { type: 'string', format: 'at-uri' },
        cid: {
          type: 'string',
          format: 'cid',
          description:
            'The repository record version the work was written against. Absent where the record names no version, which a reader treats as a pull request against whatever the repository holds.',
        },
      },
    },
  },
};

export const GIT_REVIEW_COLLECTION = 'dev.pdsjs.git.review';

/**
 * Lexicon for a review of one pull request to a git repository.
 *
 * The record is in the reviewer's own repo, so a review needs no write access
 * to the repository it is about and no account may write one for somebody
 * else. Nothing here gates a push: no PDS is the merge authority for a
 * repository copied across several servers, so a verdict is a signed
 * statement and the reader decides what it is worth. The repository's config
 * record names the reviewer DIDs it shows.
 *
 * `author` and `ref` name the pull request, and `sha` names the version of it
 * this review read. The pair survives every rewrite of the branch, so a review
 * of one version still reads against the work after a rebase, and the sha
 * still says which version it answered.
 */
export const gitReviewLexicon = {
  lexicon: 1,
  id: GIT_REVIEW_COLLECTION,
  defs: {
    main: {
      type: 'record',
      description:
        "One account's review of one pull request to a git repository stored as a dev.pdsjs.git.repo record.",
      key: 'tid',
      record: {
        type: 'object',
        required: ['subject', 'sha', 'verdict', 'reviewedAt'],
        properties: {
          subject: {
            type: 'ref',
            ref: '#subject',
            description:
              'The repository record version this review read. It names one version, so a review cannot be re-attributed to a later push.',
          },
          author: {
            type: 'string',
            format: 'did',
            description:
              'The account whose copy holds the work. With ref it names the pull request this review is about.',
          },
          ref: {
            type: 'string',
            maxLength: 512,
            description:
              "Full ref name of the branch holding the work, in that account's copy.",
          },
          sha: {
            type: 'string',
            minLength: 40,
            maxLength: 64,
            description:
              'The version reviewed: the tip the branch carried when this was written.',
          },
          verdict: {
            type: 'string',
            knownValues: ['approve', 'changesRequested', 'comment'],
            description:
              'comment carries no judgement. A reader that does not know a verdict treats it as comment.',
          },
          anchor: {
            type: 'ref',
            ref: '#anchor',
            description:
              'The line this speaks to, in the tree the sha names. Absent for a review of the whole pull request.',
          },
          replyTo: {
            type: 'ref',
            ref: '#subject',
            description:
              'The review this answers, by its exact version. The reply still names the pull request and carries its own anchor, so a reader that does not know this field shows it beside what it answers rather than losing it.',
          },
          note: { type: 'string', maxLength: 10000 },
          reviewedAt: { type: 'string', format: 'datetime' },
        },
      },
    },
    subject: {
      type: 'object',
      required: ['uri', 'cid'],
      properties: {
        uri: { type: 'string', format: 'at-uri' },
        cid: { type: 'string', format: 'cid' },
      },
    },
    anchor: {
      type: 'object',
      required: ['path'],
      properties: {
        path: { type: 'string', maxLength: 1024 },
        line: {
          type: 'integer',
          minimum: 1,
          description:
            'One-based, in the file the sha names, so placement on that version is exact whatever happens to the branch. The first line of a range, or the only line.',
        },
        endLine: {
          type: 'integer',
          minimum: 1,
          description:
            'The last line of a range, where the comment speaks to several. Absent for a comment on one line. A reader shows the thread at this line and marks the span from line to it.',
        },
        side: {
          type: 'string',
          knownValues: ['old', 'new'],
          description:
            "Which side of the change's diff counts the lines; new when absent. A range is on one side.",
        },
        snippet: {
          type: 'string',
          maxLength: 512,
          description:
            'The first line itself. A rebase moves numbers; the words are what a later version searches for to keep the comment placed.',
        },
        endSnippet: {
          type: 'string',
          maxLength: 512,
          description:
            'The last line of a range, for the same reason snippet keeps the first.',
        },
      },
    },
  },
};

export const gitRepoLexicon = {
  lexicon: 1,
  id: GIT_REPO_COLLECTION,
  defs: {
    main: {
      type: 'record',
      description:
        'A git repository: its refs plus an ordered chain of git bundles stored as chunked blobs. Replaying the bundle chain in order reproduces the full object graph.',
      key: 'any',
      record: {
        type: 'object',
        required: ['name', 'refs', 'bundles', 'createdAt'],
        properties: {
          name: {
            type: 'string',
            maxLength: 512,
            description: 'Repository name, mirrors the record key.',
          },
          description: { type: 'string', maxLength: 3000 },
          defaultBranch: {
            type: 'string',
            maxLength: 512,
            description: 'Branch name HEAD points at, e.g. main.',
          },
          upstream: {
            type: 'string',
            format: 'at-uri',
            description:
              "The repository record this copy was taken from, where the copy is somebody else's repository. A push into a fork writes it, so the push is what makes the fork findable.",
          },
          refs: {
            type: 'array',
            items: { type: 'ref', ref: '#ref' },
          },
          bundles: {
            type: 'array',
            description:
              'Bundle chain, oldest first. Later bundles may have prerequisites satisfied by earlier ones.',
            items: { type: 'ref', ref: '#bundle' },
          },
          createdAt: { type: 'string', format: 'datetime' },
          updatedAt: { type: 'string', format: 'datetime' },
        },
      },
    },
    ref: {
      type: 'object',
      required: ['name', 'sha'],
      properties: {
        name: {
          type: 'string',
          maxLength: 512,
          description: 'Full ref name, e.g. refs/heads/main.',
        },
        sha: {
          type: 'string',
          minLength: 40,
          maxLength: 64,
          description: 'Object id the ref points at (SHA-1 or SHA-256 hex).',
        },
      },
    },
    bundle: {
      type: 'object',
      required: ['heads', 'parts'],
      properties: {
        heads: {
          type: 'array',
          description:
            'Tips contained in this bundle. A client that already has all heads can skip downloading it.',
          items: { type: 'string', maxLength: 64 },
        },
        parts: {
          type: 'array',
          description:
            'Bundle bytes split into blob-sized chunks; concatenate in order to recover the bundle file.',
          items: { type: 'blob' },
        },
        index: {
          type: 'blob',
          description:
            'Object index over this bundle, listing every object id with its byte extent, type, size and delta base. Derived from the bundle bytes; a reader holding it can fetch single objects with ranged blob reads.',
        },
        indexParts: {
          type: 'array',
          description:
            'The same index, split into blob-sized chunks for a repository whose index outgrows one blob; concatenate in order to recover it. A bundle carries this or index, not both.',
          items: { type: 'blob' },
        },
      },
    },
  },
};