atjamalpha

API reference

Reference. Every public export of @atjam/lexicons, with exact signatures. For the wire-level field reference (max lengths, formats), see the JSON schemas — this page documents the TypeScript surface.

Everything below is exported from the package root:

import {
  Jam, Round, Signup, Invitation, Submission,
  Queries, Validate, Eligibility,
  NSIDS, type StrongRef,
} from "@atjam/lexicons";

Jam, Round, Signup, Invitation, Submission, Queries, Validate, and Eligibility are namespaces (export * as …). StrongRef and NSIDS are re-exported directly.

The blocks below show each export's shape as accessed through its namespace — e.g. the Jam.Jam type, the Round.deriveState function. They're reference notation, not literal declarations you can paste (you can't declare interface Jam.Jam). For copy-ready imports, see Getting started.


NSIDS

A frozen map of the five collection NSIDs.

const NSIDS: {
  jam: "at.atjam.jam";
  round: "at.atjam.round";
  signup: "at.atjam.signup";
  invitation: "at.atjam.invitation";
  submission: "at.atjam.submission";
}

StrongRef

interface StrongRef {
  uri: string;
  cid: string;
}

A content-addressed reference to a record. Used wherever one record points at another (round.jam, signup.round, signup.invitation, invitation.round, submission.round, submission.payload).


Jam

Jam.NSID; // "at.atjam.jam"

interface Jam.JamLink {
  label?: string;
  url: string;
}

interface Jam.Jam {
  $type?: "at.atjam.jam";
  name: string;
  description?: string;
  kind?: string;
  links?: Jam.JamLink[];
  createdAt: string;
}

// Type guard: true if v has string `name` and `createdAt`.
function Jam.isJam(v: unknown): v is Jam.Jam;

Round

Round.NSID; // "at.atjam.round"

interface Round.Milestone {
  label: string;
  date: string;
}

type Round.JoinMode = "open" | "hosted" | "network";
type Round.NetworkGate = "signup" | "contributed";
type Round.RoundState = "open" | "in-progress" | "closed";

interface Round.Round {
  $type?: "at.atjam.round";
  jam: StrongRef;
  name?: string;
  assignment: string;
  subject?: unknown;
  acceptedSubmissionTypes: string[];
  milestones: Round.Milestone[];
  closingEvent?: unknown;
  joinMode?: Round.JoinMode | string;
  networkGate?: Round.NetworkGate | string;
  createdAt: string;
}

// Resolve joinMode for consumer logic; unknown/absent → "open".
function Round.getJoinMode(round: Round.Round): Round.JoinMode;

// Resolve networkGate (only meaningful for "network"); unknown/absent → "signup".
function Round.getNetworkGate(round: Round.Round): Round.NetworkGate;

// True for "hosted" and "network"; false for "open" and unknown values.
function Round.requiresInvitation(round: Round.Round): boolean;

// Type guard: checks `assignment`, `acceptedSubmissionTypes`, `milestones`, `createdAt`.
function Round.isRound(v: unknown): v is Round.Round;

// Derive state from milestones. "closed" if past submission-deadline; else
// "in-progress" if past signup-deadline; else "open". Missing deadlines bias
// toward "open". Boundary comparisons use >= (the deadline moment counts as past).
function Round.deriveState(round: Round.Round, now?: Date): Round.RoundState;

interface Round.RoundPhase {
  index: number;     // 0 before the first milestone; milestones.length after the last
  since?: Round.Milestone;  // milestone that opened the current phase
  until?: Round.Milestone;  // next upcoming milestone
}

// Where now falls among the (date-sorted) milestones — a fine-grained companion
// to deriveState for rounds using custom milestone labels as phases.
function Round.deriveCurrentPhase(round: Round.Round, now?: Date): Round.RoundPhase;

Signup

Signup.NSID; // "at.atjam.signup"

interface Signup.Signup {
  $type?: "at.atjam.signup";
  round: StrongRef;
  invitation?: StrongRef; // required by readers for hosted/network rounds
  note?: string;
  createdAt: string;
}

Invitation

Invitation.NSID; // "at.atjam.invitation"

interface Invitation.Invitation {
  $type?: "at.atjam.invitation";
  round: StrongRef;
  invitee: string; // a DID
  note?: string;
  createdAt: string;
}

// Type guard: checks `round` (object), `invitee` (string), `createdAt` (string).
function Invitation.isInvitation(v: unknown): v is Invitation.Invitation;

Submission

Submission.NSID; // "at.atjam.submission"

interface Submission.Submission {
  $type?: "at.atjam.submission";
  round: StrongRef;
  payload?: StrongRef; // preferred deliverable: a record on its native app
  url?: string;        // fallback deliverable: a plain link (prefer payload)
  note?: string;
  createdAt: string;
}

// The deliverable normalized to one form — the single gate for "exactly one of".
type Submission.Deliverable =
  | { kind: "record"; ref: StrongRef }
  | { kind: "url"; url: string };

// Returns the record (preferred), else the url, else null (malformed).
function Submission.getDeliverable(s: Submission.Submission): Submission.Deliverable | null;

// Whether a submission carries any deliverable (the "at least one" rule).
function Submission.hasDeliverable(s: Submission.Submission): boolean;

Queries

The read layer. Re-exports the contents of at-uri, did, pds, constellation, and fetchers.

AT URI utilities (no network)

interface Queries.ParsedAtUri {
  did: string;
  collection: string;
  rkey: string;
}

// Parse "at://<did>/<collection>/<rkey>". Throws on a non-at:// or malformed URI.
function Queries.parseAtUri(uri: string): Queries.ParsedAtUri;

// Reassemble an AT URI from its parts.
function Queries.buildAtUri(parts: Queries.ParsedAtUri): string;

Identity and people

A handle is only reported if it checks out both ways: the DID document claims it and it resolves back to that DID. Otherwise it reads handle.invalid (Queries.INVALID_HANDLE) — render that as a warning, never as an address. Lookups go to Slingshot first and fall back to checking locally; results are cached for an hour (five minutes for failures).

interface Queries.Identity {
  did: string;
  handle: string; // verified, or "handle.invalid"
  pds: string;
}

interface Queries.Person {
  handle: string; // verified, or "handle.invalid"
  displayName?: string;
  avatar?: string; // small thumbnail URL
}
type Queries.PeopleMap = Record<string, Queries.Person>;

// A handle (with or without "@", any case) or a DID → verified identity.
function Queries.resolveIdentity(identifier: string): Promise<Queries.Identity>;

// Many DIDs → how to show each person: batched profiles (25 per call) for
// names and avatars, identity resolution for anyone without a profile.
// DIDs that can't be resolved at all are absent — fall back to the DID.
function Queries.resolvePeople(dids: Iterable<string>): Promise<Queries.PeopleMap>;

// DID → PDS endpoint (and verified handle, or undefined).
function Queries.resolveDid(did: string): Promise<{ pds: string; handle?: string }>;

function Queries.normalizeHandle(input: string): string; // trim, drop "@", lowercase
function Queries.isDid(s: string): boolean;

// Defaults: plc.directory and slingshot.microcosm.blue. No-op on empty.
function Queries.setPlcDirectoryUrl(url: string): void;
function Queries.setIdentityServiceUrl(url: string): void;

Raw PDS reads (unauthenticated)

interface Queries.PdsRecord<T = unknown> {
  uri: string;
  cid: string;
  value: T;
}

interface Queries.ListRecordsResponse<T = unknown> {
  records: Queries.PdsRecord<T>[];
  cursor?: string;
}

// com.atproto.repo.listRecords against the DID's PDS.
function Queries.listRecords<T = unknown>(args: {
  did: string;
  collection: string;
  limit?: number;
  cursor?: string;
  reverse?: boolean;
}): Promise<Queries.ListRecordsResponse<T>>;

// Every record in the collection, following cursors to the end.
function Queries.listAllRecords<T = unknown>(args: {
  did: string;
  collection: string;
  reverse?: boolean;
}): Promise<Queries.PdsRecord<T>[]>;

// com.atproto.repo.getRecord against the DID's PDS.
function Queries.getRecord<T = unknown>(args: {
  did: string;
  collection: string;
  rkey: string;
}): Promise<Queries.PdsRecord<T>>;
interface Queries.BacklinkRef {
  uri: string;
  cid?: string;
}

interface Queries.BacklinksResponse {
  links: Queries.BacklinkRef[];
  cursor?: string;
  total?: number;
}

// Configure the Constellation URL (default "https://constellation.microcosm.blue"). No-op on empty.
function Queries.setConstellationUrl(url: string): void;
function Queries.getConstellationUrl(): string;

// Given a target record, return records that link to it at the given JSON path.
function Queries.getBacklinks(args: {
  target: string;
  collection: string;
  path: string;
  limit?: number;
  cursor?: string;
}): Promise<Queries.BacklinksResponse>;

// Every backlink for the target, following cursors to the end.
function Queries.getAllBacklinks(args: {
  target: string;
  collection: string;
  path: string;
}): Promise<Queries.BacklinkRef[]>;

A single page is a sample, not an answer. Use the All variants wherever a short page would make a count or a membership check silently wrong.

Every link between atjam records is a strong-ref inside the child record, so Constellation indexes it by the child's collection and the JSON path to the ref's uri. These are the collection:path pairs atjam itself queries — and the ones anyone can use to ask what points at an atjam record:

CollectionPathTargetAnswers
at.atjam.round.jam.uria jamthe rounds of a jam
at.atjam.signup.round.uria roundwho signed up
at.atjam.submission.round.uria roundwhat was submitted
at.atjam.invitation.round.uria roundwho was invited
const signups = await Queries.getAllBacklinks({
  target: roundUri, // at://did/at.atjam.round/rkey
  collection: "at.atjam.signup",
  path: ".round.uri",
});
// → [{ uri: "at://<signer>/at.atjam.signup/<rkey>", cid? }, …]

The composed fetchers below are these four queries plus a getRecord for each result.

Composed atjam fetchers

// Direct reads.
function Queries.fetchRound(did: string, rkey: string): Promise<Queries.PdsRecord<Round.Round>>;
function Queries.fetchJam(did: string, rkey: string): Promise<Queries.PdsRecord<Jam.Jam>>;
function Queries.fetchJamByUri(jamUri: string): Promise<Queries.PdsRecord<Jam.Jam>>;

// Rounds under a jam (backlinks + fetch each body; unreadable rounds dropped).
function Queries.fetchRoundsForJam(jamUri: string): Promise<Queries.PdsRecord<Round.Round>[]>;

// A round's signups / submissions / invitations — every page, bodies fetched (unreadable dropped).
function Queries.fetchSignupsForRound(roundUri: string): Promise<Queries.PdsRecord<Signup.Signup>[]>;
function Queries.fetchSubmissionsForRound(roundUri: string): Promise<Queries.PdsRecord<Submission.Submission>[]>;
function Queries.fetchInvitationsForRound(roundUri: string): Promise<Queries.PdsRecord<Invitation.Invitation>[]>;

// Cross-organizer feed: list each DID's rounds, dedupe, sort by createdAt desc.
function Queries.fetchHomeFeed(dids: string[]): Promise<Queries.PdsRecord<Round.Round>[]>;

// A DID's own jams (newest first).
function Queries.fetchMyJams(did: string): Promise<Queries.PdsRecord<Jam.Jam>[]>;

Validate

Read-only signup validation. See the validation contract.

interface Validate.ValidationResult {
  valid: boolean;
  reason?: string;
}

// Fetchers the validator needs; defaults wrap Queries. Tests inject stubs.
interface Validate.ValidateFetchers {
  getInvitation(uri: string): Promise<Invitation.Invitation | null>;
  getSignupForRound(args: { did: string; roundUri: string }): Promise<{ uri: string; value: Signup.Signup } | null>;
  hasSubmissionForRound(args: { did: string; roundUri: string }): Promise<boolean>;
}

interface Validate.ValidateSignupArgs {
  signup: { uri: string; value: Signup.Signup };
  round: { uri: string; value: Round.Round };
  fetchers?: Partial<Validate.ValidateFetchers>;
}

function Validate.validateSignup(args: Validate.ValidateSignupArgs): Promise<Validate.ValidationResult>;

Eligibility

Pure predicates (no I/O) for prospective "may this DID invite?" questions.

interface Eligibility.InviterContext {
  round: Round.Round;
  organizerDid: string;
  validSignerDids: readonly string[];
  submitterDids: readonly string[];
}

// Is inviterDid a valid inviter for the round?
//   organizer → always; hosted → only organizer; network → valid signer
//   (and, under "contributed", must also be a submitter).
function Eligibility.isValidInviter(ctx: Eligibility.InviterContext, inviterDid: string): boolean;

// May viewerDid issue invitations? Open rounds → always false; else isValidInviter.
function Eligibility.canInvite(ctx: Eligibility.InviterContext, viewerDid: string): boolean;

// The invitation that would make viewerDid's signup valid: names the viewer
// as invitee AND comes from a valid inviter. Generic over the record shape.
function Eligibility.findUsableInvitation<
  I extends { uri: string; value: { invitee: string } },
>(
  ctx: Eligibility.InviterContext,
  viewerDid: string,
  invitations: readonly I[],
): I | undefined;

Not exported: writes

@atjam/lexicons provides no write helpers. Writing records is the application's job, via com.atproto.repo.createRecord with your own ATProto client. See Reading, writing, validating.