Radish alpha
r
rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5
Radicle web interface
Radicle
Git
Update seeding policy schema
Sebastian Martinez committed 1 year ago
commit e326f48cbf3add89c5816b78e186c40e7007a52f
parent 37a88bb
13 files changed +136 -95
modified config/default.json
@@ -1,7 +1,7 @@
{
  "nodes": {
    "fallbackPublicExplorer": "https://app.radicle.xyz/nodes/$host/$rid$path",
-
    "apiVersion": "1.0.0",
+
    "apiVersion": "1.1.0",
    "defaultHttpdPort": 443,
    "defaultLocalHttpdPort": 8080,
    "defaultHttpdHostname": "seed.radicle.garden",
modified http-client/index.ts
@@ -12,8 +12,7 @@ import type {
  SuccessResponse,
  CodeLocation,
  Range,
-
  Policy,
-
  Scope,
+
  DefaultSeedingPolicy,
} from "./lib/shared.js";
import type { Comment, Embed, Reaction } from "./lib/project/comment.js";
import type {
@@ -48,7 +47,11 @@ 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";
+
import {
+
  nodeConfigSchema,
+
  scopeSchema,
+
  successResponseSchema,
+
} from "./lib/shared.js";

export type {
  BaseUrl,
@@ -60,6 +63,7 @@ export type {
  Commit,
  CommitBlob,
  CommitHeader,
+
  DefaultSeedingPolicy,
  Diff,
  DiffBlob,
  DiffContent,
@@ -74,7 +78,6 @@ export type {
  Patch,
  PatchState,
  PatchUpdateAction,
-
  Policy,
  Project,
  ProjectListQuery,
  Range,
@@ -82,7 +85,6 @@ export type {
  Remote,
  Review,
  Revision,
-
  Scope,
  TreeStats,
  Tree,
  Verdict,
@@ -120,15 +122,18 @@ const nodeInfoSchema = object({
  ),
});

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

-
const nodeTrackingSchema = array(
-
  object({
-
    id: string(),
-
    scope: string(),
-
    policy: string(),
-
  }),
-
);
+
const nodePoliciesSchema = object({
+
  rid: string(),
+
  policy: union([
+
    object({ policy: literal("block") }),
+
    object({
+
      policy: literal("allow"),
+
      scope: scopeSchema,
+
    }),
+
  ]),
+
});

export interface NodeStats {
  repos: { total: number };
@@ -192,14 +197,28 @@ export class HttpdClient {
    );
  }

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

+
  public async getPoliciesById(
+
    id: string,
+
    options?: RequestOptions,
+
  ): Promise<NodePolicies["policy"][]> {
+
    return this.#fetcher.fetchOk(
+
      {
+
        method: "GET",
+
        path: `node/policies/repos/${id}`,
+
        options,
+
      },
+
      array(nodePoliciesSchema.shape.policy),
    );
  }

modified http-client/lib/shared.ts
@@ -10,8 +10,17 @@ 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 scopeSchema = union([literal("followed"), literal("all")]);
+

+
const defaultSeedingPolicySchema = union([
+
  object({
+
    default: literal("block"),
+
  }),
+
  object({
+
    default: literal("allow"),
+
    scope: scopeSchema,
+
  }),
+
]);

export const nodeConfigSchema = object({
  alias: string(),
@@ -61,14 +70,10 @@ export const nodeConfigSchema = object({
    }),
  }),
  workers: number(),
-
  seedingPolicy: object({
-
    default: policySchema,
-
    scope: scopeSchema.optional(),
-
  }),
+
  seedingPolicy: defaultSeedingPolicySchema,
});

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

export const rangeSchema = union([
  object({
modified src/App/Header/NodeInfo.svelte
@@ -46,21 +46,23 @@
        Your node is running and syncing with the network.
      </div>

-
      {#if node.scope && node.policy}
+
      {#if node.seedingPolicy}
        <div class="scope-policy">
          <div style:display="flex">
            Seeding Policy: <span style:margin-left="auto" class="txt-semibold">
-
              {capitalize(node.policy)}
-
            </span>
-
          </div>
-
          <div style:display="flex" style:margin-bottom="1rem">
-
            Scope:
-
            <span style:margin-left="auto" class="txt-semibold">
-
              {capitalize(node.scope)}
+
              {capitalize(node.seedingPolicy.default)}
            </span>
          </div>
+
          {#if node.seedingPolicy.default === "allow"}
+
            <div style:display="flex" style:margin-bottom="1rem">
+
              Scope:
+
              <span style:margin-left="auto" class="txt-semibold">
+
                {capitalize(node.seedingPolicy.scope)}
+
              </span>
+
            </div>
+
          {/if}

-
          <ScopePolicyExplainer scope={node.scope} policy={node.policy} />
+
          <ScopePolicyExplainer seedingPolicy={node.seedingPolicy} />
        </div>
      {/if}
      <div class="label">
modified src/components/ScopePolicyExplainer.svelte
@@ -1,10 +1,9 @@
<script lang="ts">
-
  import type { Scope, Policy } from "@http-client";
+
  import type { DefaultSeedingPolicy } from "@http-client";

  import { capitalize } from "lodash";

-
  export let scope: Scope | undefined = "all";
-
  export let policy: Policy;
+
  export let seedingPolicy: DefaultSeedingPolicy;
</script>

<style>
@@ -16,26 +15,28 @@
  }
</style>

-
<div class="section">
-
  Scope:
-
  <span class="txt-bold">{capitalize(scope)}</span>
-
</div>
-
<div class="txt-missing">
-
  {#if scope === "all"}
-
    All changes in seeded repositories, made by any peer, will be synced.
-
  {:else if scope === "followed"}
-
    Only changes made by explicitly followed peers will be synced.
-
  {/if}
-
</div>
+
{#if seedingPolicy.default === "allow"}
+
  <div class="section">
+
    Scope:
+
    <span class="txt-bold">{capitalize(seedingPolicy.scope)}</span>
+
  </div>
+
  <div class="txt-missing">
+
    {#if seedingPolicy.scope === "all"}
+
      All changes in seeded repositories, made by any peer, will be synced.
+
    {:else if seedingPolicy.scope === "followed"}
+
      Only changes made by explicitly followed peers will be synced.
+
    {/if}
+
  </div>
+
{/if}

<div class="section">
  Policy:
-
  <span class="txt-bold">{capitalize(policy)}</span>
+
  <span class="txt-bold">{capitalize(seedingPolicy.default)}</span>
</div>
<div class="txt-missing">
-
  {#if policy === "allow"}
+
  {#if seedingPolicy.default === "allow"}
    All discovered repositories will get seeded.
-
  {:else if policy === "block"}
+
  {:else if seedingPolicy.default === "block"}
    Only repositories marked as such will get seeded.
  {/if}
</div>
modified src/lib/httpd.ts
@@ -1,4 +1,4 @@
-
import type { Node, Policy, Scope } from "@http-client";
+
import type { DefaultSeedingPolicy, Node } from "@http-client";

import { get, writable } from "svelte/store";
import { withTimeout, Mutex, E_CANCELED, E_TIMEOUT } from "async-mutex";
@@ -17,8 +17,7 @@ export interface Session {
export interface HttpdNodeState {
  id: Node["id"];
  state: Node["state"];
-
  policy: Policy | undefined;
-
  scope: Scope | undefined;
+
  seedingPolicy: DefaultSeedingPolicy | undefined;
}

export type HttpdState =
@@ -86,8 +85,7 @@ export async function authenticate(params: {
        node: {
          id,
          state,
-
          policy: config?.seedingPolicy.default,
-
          scope: config?.seedingPolicy.scope,
+
          seedingPolicy: config?.seedingPolicy,
        },
      });
      return true;
@@ -116,8 +114,7 @@ export async function disconnect() {
          node: {
            id,
            state,
-
            policy: config?.seedingPolicy.default,
-
            scope: config?.seedingPolicy.scope,
+
            seedingPolicy: config?.seedingPolicy,
          },
        });
      } catch (error) {
@@ -155,8 +152,7 @@ async function checkState() {
        const node = {
          id,
          state,
-
          policy: config?.seedingPolicy.default,
-
          scope: config?.seedingPolicy.scope,
+
          seedingPolicy: config?.seedingPolicy,
        };

        if (httpdState && httpdState.state === "authenticated") {
modified src/lib/utils.ts
@@ -1,4 +1,4 @@
-
import type { BaseUrl } from "@http-client";
+
import type { BaseUrl, DefaultSeedingPolicy } from "@http-client";

import md5 from "md5";
import bs58 from "bs58";
@@ -13,6 +13,14 @@ export function formatLocationHash(hash: string | null): number | null {
  return null;
}

+
export function formatShortSeedingPolicy(
+
  policy: DefaultSeedingPolicy | undefined,
+
) {
+
  return policy?.default === "allow" && policy?.scope === "all"
+
    ? "permissive"
+
    : "restrictive";
+
}
+

export function parseNodeId(
  nid: string,
): { prefix: string; pubkey: string } | undefined {
modified src/views/nodes/ScopePolicyPopover.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { Policy, Scope } from "@http-client";
+
  import type { DefaultSeedingPolicy } from "@http-client";

  import capitalize from "lodash/capitalize";

@@ -8,8 +8,7 @@
  import IconButton from "@app/components/IconButton.svelte";
  import Popover from "@app/components/Popover.svelte";

-
  export let policy: Policy;
-
  export let scope: Scope;
+
  export let seedingPolicy: DefaultSeedingPolicy;
  export let popoverPositionRight: string | undefined = undefined;
  export let popoverPositionLeft: string | undefined = undefined;
</script>
@@ -34,15 +33,17 @@

<div class="container">
  <span>
-
    Seeding Policy: <span class="txt-semibold">
-
      {capitalize(policy)}
+
    Policy: <span class="txt-semibold">
+
      {capitalize(seedingPolicy.default)}
    </span>
  </span>
-
  <span class="separator" />
-
  <span>
-
    Scope:
-
    <span class="txt-semibold">{capitalize(scope)}</span>
-
  </span>
+
  {#if seedingPolicy.default === "allow"}
+
    <span class="separator" />
+
    <span>
+
      Scope:
+
      <span class="txt-semibold">{capitalize(seedingPolicy.scope)}</span>
+
    </span>
+
  {/if}
  <span style:color="var(--color-fill-gray)">
    <Popover
      {popoverPositionRight}
@@ -55,7 +56,7 @@
      </IconButton>

      <div slot="popover" class="popover">
-
        <ScopePolicyExplainer {scope} {policy} />
+
        <ScopePolicyExplainer {seedingPolicy} />
      </div>
    </Popover>
  </span>
modified src/views/nodes/View.svelte
@@ -1,11 +1,16 @@
<script lang="ts">
-
  import type { BaseUrl, NodeStats, Policy, Scope } from "@http-client";
+
  import type { BaseUrl, DefaultSeedingPolicy, NodeStats } from "@http-client";

  import { capitalize } from "lodash";

  import * as router from "@app/lib/router";
  import { api, httpdStore } from "@app/lib/httpd";
-
  import { baseUrlToString, isLocal, truncateId } from "@app/lib/utils";
+
  import {
+
    baseUrlToString,
+
    formatShortSeedingPolicy,
+
    isLocal,
+
    truncateId,
+
  } from "@app/lib/utils";
  import { fetchProjectInfos } from "@app/components/ProjectCard";
  import { handleError } from "@app/views/nodes/error";
  import { isDelegate } from "@app/lib/roles";
@@ -24,11 +29,9 @@
  export let stats: NodeStats;
  export let externalAddresses: string[];
  export let version: string;
-
  export let policy: Policy | undefined = undefined;
-
  export let scope: Scope | undefined = undefined;
+
  export let seedingPolicy: DefaultSeedingPolicy | undefined = undefined;

-
  $: shortScope =
-
    scope === "all" && policy === "allow" ? "permissive" : "restrictive";
+
  $: shortScope = formatShortSeedingPolicy(seedingPolicy);
  $: hostname = isLocal(baseUrl.hostname) ? "Local Node" : baseUrl.hostname;
  $: session =
    $httpdStore.state === "authenticated" && isLocal(api.baseUrl.hostname)
@@ -157,7 +160,7 @@
          {isLocal(baseUrl.hostname) ? "Seeded" : "Pinned"} repositories
        </div>
        <div class="seeding-policy">
-
          {#if policy && scope}
+
          {#if seedingPolicy}
            <span class="txt-bold">Seeding Policy:</span>
            {capitalize(shortScope)}
            <div class="global-hide-on-mobile-down">
@@ -168,7 +171,7 @@
                <IconButton slot="toggle" let:toggle on:click={toggle}>
                  <IconSmall name="help" />
                </IconButton>
-
                <ScopePolicyExplainer slot="popover" {scope} {policy} />
+
                <ScopePolicyExplainer slot="popover" {seedingPolicy} />
              </Popover>
            </div>
          {/if}
modified src/views/nodes/router.ts
@@ -1,4 +1,4 @@
-
import type { BaseUrl, NodeStats, Policy, Scope } from "@http-client";
+
import type { BaseUrl, DefaultSeedingPolicy, NodeStats } from "@http-client";
import type { ErrorRoute, NotFoundRoute } from "@app/lib/router/definitions";

import config from "virtual:config";
@@ -26,8 +26,7 @@ export interface NodesLoadedRoute {
    externalAddresses: string[];
    nid: string;
    stats: NodeStats;
-
    policy?: Policy;
-
    scope?: Scope;
+
    seedingPolicy?: DefaultSeedingPolicy;
  };
}

@@ -56,8 +55,7 @@ export async function loadNodeRoute(
        stats,
        externalAddresses: node.config?.externalAddresses ?? [],
        version: node.version,
-
        policy: node.config?.seedingPolicy.default,
-
        scope: node.config?.seedingPolicy.scope,
+
        seedingPolicy: node.config?.seedingPolicy,
      },
    };
  } catch (error) {
modified src/views/projects/Sidebar/ContextRepo.svelte
@@ -2,7 +2,7 @@
  import type { BaseUrl, Node, Project } from "@http-client";

  import { capitalize } from "lodash";
-
  import { isLocal } from "@app/lib/utils";
+
  import { formatShortSeedingPolicy, isLocal } from "@app/lib/utils";

  import IconButton from "@app/components/IconButton.svelte";
  import IconSmall from "@app/components/IconSmall.svelte";
@@ -17,11 +17,7 @@

  let expandedNode = false;

-
  $: shortSeedingPolicy =
-
    node.config?.seedingPolicy.scope === "all" &&
-
    node.config?.seedingPolicy.default === "allow"
-
      ? "permissive"
-
      : "restrictive";
+
  $: shortSeedingPolicy = formatShortSeedingPolicy(node.config?.seedingPolicy);
</script>

<style>
@@ -116,9 +112,7 @@
    </div>
    {#if expandedNode && node.config?.seedingPolicy}
      <div style:padding-left="2.3rem">
-
        <ScopePolicyExplainer
-
          scope={node.config.seedingPolicy.scope}
-
          policy={node.config.seedingPolicy.default} />
+
        <ScopePolicyExplainer seedingPolicy={node.config.seedingPolicy} />
      </div>
    {/if}
  </div>
modified src/views/projects/router.ts
@@ -259,8 +259,8 @@ async function isLocalNodeSeeding(route: ProjectRoute): Promise<boolean> {
    return false;
  }
  try {
-
    const tracking = await httpd.api.getTracking();
-
    return tracking.some(({ id }) => id === route.project);
+
    const policies = await httpd.api.getPolicies();
+
    return policies.some(({ rid }) => rid === route.project);
  } catch (error) {
    if (error instanceof ResponseError && error.status === 404) {
      return false;
modified tests/unit/utils.test.ts
@@ -11,6 +11,20 @@ describe("Format functions", () => {

  test.each([
    {
+
      input: { default: "allow", scope: "all" },
+
      expected: "permissive",
+
    } as const,
+
    {
+
      input: { default: "allow", scope: "followed" },
+
      expected: "restrictive",
+
    } as const,
+
    { input: { default: "block" }, expected: "restrictive" } as const,
+
  ])("formatShortSeedingPolicy $input => $expected", ({ input, expected }) => {
+
    expect(utils.formatShortSeedingPolicy(input)).toEqual(expected);
+
  });
+

+
  test.each([
+
    {
      id: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT",
      expected: "rad:zKtT7D…19WzjT",
    },