開発者コンソール

クイックスタート

署名の依頼(Intent)をサーバで登録します。エンドユーザーは確認画面(Connect)で Passkey を確認し、アプリケーションの URL に戻ります。完了は、サーバが status.state の completed を読んだときです。

開発者コンソール

登録の前に用意する組織(Org)、App、ホスト、秘密鍵です。

  1. 1. 組織を作成します。

  2. 2. Org を開き、プランの署名で購入します(プラン面は /developers/{orgId}です)。

  3. 3. App を登録します(/developers/{orgId}/apps/new)。ホストは Org の apex 配下の 1 ラベルです(例: app.example.com)。

  4. 4. App 詳細に出る TXT を、DNS にそのまま置きます。

  5. 5. 同じ画面で「ホストを確認」を押します。

  6. 6. App 詳細の用途で、誰が使うかと、署名をどの行為に使うかを書いて保存します。

  7. 7. operate 秘密鍵をサーバに置きます。

Intent が通るとき

次が揃った Org の App だけが、署名の Intent を登録できます。

  • 契約者の Passkey があります(Org のプラン面)。
  • 署名を購入しています(Org のプラン面)。
  • App のホスト確認が通っています(App 詳細の状態が「有効」)。
  • App の用途に署名があります。
  • 当月の上限に達していません(プラン面の残り)。

CLI / エージェント

同じ開始を @clavis.nexus/setup の CLI または MCP で進めます。Passkey・カード決済・DNS レコードの追加は人間が行います。ツール応答の needs_human を見ます。

  1. 1. npm install -g @clavis.nexus/setup のあと clavis を呼びます(または npx -y @clavis.nexus/setup)。

  2. 2. clavis login のあと、ブラウザで Passkey を確認します。

  3. 3. clavis status で足りない段を見ます。

  4. 4. 足りない段を org-anchor-*、card-checkout、app-register、app-host-*、declaration-set の順で埋めます。

  5. 5. MCP は clavis mcp です。完了後、 clavis emit-env で Intent 用の環境変数を出します。

手順

サーバで登録し、ブラウザで Connect を開き、戻ったあと completed を読みます。

  1. 1. 検証済みホスト上のサーバに npm install @clavis.nexus/app を入れます。

  2. 2. ブラウザに npm install @clavis.nexus/app-shell を入れます。

  3. 3. サーバで登録し、ブラウザへ JSON を返します。

    digest(アプリ側)
    function canonicalStringify(value: unknown): string {
      if (value === undefined) throw new Error("canonical_json_undefined");
      if (typeof value === "number" && !Number.isFinite(value)) throw new Error("canonical_json_non_finite");
      if (value === null || typeof value !== "object") return JSON.stringify(value);
      if (Array.isArray(value)) return `[${value.map((item) => canonicalStringify(item)).join(",")}]`;
      const record = value as Record<string, unknown>;
      const keys = Object.keys(record).sort();
      return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalStringify(record[key])}`).join(",")}}`;
    }
    
    async function sha256Base64Url(text: string): Promise<string> {
      const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text)));
      let bin = "";
      for (const byte of digest) bin += String.fromCharCode(byte);
      return btoa(bin).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/g, "");
    }
    
    const messageDigest = await sha256Base64Url(canonicalStringify({ v: 1, action: "example" }));
    TypeScript(サーバ)
    import { createAppClient } from "@clavis.nexus/app";
    
    function requiredEnv(name: string): string {
      const value = process.env[name];
      if (!value) throw new Error(`missing env: ${name}`);
      return value;
    }
    
    const app = createAppClient({
      apiOrigin: "https://api.clavis.nexus",
      appId: requiredEnv("CLAVIS_APP_ID"),
      operatePrivateJwk: requiredEnv("OPERATE_PRIVATE_JWK"),
      masterPublicJwkJson: requiredEnv("MASTER_PUBLIC_JWK_JSON"),
    });
    
    const { requestId, pollSecret, consumeUrl } = await app.registerGenericSign({
      messageDigest,
      purpose: "generic.sign",
      returnUrl: "https://console.example/done",
    });
    
    await store.set(requestId, { pollSecret, messageDigest });
    return Response.json({ consume_url: consumeUrl, request_id: requestId });
    • CLAVIS_APP_ID
      App の id です。
    • OPERATE_PRIVATE_JWK
      Ed25519 秘密 JWK の JSON です。サーバだけが持ちます。
    • MASTER_PUBLIC_JWK_JSON
      https://api.clavis.nexus/.well-known/jwks.json をブラウザで開き、master オブジェクトを JSON 文字列にして保存します。
    • messageDigest
      この署名で固定した digest です。
    • returnUrl
      検証済みホスト配下の https です。
    • store
      戻り先のリクエストから読める session または KV です。鍵は request_id です。

    サーバがブラウザへ返す JSON。

    {
      "consume_url": "https://api.clavis.nexus/app/intent-requests/consume/550e8400-e29b-41d4-a716-446655440000",
      "request_id": "550e8400-e29b-41d4-a716-446655440000"
    }
    • consume_urlURL必須
      ブラウザが開きます。開くと Connect に入ります。
    • request_idUUID必須
      戻ったあと、サーバが poll_secret を読む鍵です。

    poll_secret はサーバの store だけが持ちます。この JSON には含めないでください。

  4. 4. ブラウザはサーバへ POST します。パスはアプリケーションが決めます。返った consume_url を開きます。

    TypeScript(ブラウザ)
    import { openHandoffShell } from "@clavis.nexus/app-shell";
    import "@clavis.nexus/app-shell/styles.css";
    
    await openHandoffShell({
      allowedConsumeHosts: ["api.clavis.nexus"],
      async register() {
        const res = await fetch("/api/start", {
          method: "POST",
          credentials: "same-origin",
          headers: { Accept: "application/json" },
        });
        const body: unknown = await res.json();
        if (!res.ok || !body || typeof body !== "object" || !("consume_url" in body) || typeof body.consume_url !== "string") {
          const code =
            body && typeof body === "object" && "error" in body && typeof body.error === "string"
              ? body.error
              : `HTTP ${res.status}`;
          throw new Error(code);
        }
        return body.consume_url;
      },
    });
  5. 5. エンドユーザーは Connect で Passkey を確認します。

  6. 6. ブラウザは returnUrl に戻ります。クエリは request_id だけです。サーバはその鍵で store を読み、waitForCompletion の state が completed であることを読みます。

    TypeScript(完了)
    const requestId = new URL(request.url).searchParams.get("request_id");
    const stored = await store.get(requestId);
    const status = await app.waitForCompletion(requestId, stored.pollSecret, {
      expectedMessageDigest: stored.messageDigest,
    });
    // status.state === "completed"
    // status.sub / status.messageDigest / status.purpose / status.signedAt をプロダクト側に保管します

    戻り URL の例。

    https://console.example/done?request_id=550e8400-e29b-41d4-a716-446655440000
  7. 7. completed の sub、messageDigest、purpose、signedAt をプロダクト側に保管します。保管の JSON 見本は 署名の GET /status、timeout のあとは 完了待ちです。HTTP の payload.sign_proof の形は 認証です。

参照

資格と HTTP の参照です。

  • 認証 — Intent 登録と Org API に載せる資格です。
  • 署名 — register・status・完了待ち・回数・エラーの読み方です。