Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Rename httpd-client to http-client
Sebastian Martinez committed 1 year ago
commit 2ee8bae82efafa6db5ffd50815543600d896a7b6
parent f87c09c634818030224e5d34ed51d7cb1b11069b
107 files changed +2297 -2309
added .github/workflows/check-http-client-unit-test.yml
@@ -0,0 +1,21 @@
+
name: check-http-client-unit-test
+
on: push
+

+
jobs:
+
  check-http-client-unit-test:
+
    runs-on: ubuntu-latest
+
    steps:
+
      - name: Setup Node
+
        uses: actions/setup-node@v4
+
        with:
+
          node-version: "20.9.0"
+
      - name: Checkout
+
        uses: actions/checkout@v4
+
      - run: npm ci
+
      - name: Install Radicle binaries
+
        run: |
+
          mkdir -p tests/artifacts;
+
          ./scripts/install-binaries;
+
          ./scripts/install-binaries --show-path >> $GITHUB_PATH;
+
      - run: |
+
          npm run test:http-client:unit
deleted .github/workflows/check-httpd-api-unit-test.yml
@@ -1,21 +0,0 @@
-
name: check-httpd-api-unit-test
-
on: push
-

-
jobs:
-
  check-httpd-api-unit-test:
-
    runs-on: ubuntu-latest
-
    steps:
-
      - name: Setup Node
-
        uses: actions/setup-node@v4
-
        with:
-
          node-version: "20.9.0"
-
      - name: Checkout
-
        uses: actions/checkout@v4
-
      - run: npm ci
-
      - name: Install Radicle binaries
-
        run: |
-
          mkdir -p tests/artifacts;
-
          ./scripts/install-binaries;
-
          ./scripts/install-binaries --show-path >> $GITHUB_PATH;
-
      - run: |
-
          npm run test:httpd-api:unit
modified flake.nix
@@ -93,7 +93,7 @@
            scripts/check
            {
              npm run test:unit
-
              npm run test:httpd-api:unit
+
              npm run test:http-client:unit
            } | tee /dev/null
            runHook postCheck
          '';
added http-client/index.ts
@@ -0,0 +1,248 @@
+
import type { BaseUrl } from "./lib/fetcher.js";
+
import type {
+
  Blob,
+
  DiffResponse,
+
  Project,
+
  ProjectListQuery,
+
  Remote,
+
  Tree,
+
  TreeStats,
+
} from "./lib/project.js";
+
import type {
+
  SuccessResponse,
+
  CodeLocation,
+
  Range,
+
  Policy,
+
  Scope,
+
} from "./lib/shared.js";
+
import type { Comment, Embed, Reaction } from "./lib/project/comment.js";
+
import type {
+
  Commit,
+
  CommitBlob,
+
  CommitHeader,
+
  ChangesetWithDiff,
+
  ChangesetWithoutDiff,
+
  Diff,
+
  DiffBlob,
+
  DiffContent,
+
  DiffFile,
+
  HunkLine,
+
} from "./lib/project/commit.js";
+
import type { Issue, IssueState } from "./lib/project/issue.js";
+
import type {
+
  LifecycleState,
+
  Merge,
+
  Patch,
+
  PatchState,
+
  PatchUpdateAction,
+
  Review,
+
  Revision,
+
  Verdict,
+
} from "./lib/project/patch.js";
+
import type { RequestOptions } from "./lib/fetcher.js";
+
import type { ZodSchema } from "zod";
+

+
import { z, array, literal, number, object, string, union } from "zod";
+

+
import * as project from "./lib/project.js";
+
import * as profile from "./lib/profile.js";
+
import * as session from "./lib/session.js";
+
import { Fetcher } from "./lib/fetcher.js";
+
import { nodeConfigSchema, successResponseSchema } from "./lib/shared.js";
+

+
export type {
+
  BaseUrl,
+
  Blob,
+
  ChangesetWithDiff,
+
  ChangesetWithoutDiff,
+
  CodeLocation,
+
  Comment,
+
  Commit,
+
  CommitBlob,
+
  CommitHeader,
+
  Diff,
+
  DiffBlob,
+
  DiffContent,
+
  DiffFile,
+
  DiffResponse,
+
  Embed,
+
  HunkLine,
+
  Issue,
+
  IssueState,
+
  LifecycleState,
+
  Merge,
+
  Patch,
+
  PatchState,
+
  PatchUpdateAction,
+
  Policy,
+
  Project,
+
  ProjectListQuery,
+
  Range,
+
  Reaction,
+
  Remote,
+
  Review,
+
  Revision,
+
  Scope,
+
  TreeStats,
+
  Tree,
+
  Verdict,
+
};
+

+
export type Node = z.infer<typeof nodeSchema>;
+

+
const nodeSchema = object({
+
  id: string(),
+
  version: string(),
+
  config: nodeConfigSchema.nullable(),
+
  state: union([literal("running"), literal("stopped")]),
+
});
+

+
export type NodeInfo = z.infer<typeof nodeInfoSchema>;
+

+
const nodeInfoSchema = object({
+
  message: string(),
+
  service: string(),
+
  version: string(),
+
  apiVersion: string(),
+
  nid: string(),
+
  path: string(),
+
  links: array(
+
    object({
+
      href: string(),
+
      rel: string(),
+
      type: union([
+
        literal("GET"),
+
        literal("POST"),
+
        literal("PUT"),
+
        literal("DELETE"),
+
      ]),
+
    }),
+
  ),
+
});
+

+
export type NodeTracking = z.infer<typeof nodeTrackingSchema>;
+

+
const nodeTrackingSchema = array(
+
  object({
+
    id: string(),
+
    scope: string(),
+
    policy: string(),
+
  }),
+
);
+

+
export interface NodeStats {
+
  repos: { total: number };
+
}
+

+
const nodeStatsSchema = object({
+
  repos: object({ total: number() }),
+
}) satisfies ZodSchema<NodeStats>;
+

+
export class HttpdClient {
+
  #fetcher: Fetcher;
+

+
  public baseUrl: BaseUrl;
+
  public project: project.Client;
+
  public profile: profile.Client;
+
  public session: session.Client;
+

+
  public constructor(baseUrl: BaseUrl) {
+
    this.baseUrl = baseUrl;
+
    this.#fetcher = new Fetcher(this.baseUrl);
+

+
    this.project = new project.Client(this.#fetcher);
+
    this.profile = new profile.Client(this.#fetcher);
+
    this.session = new session.Client(this.#fetcher);
+
  }
+

+
  public changePort(port: number): void {
+
    this.baseUrl.port = port;
+
  }
+

+
  public get url(): string {
+
    return `${this.baseUrl.scheme}://${this.baseUrl.hostname}:${this.baseUrl.port}`;
+
  }
+

+
  public get hostname(): string {
+
    return this.baseUrl.hostname;
+
  }
+

+
  public get port(): string {
+
    return this.baseUrl.port.toString();
+
  }
+

+
  public async getNodeInfo(options?: RequestOptions): Promise<NodeInfo> {
+
    return this.#fetcher.fetchOk(
+
      {
+
        method: "GET",
+
        options,
+
      },
+
      nodeInfoSchema,
+
    );
+
  }
+

+
  public async getStats(options?: RequestOptions): Promise<NodeStats> {
+
    return this.#fetcher.fetchOk(
+
      {
+
        method: "GET",
+
        path: "stats",
+
        options,
+
      },
+
      nodeStatsSchema,
+
    );
+
  }
+

+
  public async getTracking(options?: RequestOptions): Promise<NodeTracking> {
+
    return this.#fetcher.fetchOk(
+
      {
+
        method: "GET",
+
        path: "node/policies/repos",
+
        options,
+
      },
+
      nodeTrackingSchema,
+
    );
+
  }
+

+
  public async seedById(
+
    id: string,
+
    authToken: string,
+
    options?: RequestOptions,
+
  ): Promise<SuccessResponse> {
+
    return this.#fetcher.fetchOk(
+
      {
+
        method: "PUT",
+
        path: `node/policies/repos/${id}`,
+
        headers: { Authorization: `Bearer ${authToken}` },
+
        options,
+
      },
+
      successResponseSchema,
+
    );
+
  }
+

+
  public async stopSeedingById(
+
    id: string,
+
    authToken: string,
+
    options?: RequestOptions,
+
  ): Promise<SuccessResponse> {
+
    return this.#fetcher.fetchOk(
+
      {
+
        method: "DELETE",
+
        path: `node/policies/repos/${id}`,
+
        headers: { Authorization: `Bearer ${authToken}` },
+
        options,
+
      },
+
      successResponseSchema,
+
    );
+
  }
+

+
  public async getNode(options?: RequestOptions): Promise<Node> {
+
    return this.#fetcher.fetchOk(
+
      {
+
        method: "GET",
+
        path: "node",
+
        options,
+
      },
+
      nodeSchema,
+
    );
+
  }
+
}
added http-client/lib/fetcher.ts
@@ -0,0 +1,182 @@
+
// This module provides low-level capabilities to interact with a typed
+
// JSON HTTP API.
+

+
import type { ZodIssue, ZodType, TypeOf } from "zod";
+

+
import config from "virtual:config";
+
import { compare } from "compare-versions";
+

+
export interface BaseUrl {
+
  hostname: string;
+
  port: number;
+
  scheme: string;
+
}
+

+
// Error that is thrown by `Fetcher` methods.
+
export class ResponseError extends Error {
+
  public method: string;
+
  public url: string;
+
  public status: number;
+
  public body: unknown;
+

+
  public constructor(method: string, response: Response, body_: unknown) {
+
    const body: unknown = body_;
+
    if (
+
      typeof body === "object" &&
+
      body !== null &&
+
      "message" in body &&
+
      typeof body.message === "string"
+
    ) {
+
      super(body.message);
+
    } else {
+
      super("Response error");
+
    }
+

+
    this.method = method;
+
    this.body = body_;
+
    this.status = response.status;
+
    this.url = response.url;
+
  }
+
}
+

+
// Error that is thrown by `Fetcher` methods when parsing the response
+
// body fails.
+
export class ResponseParseError extends Error {
+
  public method: string;
+
  public body: unknown;
+
  public description: string;
+
  public apiVersion: string | undefined;
+
  public zodIssues: ZodIssue[];
+
  public path?: string;
+

+
  public constructor(
+
    method: string,
+
    body: unknown,
+
    apiVersion: string | undefined,
+
    zodIssues: ZodIssue[],
+
    path?: string,
+
  ) {
+
    super("Failed to parse response body");
+

+
    let description: string;
+
    if (
+
      apiVersion === undefined ||
+
      compare(apiVersion, config.nodes.apiVersion, "<")
+
    ) {
+
      description = `The node you are fetching from seems to be outdated, make sure the httpd API version is at least ${config.nodes.apiVersion} currently ${apiVersion ?? "unknown"}.`;
+
    } else if (
+
      config.nodes.apiVersion === undefined ||
+
      compare(apiVersion, config.nodes.apiVersion, ">")
+
    ) {
+
      description = `The web client you are using is outdated, make sure it supports at least ${apiVersion} to interact with this node currently ${config.nodes.apiVersion ?? "unknown"}.`;
+
    } else {
+
      description =
+
        "This is usually due to a version mismatch between the seed and the web interface.";
+
    }
+
    this.apiVersion = apiVersion;
+
    this.description =
+
      "The response received from the seed does not match the expected schema.<br/>".concat(
+
        description,
+
      );
+

+
    this.method = method;
+
    this.path = path;
+
    this.body = body;
+
    this.zodIssues = zodIssues;
+
  }
+
}
+

+
export interface RequestOptions {
+
  abort?: AbortSignal;
+
}
+

+
export interface FetchParams {
+
  method: Method;
+
  // Path to append to the `Fetcher`s base URL to get the final URL.
+
  path?: string;
+
  // Object that is serialized into JSON and sent as the data.
+
  body?: unknown;
+
  // Query parameters to be serialized with URLSearchParams.
+
  query?: Record<string, string | number | boolean>;
+
  options?: RequestOptions;
+
  headers?: Record<string, string>;
+
}
+

+
type Method = "DELETE" | "GET" | "PATCH" | "POST" | "PUT";
+

+
export class Fetcher {
+
  #baseUrl: BaseUrl;
+

+
  public constructor(baseUrl: BaseUrl) {
+
    this.#baseUrl = baseUrl;
+
  }
+

+
  // Execute a fetch and parse the result with the provided schema.
+
  // Return the parsed payload.
+
  //
+
  // Throws `ResponseError` if the response status code is not `200`.
+
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+
  public async fetchOk<T extends ZodType<any>>(
+
    params: FetchParams,
+
    schema: T,
+
  ): Promise<TypeOf<T>> {
+
    const response = await this.fetch({
+
      ...params,
+
      query: { ...params.query, v: config.nodes.apiVersion },
+
    });
+

+
    if (!response.ok) {
+
      let responseBody = await response.text();
+
      try {
+
        responseBody = JSON.parse(responseBody);
+
      } catch (_e: unknown) {
+
        // We keep the original text response body.
+
      }
+
      throw new ResponseError(params.method, response, responseBody);
+
    }
+

+
    const responseBody = await response.json();
+
    const result = schema.safeParse(responseBody);
+
    if (result.success) {
+
      return result.data;
+
    } else {
+
      const response = await this.fetch({ method: "GET" });
+
      const info = await response.json();
+
      throw new ResponseParseError(
+
        params.method,
+
        responseBody,
+
        info.apiVersion,
+
        result.error.errors,
+
        params.path,
+
      );
+
    }
+
  }
+

+
  private async fetch({
+
    method,
+
    path,
+
    body,
+
    options = {},
+
    query,
+
    headers = {},
+
  }: FetchParams): Promise<Response> {
+
    if (body !== undefined && headers["content-type"] === undefined) {
+
      headers["content-type"] = "application/json";
+
    }
+

+
    const pathSegment = path === undefined ? "" : `/${path}`;
+

+
    let url = `${this.#baseUrl.scheme}://${this.#baseUrl.hostname}:${this.#baseUrl.port}/api/v1${pathSegment}`;
+

+
    if (query) {
+
      const searchparams = new URLSearchParams(query as Record<string, string>);
+
      url = `${url}?${searchparams.toString()}`;
+
    }
+
    return globalThis.fetch(url, {
+
      method,
+
      headers,
+
      body: body === undefined ? undefined : JSON.stringify(body),
+
      signal: options.abort,
+
    });
+
  }
+
}
added http-client/lib/profile.ts
@@ -0,0 +1,36 @@
+
import type { Fetcher, RequestOptions } from "./fetcher.js";
+
import type { z } from "zod";
+

+
import { array, boolean, object, string } from "zod";
+
import { nodeConfigSchema } from "./shared.js";
+

+
const profileSchema = object({
+
  config: object({
+
    publicExplorer: string(),
+
    preferredSeeds: array(string()),
+
    cli: object({ hints: boolean() }),
+
    node: nodeConfigSchema,
+
  }),
+
  home: string(),
+
});
+

+
export type Profile = z.infer<typeof profileSchema>;
+

+
export class Client {
+
  #fetcher: Fetcher;
+

+
  public constructor(fetcher: Fetcher) {
+
    this.#fetcher = fetcher;
+
  }
+

+
  public async getProfile(options?: RequestOptions): Promise<Profile> {
+
    return this.#fetcher.fetchOk(
+
      {
+
        method: "GET",
+
        path: `profile`,
+
        options,
+
      },
+
      profileSchema,
+
    );
+
  }
+
}
added http-client/lib/project.ts
@@ -0,0 +1,503 @@
+
import type { Commit, Commits } from "./project/commit.js";
+
import type { Embed } from "./project/comment.js";
+
import type { Fetcher, RequestOptions } from "./fetcher.js";
+
import type {
+
  Issue,
+
  IssueCreated,
+
  IssueUpdateAction,
+
} from "./project/issue.js";
+
import type {
+
  Patch,
+
  PatchCreate,
+
  PatchCreated,
+
  PatchUpdateAction,
+
} from "./project/patch.js";
+
import type { SuccessResponse } from "./shared.js";
+
import type { ZodSchema } from "zod";
+

+
import { successResponseSchema } from "./shared.js";
+
import {
+
  array,
+
  boolean,
+
  literal,
+
  number,
+
  optional,
+
  record,
+
  object,
+
  string,
+
  union,
+
  z,
+
} from "zod";
+

+
import {
+
  diffBlobSchema,
+
  commitHeaderSchema,
+
  commitSchema,
+
  commitsSchema,
+
  diffSchema,
+
} from "./project/commit.js";
+

+
import {
+
  issueCreatedSchema,
+
  issueSchema,
+
  issuesSchema,
+
} from "./project/issue.js";
+

+
import {
+
  patchSchema,
+
  patchesSchema,
+
  patchCreatedSchema,
+
} from "./project/patch.js";
+

+
const projectSchema = object({
+
  id: string(),
+
  name: string(),
+
  description: string(),
+
  defaultBranch: string(),
+
  delegates: array(object({ id: string(), alias: optional(string()) })),
+
  head: string(),
+
  threshold: number(),
+
  visibility: union([
+
    object({ type: literal("public") }),
+
    object({ type: literal("private"), allow: optional(array(string())) }),
+
  ]).optional(),
+
  patches: object({
+
    open: number(),
+
    draft: number(),
+
    archived: number(),
+
    merged: number(),
+
  }),
+
  issues: object({
+
    open: number(),
+
    closed: number(),
+
  }),
+
  seeding: number(),
+
});
+
const projectsSchema = array(projectSchema);
+

+
export type Project = z.infer<typeof projectSchema>;
+

+
const activitySchema = object({
+
  activity: array(number()),
+
});
+

+
export type Activity = z.infer<typeof activitySchema>;
+

+
const blobSchema = object({
+
  binary: boolean(),
+
  content: optional(string()),
+
  name: string(),
+
  path: string(),
+
  lastCommit: commitHeaderSchema,
+
});
+

+
export type Blob = z.infer<typeof blobSchema>;
+

+
const treeEntrySchema = object({
+
  path: string(),
+
  name: string(),
+
  oid: string(),
+
  kind: union([literal("blob"), literal("tree"), literal("submodule")]),
+
});
+

+
export type TreeEntry = z.infer<typeof treeEntrySchema>;
+

+
const treeStatsSchema = object({
+
  commits: number(),
+
  branches: number(),
+
  contributors: number(),
+
});
+

+
export type TreeStats = z.infer<typeof treeStatsSchema>;
+

+
export type Tree = z.infer<typeof treeSchema>;
+

+
const treeSchema = object({
+
  entries: array(treeEntrySchema),
+
  lastCommit: commitHeaderSchema,
+
  name: string(),
+
  path: string(),
+
});
+

+
export type Remote = z.infer<typeof remoteSchema>;
+

+
const remoteSchema = object({
+
  id: string(),
+
  alias: string().optional(),
+
  heads: record(string(), string()),
+
  delegate: boolean(),
+
});
+

+
const remotesSchema = array(remoteSchema) satisfies ZodSchema<Remote[]>;
+

+
export type DiffResponse = z.infer<typeof diffResponseSchema>;
+

+
const diffResponseSchema = object({
+
  commits: array(commitHeaderSchema),
+
  diff: diffSchema,
+
  files: record(string(), diffBlobSchema),
+
});
+

+
export type ProjectListQuery = {
+
  page?: number;
+
  perPage?: number;
+
  show?: "pinned" | "all";
+
};
+
export class Client {
+
  #fetcher: Fetcher;
+

+
  public constructor(fetcher: Fetcher) {
+
    this.#fetcher = fetcher;
+
  }
+

+
  public async getByDelegate(
+
    delegateId: string,
+
    options?: RequestOptions,
+
  ): Promise<Project[]> {
+
    return this.#fetcher.fetchOk(
+
      {
+
        method: "GET",
+
        path: `delegates/${delegateId}/projects`,
+
        options,
+
      },
+
      projectsSchema,
+
    );
+
  }
+

+
  public async getById(id: string, options?: RequestOptions): Promise<Project> {
+
    return this.#fetcher.fetchOk(
+
      {
+
        method: "GET",
+
        path: `projects/${id}`,
+
        options,
+
      },
+
      projectSchema,
+
    );
+
  }
+

+
  public async getAll(
+
    query?: ProjectListQuery,
+
    options?: RequestOptions,
+
  ): Promise<Project[]> {
+
    return this.#fetcher.fetchOk(
+
      {
+
        method: "GET",
+
        path: "projects",
+
        query,
+
        options,
+
      },
+
      projectsSchema,
+
    );
+
  }
+

+
  public async getActivity(
+
    id: string,
+
    options?: RequestOptions,
+
  ): Promise<Activity> {
+
    return this.#fetcher.fetchOk(
+
      {
+
        method: "GET",
+
        path: `projects/${id}/activity`,
+
        options,
+
      },
+
      activitySchema,
+
    );
+
  }
+

+
  public async getReadme(
+
    id: string,
+
    sha: string,
+
    options?: RequestOptions,
+
  ): Promise<Blob> {
+
    return this.#fetcher.fetchOk(
+
      {
+
        method: "GET",
+
        path: `projects/${id}/readme/${sha}`,
+
        options,
+
      },
+
      blobSchema,
+
    );
+
  }
+

+
  public async getBlob(
+
    id: string,
+
    sha: string,
+
    path: string,
+
    options?: RequestOptions,
+
  ): Promise<Blob> {
+
    const blob = await this.#fetcher.fetchOk(
+
      {
+
        method: "GET",
+
        path: `projects/${id}/blob/${sha}/${path}`,
+
        options,
+
      },
+
      blobSchema,
+
    );
+
    return blob;
+
  }
+

+
  public async getTree(
+
    id: string,
+
    sha: string,
+
    path?: string,
+
    options?: RequestOptions,
+
  ): Promise<Tree> {
+
    const tree = await this.#fetcher.fetchOk(
+
      {
+
        method: "GET",
+
        path: `projects/${id}/tree/${sha}/${path ?? ""}`,
+
        options,
+
      },
+
      treeSchema,
+
    );
+
    return tree;
+
  }
+

+
  public async getTreeStatsBySha(
+
    id: string,
+
    sha: string,
+
    options?: RequestOptions,
+
  ): Promise<TreeStats> {
+
    const tree = await this.#fetcher.fetchOk(
+
      {
+
        method: "GET",
+
        path: `projects/${id}/stats/tree/${sha}`,
+
        options,
+
      },
+
      treeStatsSchema,
+
    );
+
    return tree;
+
  }
+

+
  public async getAllRemotes(
+
    id: string,
+
    options?: RequestOptions,
+
  ): Promise<Remote[]> {
+
    return this.#fetcher.fetchOk(
+
      {
+
        method: "GET",
+
        path: `projects/${id}/remotes`,
+
        options,
+
      },
+
      remotesSchema,
+
    );
+
  }
+

+
  public async getRemoteByPeer(
+
    id: string,
+
    peer: string,
+
    options?: RequestOptions,
+
  ): Promise<Remote> {
+
    return this.#fetcher.fetchOk(
+
      {
+
        method: "GET",
+
        path: `projects/${id}/remotes/${peer}`,
+
        options,
+
      },
+
      remoteSchema,
+
    );
+
  }
+

+
  public async getAllCommits(
+
    id: string,
+
    query?: {
+
      parent?: string;
+
      since?: number;
+
      until?: number;
+
      page?: number;
+
      perPage?: number;
+
    },
+
    options?: RequestOptions,
+
  ): Promise<Commits> {
+
    return this.#fetcher.fetchOk(
+
      {
+
        method: "GET",
+
        path: `projects/${id}/commits`,
+
        query,
+
        options,
+
      },
+
      commitsSchema,
+
    );
+
  }
+

+
  public async getCommitBySha(
+
    id: string,
+
    sha: string,
+
    options?: RequestOptions,
+
  ): Promise<Commit> {
+
    return this.#fetcher.fetchOk(
+
      {
+
        method: "GET",
+
        path: `projects/${id}/commits/${sha}`,
+
        options,
+
      },
+
      commitSchema,
+
    );
+
  }
+

+
  public async getDiff(
+
    id: string,
+
    revisionBase: string,
+
    revisionOid: string,
+
    options?: RequestOptions,
+
  ): Promise<DiffResponse> {
+
    return this.#fetcher.fetchOk(
+
      {
+
        method: "GET",
+
        path: `projects/${id}/diff/${revisionBase}/${revisionOid}`,
+
        options,
+
      },
+
      diffResponseSchema,
+
    );
+
  }
+

+
  public async getIssueById(
+
    id: string,
+
    issueId: string,
+
    options?: RequestOptions,
+
  ): Promise<Issue> {
+
    return this.#fetcher.fetchOk(
+
      {
+
        method: "GET",
+
        path: `projects/${id}/issues/${issueId}`,
+
        options,
+
      },
+
      issueSchema,
+
    );
+
  }
+

+
  public async getAllIssues(
+
    id: string,
+
    query?: {
+
      page?: number;
+
      perPage?: number;
+
      state?: string;
+
    },
+
    options?: RequestOptions,
+
  ): Promise<Issue[]> {
+
    return this.#fetcher.fetchOk(
+
      {
+
        method: "GET",
+
        path: `projects/${id}/issues`,
+
        query,
+
        options,
+
      },
+
      issuesSchema,
+
    );
+
  }
+

+
  public async createIssue(
+
    id: string,
+
    body: {
+
      title: string;
+
      description: string;
+
      assignees: string[];
+
      embeds: Embed[];
+
      labels: string[];
+
    },
+
    authToken: string,
+
    options?: RequestOptions,
+
  ): Promise<IssueCreated> {
+
    return this.#fetcher.fetchOk(
+
      {
+
        method: "POST",
+
        path: `projects/${id}/issues`,
+
        headers: { Authorization: `Bearer ${authToken}` },
+
        body,
+
        options,
+
      },
+
      issueCreatedSchema,
+
    );
+
  }
+

+
  public async updateIssue(
+
    id: string,
+
    issueId: string,
+
    body: IssueUpdateAction,
+
    authToken: string,
+
    options?: RequestOptions,
+
  ): Promise<SuccessResponse> {
+
    return this.#fetcher.fetchOk(
+
      {
+
        method: "PATCH",
+
        path: `projects/${id}/issues/${issueId}`,
+
        headers: { Authorization: `Bearer ${authToken}` },
+
        body,
+
        options,
+
      },
+
      successResponseSchema,
+
    );
+
  }
+

+
  public async getPatchById(
+
    id: string,
+
    patchId: string,
+
    options?: RequestOptions,
+
  ): Promise<Patch> {
+
    return this.#fetcher.fetchOk(
+
      {
+
        method: "GET",
+
        path: `projects/${id}/patches/${patchId}`,
+
        options,
+
      },
+
      patchSchema,
+
    );
+
  }
+

+
  public async getAllPatches(
+
    id: string,
+
    query?: {
+
      page?: number;
+
      perPage?: number;
+
      state?: string;
+
    },
+
    options?: RequestOptions,
+
  ): Promise<Patch[]> {
+
    return this.#fetcher.fetchOk(
+
      {
+
        method: "GET",
+
        path: `projects/${id}/patches`,
+
        query,
+
        options,
+
      },
+
      patchesSchema,
+
    );
+
  }
+

+
  public async createPatch(
+
    id: string,
+
    body: PatchCreate,
+
    authToken: string,
+
    options?: RequestOptions,
+
  ): Promise<PatchCreated> {
+
    return this.#fetcher.fetchOk(
+
      {
+
        method: "POST",
+
        path: `projects/${id}/patches`,
+
        headers: { Authorization: `Bearer ${authToken}` },
+
        body,
+
        options,
+
      },
+
      patchCreatedSchema,
+
    );
+
  }
+

+
  public async updatePatch(
+
    id: string,
+
    patchId: string,
+
    body: PatchUpdateAction,
+
    authToken: string,
+
    options?: RequestOptions,
+
  ): Promise<SuccessResponse> {
+
    return this.#fetcher.fetchOk(
+
      {
+
        method: "PATCH",
+
        path: `projects/${id}/patches/${patchId}`,
+
        headers: { Authorization: `Bearer ${authToken}` },
+
        body,
+
        options,
+
      },
+
      successResponseSchema,
+
    );
+
  }
+
}
added http-client/lib/project/comment.ts
@@ -0,0 +1,27 @@
+
import type { z } from "zod";
+
import { array, boolean, number, object, string } from "zod";
+
import { authorSchema, codeLocationSchema } from "../shared";
+

+
export type Comment = z.infer<typeof commentSchema>;
+
export type Embed = Comment["embeds"][0];
+
export type Reaction = Comment["reactions"][0];
+

+
export const commentSchema = object({
+
  id: string(),
+
  author: authorSchema,
+
  body: string(),
+
  edits: array(
+
    object({
+
      author: authorSchema,
+
      body: string(),
+
      embeds: array(object({ name: string(), content: string() })),
+
      timestamp: number(),
+
    }),
+
  ),
+
  embeds: array(object({ name: string(), content: string() })),
+
  reactions: array(object({ emoji: string(), authors: array(authorSchema) })),
+
  timestamp: number(),
+
  location: codeLocationSchema.nullable().optional(),
+
  resolved: boolean(),
+
  replyTo: string().nullable(),
+
});
added http-client/lib/project/commit.ts
@@ -0,0 +1,239 @@
+
import type { z } from "zod";
+
export type {
+
  Commit,
+
  CommitHeader,
+
  Commits,
+
  Diff,
+
  DiffContent,
+
  DiffFile,
+
  ChangesetWithDiff,
+
  ChangesetWithoutDiff,
+
  HunkLine,
+
  Hunks,
+
};
+

+
import {
+
  array,
+
  boolean,
+
  discriminatedUnion,
+
  literal,
+
  number,
+
  object,
+
  record,
+
  string,
+
  union,
+
} from "zod";
+
export {
+
  commitBlobSchema,
+
  commitHeaderSchema,
+
  commitSchema,
+
  commitsSchema,
+
  diffBlobSchema,
+
  diffSchema,
+
};
+

+
const gitPersonSchema = object({
+
  name: string(),
+
  email: string(),
+
});
+

+
type CommitHeader = z.infer<typeof commitHeaderSchema>;
+

+
const commitHeaderSchema = object({
+
  id: string(),
+
  author: gitPersonSchema,
+
  summary: string(),
+
  description: string(),
+
  parents: array(string()),
+
  committer: gitPersonSchema.merge(object({ time: number() })),
+
});
+

+
const diffBlobSchema = object({
+
  binary: boolean(),
+
  content: string(),
+
  id: string(),
+
});
+

+
export type DiffBlob = z.infer<typeof diffBlobSchema>;
+

+
const commitBlobSchema = object({
+
  binary: boolean(),
+
  content: string(),
+
});
+

+
export type CommitBlob = z.infer<typeof commitBlobSchema>;
+

+
type AdditionHunkLine = z.infer<typeof additionHunkLineSchema>;
+

+
const additionHunkLineSchema = object({
+
  line: string(),
+
  lineNo: number(),
+
  type: literal("addition"),
+
});
+

+
type DeletionHunkLine = z.infer<typeof deletionHunkLineSchema>;
+

+
const deletionHunkLineSchema = object({
+
  line: string(),
+
  lineNo: number(),
+
  type: literal("deletion"),
+
});
+

+
type DiffFile = z.infer<typeof diffFileSchema>;
+

+
const diffFileSchema = object({
+
  oid: string(),
+
  mode: union([
+
    literal("blob"),
+
    literal("blobExecutable"),
+
    literal("tree"),
+
    literal("link"),
+
    literal("commit"),
+
  ]),
+
});
+

+
type ContextHunkLine = z.infer<typeof contextHunkLineSchema>;
+

+
const contextHunkLineSchema = object({
+
  line: string(),
+
  lineNoNew: number(),
+
  lineNoOld: number(),
+
  type: literal("context"),
+
});
+

+
type HunkLine = AdditionHunkLine | DeletionHunkLine | ContextHunkLine;
+

+
const hunkLineSchema = union([
+
  additionHunkLineSchema,
+
  deletionHunkLineSchema,
+
  contextHunkLineSchema,
+
]);
+

+
type Hunks = z.infer<typeof changesetHunkSchema>;
+

+
const changesetHunkSchema = object({
+
  header: string(),
+
  lines: array(hunkLineSchema),
+
});
+

+
type DiffContent = z.infer<typeof diffContentSchema>;
+

+
const diffContentSchema = discriminatedUnion("type", [
+
  object({
+
    type: literal("plain"),
+
    stats: object({
+
      additions: number(),
+
      deletions: number(),
+
    }),
+
    hunks: array(changesetHunkSchema),
+
    eof: union([
+
      literal("noneMissing"),
+
      literal("oldMissing"),
+
      literal("newMissing"),
+
      literal("bothMissing"),
+
    ]),
+
  }),
+
  object({ type: literal("binary") }),
+
  object({ type: literal("empty") }),
+
]);
+
const diffChangesetSchema = object({
+
  path: string(),
+
  diff: diffContentSchema,
+
});
+

+
const diffAddedChangesetSchema = diffChangesetSchema.merge(
+
  object({ state: literal("added"), new: diffFileSchema }),
+
);
+

+
const diffDeletedChangesetSchema = diffChangesetSchema.merge(
+
  object({ state: literal("deleted"), old: diffFileSchema }),
+
);
+

+
const diffModifiedChangesetSchema = diffChangesetSchema.merge(
+
  object({
+
    state: literal("modified"),
+
    new: diffFileSchema,
+
    old: diffFileSchema,
+
  }),
+
);
+

+
const diffCopiedChangesetSchema = object({
+
  state: literal("copied"),
+
  newPath: string(),
+
  oldPath: string(),
+
});
+

+
const diffCopiedWithModificationsChangesetSchema =
+
  diffCopiedChangesetSchema.merge(
+
    object({
+
      old: diffFileSchema,
+
      new: diffFileSchema,
+
      diff: diffContentSchema,
+
    }),
+
  );
+

+
const diffMovedChangesetSchema = object({
+
  state: literal("moved"),
+
  newPath: string(),
+
  oldPath: string(),
+
  current: diffFileSchema,
+
});
+

+
const diffMovedWithModificationsChangesetSchema = diffMovedChangesetSchema
+
  .omit({ current: true })
+
  .merge(
+
    object({
+
      old: diffFileSchema,
+
      new: diffFileSchema,
+
      diff: diffContentSchema,
+
    }),
+
  );
+

+
type Diff = z.infer<typeof diffSchema>;
+

+
type ChangesetWithDiff = z.infer<typeof changesetWithDiffSchema>;
+
type ChangesetWithoutDiff = z.infer<typeof changesetWithoutDiffSchema>;
+

+
const changesetWithDiffSchema = union([
+
  diffAddedChangesetSchema,
+
  diffDeletedChangesetSchema,
+
  diffModifiedChangesetSchema,
+
  diffMovedWithModificationsChangesetSchema,
+
  diffCopiedWithModificationsChangesetSchema,
+
]);
+
const changesetWithoutDiffSchema = union([
+
  diffMovedChangesetSchema,
+
  diffCopiedChangesetSchema,
+
]);
+

+
const diffSchema = object({
+
  files: array(
+
    union([
+
      diffAddedChangesetSchema,
+
      diffDeletedChangesetSchema,
+
      diffModifiedChangesetSchema,
+
      diffMovedChangesetSchema,
+
      diffMovedWithModificationsChangesetSchema,
+
      diffCopiedChangesetSchema,
+
      diffCopiedWithModificationsChangesetSchema,
+
    ]),
+
  ),
+
  stats: object({
+
    filesChanged: number(),
+
    insertions: number(),
+
    deletions: number(),
+
  }),
+
});
+

+
type Commit = z.infer<typeof commitSchema>;
+

+
const commitSchema = object({
+
  commit: commitHeaderSchema,
+
  diff: diffSchema,
+
  branches: array(string()),
+
  files: record(string(), commitBlobSchema),
+
});
+

+
type Commits = z.infer<typeof commitsSchema>;
+

+
const commitsSchema = array(commitHeaderSchema);
added http-client/lib/project/issue.ts
@@ -0,0 +1,70 @@
+
import type { Embed } from "./comment.js";
+
import type { ZodSchema, z } from "zod";
+
import { array, boolean, literal, object, string, union } from "zod";
+

+
import { commentSchema } from "./comment.js";
+
import { authorSchema } from "../shared.js";
+

+
export type IssueState =
+
  | { status: "open" }
+
  | { status: "closed"; reason: "other" | "solved" };
+

+
const issueStateSchema = union([
+
  object({ status: literal("open") }),
+
  object({
+
    status: literal("closed"),
+
    reason: union([literal("other"), literal("solved")]),
+
  }),
+
]) satisfies ZodSchema<IssueState>;
+

+
export const issueSchema = object({
+
  id: string(),
+
  author: authorSchema,
+
  title: string(),
+
  state: issueStateSchema,
+
  discussion: array(commentSchema),
+
  labels: array(string()),
+
  assignees: array(authorSchema),
+
});
+

+
export type Issue = z.infer<typeof issueSchema>;
+

+
export interface IssueCreated {
+
  success: boolean;
+
  id: string;
+
}
+

+
export const issueCreatedSchema = object({
+
  success: boolean(),
+
  id: string(),
+
}) satisfies ZodSchema<IssueCreated>;
+

+
export const issuesSchema = array(issueSchema) satisfies ZodSchema<Issue[]>;
+

+
export type IssueUpdateAction =
+
  | { type: "edit"; title: string }
+
  | { type: "label"; labels: string[] }
+
  | {
+
      type: "assign";
+
      assignees: string[];
+
    }
+
  | { type: "lifecycle"; state: IssueState }
+
  | {
+
      type: "comment";
+
      body: string;
+
      embeds?: Embed[];
+
      replyTo?: string;
+
    }
+
  | {
+
      type: "comment.edit";
+
      id: string;
+
      body: string;
+
      embeds: Embed[];
+
    }
+
  | { type: "comment.redact"; id: string }
+
  | {
+
      type: "comment.react";
+
      id: string;
+
      reaction: string;
+
      active: boolean;
+
    };
added http-client/lib/project/patch.ts
@@ -0,0 +1,209 @@
+
import type { Embed } from "./comment.js";
+
import type { ZodSchema, z } from "zod";
+
import type { CodeLocation } from "../shared.js";
+

+
import { commentSchema } from "./comment.js";
+

+
import {
+
  array,
+
  boolean,
+
  literal,
+
  number,
+
  optional,
+
  object,
+
  string,
+
  tuple,
+
  union,
+
} from "zod";
+
import { authorSchema } from "../shared.js";
+

+
export type PatchState = z.infer<typeof patchStateSchema>;
+

+
const patchStateSchema = union([
+
  object({
+
    status: literal("draft"),
+
  }),
+
  object({
+
    status: literal("open"),
+
    conflicts: array(tuple([string(), string()])).optional(),
+
  }),
+
  object({
+
    status: literal("archived"),
+
  }),
+
  object({
+
    status: literal("merged"),
+
    revision: string(),
+
    commit: string(),
+
  }),
+
]);
+

+
export type Merge = z.infer<typeof mergeSchema>;
+

+
const mergeSchema = object({
+
  author: authorSchema,
+
  revision: string(),
+
  commit: string(),
+
  timestamp: number(),
+
});
+

+
export type Verdict = "accept" | "reject";
+

+
const reviewSchema = object({
+
  author: authorSchema,
+
  verdict: optional(union([literal("accept"), literal("reject")]).nullable()),
+
  comments: array(commentSchema),
+
  summary: string().nullable(),
+
  timestamp: number(),
+
});
+

+
export type Review = z.infer<typeof reviewSchema>;
+

+
const revisionSchema = object({
+
  id: string(),
+
  author: authorSchema,
+
  description: string(),
+
  edits: array(
+
    object({
+
      author: authorSchema,
+
      body: string(),
+
      embeds: array(object({ name: string(), content: string() })),
+
      timestamp: number(),
+
    }),
+
  ),
+
  reactions: array(
+
    object({
+
      emoji: string(),
+
      authors: array(authorSchema),
+
    }),
+
  ),
+
  base: string(),
+
  oid: string(),
+
  refs: array(string()),
+
  discussions: array(commentSchema),
+
  reviews: array(reviewSchema),
+
  timestamp: number(),
+
});
+

+
export type Revision = z.infer<typeof revisionSchema>;
+

+
export const patchSchema = object({
+
  id: string(),
+
  author: authorSchema,
+
  title: string(),
+
  state: patchStateSchema,
+
  target: string(),
+
  labels: array(string()),
+
  merges: array(mergeSchema),
+
  assignees: array(string()),
+
  revisions: array(revisionSchema),
+
});
+

+
export type Patch = z.infer<typeof patchSchema>;
+

+
export const patchesSchema = array(patchSchema) satisfies ZodSchema<Patch[]>;
+

+
export type LifecycleState =
+
  | { status: "draft" }
+
  | { status: "open" }
+
  | { status: "archived" };
+

+
export type PatchUpdateAction =
+
  | { type: "edit"; title: string; target: "delegates" }
+
  | { type: "label"; labels: string[] }
+
  | { type: "assign"; assignees: string[] }
+
  | { type: "merge"; revision: string; commit: string }
+
  | { type: "lifecycle"; state: LifecycleState }
+
  | {
+
      type: "review";
+
      revision: string;
+
      summary?: string;
+
      verdict?: Verdict | null;
+
    }
+
  | { type: "review.edit"; review: string; summary?: string }
+
  | { type: "review.redact"; review: string }
+
  | {
+
      type: "review.comment";
+
      review: string;
+
      body: string;
+
      location: CodeLocation;
+
      replyTo?: string;
+
      embeds?: Embed[];
+
    }
+
  | {
+
      type: "review.comment.edit";
+
      review: string;
+
      comment: string;
+
      body: string;
+
      embeds: Embed[];
+
    }
+
  | {
+
      type: "review.comment.redact";
+
      review: string;
+
      comment: string;
+
    }
+
  | {
+
      type: "review.comment.react";
+
      review: string;
+
      comment: string;
+
      reaction: string;
+
      active: boolean;
+
    }
+
  | { type: "revision"; description: string; base: string; oid: string }
+
  | {
+
      type: "revision.edit";
+
      revision: string;
+
      description: string;
+
      embeds?: Embed[];
+
    }
+
  | {
+
      type: "revision.react";
+
      revision: string;
+
      reaction: string;
+
      location?: CodeLocation;
+
      active: boolean;
+
    }
+
  | { type: "revision.redact"; revision: string }
+
  | {
+
      type: "revision.comment";
+
      revision: string;
+
      body: string;
+
      embeds?: Embed[];
+
      location?: CodeLocation;
+
      replyTo?: string;
+
    }
+
  | {
+
      type: "revision.comment.edit";
+
      revision: string;
+
      comment: string;
+
      body: string;
+
      embeds: Embed[];
+
    }
+
  | {
+
      type: "revision.comment.redact";
+
      revision: string;
+
      comment: string;
+
    }
+
  | {
+
      type: "revision.comment.react";
+
      revision: string;
+
      comment: string;
+
      reaction: string;
+
      active: boolean;
+
    };
+

+
export const patchCreateSchema = object({
+
  title: string(),
+
  description: string(),
+
  target: string(),
+
  oid: string(),
+
  labels: array(string()),
+
});
+

+
export type PatchCreate = z.infer<typeof patchCreateSchema>;
+

+
export const patchCreatedSchema = object({
+
  success: boolean(),
+
  id: string(),
+
});
+

+
export type PatchCreated = z.infer<typeof patchCreatedSchema>;
added http-client/lib/session.ts
@@ -0,0 +1,90 @@
+
import type { Fetcher, RequestOptions } from "./fetcher.js";
+
import type { SuccessResponse } from "./shared.js";
+
import type { z } from "zod";
+

+
import { number, object, string } from "zod";
+

+
import { successResponseSchema } from "./shared.js";
+

+
export const sessionPayloadSchema = object({
+
  sessionId: string(),
+
  signature: string(),
+
  publicKey: string(),
+
});
+

+
export type SessionPayload = z.infer<typeof sessionPayloadSchema>;
+

+
const sessionSchema = object({
+
  sessionId: string(),
+
  status: string(),
+
  publicKey: string(),
+
  alias: string(),
+
  issuedAt: number(),
+
  expiresAt: number(),
+
});
+

+
export type Session = z.infer<typeof sessionSchema>;
+

+
export class Client {
+
  #fetcher: Fetcher;
+

+
  public constructor(fetcher: Fetcher) {
+
    this.#fetcher = fetcher;
+
  }
+

+
  public async getById(id: string, options?: RequestOptions): Promise<Session> {
+
    return this.#fetcher.fetchOk(
+
      {
+
        method: "GET",
+
        path: `sessions/${id}`,
+
        options,
+
      },
+
      sessionSchema,
+
    );
+
  }
+

+
  public async create(options?: RequestOptions): Promise<Session> {
+
    return this.#fetcher.fetchOk(
+
      {
+
        method: "POST",
+
        path: "sessions",
+
        options,
+
      },
+
      sessionSchema,
+
    );
+
  }
+

+
  public async update(
+
    id: string,
+
    body: {
+
      sig: string;
+
      pk: string;
+
    },
+
    options?: RequestOptions,
+
  ): Promise<SuccessResponse> {
+
    return this.#fetcher.fetchOk(
+
      {
+
        method: "PUT",
+
        path: `sessions/${id}`,
+
        body,
+
        options,
+
      },
+
      successResponseSchema,
+
    );
+
  }
+

+
  public async delete(
+
    id: string,
+
    options?: RequestOptions,
+
  ): Promise<SuccessResponse> {
+
    return this.#fetcher.fetchOk(
+
      {
+
        method: "DELETE",
+
        path: `sessions/${id}`,
+
        headers: { Authorization: `Bearer ${id}` },
+
        options,
+
      },
+
      successResponseSchema,
+
    );
+
  }
+
}
added http-client/lib/shared.ts
@@ -0,0 +1,77 @@
+
import type { ZodSchema, z } from "zod";
+

+
import { array, literal, number, object, string, union } from "zod";
+

+
export interface SuccessResponse {
+
  success: true;
+
}
+

+
export const successResponseSchema = object({
+
  success: literal(true),
+
}) satisfies ZodSchema<SuccessResponse>;
+

+
const policySchema = union([literal("allow"), literal("block")]);
+
const scopeSchema = union([literal("followed"), literal("all")]);
+

+
export const nodeConfigSchema = object({
+
  alias: string(),
+
  peers: union([
+
    object({ type: literal("static") }),
+
    object({ type: literal("dynamic"), target: number() }),
+
  ]),
+
  listen: array(string()),
+
  connect: array(string()),
+
  externalAddresses: array(string()),
+
  network: union([literal("main"), literal("test")]),
+
  relay: union([literal("always"), literal("never"), literal("auto")]),
+
  limits: object({
+
    routingMaxSize: number(),
+
    routingMaxAge: number(),
+
    fetchConcurrency: number(),
+
    gossipMaxAge: number(),
+
    maxOpenFiles: number(),
+
    rate: object({
+
      inbound: object({
+
        fillRate: number(),
+
        capacity: number(),
+
      }),
+
      outbound: object({
+
        fillRate: number(),
+
        capacity: number(),
+
      }),
+
    }),
+
  }),
+
  policy: policySchema,
+
  scope: scopeSchema,
+
});
+

+
export type Policy = z.infer<typeof policySchema>;
+
export type Scope = z.infer<typeof scopeSchema>;
+

+
export const rangeSchema = union([
+
  object({
+
    type: literal("lines"),
+
    range: object({ start: number(), end: number() }),
+
  }),
+
  object({
+
    type: literal("chars"),
+
    line: number(),
+
    range: object({ start: number(), end: number() }),
+
  }),
+
]);
+

+
export type Range = z.infer<typeof rangeSchema>;
+

+
export const codeLocationSchema = object({
+
  commit: string(),
+
  path: string(),
+
  old: rangeSchema.nullable(),
+
  new: rangeSchema.nullable(),
+
});
+

+
export type CodeLocation = z.infer<typeof codeLocationSchema>;
+

+
export const authorSchema = object({
+
  id: string(),
+
  alias: string().optional(),
+
});
added http-client/tests/client.test.ts
@@ -0,0 +1,24 @@
+
import { describe, test } from "vitest";
+

+
import { HttpdClient } from "@http-client";
+
import { defaultHttpdPort } from "@tests/support/fixtures";
+

+
const api = new HttpdClient({
+
  hostname: "127.0.0.1",
+
  port: defaultHttpdPort,
+
  scheme: "http",
+
});
+

+
describe("client", () => {
+
  test("#getNodeInfo()", async () => {
+
    await api.getNodeInfo();
+
  });
+

+
  test("#getStats()", async () => {
+
    await api.getStats();
+
  });
+

+
  test("#getNode()", async () => {
+
    await api.getNode();
+
  });
+
});
added http-client/tests/project.test.ts
@@ -0,0 +1,291 @@
+
import { describe, test } from "vitest";
+

+
import { HttpdClient } from "@http-client";
+
import {
+
  aliceMainHead,
+
  aliceRemote,
+
  bobRemote,
+
  cobRid,
+
  defaultHttpdPort,
+
  sourceBrowsingRid,
+
} from "@tests/support/fixtures.js";
+
import {
+
  assertIssue,
+
  assertPatch,
+
  createIssueToBeModified,
+
  createPatchToBeModified,
+
} from "@http-client/tests/support/support";
+
import { authenticate } from "@http-client/tests/support/httpd.js";
+
import { testFixture as testWithAPI } from "@http-client/tests/support/fixtures.js";
+

+
describe("project", () => {
+
  const api = new HttpdClient({
+
    hostname: "127.0.0.1",
+
    port: defaultHttpdPort,
+
    scheme: "http",
+
  });
+

+
  test("#getByDelegate(delegateId)", async () => {
+
    await api.project.getByDelegate(aliceRemote);
+
  });
+

+
  test("#getAll()", async () => {
+
    await api.project.getAll();
+
  });
+

+
  test("#getById(id)", async () => {
+
    await api.project.getById(sourceBrowsingRid);
+
  });
+

+
  test("#getActivity(id)", async () => {
+
    await api.project.getActivity(sourceBrowsingRid);
+
  });
+

+
  test("#getReadme(id, sha)", async () => {
+
    await api.project.getReadme(sourceBrowsingRid, aliceMainHead);
+
  });
+

+
  test("#getBlob(id, sha, path)", async () => {
+
    await api.project.getBlob(sourceBrowsingRid, aliceMainHead, "src/true.c");
+
  });
+

+
  test("#getTree(id, sha)", async () => {
+
    await api.project.getTree(sourceBrowsingRid, aliceMainHead);
+
  });
+

+
  test("#getTreeStats(id, sha)", async () => {
+
    await api.project.getTreeStatsBySha(sourceBrowsingRid, aliceMainHead);
+
  });
+

+
  test("#getTree(id, sha, path)", async () => {
+
    await api.project.getTree(sourceBrowsingRid, aliceMainHead, "src");
+
  });
+

+
  test("#getAllRemotes(id)", async () => {
+
    await api.project.getAllRemotes(sourceBrowsingRid);
+
  });
+

+
  test("#getRemoteByPeer(id, peer)", async () => {
+
    await api.project.getRemoteByPeer(
+
      sourceBrowsingRid,
+
      aliceRemote.substring(8),
+
    );
+
  });
+

+
  test("#getAllCommits(id)", async () => {
+
    await api.project.getAllCommits(sourceBrowsingRid);
+
  });
+

+
  // TODO: test since/until properly.
+
  test("#getAllCommits(id, {parent, since, until, page, perPage})", async () => {
+
    await api.project.getAllCommits(sourceBrowsingRid, {
+
      parent: aliceMainHead,
+
      since: 1679065819581,
+
      until: 1679065819590,
+
      page: 1,
+
      perPage: 2,
+
    });
+
  });
+

+
  test("#getCommitBySha(id, sha)", async () => {
+
    await api.project.getCommitBySha(sourceBrowsingRid, aliceMainHead);
+
  });
+

+
  test("#getDiff(id, revisionBase, revisionOid)", async () => {
+
    await api.project.getDiff(
+
      sourceBrowsingRid,
+
      "90f6d058ece12f75f349bc7bbe88142187fe0379",
+
      aliceMainHead,
+
    );
+
  });
+

+
  test("#getIssueById(id, issueId)", async () => {
+
    await api.project.getIssueById(
+
      cobRid,
+
      "d481fe6e562dd78129589d4738f171a8380fcb19",
+
    );
+
  });
+

+
  test("#getAllIssues(id)", async () => {
+
    await api.project.getAllIssues(cobRid, {
+
      page: 0,
+
      perPage: 5,
+
      state: "open",
+
    });
+
  });
+

+
  testWithAPI(
+
    "#createIssue(id, { title, description, assignees, labels })",
+
    async ({ httpd: { api, peer } }) => {
+
      const sessionId = await authenticate(api, peer);
+
      const { id: issueId } = await api.project.createIssue(
+
        cobRid,
+
        {
+
          title: "aaa",
+
          description: "bbb",
+
          assignees: [],
+
          embeds: [],
+
          labels: ["bug", "documentation"],
+
        },
+
        sessionId,
+
      );
+
      await assertIssue(
+
        issueId,
+
        {
+
          title: "aaa",
+
          discussion: [{ body: "bbb" }],
+
          assignees: [],
+
          labels: ["bug", "documentation"],
+
        },
+
        api,
+
      );
+
    },
+
  );
+

+
  testWithAPI(
+
    "#updateIssue(id, issueId, { type: 'edit' }, authToken)",
+
    async ({ httpd: { api, peer } }) => {
+
      const sessionId = await authenticate(api, peer);
+
      const issueId = await createIssueToBeModified(api, sessionId);
+
      await api.project.updateIssue(
+
        cobRid,
+
        issueId,
+
        { type: "edit", title: "ccc" },
+
        sessionId,
+
      );
+
      await assertIssue(issueId, { title: "ccc" }, api);
+
    },
+
  );
+

+
  testWithAPI(
+
    "#updateIssue(id, issueId, { type: 'label' }, authToken)",
+
    async ({ httpd: { api, peer } }) => {
+
      const sessionId = await authenticate(api, peer);
+
      const issueId = await createIssueToBeModified(api, sessionId);
+
      await api.project.updateIssue(
+
        cobRid,
+
        issueId,
+
        { type: "label", labels: ["bug"] },
+
        sessionId,
+
      );
+
      await assertIssue(issueId, { labels: ["bug"] }, api);
+
    },
+
  );
+

+
  testWithAPI(
+
    "#updateIssue(id, issueId, { type: 'assign' }, authToken)",
+
    async ({ httpd: { api, peer } }) => {
+
      const sessionId = await authenticate(api, peer);
+
      const issueId = await createIssueToBeModified(api, sessionId);
+
      await api.project.updateIssue(
+
        cobRid,
+
        issueId,
+
        {
+
          type: "assign",
+
          assignees: [bobRemote],
+
        },
+
        sessionId,
+
      );
+
      await assertIssue(
+
        issueId,
+
        {
+
          assignees: [
+
            {
+
              id: "did:key:z6Mkg49NtQR2LyYRDCQFK4w1VVHqhypZSSRo7HsyuN7SV7v5",
+
              alias: "bob",
+
            },
+
          ],
+
        },
+
        api,
+
      );
+
    },
+
  );
+

+
  testWithAPI(
+
    "#updateIssue(id, issueId, { type: 'lifecycle' }, authToken)",
+
    async ({ httpd: { api, peer } }) => {
+
      const sessionId = await authenticate(api, peer);
+
      const issueId = await createIssueToBeModified(api, sessionId);
+
      await api.project.updateIssue(
+
        cobRid,
+
        issueId,
+
        { type: "lifecycle", state: { status: "closed", reason: "solved" } },
+
        sessionId,
+
      );
+
      await assertIssue(
+
        issueId,
+
        {
+
          state: { status: "closed", reason: "solved" },
+
        },
+
        api,
+
      );
+
    },
+
  );
+

+
  test("#getPatchById(id, patchId)", async () => {
+
    await api.project.getPatchById(
+
      cobRid,
+
      "59a0821edc73630bce540596cffc7854da557365",
+
    );
+
  });
+

+
  test("#getAllPatches(id)", async () => {
+
    await api.project.getAllPatches(cobRid);
+
  });
+

+
  testWithAPI(
+
    "#createPatch(id, patchCreate, authToken)",
+
    async ({ httpd: { api, peer } }) => {
+
      const sessionId = await authenticate(api, peer);
+
      const { id: oid } = await api.project.createPatch(
+
        cobRid,
+
        {
+
          title: "ppp",
+
          description: "qqq",
+
          target: "d7dd8cecae16b1108234e09dbdb5d64ae394bc25",
+
          oid: "38c225e2a0b47ba59def211f4e4825c31d9463ec",
+
          labels: [],
+
        },
+
        sessionId,
+
      );
+
      await assertPatch(
+
        oid,
+
        {
+
          title: "ppp",
+
          state: { status: "open" },
+
          target: "delegates",
+
          labels: [],
+
          revisions: [
+
            {
+
              description: "qqq",
+
              base: "d7dd8cecae16b1108234e09dbdb5d64ae394bc25",
+
              oid: "38c225e2a0b47ba59def211f4e4825c31d9463ec",
+
            },
+
          ],
+
        },
+
        api,
+
      );
+
    },
+
  );
+

+
  testWithAPI(
+
    "#updatePatch(id, patchId, { type: 'edit' }, authToken)",
+
    async ({ httpd: { api, peer } }) => {
+
      const sessionId = await authenticate(api, peer);
+
      const patchId = await createPatchToBeModified(api, sessionId);
+
      await api.project.updatePatch(
+
        cobRid,
+
        patchId,
+
        { type: "label", labels: ["bug"] },
+
        sessionId,
+
      );
+
      await assertPatch(
+
        patchId,
+
        {
+
          labels: ["bug"],
+
        },
+
        api,
+
      );
+
    },
+
  );
+
});
added http-client/tests/session.test.ts
@@ -0,0 +1,38 @@
+
import * as FsSync from "node:fs";
+
import * as Path from "node:path";
+
import { describe, test } from "vitest";
+

+
import { HttpdClient } from "@http-client";
+
import { authenticate } from "@http-client/tests/support/httpd.js";
+
import { createPeerManager } from "@tests/support/peerManager.js";
+
import { gitOptions } from "@tests/support/fixtures.js";
+
import { tmpDir } from "@tests/support/support.js";
+

+
describe("session", async () => {
+
  const peerManager = await createPeerManager({
+
    dataDir: Path.resolve(Path.join(tmpDir, "peers")),
+
    outputLog: FsSync.createWriteStream(
+
      Path.join(tmpDir, "peerManager.log"),
+
    ).setMaxListeners(16),
+
  });
+
  const peer = await peerManager.createPeer({
+
    name: "session",
+
    gitOptions: gitOptions["alice"],
+
  });
+
  await peer.startHttpd();
+
  const api = new HttpdClient(peer.httpdBaseUrl);
+

+
  test("#getById(id)", async () => {
+
    const id = await authenticate(api, peer);
+
    await api.session.getById(id);
+
  });
+

+
  test("#update(id, {sig, pk})", async () => {
+
    await authenticate(api, peer);
+
  });
+

+
  test("#delete(id)", async () => {
+
    const id = await authenticate(api, peer);
+
    await api.session.delete(id);
+
  });
+
});
added http-client/tests/support/fixtures.ts
@@ -0,0 +1,32 @@
+
import * as FsSync from "node:fs";
+
import * as Path from "node:path";
+
import { test } from "vitest";
+

+
import { HttpdClient } from "@http-client";
+
import { RadiclePeer, createPeerManager } from "@tests/support/peerManager.js";
+
import { gitOptions } from "@tests/support/fixtures.js";
+
import { tmpDir } from "@tests/support/support.js";
+

+
interface TestFixtures {
+
  httpd: { api: HttpdClient; peer: RadiclePeer };
+
}
+

+
export const testFixture = test.extend<TestFixtures>({
+
  // eslint-disable-next-line no-empty-pattern
+
  httpd: async ({}, use) => {
+
    const peerManager = await createPeerManager({
+
      dataDir: Path.resolve(Path.join(tmpDir, "peers")),
+
      outputLog: FsSync.createWriteStream(
+
        Path.join(tmpDir, "peerManager.log"),
+
      ).setMaxListeners(16),
+
    });
+
    const peer = await peerManager.createPeer({
+
      name: "palm",
+
      gitOptions: gitOptions["alice"],
+
    });
+
    await peer.startHttpd();
+
    const api = new HttpdClient(peer.httpdBaseUrl);
+
    await use({ api, peer });
+
    await peer.shutdown();
+
  },
+
});
added http-client/tests/support/httpd.ts
@@ -0,0 +1,30 @@
+
import type { HttpdClient } from "@http-client";
+
import type { RadiclePeer } from "@tests/support/peerManager.js";
+

+
import assert from "node:assert";
+

+
export async function authenticate(
+
  api: HttpdClient,
+
  peer: RadiclePeer,
+
): Promise<string> {
+
  const { stdout } = await peer.spawn("rad-web", [
+
    "http://localhost:3001",
+
    "--no-open",
+
    "--connect",
+
    `${peer.httpdBaseUrl.hostname}:${peer.httpdBaseUrl.port}`,
+
  ]);
+
  const match = stdout.match(/Visit (http:\/\/\S+) to connect/);
+
  assert(
+
    match !== null && match[1],
+
    `Failed to get authentication URL from: ${stdout}`,
+
  );
+

+
  const authUrl = new URL(match[1]);
+
  const sessionId = authUrl.pathname.split("/")[2];
+

+
  await api.session.update(sessionId, {
+
    sig: authUrl.searchParams.get("sig")!,
+
    pk: authUrl.searchParams.get("pk")!,
+
  });
+
  return sessionId;
+
}
added http-client/tests/support/support.ts
@@ -0,0 +1,59 @@
+
import type { HttpdClient } from "@http-client";
+

+
import { expect } from "vitest";
+
import isMatch from "lodash/isMatch";
+

+
import { cobRid } from "@tests/support/fixtures";
+

+
export async function createIssueToBeModified(
+
  api: HttpdClient,
+
  sessionId: string,
+
) {
+
  const { id } = await api.project.createIssue(
+
    cobRid,
+
    { title: "aaa", description: "bbb", embeds: [], assignees: [], labels: [] },
+
    sessionId,
+
  );
+

+
  return id;
+
}
+

+
export async function createPatchToBeModified(
+
  api: HttpdClient,
+
  sessionId: string,
+
) {
+
  const { id } = await api.project.createPatch(
+
    cobRid,
+
    {
+
      title: "rrr",
+
      description: "ttt",
+
      target: "d7dd8cecae16b1108234e09dbdb5d64ae394bc25",
+
      oid: "38c225e2a0b47ba59def211f4e4825c31d9463ec",
+
      labels: [],
+
    },
+
    sessionId,
+
  );
+

+
  return id;
+
}
+
export async function assertIssue(
+
  oid: string,
+
  change: Record<string, unknown>,
+
  api: HttpdClient,
+
) {
+
  expect(
+
    //@prettier-ignore looks more readable than what prettier suggests.
+
    isMatch(await api.project.getIssueById(cobRid, oid), change),
+
  ).toBe(true);
+
}
+

+
export async function assertPatch(
+
  oid: string,
+
  change: Record<string, unknown>,
+
  api: HttpdClient,
+
) {
+
  expect(
+
    //@prettier-ignore looks more readable than what prettier suggests.
+
    isMatch(await api.project.getPatchById(cobRid, oid), change),
+
  ).toBe(true);
+
}
added http-client/vite.config.ts
@@ -0,0 +1,25 @@
+
import nodeConfig from "config";
+
import path from "node:path";
+
import virtual from "vite-plugin-virtual";
+
import { defineConfig } from "vite";
+

+
export default defineConfig({
+
  plugins: [
+
    virtual({
+
      "virtual:config": nodeConfig.util.toObject(),
+
    }),
+
  ],
+
  test: {
+
    environment: "node",
+
    include: ["http-client/tests/*.test.ts"],
+
    reporters: "verbose",
+
    globalSetup: "./tests/support/globalSetup",
+
  },
+
  resolve: {
+
    alias: {
+
      "@tests": path.resolve("./tests"),
+
      "@app": path.resolve("./src"),
+
      "@http-client": path.resolve("./http-client"),
+
    },
+
  },
+
});
deleted httpd-client/index.ts
@@ -1,248 +0,0 @@
-
import type { BaseUrl } from "./lib/fetcher.js";
-
import type {
-
  Blob,
-
  DiffResponse,
-
  Project,
-
  ProjectListQuery,
-
  Remote,
-
  Tree,
-
  TreeStats,
-
} from "./lib/project.js";
-
import type {
-
  SuccessResponse,
-
  CodeLocation,
-
  Range,
-
  Policy,
-
  Scope,
-
} from "./lib/shared.js";
-
import type { Comment, Embed, Reaction } from "./lib/project/comment.js";
-
import type {
-
  Commit,
-
  CommitBlob,
-
  CommitHeader,
-
  ChangesetWithDiff,
-
  ChangesetWithoutDiff,
-
  Diff,
-
  DiffBlob,
-
  DiffContent,
-
  DiffFile,
-
  HunkLine,
-
} from "./lib/project/commit.js";
-
import type { Issue, IssueState } from "./lib/project/issue.js";
-
import type {
-
  LifecycleState,
-
  Merge,
-
  Patch,
-
  PatchState,
-
  PatchUpdateAction,
-
  Review,
-
  Revision,
-
  Verdict,
-
} from "./lib/project/patch.js";
-
import type { RequestOptions } from "./lib/fetcher.js";
-
import type { ZodSchema } from "zod";
-

-
import { z, array, literal, number, object, string, union } from "zod";
-

-
import * as project from "./lib/project.js";
-
import * as profile from "./lib/profile.js";
-
import * as session from "./lib/session.js";
-
import { Fetcher } from "./lib/fetcher.js";
-
import { nodeConfigSchema, successResponseSchema } from "./lib/shared.js";
-

-
export type {
-
  BaseUrl,
-
  Blob,
-
  ChangesetWithDiff,
-
  ChangesetWithoutDiff,
-
  CodeLocation,
-
  Comment,
-
  Commit,
-
  CommitBlob,
-
  CommitHeader,
-
  Diff,
-
  DiffBlob,
-
  DiffContent,
-
  DiffFile,
-
  DiffResponse,
-
  Embed,
-
  HunkLine,
-
  Issue,
-
  IssueState,
-
  LifecycleState,
-
  Merge,
-
  Patch,
-
  PatchState,
-
  PatchUpdateAction,
-
  Policy,
-
  Project,
-
  ProjectListQuery,
-
  Range,
-
  Reaction,
-
  Remote,
-
  Review,
-
  Revision,
-
  Scope,
-
  TreeStats,
-
  Tree,
-
  Verdict,
-
};
-

-
export type Node = z.infer<typeof nodeSchema>;
-

-
const nodeSchema = object({
-
  id: string(),
-
  version: string(),
-
  config: nodeConfigSchema.nullable(),
-
  state: union([literal("running"), literal("stopped")]),
-
});
-

-
export type NodeInfo = z.infer<typeof nodeInfoSchema>;
-

-
const nodeInfoSchema = object({
-
  message: string(),
-
  service: string(),
-
  version: string(),
-
  apiVersion: string(),
-
  nid: string(),
-
  path: string(),
-
  links: array(
-
    object({
-
      href: string(),
-
      rel: string(),
-
      type: union([
-
        literal("GET"),
-
        literal("POST"),
-
        literal("PUT"),
-
        literal("DELETE"),
-
      ]),
-
    }),
-
  ),
-
});
-

-
export type NodeTracking = z.infer<typeof nodeTrackingSchema>;
-

-
const nodeTrackingSchema = array(
-
  object({
-
    id: string(),
-
    scope: string(),
-
    policy: string(),
-
  }),
-
);
-

-
export interface NodeStats {
-
  repos: { total: number };
-
}
-

-
const nodeStatsSchema = object({
-
  repos: object({ total: number() }),
-
}) satisfies ZodSchema<NodeStats>;
-

-
export class HttpdClient {
-
  #fetcher: Fetcher;
-

-
  public baseUrl: BaseUrl;
-
  public project: project.Client;
-
  public profile: profile.Client;
-
  public session: session.Client;
-

-
  public constructor(baseUrl: BaseUrl) {
-
    this.baseUrl = baseUrl;
-
    this.#fetcher = new Fetcher(this.baseUrl);
-

-
    this.project = new project.Client(this.#fetcher);
-
    this.profile = new profile.Client(this.#fetcher);
-
    this.session = new session.Client(this.#fetcher);
-
  }
-

-
  public changePort(port: number): void {
-
    this.baseUrl.port = port;
-
  }
-

-
  public get url(): string {
-
    return `${this.baseUrl.scheme}://${this.baseUrl.hostname}:${this.baseUrl.port}`;
-
  }
-

-
  public get hostname(): string {
-
    return this.baseUrl.hostname;
-
  }
-

-
  public get port(): string {
-
    return this.baseUrl.port.toString();
-
  }
-

-
  public async getNodeInfo(options?: RequestOptions): Promise<NodeInfo> {
-
    return this.#fetcher.fetchOk(
-
      {
-
        method: "GET",
-
        options,
-
      },
-
      nodeInfoSchema,
-
    );
-
  }
-

-
  public async getStats(options?: RequestOptions): Promise<NodeStats> {
-
    return this.#fetcher.fetchOk(
-
      {
-
        method: "GET",
-
        path: "stats",
-
        options,
-
      },
-
      nodeStatsSchema,
-
    );
-
  }
-

-
  public async getTracking(options?: RequestOptions): Promise<NodeTracking> {
-
    return this.#fetcher.fetchOk(
-
      {
-
        method: "GET",
-
        path: "node/policies/repos",
-
        options,
-
      },
-
      nodeTrackingSchema,
-
    );
-
  }
-

-
  public async seedById(
-
    id: string,
-
    authToken: string,
-
    options?: RequestOptions,
-
  ): Promise<SuccessResponse> {
-
    return this.#fetcher.fetchOk(
-
      {
-
        method: "PUT",
-
        path: `node/policies/repos/${id}`,
-
        headers: { Authorization: `Bearer ${authToken}` },
-
        options,
-
      },
-
      successResponseSchema,
-
    );
-
  }
-

-
  public async stopSeedingById(
-
    id: string,
-
    authToken: string,
-
    options?: RequestOptions,
-
  ): Promise<SuccessResponse> {
-
    return this.#fetcher.fetchOk(
-
      {
-
        method: "DELETE",
-
        path: `node/policies/repos/${id}`,
-
        headers: { Authorization: `Bearer ${authToken}` },
-
        options,
-
      },
-
      successResponseSchema,
-
    );
-
  }
-

-
  public async getNode(options?: RequestOptions): Promise<Node> {
-
    return this.#fetcher.fetchOk(
-
      {
-
        method: "GET",
-
        path: "node",
-
        options,
-
      },
-
      nodeSchema,
-
    );
-
  }
-
}
deleted httpd-client/lib/fetcher.ts
@@ -1,182 +0,0 @@
-
// This module provides low-level capabilities to interact with a typed
-
// JSON HTTP API.
-

-
import type { ZodIssue, ZodType, TypeOf } from "zod";
-

-
import config from "virtual:config";
-
import { compare } from "compare-versions";
-

-
export interface BaseUrl {
-
  hostname: string;
-
  port: number;
-
  scheme: string;
-
}
-

-
// Error that is thrown by `Fetcher` methods.
-
export class ResponseError extends Error {
-
  public method: string;
-
  public url: string;
-
  public status: number;
-
  public body: unknown;
-

-
  public constructor(method: string, response: Response, body_: unknown) {
-
    const body: unknown = body_;
-
    if (
-
      typeof body === "object" &&
-
      body !== null &&
-
      "message" in body &&
-
      typeof body.message === "string"
-
    ) {
-
      super(body.message);
-
    } else {
-
      super("Response error");
-
    }
-

-
    this.method = method;
-
    this.body = body_;
-
    this.status = response.status;
-
    this.url = response.url;
-
  }
-
}
-

-
// Error that is thrown by `Fetcher` methods when parsing the response
-
// body fails.
-
export class ResponseParseError extends Error {
-
  public method: string;
-
  public body: unknown;
-
  public description: string;
-
  public apiVersion: string | undefined;
-
  public zodIssues: ZodIssue[];
-
  public path?: string;
-

-
  public constructor(
-
    method: string,
-
    body: unknown,
-
    apiVersion: string | undefined,
-
    zodIssues: ZodIssue[],
-
    path?: string,
-
  ) {
-
    super("Failed to parse response body");
-

-
    let description: string;
-
    if (
-
      apiVersion === undefined ||
-
      compare(apiVersion, config.nodes.apiVersion, "<")
-
    ) {
-
      description = `The node you are fetching from seems to be outdated, make sure the httpd API version is at least ${config.nodes.apiVersion} currently ${apiVersion ?? "unknown"}.`;
-
    } else if (
-
      config.nodes.apiVersion === undefined ||
-
      compare(apiVersion, config.nodes.apiVersion, ">")
-
    ) {
-
      description = `The web client you are using is outdated, make sure it supports at least ${apiVersion} to interact with this node currently ${config.nodes.apiVersion ?? "unknown"}.`;
-
    } else {
-
      description =
-
        "This is usually due to a version mismatch between the seed and the web interface.";
-
    }
-
    this.apiVersion = apiVersion;
-
    this.description =
-
      "The response received from the seed does not match the expected schema.<br/>".concat(
-
        description,
-
      );
-

-
    this.method = method;
-
    this.path = path;
-
    this.body = body;
-
    this.zodIssues = zodIssues;
-
  }
-
}
-

-
export interface RequestOptions {
-
  abort?: AbortSignal;
-
}
-

-
export interface FetchParams {
-
  method: Method;
-
  // Path to append to the `Fetcher`s base URL to get the final URL.
-
  path?: string;
-
  // Object that is serialized into JSON and sent as the data.
-
  body?: unknown;
-
  // Query parameters to be serialized with URLSearchParams.
-
  query?: Record<string, string | number | boolean>;
-
  options?: RequestOptions;
-
  headers?: Record<string, string>;
-
}
-

-
type Method = "DELETE" | "GET" | "PATCH" | "POST" | "PUT";
-

-
export class Fetcher {
-
  #baseUrl: BaseUrl;
-

-
  public constructor(baseUrl: BaseUrl) {
-
    this.#baseUrl = baseUrl;
-
  }
-

-
  // Execute a fetch and parse the result with the provided schema.
-
  // Return the parsed payload.
-
  //
-
  // Throws `ResponseError` if the response status code is not `200`.
-
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-
  public async fetchOk<T extends ZodType<any>>(
-
    params: FetchParams,
-
    schema: T,
-
  ): Promise<TypeOf<T>> {
-
    const response = await this.fetch({
-
      ...params,
-
      query: { ...params.query, v: config.nodes.apiVersion },
-
    });
-

-
    if (!response.ok) {
-
      let responseBody = await response.text();
-
      try {
-
        responseBody = JSON.parse(responseBody);
-
      } catch (_e: unknown) {
-
        // We keep the original text response body.
-
      }
-
      throw new ResponseError(params.method, response, responseBody);
-
    }
-

-
    const responseBody = await response.json();
-
    const result = schema.safeParse(responseBody);
-
    if (result.success) {
-
      return result.data;
-
    } else {
-
      const response = await this.fetch({ method: "GET" });
-
      const info = await response.json();
-
      throw new ResponseParseError(
-
        params.method,
-
        responseBody,
-
        info.apiVersion,
-
        result.error.errors,
-
        params.path,
-
      );
-
    }
-
  }
-

-
  private async fetch({
-
    method,
-
    path,
-
    body,
-
    options = {},
-
    query,
-
    headers = {},
-
  }: FetchParams): Promise<Response> {
-
    if (body !== undefined && headers["content-type"] === undefined) {
-
      headers["content-type"] = "application/json";
-
    }
-

-
    const pathSegment = path === undefined ? "" : `/${path}`;
-

-
    let url = `${this.#baseUrl.scheme}://${this.#baseUrl.hostname}:${this.#baseUrl.port}/api/v1${pathSegment}`;
-

-
    if (query) {
-
      const searchparams = new URLSearchParams(query as Record<string, string>);
-
      url = `${url}?${searchparams.toString()}`;
-
    }
-
    return globalThis.fetch(url, {
-
      method,
-
      headers,
-
      body: body === undefined ? undefined : JSON.stringify(body),
-
      signal: options.abort,
-
    });
-
  }
-
}
deleted httpd-client/lib/profile.ts
@@ -1,36 +0,0 @@
-
import type { Fetcher, RequestOptions } from "./fetcher.js";
-
import type { z } from "zod";
-

-
import { array, boolean, object, string } from "zod";
-
import { nodeConfigSchema } from "./shared.js";
-

-
const profileSchema = object({
-
  config: object({
-
    publicExplorer: string(),
-
    preferredSeeds: array(string()),
-
    cli: object({ hints: boolean() }),
-
    node: nodeConfigSchema,
-
  }),
-
  home: string(),
-
});
-

-
export type Profile = z.infer<typeof profileSchema>;
-

-
export class Client {
-
  #fetcher: Fetcher;
-

-
  public constructor(fetcher: Fetcher) {
-
    this.#fetcher = fetcher;
-
  }
-

-
  public async getProfile(options?: RequestOptions): Promise<Profile> {
-
    return this.#fetcher.fetchOk(
-
      {
-
        method: "GET",
-
        path: `profile`,
-
        options,
-
      },
-
      profileSchema,
-
    );
-
  }
-
}
deleted httpd-client/lib/project.ts
@@ -1,503 +0,0 @@
-
import type { Commit, Commits } from "./project/commit.js";
-
import type { Embed } from "./project/comment.js";
-
import type { Fetcher, RequestOptions } from "./fetcher.js";
-
import type {
-
  Issue,
-
  IssueCreated,
-
  IssueUpdateAction,
-
} from "./project/issue.js";
-
import type {
-
  Patch,
-
  PatchCreate,
-
  PatchCreated,
-
  PatchUpdateAction,
-
} from "./project/patch.js";
-
import type { SuccessResponse } from "./shared.js";
-
import type { ZodSchema } from "zod";
-

-
import { successResponseSchema } from "./shared.js";
-
import {
-
  array,
-
  boolean,
-
  literal,
-
  number,
-
  optional,
-
  record,
-
  object,
-
  string,
-
  union,
-
  z,
-
} from "zod";
-

-
import {
-
  diffBlobSchema,
-
  commitHeaderSchema,
-
  commitSchema,
-
  commitsSchema,
-
  diffSchema,
-
} from "./project/commit.js";
-

-
import {
-
  issueCreatedSchema,
-
  issueSchema,
-
  issuesSchema,
-
} from "./project/issue.js";
-

-
import {
-
  patchSchema,
-
  patchesSchema,
-
  patchCreatedSchema,
-
} from "./project/patch.js";
-

-
const projectSchema = object({
-
  id: string(),
-
  name: string(),
-
  description: string(),
-
  defaultBranch: string(),
-
  delegates: array(object({ id: string(), alias: optional(string()) })),
-
  head: string(),
-
  threshold: number(),
-
  visibility: union([
-
    object({ type: literal("public") }),
-
    object({ type: literal("private"), allow: optional(array(string())) }),
-
  ]).optional(),
-
  patches: object({
-
    open: number(),
-
    draft: number(),
-
    archived: number(),
-
    merged: number(),
-
  }),
-
  issues: object({
-
    open: number(),
-
    closed: number(),
-
  }),
-
  seeding: number(),
-
});
-
const projectsSchema = array(projectSchema);
-

-
export type Project = z.infer<typeof projectSchema>;
-

-
const activitySchema = object({
-
  activity: array(number()),
-
});
-

-
export type Activity = z.infer<typeof activitySchema>;
-

-
const blobSchema = object({
-
  binary: boolean(),
-
  content: optional(string()),
-
  name: string(),
-
  path: string(),
-
  lastCommit: commitHeaderSchema,
-
});
-

-
export type Blob = z.infer<typeof blobSchema>;
-

-
const treeEntrySchema = object({
-
  path: string(),
-
  name: string(),
-
  oid: string(),
-
  kind: union([literal("blob"), literal("tree"), literal("submodule")]),
-
});
-

-
export type TreeEntry = z.infer<typeof treeEntrySchema>;
-

-
const treeStatsSchema = object({
-
  commits: number(),
-
  branches: number(),
-
  contributors: number(),
-
});
-

-
export type TreeStats = z.infer<typeof treeStatsSchema>;
-

-
export type Tree = z.infer<typeof treeSchema>;
-

-
const treeSchema = object({
-
  entries: array(treeEntrySchema),
-
  lastCommit: commitHeaderSchema,
-
  name: string(),
-
  path: string(),
-
});
-

-
export type Remote = z.infer<typeof remoteSchema>;
-

-
const remoteSchema = object({
-
  id: string(),
-
  alias: string().optional(),
-
  heads: record(string(), string()),
-
  delegate: boolean(),
-
});
-

-
const remotesSchema = array(remoteSchema) satisfies ZodSchema<Remote[]>;
-

-
export type DiffResponse = z.infer<typeof diffResponseSchema>;
-

-
const diffResponseSchema = object({
-
  commits: array(commitHeaderSchema),
-
  diff: diffSchema,
-
  files: record(string(), diffBlobSchema),
-
});
-

-
export type ProjectListQuery = {
-
  page?: number;
-
  perPage?: number;
-
  show?: "pinned" | "all";
-
};
-
export class Client {
-
  #fetcher: Fetcher;
-

-
  public constructor(fetcher: Fetcher) {
-
    this.#fetcher = fetcher;
-
  }
-

-
  public async getByDelegate(
-
    delegateId: string,
-
    options?: RequestOptions,
-
  ): Promise<Project[]> {
-
    return this.#fetcher.fetchOk(
-
      {
-
        method: "GET",
-
        path: `delegates/${delegateId}/projects`,
-
        options,
-
      },
-
      projectsSchema,
-
    );
-
  }
-

-
  public async getById(id: string, options?: RequestOptions): Promise<Project> {
-
    return this.#fetcher.fetchOk(
-
      {
-
        method: "GET",
-
        path: `projects/${id}`,
-
        options,
-
      },
-
      projectSchema,
-
    );
-
  }
-

-
  public async getAll(
-
    query?: ProjectListQuery,
-
    options?: RequestOptions,
-
  ): Promise<Project[]> {
-
    return this.#fetcher.fetchOk(
-
      {
-
        method: "GET",
-
        path: "projects",
-
        query,
-
        options,
-
      },
-
      projectsSchema,
-
    );
-
  }
-

-
  public async getActivity(
-
    id: string,
-
    options?: RequestOptions,
-
  ): Promise<Activity> {
-
    return this.#fetcher.fetchOk(
-
      {
-
        method: "GET",
-
        path: `projects/${id}/activity`,
-
        options,
-
      },
-
      activitySchema,
-
    );
-
  }
-

-
  public async getReadme(
-
    id: string,
-
    sha: string,
-
    options?: RequestOptions,
-
  ): Promise<Blob> {
-
    return this.#fetcher.fetchOk(
-
      {
-
        method: "GET",
-
        path: `projects/${id}/readme/${sha}`,
-
        options,
-
      },
-
      blobSchema,
-
    );
-
  }
-

-
  public async getBlob(
-
    id: string,
-
    sha: string,
-
    path: string,
-
    options?: RequestOptions,
-
  ): Promise<Blob> {
-
    const blob = await this.#fetcher.fetchOk(
-
      {
-
        method: "GET",
-
        path: `projects/${id}/blob/${sha}/${path}`,
-
        options,
-
      },
-
      blobSchema,
-
    );
-
    return blob;
-
  }
-

-
  public async getTree(
-
    id: string,
-
    sha: string,
-
    path?: string,
-
    options?: RequestOptions,
-
  ): Promise<Tree> {
-
    const tree = await this.#fetcher.fetchOk(
-
      {
-
        method: "GET",
-
        path: `projects/${id}/tree/${sha}/${path ?? ""}`,
-
        options,
-
      },
-
      treeSchema,
-
    );
-
    return tree;
-
  }
-

-
  public async getTreeStatsBySha(
-
    id: string,
-
    sha: string,
-
    options?: RequestOptions,
-
  ): Promise<TreeStats> {
-
    const tree = await this.#fetcher.fetchOk(
-
      {
-
        method: "GET",
-
        path: `projects/${id}/stats/tree/${sha}`,
-
        options,
-
      },
-
      treeStatsSchema,
-
    );
-
    return tree;
-
  }
-

-
  public async getAllRemotes(
-
    id: string,
-
    options?: RequestOptions,
-
  ): Promise<Remote[]> {
-
    return this.#fetcher.fetchOk(
-
      {
-
        method: "GET",
-
        path: `projects/${id}/remotes`,
-
        options,
-
      },
-
      remotesSchema,
-
    );
-
  }
-

-
  public async getRemoteByPeer(
-
    id: string,
-
    peer: string,
-
    options?: RequestOptions,
-
  ): Promise<Remote> {
-
    return this.#fetcher.fetchOk(
-
      {
-
        method: "GET",
-
        path: `projects/${id}/remotes/${peer}`,
-
        options,
-
      },
-
      remoteSchema,
-
    );
-
  }
-

-
  public async getAllCommits(
-
    id: string,
-
    query?: {
-
      parent?: string;
-
      since?: number;
-
      until?: number;
-
      page?: number;
-
      perPage?: number;
-
    },
-
    options?: RequestOptions,
-
  ): Promise<Commits> {
-
    return this.#fetcher.fetchOk(
-
      {
-
        method: "GET",
-
        path: `projects/${id}/commits`,
-
        query,
-
        options,
-
      },
-
      commitsSchema,
-
    );
-
  }
-

-
  public async getCommitBySha(
-
    id: string,
-
    sha: string,
-
    options?: RequestOptions,
-
  ): Promise<Commit> {
-
    return this.#fetcher.fetchOk(
-
      {
-
        method: "GET",
-
        path: `projects/${id}/commits/${sha}`,
-
        options,
-
      },
-
      commitSchema,
-
    );
-
  }
-

-
  public async getDiff(
-
    id: string,
-
    revisionBase: string,
-
    revisionOid: string,
-
    options?: RequestOptions,
-
  ): Promise<DiffResponse> {
-
    return this.#fetcher.fetchOk(
-
      {
-
        method: "GET",
-
        path: `projects/${id}/diff/${revisionBase}/${revisionOid}`,
-
        options,
-
      },
-
      diffResponseSchema,
-
    );
-
  }
-

-
  public async getIssueById(
-
    id: string,
-
    issueId: string,
-
    options?: RequestOptions,
-
  ): Promise<Issue> {
-
    return this.#fetcher.fetchOk(
-
      {
-
        method: "GET",
-
        path: `projects/${id}/issues/${issueId}`,
-
        options,
-
      },
-
      issueSchema,
-
    );
-
  }
-

-
  public async getAllIssues(
-
    id: string,
-
    query?: {
-
      page?: number;
-
      perPage?: number;
-
      state?: string;
-
    },
-
    options?: RequestOptions,
-
  ): Promise<Issue[]> {
-
    return this.#fetcher.fetchOk(
-
      {
-
        method: "GET",
-
        path: `projects/${id}/issues`,
-
        query,
-
        options,
-
      },
-
      issuesSchema,
-
    );
-
  }
-

-
  public async createIssue(
-
    id: string,
-
    body: {
-
      title: string;
-
      description: string;
-
      assignees: string[];
-
      embeds: Embed[];
-
      labels: string[];
-
    },
-
    authToken: string,
-
    options?: RequestOptions,
-
  ): Promise<IssueCreated> {
-
    return this.#fetcher.fetchOk(
-
      {
-
        method: "POST",
-
        path: `projects/${id}/issues`,
-
        headers: { Authorization: `Bearer ${authToken}` },
-
        body,
-
        options,
-
      },
-
      issueCreatedSchema,
-
    );
-
  }
-

-
  public async updateIssue(
-
    id: string,
-
    issueId: string,
-
    body: IssueUpdateAction,
-
    authToken: string,
-
    options?: RequestOptions,
-
  ): Promise<SuccessResponse> {
-
    return this.#fetcher.fetchOk(
-
      {
-
        method: "PATCH",
-
        path: `projects/${id}/issues/${issueId}`,
-
        headers: { Authorization: `Bearer ${authToken}` },
-
        body,
-
        options,
-
      },
-
      successResponseSchema,
-
    );
-
  }
-

-
  public async getPatchById(
-
    id: string,
-
    patchId: string,
-
    options?: RequestOptions,
-
  ): Promise<Patch> {
-
    return this.#fetcher.fetchOk(
-
      {
-
        method: "GET",
-
        path: `projects/${id}/patches/${patchId}`,
-
        options,
-
      },
-
      patchSchema,
-
    );
-
  }
-

-
  public async getAllPatches(
-
    id: string,
-
    query?: {
-
      page?: number;
-
      perPage?: number;
-
      state?: string;
-
    },
-
    options?: RequestOptions,
-
  ): Promise<Patch[]> {
-
    return this.#fetcher.fetchOk(
-
      {
-
        method: "GET",
-
        path: `projects/${id}/patches`,
-
        query,
-
        options,
-
      },
-
      patchesSchema,
-
    );
-
  }
-

-
  public async createPatch(
-
    id: string,
-
    body: PatchCreate,
-
    authToken: string,
-
    options?: RequestOptions,
-
  ): Promise<PatchCreated> {
-
    return this.#fetcher.fetchOk(
-
      {
-
        method: "POST",
-
        path: `projects/${id}/patches`,
-
        headers: { Authorization: `Bearer ${authToken}` },
-
        body,
-
        options,
-
      },
-
      patchCreatedSchema,
-
    );
-
  }
-

-
  public async updatePatch(
-
    id: string,
-
    patchId: string,
-
    body: PatchUpdateAction,
-
    authToken: string,
-
    options?: RequestOptions,
-
  ): Promise<SuccessResponse> {
-
    return this.#fetcher.fetchOk(
-
      {
-
        method: "PATCH",
-
        path: `projects/${id}/patches/${patchId}`,
-
        headers: { Authorization: `Bearer ${authToken}` },
-
        body,
-
        options,
-
      },
-
      successResponseSchema,
-
    );
-
  }
-
}
deleted httpd-client/lib/project/comment.ts
@@ -1,27 +0,0 @@
-
import type { z } from "zod";
-
import { array, boolean, number, object, string } from "zod";
-
import { authorSchema, codeLocationSchema } from "../shared";
-

-
export type Comment = z.infer<typeof commentSchema>;
-
export type Embed = Comment["embeds"][0];
-
export type Reaction = Comment["reactions"][0];
-

-
export const commentSchema = object({
-
  id: string(),
-
  author: authorSchema,
-
  body: string(),
-
  edits: array(
-
    object({
-
      author: authorSchema,
-
      body: string(),
-
      embeds: array(object({ name: string(), content: string() })),
-
      timestamp: number(),
-
    }),
-
  ),
-
  embeds: array(object({ name: string(), content: string() })),
-
  reactions: array(object({ emoji: string(), authors: array(authorSchema) })),
-
  timestamp: number(),
-
  location: codeLocationSchema.nullable().optional(),
-
  resolved: boolean(),
-
  replyTo: string().nullable(),
-
});
deleted httpd-client/lib/project/commit.ts
@@ -1,239 +0,0 @@
-
import type { z } from "zod";
-
export type {
-
  Commit,
-
  CommitHeader,
-
  Commits,
-
  Diff,
-
  DiffContent,
-
  DiffFile,
-
  ChangesetWithDiff,
-
  ChangesetWithoutDiff,
-
  HunkLine,
-
  Hunks,
-
};
-

-
import {
-
  array,
-
  boolean,
-
  discriminatedUnion,
-
  literal,
-
  number,
-
  object,
-
  record,
-
  string,
-
  union,
-
} from "zod";
-
export {
-
  commitBlobSchema,
-
  commitHeaderSchema,
-
  commitSchema,
-
  commitsSchema,
-
  diffBlobSchema,
-
  diffSchema,
-
};
-

-
const gitPersonSchema = object({
-
  name: string(),
-
  email: string(),
-
});
-

-
type CommitHeader = z.infer<typeof commitHeaderSchema>;
-

-
const commitHeaderSchema = object({
-
  id: string(),
-
  author: gitPersonSchema,
-
  summary: string(),
-
  description: string(),
-
  parents: array(string()),
-
  committer: gitPersonSchema.merge(object({ time: number() })),
-
});
-

-
const diffBlobSchema = object({
-
  binary: boolean(),
-
  content: string(),
-
  id: string(),
-
});
-

-
export type DiffBlob = z.infer<typeof diffBlobSchema>;
-

-
const commitBlobSchema = object({
-
  binary: boolean(),
-
  content: string(),
-
});
-

-
export type CommitBlob = z.infer<typeof commitBlobSchema>;
-

-
type AdditionHunkLine = z.infer<typeof additionHunkLineSchema>;
-

-
const additionHunkLineSchema = object({
-
  line: string(),
-
  lineNo: number(),
-
  type: literal("addition"),
-
});
-

-
type DeletionHunkLine = z.infer<typeof deletionHunkLineSchema>;
-

-
const deletionHunkLineSchema = object({
-
  line: string(),
-
  lineNo: number(),
-
  type: literal("deletion"),
-
});
-

-
type DiffFile = z.infer<typeof diffFileSchema>;
-

-
const diffFileSchema = object({
-
  oid: string(),
-
  mode: union([
-
    literal("blob"),
-
    literal("blobExecutable"),
-
    literal("tree"),
-
    literal("link"),
-
    literal("commit"),
-
  ]),
-
});
-

-
type ContextHunkLine = z.infer<typeof contextHunkLineSchema>;
-

-
const contextHunkLineSchema = object({
-
  line: string(),
-
  lineNoNew: number(),
-
  lineNoOld: number(),
-
  type: literal("context"),
-
});
-

-
type HunkLine = AdditionHunkLine | DeletionHunkLine | ContextHunkLine;
-

-
const hunkLineSchema = union([
-
  additionHunkLineSchema,
-
  deletionHunkLineSchema,
-
  contextHunkLineSchema,
-
]);
-

-
type Hunks = z.infer<typeof changesetHunkSchema>;
-

-
const changesetHunkSchema = object({
-
  header: string(),
-
  lines: array(hunkLineSchema),
-
});
-

-
type DiffContent = z.infer<typeof diffContentSchema>;
-

-
const diffContentSchema = discriminatedUnion("type", [
-
  object({
-
    type: literal("plain"),
-
    stats: object({
-
      additions: number(),
-
      deletions: number(),
-
    }),
-
    hunks: array(changesetHunkSchema),
-
    eof: union([
-
      literal("noneMissing"),
-
      literal("oldMissing"),
-
      literal("newMissing"),
-
      literal("bothMissing"),
-
    ]),
-
  }),
-
  object({ type: literal("binary") }),
-
  object({ type: literal("empty") }),
-
]);
-
const diffChangesetSchema = object({
-
  path: string(),
-
  diff: diffContentSchema,
-
});
-

-
const diffAddedChangesetSchema = diffChangesetSchema.merge(
-
  object({ state: literal("added"), new: diffFileSchema }),
-
);
-

-
const diffDeletedChangesetSchema = diffChangesetSchema.merge(
-
  object({ state: literal("deleted"), old: diffFileSchema }),
-
);
-

-
const diffModifiedChangesetSchema = diffChangesetSchema.merge(
-
  object({
-
    state: literal("modified"),
-
    new: diffFileSchema,
-
    old: diffFileSchema,
-
  }),
-
);
-

-
const diffCopiedChangesetSchema = object({
-
  state: literal("copied"),
-
  newPath: string(),
-
  oldPath: string(),
-
});
-

-
const diffCopiedWithModificationsChangesetSchema =
-
  diffCopiedChangesetSchema.merge(
-
    object({
-
      old: diffFileSchema,
-
      new: diffFileSchema,
-
      diff: diffContentSchema,
-
    }),
-
  );
-

-
const diffMovedChangesetSchema = object({
-
  state: literal("moved"),
-
  newPath: string(),
-
  oldPath: string(),
-
  current: diffFileSchema,
-
});
-

-
const diffMovedWithModificationsChangesetSchema = diffMovedChangesetSchema
-
  .omit({ current: true })
-
  .merge(
-
    object({
-
      old: diffFileSchema,
-
      new: diffFileSchema,
-
      diff: diffContentSchema,
-
    }),
-
  );
-

-
type Diff = z.infer<typeof diffSchema>;
-

-
type ChangesetWithDiff = z.infer<typeof changesetWithDiffSchema>;
-
type ChangesetWithoutDiff = z.infer<typeof changesetWithoutDiffSchema>;
-

-
const changesetWithDiffSchema = union([
-
  diffAddedChangesetSchema,
-
  diffDeletedChangesetSchema,
-
  diffModifiedChangesetSchema,
-
  diffMovedWithModificationsChangesetSchema,
-
  diffCopiedWithModificationsChangesetSchema,
-
]);
-
const changesetWithoutDiffSchema = union([
-
  diffMovedChangesetSchema,
-
  diffCopiedChangesetSchema,
-
]);
-

-
const diffSchema = object({
-
  files: array(
-
    union([
-
      diffAddedChangesetSchema,
-
      diffDeletedChangesetSchema,
-
      diffModifiedChangesetSchema,
-
      diffMovedChangesetSchema,
-
      diffMovedWithModificationsChangesetSchema,
-
      diffCopiedChangesetSchema,
-
      diffCopiedWithModificationsChangesetSchema,
-
    ]),
-
  ),
-
  stats: object({
-
    filesChanged: number(),
-
    insertions: number(),
-
    deletions: number(),
-
  }),
-
});
-

-
type Commit = z.infer<typeof commitSchema>;
-

-
const commitSchema = object({
-
  commit: commitHeaderSchema,
-
  diff: diffSchema,
-
  branches: array(string()),
-
  files: record(string(), commitBlobSchema),
-
});
-

-
type Commits = z.infer<typeof commitsSchema>;
-

-
const commitsSchema = array(commitHeaderSchema);
deleted httpd-client/lib/project/issue.ts
@@ -1,70 +0,0 @@
-
import type { Embed } from "./comment.js";
-
import type { ZodSchema, z } from "zod";
-
import { array, boolean, literal, object, string, union } from "zod";
-

-
import { commentSchema } from "./comment.js";
-
import { authorSchema } from "../shared.js";
-

-
export type IssueState =
-
  | { status: "open" }
-
  | { status: "closed"; reason: "other" | "solved" };
-

-
const issueStateSchema = union([
-
  object({ status: literal("open") }),
-
  object({
-
    status: literal("closed"),
-
    reason: union([literal("other"), literal("solved")]),
-
  }),
-
]) satisfies ZodSchema<IssueState>;
-

-
export const issueSchema = object({
-
  id: string(),
-
  author: authorSchema,
-
  title: string(),
-
  state: issueStateSchema,
-
  discussion: array(commentSchema),
-
  labels: array(string()),
-
  assignees: array(authorSchema),
-
});
-

-
export type Issue = z.infer<typeof issueSchema>;
-

-
export interface IssueCreated {
-
  success: boolean;
-
  id: string;
-
}
-

-
export const issueCreatedSchema = object({
-
  success: boolean(),
-
  id: string(),
-
}) satisfies ZodSchema<IssueCreated>;
-

-
export const issuesSchema = array(issueSchema) satisfies ZodSchema<Issue[]>;
-

-
export type IssueUpdateAction =
-
  | { type: "edit"; title: string }
-
  | { type: "label"; labels: string[] }
-
  | {
-
      type: "assign";
-
      assignees: string[];
-
    }
-
  | { type: "lifecycle"; state: IssueState }
-
  | {
-
      type: "comment";
-
      body: string;
-
      embeds?: Embed[];
-
      replyTo?: string;
-
    }
-
  | {
-
      type: "comment.edit";
-
      id: string;
-
      body: string;
-
      embeds: Embed[];
-
    }
-
  | { type: "comment.redact"; id: string }
-
  | {
-
      type: "comment.react";
-
      id: string;
-
      reaction: string;
-
      active: boolean;
-
    };
deleted httpd-client/lib/project/patch.ts
@@ -1,209 +0,0 @@
-
import type { Embed } from "./comment.js";
-
import type { ZodSchema, z } from "zod";
-
import type { CodeLocation } from "../shared.js";
-

-
import { commentSchema } from "./comment.js";
-

-
import {
-
  array,
-
  boolean,
-
  literal,
-
  number,
-
  optional,
-
  object,
-
  string,
-
  tuple,
-
  union,
-
} from "zod";
-
import { authorSchema } from "../shared.js";
-

-
export type PatchState = z.infer<typeof patchStateSchema>;
-

-
const patchStateSchema = union([
-
  object({
-
    status: literal("draft"),
-
  }),
-
  object({
-
    status: literal("open"),
-
    conflicts: array(tuple([string(), string()])).optional(),
-
  }),
-
  object({
-
    status: literal("archived"),
-
  }),
-
  object({
-
    status: literal("merged"),
-
    revision: string(),
-
    commit: string(),
-
  }),
-
]);
-

-
export type Merge = z.infer<typeof mergeSchema>;
-

-
const mergeSchema = object({
-
  author: authorSchema,
-
  revision: string(),
-
  commit: string(),
-
  timestamp: number(),
-
});
-

-
export type Verdict = "accept" | "reject";
-

-
const reviewSchema = object({
-
  author: authorSchema,
-
  verdict: optional(union([literal("accept"), literal("reject")]).nullable()),
-
  comments: array(commentSchema),
-
  summary: string().nullable(),
-
  timestamp: number(),
-
});
-

-
export type Review = z.infer<typeof reviewSchema>;
-

-
const revisionSchema = object({
-
  id: string(),
-
  author: authorSchema,
-
  description: string(),
-
  edits: array(
-
    object({
-
      author: authorSchema,
-
      body: string(),
-
      embeds: array(object({ name: string(), content: string() })),
-
      timestamp: number(),
-
    }),
-
  ),
-
  reactions: array(
-
    object({
-
      emoji: string(),
-
      authors: array(authorSchema),
-
    }),
-
  ),
-
  base: string(),
-
  oid: string(),
-
  refs: array(string()),
-
  discussions: array(commentSchema),
-
  reviews: array(reviewSchema),
-
  timestamp: number(),
-
});
-

-
export type Revision = z.infer<typeof revisionSchema>;
-

-
export const patchSchema = object({
-
  id: string(),
-
  author: authorSchema,
-
  title: string(),
-
  state: patchStateSchema,
-
  target: string(),
-
  labels: array(string()),
-
  merges: array(mergeSchema),
-
  assignees: array(string()),
-
  revisions: array(revisionSchema),
-
});
-

-
export type Patch = z.infer<typeof patchSchema>;
-

-
export const patchesSchema = array(patchSchema) satisfies ZodSchema<Patch[]>;
-

-
export type LifecycleState =
-
  | { status: "draft" }
-
  | { status: "open" }
-
  | { status: "archived" };
-

-
export type PatchUpdateAction =
-
  | { type: "edit"; title: string; target: "delegates" }
-
  | { type: "label"; labels: string[] }
-
  | { type: "assign"; assignees: string[] }
-
  | { type: "merge"; revision: string; commit: string }
-
  | { type: "lifecycle"; state: LifecycleState }
-
  | {
-
      type: "review";
-
      revision: string;
-
      summary?: string;
-
      verdict?: Verdict | null;
-
    }
-
  | { type: "review.edit"; review: string; summary?: string }
-
  | { type: "review.redact"; review: string }
-
  | {
-
      type: "review.comment";
-
      review: string;
-
      body: string;
-
      location: CodeLocation;
-
      replyTo?: string;
-
      embeds?: Embed[];
-
    }
-
  | {
-
      type: "review.comment.edit";
-
      review: string;
-
      comment: string;
-
      body: string;
-
      embeds: Embed[];
-
    }
-
  | {
-
      type: "review.comment.redact";
-
      review: string;
-
      comment: string;
-
    }
-
  | {
-
      type: "review.comment.react";
-
      review: string;
-
      comment: string;
-
      reaction: string;
-
      active: boolean;
-
    }
-
  | { type: "revision"; description: string; base: string; oid: string }
-
  | {
-
      type: "revision.edit";
-
      revision: string;
-
      description: string;
-
      embeds?: Embed[];
-
    }
-
  | {
-
      type: "revision.react";
-
      revision: string;
-
      reaction: string;
-
      location?: CodeLocation;
-
      active: boolean;
-
    }
-
  | { type: "revision.redact"; revision: string }
-
  | {
-
      type: "revision.comment";
-
      revision: string;
-
      body: string;
-
      embeds?: Embed[];
-
      location?: CodeLocation;
-
      replyTo?: string;
-
    }
-
  | {
-
      type: "revision.comment.edit";
-
      revision: string;
-
      comment: string;
-
      body: string;
-
      embeds: Embed[];
-
    }
-
  | {
-
      type: "revision.comment.redact";
-
      revision: string;
-
      comment: string;
-
    }
-
  | {
-
      type: "revision.comment.react";
-
      revision: string;
-
      comment: string;
-
      reaction: string;
-
      active: boolean;
-
    };
-

-
export const patchCreateSchema = object({
-
  title: string(),
-
  description: string(),
-
  target: string(),
-
  oid: string(),
-
  labels: array(string()),
-
});
-

-
export type PatchCreate = z.infer<typeof patchCreateSchema>;
-

-
export const patchCreatedSchema = object({
-
  success: boolean(),
-
  id: string(),
-
});
-

-
export type PatchCreated = z.infer<typeof patchCreatedSchema>;
deleted httpd-client/lib/session.ts
@@ -1,90 +0,0 @@
-
import type { Fetcher, RequestOptions } from "./fetcher.js";
-
import type { SuccessResponse } from "./shared.js";
-
import type { z } from "zod";
-

-
import { number, object, string } from "zod";
-

-
import { successResponseSchema } from "./shared.js";
-

-
export const sessionPayloadSchema = object({
-
  sessionId: string(),
-
  signature: string(),
-
  publicKey: string(),
-
});
-

-
export type SessionPayload = z.infer<typeof sessionPayloadSchema>;
-

-
const sessionSchema = object({
-
  sessionId: string(),
-
  status: string(),
-
  publicKey: string(),
-
  alias: string(),
-
  issuedAt: number(),
-
  expiresAt: number(),
-
});
-

-
export type Session = z.infer<typeof sessionSchema>;
-

-
export class Client {
-
  #fetcher: Fetcher;
-

-
  public constructor(fetcher: Fetcher) {
-
    this.#fetcher = fetcher;
-
  }
-

-
  public async getById(id: string, options?: RequestOptions): Promise<Session> {
-
    return this.#fetcher.fetchOk(
-
      {
-
        method: "GET",
-
        path: `sessions/${id}`,
-
        options,
-
      },
-
      sessionSchema,
-
    );
-
  }
-

-
  public async create(options?: RequestOptions): Promise<Session> {
-
    return this.#fetcher.fetchOk(
-
      {
-
        method: "POST",
-
        path: "sessions",
-
        options,
-
      },
-
      sessionSchema,
-
    );
-
  }
-

-
  public async update(
-
    id: string,
-
    body: {
-
      sig: string;
-
      pk: string;
-
    },
-
    options?: RequestOptions,
-
  ): Promise<SuccessResponse> {
-
    return this.#fetcher.fetchOk(
-
      {
-
        method: "PUT",
-
        path: `sessions/${id}`,
-
        body,
-
        options,
-
      },
-
      successResponseSchema,
-
    );
-
  }
-

-
  public async delete(
-
    id: string,
-
    options?: RequestOptions,
-
  ): Promise<SuccessResponse> {
-
    return this.#fetcher.fetchOk(
-
      {
-
        method: "DELETE",
-
        path: `sessions/${id}`,
-
        headers: { Authorization: `Bearer ${id}` },
-
        options,
-
      },
-
      successResponseSchema,
-
    );
-
  }
-
}
deleted httpd-client/lib/shared.ts
@@ -1,77 +0,0 @@
-
import type { ZodSchema, z } from "zod";
-

-
import { array, literal, number, object, string, union } from "zod";
-

-
export interface SuccessResponse {
-
  success: true;
-
}
-

-
export const successResponseSchema = object({
-
  success: literal(true),
-
}) satisfies ZodSchema<SuccessResponse>;
-

-
const policySchema = union([literal("allow"), literal("block")]);
-
const scopeSchema = union([literal("followed"), literal("all")]);
-

-
export const nodeConfigSchema = object({
-
  alias: string(),
-
  peers: union([
-
    object({ type: literal("static") }),
-
    object({ type: literal("dynamic"), target: number() }),
-
  ]),
-
  listen: array(string()),
-
  connect: array(string()),
-
  externalAddresses: array(string()),
-
  network: union([literal("main"), literal("test")]),
-
  relay: union([literal("always"), literal("never"), literal("auto")]),
-
  limits: object({
-
    routingMaxSize: number(),
-
    routingMaxAge: number(),
-
    fetchConcurrency: number(),
-
    gossipMaxAge: number(),
-
    maxOpenFiles: number(),
-
    rate: object({
-
      inbound: object({
-
        fillRate: number(),
-
        capacity: number(),
-
      }),
-
      outbound: object({
-
        fillRate: number(),
-
        capacity: number(),
-
      }),
-
    }),
-
  }),
-
  policy: policySchema,
-
  scope: scopeSchema,
-
});
-

-
export type Policy = z.infer<typeof policySchema>;
-
export type Scope = z.infer<typeof scopeSchema>;
-

-
export const rangeSchema = union([
-
  object({
-
    type: literal("lines"),
-
    range: object({ start: number(), end: number() }),
-
  }),
-
  object({
-
    type: literal("chars"),
-
    line: number(),
-
    range: object({ start: number(), end: number() }),
-
  }),
-
]);
-

-
export type Range = z.infer<typeof rangeSchema>;
-

-
export const codeLocationSchema = object({
-
  commit: string(),
-
  path: string(),
-
  old: rangeSchema.nullable(),
-
  new: rangeSchema.nullable(),
-
});
-

-
export type CodeLocation = z.infer<typeof codeLocationSchema>;
-

-
export const authorSchema = object({
-
  id: string(),
-
  alias: string().optional(),
-
});
deleted httpd-client/tests/client.test.ts
@@ -1,24 +0,0 @@
-
import { describe, test } from "vitest";
-

-
import { HttpdClient } from "@httpd-client";
-
import { defaultHttpdPort } from "@tests/support/fixtures";
-

-
const api = new HttpdClient({
-
  hostname: "127.0.0.1",
-
  port: defaultHttpdPort,
-
  scheme: "http",
-
});
-

-
describe("client", () => {
-
  test("#getNodeInfo()", async () => {
-
    await api.getNodeInfo();
-
  });
-

-
  test("#getStats()", async () => {
-
    await api.getStats();
-
  });
-

-
  test("#getNode()", async () => {
-
    await api.getNode();
-
  });
-
});
deleted httpd-client/tests/project.test.ts
@@ -1,291 +0,0 @@
-
import { describe, test } from "vitest";
-

-
import { HttpdClient } from "@httpd-client";
-
import {
-
  aliceMainHead,
-
  aliceRemote,
-
  bobRemote,
-
  cobRid,
-
  defaultHttpdPort,
-
  sourceBrowsingRid,
-
} from "@tests/support/fixtures.js";
-
import {
-
  assertIssue,
-
  assertPatch,
-
  createIssueToBeModified,
-
  createPatchToBeModified,
-
} from "@httpd-client/tests/support/support";
-
import { authenticate } from "@httpd-client/tests/support/httpd.js";
-
import { testFixture as testWithAPI } from "@httpd-client/tests/support/fixtures.js";
-

-
describe("project", () => {
-
  const api = new HttpdClient({
-
    hostname: "127.0.0.1",
-
    port: defaultHttpdPort,
-
    scheme: "http",
-
  });
-

-
  test("#getByDelegate(delegateId)", async () => {
-
    await api.project.getByDelegate(aliceRemote);
-
  });
-

-
  test("#getAll()", async () => {
-
    await api.project.getAll();
-
  });
-

-
  test("#getById(id)", async () => {
-
    await api.project.getById(sourceBrowsingRid);
-
  });
-

-
  test("#getActivity(id)", async () => {
-
    await api.project.getActivity(sourceBrowsingRid);
-
  });
-

-
  test("#getReadme(id, sha)", async () => {
-
    await api.project.getReadme(sourceBrowsingRid, aliceMainHead);
-
  });
-

-
  test("#getBlob(id, sha, path)", async () => {
-
    await api.project.getBlob(sourceBrowsingRid, aliceMainHead, "src/true.c");
-
  });
-

-
  test("#getTree(id, sha)", async () => {
-
    await api.project.getTree(sourceBrowsingRid, aliceMainHead);
-
  });
-

-
  test("#getTreeStats(id, sha)", async () => {
-
    await api.project.getTreeStatsBySha(sourceBrowsingRid, aliceMainHead);
-
  });
-

-
  test("#getTree(id, sha, path)", async () => {
-
    await api.project.getTree(sourceBrowsingRid, aliceMainHead, "src");
-
  });
-

-
  test("#getAllRemotes(id)", async () => {
-
    await api.project.getAllRemotes(sourceBrowsingRid);
-
  });
-

-
  test("#getRemoteByPeer(id, peer)", async () => {
-
    await api.project.getRemoteByPeer(
-
      sourceBrowsingRid,
-
      aliceRemote.substring(8),
-
    );
-
  });
-

-
  test("#getAllCommits(id)", async () => {
-
    await api.project.getAllCommits(sourceBrowsingRid);
-
  });
-

-
  // TODO: test since/until properly.
-
  test("#getAllCommits(id, {parent, since, until, page, perPage})", async () => {
-
    await api.project.getAllCommits(sourceBrowsingRid, {
-
      parent: aliceMainHead,
-
      since: 1679065819581,
-
      until: 1679065819590,
-
      page: 1,
-
      perPage: 2,
-
    });
-
  });
-

-
  test("#getCommitBySha(id, sha)", async () => {
-
    await api.project.getCommitBySha(sourceBrowsingRid, aliceMainHead);
-
  });
-

-
  test("#getDiff(id, revisionBase, revisionOid)", async () => {
-
    await api.project.getDiff(
-
      sourceBrowsingRid,
-
      "90f6d058ece12f75f349bc7bbe88142187fe0379",
-
      aliceMainHead,
-
    );
-
  });
-

-
  test("#getIssueById(id, issueId)", async () => {
-
    await api.project.getIssueById(
-
      cobRid,
-
      "d481fe6e562dd78129589d4738f171a8380fcb19",
-
    );
-
  });
-

-
  test("#getAllIssues(id)", async () => {
-
    await api.project.getAllIssues(cobRid, {
-
      page: 0,
-
      perPage: 5,
-
      state: "open",
-
    });
-
  });
-

-
  testWithAPI(
-
    "#createIssue(id, { title, description, assignees, labels })",
-
    async ({ httpd: { api, peer } }) => {
-
      const sessionId = await authenticate(api, peer);
-
      const { id: issueId } = await api.project.createIssue(
-
        cobRid,
-
        {
-
          title: "aaa",
-
          description: "bbb",
-
          assignees: [],
-
          embeds: [],
-
          labels: ["bug", "documentation"],
-
        },
-
        sessionId,
-
      );
-
      await assertIssue(
-
        issueId,
-
        {
-
          title: "aaa",
-
          discussion: [{ body: "bbb" }],
-
          assignees: [],
-
          labels: ["bug", "documentation"],
-
        },
-
        api,
-
      );
-
    },
-
  );
-

-
  testWithAPI(
-
    "#updateIssue(id, issueId, { type: 'edit' }, authToken)",
-
    async ({ httpd: { api, peer } }) => {
-
      const sessionId = await authenticate(api, peer);
-
      const issueId = await createIssueToBeModified(api, sessionId);
-
      await api.project.updateIssue(
-
        cobRid,
-
        issueId,
-
        { type: "edit", title: "ccc" },
-
        sessionId,
-
      );
-
      await assertIssue(issueId, { title: "ccc" }, api);
-
    },
-
  );
-

-
  testWithAPI(
-
    "#updateIssue(id, issueId, { type: 'label' }, authToken)",
-
    async ({ httpd: { api, peer } }) => {
-
      const sessionId = await authenticate(api, peer);
-
      const issueId = await createIssueToBeModified(api, sessionId);
-
      await api.project.updateIssue(
-
        cobRid,
-
        issueId,
-
        { type: "label", labels: ["bug"] },
-
        sessionId,
-
      );
-
      await assertIssue(issueId, { labels: ["bug"] }, api);
-
    },
-
  );
-

-
  testWithAPI(
-
    "#updateIssue(id, issueId, { type: 'assign' }, authToken)",
-
    async ({ httpd: { api, peer } }) => {
-
      const sessionId = await authenticate(api, peer);
-
      const issueId = await createIssueToBeModified(api, sessionId);
-
      await api.project.updateIssue(
-
        cobRid,
-
        issueId,
-
        {
-
          type: "assign",
-
          assignees: [bobRemote],
-
        },
-
        sessionId,
-
      );
-
      await assertIssue(
-
        issueId,
-
        {
-
          assignees: [
-
            {
-
              id: "did:key:z6Mkg49NtQR2LyYRDCQFK4w1VVHqhypZSSRo7HsyuN7SV7v5",
-
              alias: "bob",
-
            },
-
          ],
-
        },
-
        api,
-
      );
-
    },
-
  );
-

-
  testWithAPI(
-
    "#updateIssue(id, issueId, { type: 'lifecycle' }, authToken)",
-
    async ({ httpd: { api, peer } }) => {
-
      const sessionId = await authenticate(api, peer);
-
      const issueId = await createIssueToBeModified(api, sessionId);
-
      await api.project.updateIssue(
-
        cobRid,
-
        issueId,
-
        { type: "lifecycle", state: { status: "closed", reason: "solved" } },
-
        sessionId,
-
      );
-
      await assertIssue(
-
        issueId,
-
        {
-
          state: { status: "closed", reason: "solved" },
-
        },
-
        api,
-
      );
-
    },
-
  );
-

-
  test("#getPatchById(id, patchId)", async () => {
-
    await api.project.getPatchById(
-
      cobRid,
-
      "59a0821edc73630bce540596cffc7854da557365",
-
    );
-
  });
-

-
  test("#getAllPatches(id)", async () => {
-
    await api.project.getAllPatches(cobRid);
-
  });
-

-
  testWithAPI(
-
    "#createPatch(id, patchCreate, authToken)",
-
    async ({ httpd: { api, peer } }) => {
-
      const sessionId = await authenticate(api, peer);
-
      const { id: oid } = await api.project.createPatch(
-
        cobRid,
-
        {
-
          title: "ppp",
-
          description: "qqq",
-
          target: "d7dd8cecae16b1108234e09dbdb5d64ae394bc25",
-
          oid: "38c225e2a0b47ba59def211f4e4825c31d9463ec",
-
          labels: [],
-
        },
-
        sessionId,
-
      );
-
      await assertPatch(
-
        oid,
-
        {
-
          title: "ppp",
-
          state: { status: "open" },
-
          target: "delegates",
-
          labels: [],
-
          revisions: [
-
            {
-
              description: "qqq",
-
              base: "d7dd8cecae16b1108234e09dbdb5d64ae394bc25",
-
              oid: "38c225e2a0b47ba59def211f4e4825c31d9463ec",
-
            },
-
          ],
-
        },
-
        api,
-
      );
-
    },
-
  );
-

-
  testWithAPI(
-
    "#updatePatch(id, patchId, { type: 'edit' }, authToken)",
-
    async ({ httpd: { api, peer } }) => {
-
      const sessionId = await authenticate(api, peer);
-
      const patchId = await createPatchToBeModified(api, sessionId);
-
      await api.project.updatePatch(
-
        cobRid,
-
        patchId,
-
        { type: "label", labels: ["bug"] },
-
        sessionId,
-
      );
-
      await assertPatch(
-
        patchId,
-
        {
-
          labels: ["bug"],
-
        },
-
        api,
-
      );
-
    },
-
  );
-
});
deleted httpd-client/tests/session.test.ts
@@ -1,38 +0,0 @@
-
import * as FsSync from "node:fs";
-
import * as Path from "node:path";
-
import { describe, test } from "vitest";
-

-
import { HttpdClient } from "@httpd-client";
-
import { authenticate } from "@httpd-client/tests/support/httpd.js";
-
import { createPeerManager } from "@tests/support/peerManager.js";
-
import { gitOptions } from "@tests/support/fixtures.js";
-
import { tmpDir } from "@tests/support/support.js";
-

-
describe("session", async () => {
-
  const peerManager = await createPeerManager({
-
    dataDir: Path.resolve(Path.join(tmpDir, "peers")),
-
    outputLog: FsSync.createWriteStream(
-
      Path.join(tmpDir, "peerManager.log"),
-
    ).setMaxListeners(16),
-
  });
-
  const peer = await peerManager.createPeer({
-
    name: "session",
-
    gitOptions: gitOptions["alice"],
-
  });
-
  await peer.startHttpd();
-
  const api = new HttpdClient(peer.httpdBaseUrl);
-

-
  test("#getById(id)", async () => {
-
    const id = await authenticate(api, peer);
-
    await api.session.getById(id);
-
  });
-

-
  test("#update(id, {sig, pk})", async () => {
-
    await authenticate(api, peer);
-
  });
-

-
  test("#delete(id)", async () => {
-
    const id = await authenticate(api, peer);
-
    await api.session.delete(id);
-
  });
-
});
deleted httpd-client/tests/support/fixtures.ts
@@ -1,32 +0,0 @@
-
import * as FsSync from "node:fs";
-
import * as Path from "node:path";
-
import { test } from "vitest";
-

-
import { HttpdClient } from "@httpd-client";
-
import { RadiclePeer, createPeerManager } from "@tests/support/peerManager.js";
-
import { gitOptions } from "@tests/support/fixtures.js";
-
import { tmpDir } from "@tests/support/support.js";
-

-
interface TestFixtures {
-
  httpd: { api: HttpdClient; peer: RadiclePeer };
-
}
-

-
export const testFixture = test.extend<TestFixtures>({
-
  // eslint-disable-next-line no-empty-pattern
-
  httpd: async ({}, use) => {
-
    const peerManager = await createPeerManager({
-
      dataDir: Path.resolve(Path.join(tmpDir, "peers")),
-
      outputLog: FsSync.createWriteStream(
-
        Path.join(tmpDir, "peerManager.log"),
-
      ).setMaxListeners(16),
-
    });
-
    const peer = await peerManager.createPeer({
-
      name: "palm",
-
      gitOptions: gitOptions["alice"],
-
    });
-
    await peer.startHttpd();
-
    const api = new HttpdClient(peer.httpdBaseUrl);
-
    await use({ api, peer });
-
    await peer.shutdown();
-
  },
-
});
deleted httpd-client/tests/support/httpd.ts
@@ -1,30 +0,0 @@
-
import type { HttpdClient } from "@httpd-client";
-
import type { RadiclePeer } from "@tests/support/peerManager.js";
-

-
import assert from "node:assert";
-

-
export async function authenticate(
-
  api: HttpdClient,
-
  peer: RadiclePeer,
-
): Promise<string> {
-
  const { stdout } = await peer.spawn("rad-web", [
-
    "http://localhost:3001",
-
    "--no-open",
-
    "--connect",
-
    `${peer.httpdBaseUrl.hostname}:${peer.httpdBaseUrl.port}`,
-
  ]);
-
  const match = stdout.match(/Visit (http:\/\/\S+) to connect/);
-
  assert(
-
    match !== null && match[1],
-
    `Failed to get authentication URL from: ${stdout}`,
-
  );
-

-
  const authUrl = new URL(match[1]);
-
  const sessionId = authUrl.pathname.split("/")[2];
-

-
  await api.session.update(sessionId, {
-
    sig: authUrl.searchParams.get("sig")!,
-
    pk: authUrl.searchParams.get("pk")!,
-
  });
-
  return sessionId;
-
}
deleted httpd-client/tests/support/support.ts
@@ -1,59 +0,0 @@
-
import type { HttpdClient } from "@httpd-client";
-

-
import { expect } from "vitest";
-
import isMatch from "lodash/isMatch";
-

-
import { cobRid } from "@tests/support/fixtures";
-

-
export async function createIssueToBeModified(
-
  api: HttpdClient,
-
  sessionId: string,
-
) {
-
  const { id } = await api.project.createIssue(
-
    cobRid,
-
    { title: "aaa", description: "bbb", embeds: [], assignees: [], labels: [] },
-
    sessionId,
-
  );
-

-
  return id;
-
}
-

-
export async function createPatchToBeModified(
-
  api: HttpdClient,
-
  sessionId: string,
-
) {
-
  const { id } = await api.project.createPatch(
-
    cobRid,
-
    {
-
      title: "rrr",
-
      description: "ttt",
-
      target: "d7dd8cecae16b1108234e09dbdb5d64ae394bc25",
-
      oid: "38c225e2a0b47ba59def211f4e4825c31d9463ec",
-
      labels: [],
-
    },
-
    sessionId,
-
  );
-

-
  return id;
-
}
-
export async function assertIssue(
-
  oid: string,
-
  change: Record<string, unknown>,
-
  api: HttpdClient,
-
) {
-
  expect(
-
    //@prettier-ignore looks more readable than what prettier suggests.
-
    isMatch(await api.project.getIssueById(cobRid, oid), change),
-
  ).toBe(true);
-
}
-

-
export async function assertPatch(
-
  oid: string,
-
  change: Record<string, unknown>,
-
  api: HttpdClient,
-
) {
-
  expect(
-
    //@prettier-ignore looks more readable than what prettier suggests.
-
    isMatch(await api.project.getPatchById(cobRid, oid), change),
-
  ).toBe(true);
-
}
deleted httpd-client/vite.config.ts
@@ -1,25 +0,0 @@
-
import nodeConfig from "config";
-
import path from "node:path";
-
import virtual from "vite-plugin-virtual";
-
import { defineConfig } from "vite";
-

-
export default defineConfig({
-
  plugins: [
-
    virtual({
-
      "virtual:config": nodeConfig.util.toObject(),
-
    }),
-
  ],
-
  test: {
-
    environment: "node",
-
    include: ["httpd-client/tests/*.test.ts"],
-
    reporters: "verbose",
-
    globalSetup: "./tests/support/globalSetup",
-
  },
-
  resolve: {
-
    alias: {
-
      "@tests": path.resolve("./tests"),
-
      "@app": path.resolve("./src"),
-
      "@httpd-client": path.resolve("./httpd-client"),
-
    },
-
  },
-
});
modified package.json
@@ -11,7 +11,7 @@
    "format": "npx prettier '**/*.@(ts|js|svelte|json|css|html|yml)' --write",
    "test:unit": "TZ='UTC' vitest run",
    "test:e2e": "NODE_CONFIG_ENV='test' TZ='UTC' playwright test",
-
    "test:httpd-api:unit": "NODE_CONFIG_ENV='test' TZ='UTC' vitest run --config httpd-client/vite.config.ts --reporter verbose"
+
    "test:http-client:unit": "NODE_CONFIG_ENV='test' TZ='UTC' vitest run --config http-client/vite.config.ts --reporter verbose"
  },
  "type": "module",
  "engines": {
modified src/App/Header/Breadcrumbs/NodeSegment.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { BaseUrl } from "@httpd-client";
+
  import type { BaseUrl } from "@http-client";

  import { isLocal } from "@app/lib/utils";

modified src/components/Comment.svelte
@@ -1,5 +1,5 @@
<script lang="ts" strictEvents>
-
  import type { Comment, Embed } from "@httpd-client";
+
  import type { Comment, Embed } from "@http-client";

  import { tick } from "svelte";

modified src/components/CommentToggleInput.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { Embed } from "@httpd-client";
+
  import type { Embed } from "@http-client";

  import ExtendedTextarea from "@app/components/ExtendedTextarea.svelte";

modified src/components/CompactCommitAuthorship.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { CommitHeader } from "@httpd-client";
+
  import type { CommitHeader } from "@http-client";

  import * as utils from "@app/lib/utils";
  import HoverPopover from "./HoverPopover.svelte";
modified src/components/ExtendedTextarea.svelte
@@ -1,5 +1,5 @@
<script lang="ts" strictEvents>
-
  import type { Embed } from "@httpd-client";
+
  import type { Embed } from "@http-client";

  import { createEventDispatcher } from "svelte";

modified src/components/Markdown.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { Embed } from "@httpd-client";
+
  import type { Embed } from "@http-client";

  import dompurify from "dompurify";
  import matter from "@radicle/gray-matter";
modified src/components/ProjectCard.ts
@@ -1,4 +1,4 @@
-
import type { ProjectListQuery } from "@httpd-client";
+
import type { ProjectListQuery } from "@http-client";

import { loadProjectActivity, type WeeklyActivity } from "@app/lib/commit";
import {
@@ -6,7 +6,7 @@ import {
  type BaseUrl,
  type Commit,
  type Project,
-
} from "@httpd-client";
+
} from "@http-client";

export interface ProjectInfo {
  project: Project;
modified src/components/ReactionSelector.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { Comment } from "@httpd-client";
+
  import type { Comment } from "@http-client";

  import { createEventDispatcher } from "svelte";

modified src/components/Reactions.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { Comment } from "@httpd-client";
+
  import type { Comment } from "@http-client";

  import IconButton from "./IconButton.svelte";

modified src/components/ScopePolicyExplainer.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { Scope, Policy } from "@httpd-client";
+
  import type { Scope, Policy } from "@http-client";

  import { capitalize } from "lodash";

modified src/components/Thread.svelte
@@ -1,6 +1,6 @@
<script lang="ts" strictEvents>
-
  import type { Embed } from "@httpd-client";
-
  import type { Comment } from "@httpd-client";
+
  import type { Embed } from "@http-client";
+
  import type { Comment } from "@http-client";

  import * as utils from "@app/lib/utils";
  import partial from "lodash/partial";
modified src/lib/commit.ts
@@ -1,7 +1,7 @@
-
import type { BaseUrl, CommitHeader } from "@httpd-client";
+
import type { BaseUrl, CommitHeader } from "@http-client";

import { getDaysPassed } from "@app/lib/utils";
-
import { HttpdClient } from "@httpd-client";
+
import { HttpdClient } from "@http-client";

// A set of commits grouped by time.
interface CommitGroup {
modified src/lib/file.ts
@@ -1,4 +1,4 @@
-
import type { Embed } from "@httpd-client";
+
import type { Embed } from "@http-client";

async function parseGitOid(bytes: Uint8Array): Promise<string> {
  // Create the header
modified src/lib/httpd.ts
@@ -1,9 +1,9 @@
-
import type { Node, Policy, Scope } from "@httpd-client";
+
import type { Node, Policy, Scope } from "@http-client";

import { get, writable } from "svelte/store";
import { withTimeout, Mutex, E_CANCELED, E_TIMEOUT } from "async-mutex";

-
import { HttpdClient } from "@httpd-client";
+
import { HttpdClient } from "@http-client";
import config from "virtual:config";
import { deduplicateStore } from "@app/lib/deduplicateStore";
import { experimental } from "./appearance";
modified src/lib/projects.ts
@@ -1,6 +1,6 @@
-
import type { BaseUrl, Project } from "@httpd-client";
+
import type { BaseUrl, Project } from "@http-client";

-
import { HttpdClient } from "@httpd-client";
+
import { HttpdClient } from "@http-client";
import { isFulfilled } from "@app/lib/utils";

export interface ProjectBaseUrl {
modified src/lib/router.ts
@@ -1,4 +1,4 @@
-
import type { BaseUrl } from "@httpd-client";
+
import type { BaseUrl } from "@http-client";
import type { LoadedRoute, Route } from "@app/lib/router/definitions";

import { get, writable } from "svelte/store";
modified src/lib/router/definitions.ts
@@ -1,7 +1,7 @@
import type {
  ResponseError,
  ResponseParseError,
-
} from "@httpd-client/lib/fetcher";
+
} from "@http-client/lib/fetcher";
import type { HomeRoute, HomeLoadedRoute } from "@app/views/home/router";
import type {
  ProjectLoadedRoute,
modified src/lib/seeds.ts
@@ -1,4 +1,4 @@
-
import type { BaseUrl } from "@httpd-client";
+
import type { BaseUrl } from "@http-client";

import storedWritable from "@efstajas/svelte-stored-writable";
import unionBy from "lodash/unionBy";
modified src/lib/utils.ts
@@ -1,4 +1,4 @@
-
import type { BaseUrl } from "@httpd-client";
+
import type { BaseUrl } from "@http-client";

import md5 from "md5";
import bs58 from "bs58";
modified src/views/home/Index.svelte
@@ -1,7 +1,7 @@
<script lang="ts">
  import type { ComponentProps } from "svelte";
  import type { ProjectInfo } from "@app/components/ProjectCard";
-
  import type { BaseUrl, ProjectListQuery } from "@httpd-client";
+
  import type { BaseUrl, ProjectListQuery } from "@http-client";

  import storedWritable from "@efstajas/svelte-stored-writable";
  import { derived } from "svelte/store";
modified src/views/home/components/PreferredSeedDropdown.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import { HttpdClient, type BaseUrl } from "@httpd-client";
+
  import { HttpdClient, type BaseUrl } from "@http-client";

  import config from "virtual:config";
  import {
modified src/views/home/error.ts
@@ -1,4 +1,4 @@
-
import { ResponseParseError, ResponseError } from "@httpd-client/lib/fetcher";
+
import { ResponseParseError, ResponseError } from "@http-client/lib/fetcher";

export function handleError(
  error: Error | ResponseParseError | ResponseError,
modified src/views/home/router.ts
@@ -1,4 +1,4 @@
-
import type { BaseUrl } from "@httpd-client";
+
import type { BaseUrl } from "@http-client";
import type { ErrorRoute } from "@app/lib/router/definitions";

import * as seeds from "@app/lib/seeds";
modified src/views/nodes/ScopePolicyPopover.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { Policy, Scope } from "@httpd-client";
+
  import type { Policy, Scope } from "@http-client";

  import capitalize from "lodash/capitalize";

modified src/views/nodes/View.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { BaseUrl, NodeStats, Policy, Scope } from "@httpd-client";
+
  import type { BaseUrl, NodeStats, Policy, Scope } from "@http-client";

  import { capitalize } from "lodash";

modified src/views/nodes/error.ts
@@ -1,5 +1,5 @@
import type { ErrorRoute, NotFoundRoute } from "@app/lib/router/definitions";
-
import { ResponseParseError, ResponseError } from "@httpd-client/lib/fetcher";
+
import { ResponseParseError, ResponseError } from "@http-client/lib/fetcher";

export function handleError(
  error: Error | ResponseParseError | ResponseError,
modified src/views/nodes/router.ts
@@ -1,9 +1,9 @@
-
import type { BaseUrl, NodeStats, Policy, Scope } from "@httpd-client";
+
import type { BaseUrl, NodeStats, Policy, Scope } from "@http-client";
import type { ErrorRoute, NotFoundRoute } from "@app/lib/router/definitions";

import config from "virtual:config";
-
import { HttpdClient } from "@httpd-client";
-
import { ResponseError, ResponseParseError } from "@httpd-client/lib/fetcher";
+
import { HttpdClient } from "@http-client";
+
import { ResponseError, ResponseParseError } from "@http-client/lib/fetcher";
import { baseUrlToString } from "@app/lib/utils";
import { handleError } from "@app/views/nodes/error";
import { unreachableError } from "@app/views/projects/error";
modified src/views/projects/Changeset.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { BaseUrl, CommitBlob, Diff } from "@httpd-client";
+
  import type { BaseUrl, CommitBlob, Diff } from "@http-client";

  import FileDiff from "@app/views/projects/Changeset/FileDiff.svelte";
  import FileLocationChange from "@app/views/projects/Changeset/FileLocationChange.svelte";
modified src/views/projects/Changeset/FileDiff.svelte
@@ -4,7 +4,7 @@
    ChangesetWithDiff,
    DiffContent,
    HunkLine,
-
  } from "@httpd-client";
+
  } from "@http-client";

  import { onDestroy, onMount } from "svelte";
  import { toHtml } from "hast-util-to-html";
modified src/views/projects/Changeset/FileLocationChange.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { BaseUrl, ChangesetWithoutDiff } from "@httpd-client";
+
  import type { BaseUrl, ChangesetWithoutDiff } from "@http-client";

  import Badge from "@app/components/Badge.svelte";
  import IconButton from "@app/components/IconButton.svelte";
modified src/views/projects/Cob/AssigneeInput.svelte
@@ -1,5 +1,5 @@
<script lang="ts" strictEvents>
-
  import type { Reaction } from "@httpd-client";
+
  import type { Reaction } from "@http-client";

  import { createEventDispatcher } from "svelte";

modified src/views/projects/Cob/CobCommitTeaser.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { BaseUrl, CommitHeader } from "@httpd-client";
+
  import type { BaseUrl, CommitHeader } from "@http-client";

  import { twemoji } from "@app/lib/utils";

modified src/views/projects/Cob/Embeds.svelte
@@ -1,5 +1,5 @@
<script lang="ts" strictEvents>
-
  import type { Embed } from "@httpd-client";
+
  import type { Embed } from "@http-client";

  import Badge from "@app/components/Badge.svelte";
  import Clipboard from "@app/components/Clipboard.svelte";
modified src/views/projects/Cob/Revision.svelte
@@ -7,11 +7,11 @@
    PatchState,
    Revision,
    Verdict,
-
  } from "@httpd-client";
+
  } from "@http-client";
  import type { Timeline } from "@app/views/projects/Patch.svelte";

  import * as utils from "@app/lib/utils";
-
  import { HttpdClient } from "@httpd-client";
+
  import { HttpdClient } from "@http-client";
  import { closeFocused } from "@app/components/Popover.svelte";
  import { onMount } from "svelte";
  import { parseEmbedIntoMap } from "@app/lib/file";
modified src/views/projects/Commit.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { BaseUrl, Commit, Node, Project } from "@httpd-client";
+
  import type { BaseUrl, Commit, Node, Project } from "@http-client";

  import { formatCommit } from "@app/lib/utils";

modified src/views/projects/Commit/CommitAuthorship.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { CommitHeader } from "@httpd-client";
+
  import type { CommitHeader } from "@http-client";

  import {
    absoluteTimestamp,
modified src/views/projects/Commit/CommitTeaser.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { BaseUrl, CommitHeader } from "@httpd-client";
+
  import type { BaseUrl, CommitHeader } from "@http-client";

  import { twemoji } from "@app/lib/utils";

modified src/views/projects/DiffStatBadgeLoader.svelte
@@ -1,7 +1,7 @@
<script lang="ts">
-
  import type { BaseUrl, Patch, Revision } from "@httpd-client";
+
  import type { BaseUrl, Patch, Revision } from "@http-client";

-
  import { HttpdClient } from "@httpd-client";
+
  import { HttpdClient } from "@http-client";
  import { formatCommit } from "@app/lib/utils";

  import DiffStatBadge from "@app/components/DiffStatBadge.svelte";
modified src/views/projects/Header.svelte
@@ -3,7 +3,7 @@
</script>

<script lang="ts">
-
  import type { BaseUrl, Project } from "@httpd-client";
+
  import type { BaseUrl, Project } from "@http-client";

  import Link from "@app/components/Link.svelte";
  import Button from "@app/components/Button.svelte";
modified src/views/projects/Header/CloneButton.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { BaseUrl } from "@httpd-client";
+
  import type { BaseUrl } from "@http-client";

  import config from "virtual:config";
  import { parseRepositoryId } from "@app/lib/utils";
modified src/views/projects/History.svelte
@@ -6,11 +6,11 @@
    Remote,
    Node,
    Tree,
-
  } from "@httpd-client";
+
  } from "@http-client";
  import type { Route } from "@app/lib/router";

  import { COMMITS_PER_PAGE } from "./router";
-
  import { HttpdClient } from "@httpd-client";
+
  import { HttpdClient } from "@http-client";
  import { baseUrlToString } from "@app/lib/utils";
  import { groupCommits } from "@app/lib/commit";

modified src/views/projects/Issue.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { Reaction } from "@httpd-client/lib/project/comment";
+
  import type { Reaction } from "@http-client/lib/project/comment";
  import type {
    BaseUrl,
    Comment,
@@ -8,7 +8,7 @@
    IssueState,
    Project,
    Node,
-
  } from "@httpd-client";
+
  } from "@http-client";
  import type { Session } from "@app/lib/httpd";

  import capitalize from "lodash/capitalize";
@@ -21,7 +21,7 @@
  import * as router from "@app/lib/router";
  import * as utils from "@app/lib/utils";
  import { experimental } from "@app/lib/appearance";
-
  import { HttpdClient } from "@httpd-client";
+
  import { HttpdClient } from "@http-client";
  import { closeFocused } from "@app/components/Popover.svelte";
  import { httpdStore } from "@app/lib/httpd";
  import { parseEmbedIntoMap } from "@app/lib/file";
modified src/views/projects/Issue/IssueTeaser.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { BaseUrl, Issue } from "@httpd-client";
+
  import type { BaseUrl, Issue } from "@http-client";

  import {
    absoluteTimestamp,
modified src/views/projects/Issue/New.svelte
@@ -1,10 +1,10 @@
<script lang="ts">
-
  import type { BaseUrl, Embed, Node, Project, Reaction } from "@httpd-client";
+
  import type { BaseUrl, Embed, Node, Project, Reaction } from "@http-client";

  import * as modal from "@app/lib/modal";
  import * as router from "@app/lib/router";
  import * as utils from "@app/lib/utils";
-
  import { HttpdClient } from "@httpd-client";
+
  import { HttpdClient } from "@http-client";
  import { httpdStore } from "@app/lib/httpd";

  import AssigneeInput from "@app/views/projects/Cob/AssigneeInput.svelte";
modified src/views/projects/Issues.svelte
@@ -1,14 +1,8 @@
<script lang="ts">
-
  import type {
-
    BaseUrl,
-
    Issue,
-
    IssueState,
-
    Node,
-
    Project,
-
  } from "@httpd-client";
+
  import type { BaseUrl, Issue, IssueState, Node, Project } from "@http-client";

  import capitalize from "lodash/capitalize";
-
  import { HttpdClient } from "@httpd-client";
+
  import { HttpdClient } from "@http-client";
  import { ISSUES_PER_PAGE } from "./router";
  import { baseUrlToString, isLocal } from "@app/lib/utils";
  import { closeFocused } from "@app/components/Popover.svelte";
modified src/views/projects/Layout.svelte
@@ -1,6 +1,6 @@
<script lang="ts">
  import type { ActiveTab } from "./Header.svelte";
-
  import type { BaseUrl, Node, Project } from "@httpd-client";
+
  import type { BaseUrl, Node, Project } from "@http-client";

  import AppHeader from "@app/App/Header.svelte";

modified src/views/projects/Patch.svelte
@@ -9,7 +9,7 @@
    Revision,
    Diff,
    Node,
-
  } from "@httpd-client";
+
  } from "@http-client";

  interface Thread {
    root: Comment;
@@ -42,7 +42,7 @@
</script>

<script lang="ts">
-
  import type { BaseUrl, Embed, Patch } from "@httpd-client";
+
  import type { BaseUrl, Embed, Patch } from "@http-client";
  import type { PatchView } from "./router";
  import type { Route } from "@app/lib/router";
  import type { ComponentProps } from "svelte";
@@ -57,7 +57,7 @@
  import isEqual from "lodash/isEqual";
  import partial from "lodash/partial";
  import uniqBy from "lodash/uniqBy";
-
  import { HttpdClient } from "@httpd-client";
+
  import { HttpdClient } from "@http-client";
  import { httpdStore } from "@app/lib/httpd";
  import { parseEmbedIntoMap } from "@app/lib/file";

modified src/views/projects/Patch/PatchTeaser.svelte
@@ -1,6 +1,6 @@
<script lang="ts">
-
  import type { BaseUrl } from "@httpd-client";
-
  import type { Patch } from "@httpd-client";
+
  import type { BaseUrl } from "@http-client";
+
  import type { Patch } from "@http-client";

  import {
    absoluteTimestamp,
modified src/views/projects/Patch/RevisionSelector.svelte
@@ -1,6 +1,6 @@
<script lang="ts">
  import type { PatchView } from "../router";
-
  import type { BaseUrl, Patch, Project } from "@httpd-client";
+
  import type { BaseUrl, Patch, Project } from "@http-client";
  import * as utils from "@app/lib/utils";

  import Button from "@app/components/Button.svelte";
modified src/views/projects/Patches.svelte
@@ -1,13 +1,7 @@
<script lang="ts">
-
  import type {
-
    BaseUrl,
-
    Node,
-
    Patch,
-
    PatchState,
-
    Project,
-
  } from "@httpd-client";
-

-
  import { HttpdClient } from "@httpd-client";
+
  import type { BaseUrl, Node, Patch, PatchState, Project } from "@http-client";
+

+
  import { HttpdClient } from "@http-client";
  import capitalize from "lodash/capitalize";

  import { PATCHES_PER_PAGE } from "./router";
modified src/views/projects/Share.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { BaseUrl } from "@httpd-client";
+
  import type { BaseUrl } from "@http-client";

  import debounce from "lodash/debounce";
  import { api, httpdStore } from "@app/lib/httpd";
modified src/views/projects/Sidebar.svelte
@@ -1,6 +1,6 @@
<script lang="ts">
  import type { ActiveTab } from "./Header.svelte";
-
  import type { BaseUrl, Node, Project } from "@httpd-client";
+
  import type { BaseUrl, Node, Project } from "@http-client";

  import { onMount } from "svelte";

modified src/views/projects/Sidebar/ContextHelp.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { BaseUrl } from "@httpd-client";
+
  import type { BaseUrl } from "@http-client";
  import type { Route } from "@app/lib/router/definitions";

  import { activeUnloadedRouteStore } from "@app/lib/router";
modified src/views/projects/Sidebar/ContextRepo.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { BaseUrl, Node, Project } from "@httpd-client";
+
  import type { BaseUrl, Node, Project } from "@http-client";

  import IconButton from "@app/components/IconButton.svelte";
  import IconSmall from "@app/components/IconSmall.svelte";
modified src/views/projects/Source.svelte
@@ -1,9 +1,9 @@
<script lang="ts">
-
  import type { BaseUrl, Node, Project, Remote, Tree } from "@httpd-client";
+
  import type { BaseUrl, Node, Project, Remote, Tree } from "@http-client";
  import type { BlobResult } from "./router";
  import type { Route } from "@app/lib/router";

-
  import { HttpdClient } from "@httpd-client";
+
  import { HttpdClient } from "@http-client";

  import Button from "@app/components/Button.svelte";
  import Header from "./Source/Header.svelte";
modified src/views/projects/Source/Blob.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { BaseUrl, Blob } from "@httpd-client";
+
  import type { BaseUrl, Blob } from "@http-client";

  import { afterUpdate, onDestroy, onMount } from "svelte";
  import { toHtml } from "hast-util-to-html";
modified src/views/projects/Source/BranchSelector.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { BaseUrl, Commit, Project } from "@httpd-client";
+
  import type { BaseUrl, Commit, Project } from "@http-client";
  import type { Route } from "@app/lib/router";

  import { activeUnloadedRouteStore } from "@app/lib/router";
modified src/views/projects/Source/Header.svelte
@@ -1,8 +1,8 @@
<script lang="ts">
-
  import type { BaseUrl, Project, Remote, Tree } from "@httpd-client";
+
  import type { BaseUrl, Project, Remote, Tree } from "@http-client";
  import type { Route } from "@app/lib/router";

-
  import { HttpdClient } from "@httpd-client";
+
  import { HttpdClient } from "@http-client";

  import BranchSelector from "./BranchSelector.svelte";
  import PeerSelector from "./PeerSelector.svelte";
modified src/views/projects/Source/PeerSelector.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { BaseUrl, Project, Remote } from "@httpd-client";
+
  import type { BaseUrl, Project, Remote } from "@http-client";
  import type { Route } from "@app/lib/router";

  import { closeFocused } from "@app/components/Popover.svelte";
modified src/views/projects/Source/ProjectNameHeader.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { BaseUrl, Project } from "@httpd-client";
+
  import type { BaseUrl, Project } from "@http-client";

  import { twemoji } from "@app/lib/utils";

modified src/views/projects/Source/Tree.svelte
@@ -1,5 +1,5 @@
<script lang="ts" strictEvents>
-
  import type { BaseUrl, Tree } from "@httpd-client";
+
  import type { BaseUrl, Tree } from "@http-client";

  import { createEventDispatcher } from "svelte";

modified src/views/projects/Source/Tree/Folder.svelte
@@ -1,5 +1,5 @@
<script lang="ts" strictEvents>
-
  import type { BaseUrl, Tree } from "@httpd-client";
+
  import type { BaseUrl, Tree } from "@http-client";

  import { createEventDispatcher } from "svelte";

modified src/views/projects/components/CommitButton.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { BaseUrl, Commit } from "@httpd-client";
+
  import type { BaseUrl, Commit } from "@http-client";

  import Button from "@app/components/Button.svelte";
  import Link from "@app/components/Link.svelte";
modified src/views/projects/components/CommitLink.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { BaseUrl } from "@httpd-client";
+
  import type { BaseUrl } from "@http-client";

  import { formatCommit } from "@app/lib/utils";
  import Link from "@app/components/Link.svelte";
modified src/views/projects/error.ts
@@ -2,7 +2,7 @@ import type { ErrorRoute, NotFoundRoute } from "@app/lib/router/definitions";
import type { ProjectRoute } from "@app/views/projects/router";

import { baseUrlToString, isLocal } from "@app/lib/utils";
-
import { ResponseParseError, ResponseError } from "@httpd-client/lib/fetcher";
+
import { ResponseParseError, ResponseError } from "@http-client/lib/fetcher";
import { httpdStore } from "@app/lib/httpd";
import { get } from "svelte/store";

modified src/views/projects/router.ts
@@ -19,12 +19,12 @@ import type {
  Project,
  Remote,
  Tree,
-
} from "@httpd-client";
+
} from "@http-client";

import * as Syntax from "@app/lib/syntax";
import * as httpd from "@app/lib/httpd";
-
import { HttpdClient } from "@httpd-client";
-
import { ResponseError, ResponseParseError } from "@httpd-client/lib/fetcher";
+
import { HttpdClient } from "@http-client";
+
import { ResponseError, ResponseParseError } from "@http-client/lib/fetcher";
import { experimental } from "@app/lib/appearance";
import { get } from "svelte/store";
import { handleError, unreachableError } from "@app/views/projects/error";
modified tests/support/peerManager.ts
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/naming-convention */
-
import type { BaseUrl } from "@httpd-client";
+
import type { BaseUrl } from "@http-client";
import type * as Execa from "execa";
import { execa } from "execa";
import * as Fs from "node:fs/promises";
modified tsconfig.json
@@ -1,6 +1,6 @@
{
  "extends": "@tsconfig/svelte/tsconfig.json",
-
  "include": ["src", "tests", "httpd-client", "./*.js", "./*.ts"],
+
  "include": ["src", "tests", "http-client", "./*.js", "./*.ts"],
  "exclude": ["node_modules/*"],
  "compilerOptions": {
    "noEmit": true,
@@ -20,8 +20,8 @@
    "skipLibCheck": true,
    "paths": {
      "@app/*": ["./src/*"],
-
      "@httpd-client": ["./httpd-client/index.ts"],
-
      "@httpd-client/*": ["./httpd-client/*"],
+
      "@http-client": ["./http-client/index.ts"],
+
      "@http-client/*": ["./http-client/*"],
      "@public/*": ["./public/*"],
      "@tests/*": ["./tests/*"]
    }
modified vite.config.ts
@@ -39,7 +39,7 @@ export default defineConfig({
    alias: {
      "@app": path.resolve("./src"),
      "@public": path.resolve("./public"),
-
      "@httpd-client": path.resolve("./httpd-client"),
+
      "@http-client": path.resolve("./http-client"),
      "@tests": path.resolve("./tests"),
    },
  },