開発者コンソール

署名

サーバで依頼を登録し、検証済みの https URL へ戻します。完了はサーバが読みます。

Base URL は https://api.clavis.nexus です。パスは /app/intent-requestsです。手順は クイックスタートです。

前提

  1. 1. 組織(Org)を作成します。

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

  3. 3. App を登録し、ホストを確認します(クイックスタート)。

  4. 4. operate 秘密鍵をサーバに置きます。Master 公開 JWK の環境変数への保存は 認証です。

SDK

サーバは npm install @clavis.nexus/app です。ブラウザの待ちは npm install @clavis.nexus/app-shell です。

手順

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

    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"),
    });
    
    async function start(messageDigest: string): Promise<Response> {
      const { requestId, pollSecret, consumeUrl } = await app.registerGenericSign({
        messageDigest,
        purpose: "generic.sign",
        displayHint: "請求先口座の変更",
        returnUrl: "https://console.example/done",
      });
      await storePollSecret(requestId, pollSecret, messageDigest);
      return Response.json({ consume_url: consumeUrl, request_id: requestId });
    }
    • CLAVIS_APP_ID
      App の id です。
    • OPERATE_PRIVATE_JWK
      Ed25519 秘密 JWK(OKP、crv Ed25519、d あり)の JSON です。サーバだけが持ちます。
    • MASTER_PUBLIC_JWK_JSON
      集約 JWKS の master オブジェクトを JSON 文字列にして保存します。
    • returnUrl
      検証済みホスト配下の https です。
    • storePollSecret
      poll_secret の保管です。クエリ、cookie、ブラウザへ返す JSON には含めないでください。

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

    {
      "consume_url": "https://api.clavis.nexus/app/intent-requests/consume/550e8400-e29b-41d4-a716-446655440000",
      "request_id": "550e8400-e29b-41d4-a716-446655440000"
    }
  2. 2. ブラウザは @clavis.nexus/app-shell のあと consume_url へ遷移します。CSS は import "@clavis.nexus/app-shell/styles.css" です。手順は クイックスタートです。

  3. 3. 戻りクエリの request_id で保管した poll_secret を読み、waitForCompletion の state が completed であることを読みます。sub/messageDigest/purpose/signedAt をプロダクト側に保管します。

    TypeScript(完了)
    const stored = await loadPollSecret(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
    • requestId
      戻りクエリの request_id です。store を読む鍵です。
    • pollSecret
      登録時にサーバが保管した値です。
    • state
      completed のとき完了です。
    • sub / messageDigest / purpose / signedAt
      completed の署名結果です。プロダクトが保管し、提示します。SDK は HTTP の sign_proof を flatten します。

    プロダクト側に保管する JSON の例。

    {
      "request_id": "550e8400-e29b-41d4-a716-446655440000",
      "intent_id": "<intent_id>",
      "sub": "<passkey sub>",
      "message_digest": "<base64url SHA-256>",
      "purpose": "generic.sign",
      "signed_at": 1710000000
    }

digest

message_digest の元にする JSON と、身元確認の参照の入れ方です。

digest の元にする JSON の例。

{
  "v": 1,
  "operation": {
    "action": "billing_account.change",
    "account_id": "acct_8f2c",
    "bank_account_last4": "4821"
  },
  "identity_binding": {
    "provider": "jpki",
    "ref": "<確認先で結果を引ける参照>",
    "assurance": "<プロダクトが決める水準>",
    "verified_at": "2026-10-09T00:00:00Z"
  }
}
  • vnumber
    JSON の形の版です。形を変えるときに上げます。
  • operationobject
    操作の本文です。display_hint で見せる操作と同じものを入れます。
  • identity_bindingobject任意
    プロダクトが身元確認を行ったときだけ入れます。
  • identity_binding.providerstring
    確認の手段または事業者のラベルです。値はプロダクトが決めます。
  • identity_binding.refstring
    確認先で結果を引ける参照です。氏名、番号、画像は入れないでください。
  • identity_binding.assurancestring
    確認の水準です。値はプロダクトが決めます。
  • identity_binding.verified_atstring (RFC 3339)
    身元確認を行った時刻です。
TypeScript(digest)
const preimage = await loadOperation(operationId); // 上の JSON
const messageDigest = await sha256Base64Url(canonicalStringify(preimage));
await storePreimage(operationId, preimage);
  • canonicalStringify と sha256Base64Url は クイックスタートの見本です。
  • Clavis に届くのは message_digest だけです。元の JSON はプロダクトが保管します。
  • identity_binding は digest に含まれるため、署名とともに固定されます。参照の中身は、プロダクトと確認先が保ちます。
  • 署名結果を相手に示すときは、保管した JSON から digest を計算し直し、message_digest と照合します。

POST /register

SDK が送る HTTP です。registration は operate 秘密鍵の Compact JWS です。

JWT claims

{
  "iss": "app:<app_id>",
  "aud": "app-intent-registration",
  "app_id": "<app_id>",
  "intent_type": "generic_sign",
  "iat": 1710000000,
  "exp": 1710000120,
  "jti": "<unique-id>",
  "return_url": "https://console.example/done",
  "ui_locales": "ja en",
  "payload": {
    "purpose": "generic.sign",
    "message_digest": "<base64url SHA-256>",
    "display_hint": "請求先口座の変更"
  }
}

Request

POST https://api.clavis.nexus/app/intent-requests/register
Content-Type: application/json

{
  "registration": "<operate 鍵で署名した Compact JWT>"
}

Response

{
  "payload": {
    "request_id": "550e8400-e29b-41d4-a716-446655440000",
    "display_code": "AB12CD34EF",
    "consume_url": "https://api.clavis.nexus/app/intent-requests/consume/550e8400-e29b-41d4-a716-446655440000",
    "link_consume_url": "https://api.clavis.nexus/app/intent-requests/consume-link/550e8400-e29b-41d4-a716-446655440000",
    "poll_secret": "<48 hex>"
  }
}
  • payload.request_idstring (UUID)必須
    完了を読むときの識別子です。
  • payload.display_codestring必須
    画面照合用の短いコードです。
  • payload.consume_urlstring (URL)必須
    エンドユーザーが Connect に入る URL です。
  • payload.link_consume_urlstring (URL)必須
    federation.link 用の URL です。
  • payload.poll_secretstring (48 hex)必須
    登録のたびに発行されます。サーバだけが持ちます。クエリ・cookie・ブラウザに載せないでください。
  • registration.payload.display_hintstring(最大 500 文字)任意
    Connect の確認でエンドユーザーに見せる操作名です。SDK では displayHint です。見せた一行は署名に含まれ、SignProof の shown に写ります。操作名と digest の中身が合っていることは、プロダクトが保ちます。
  • registration.ui_localesstring(BCP 47、空白区切り)任意
    プロダクトの画面の言語を、希望の順に書きます(例: "ja en")。Connect は、このうち提供している最初の言語で確認を出し、書いた操作名と言語をそろえます。提供していない言語は拒まずに飛ばします。どれも合わないときは、本人が選んだ言語、ブラウザの言語の順に決めます。エンドユーザーは Connect で言語を切り替えられます。SDK では uiLocales(配列)です。
  • registration.expUnix 秒必須
    SDK の registerGenericSign は iat + 120 秒です。この窓のあいだに Connect の Passkey まで進みます。切れたら新しい jti で登録し直します。

GET /status

SDK の getStatus が読む HTTP です。signature の検証は SDK が行います。

Request

GET https://api.clavis.nexus/app/intent-requests/status/550e8400-e29b-41d4-a716-446655440000
X-Intent-Poll-Secret: <poll_secret>

Response (pending)

{
  "payload": {
    "state": "pending"
  },
  "signature": "<response signature>"
}

Response (intent_issued)

{
  "payload": {
    "state": "intent_issued",
    "intent_id": "<intent_id>",
    "intent_type": "generic_sign"
  },
  "signature": "<response signature>"
}

Response (completed)

{
  "payload": {
    "state": "completed",
    "intent_id": "<intent_id>",
    "intent_type": "generic_sign",
    "sign_proof": {
      "v": 1,
      "intent_type": "generic_sign",
      "intent_id": "<intent_id>",
      "message_digest": "<base64url SHA-256>",
      "purpose": "generic.sign",
      "sub": "<passkey sub>",
      "shown": {
        "app": "Example Console",
        "host": "console.example",
        "action": "請求先口座の変更"
      },
      "signed_at": 1710000000
    },
    "sub": "<passkey sub>",
    "message_digest": "<base64url SHA-256>",
    "signed_at": 1710000000,
    "purpose": "generic.sign"
  },
  "signature": "<response signature>"
}
  • payload.statepending | intent_issued | completed必須
    pending は Connect 前、intent_issued は Passkey 後の確定待ち、completed で完了です。前二者のあいだも同じ GET を続けます。
  • payload.sign_proofobject任意
    completed のとき含まれます。形は 認証 の SignProof です。検証鍵には使いません。SDK の waitForCompletion は sub などへ flatten し、このオブジェクトは返しません。プロダクトの保管は flatten した項目で足ります。
  • payload.substring任意
    completed のとき sign_proof から flatten されます。
  • payload.message_digeststring任意
    completed のとき flatten されます。登録時の digest と照合します。
  • payload.signed_atnumber (Unix 秒)任意
    completed のとき flatten されます。
  • signaturestring必須
    Master 公開 JWK で検証する応答署名です。

プロダクト側に保管する JSON の例。

{
  "request_id": "550e8400-e29b-41d4-a716-446655440000",
  "intent_id": "<intent_id>",
  "sub": "<passkey sub>",
  "message_digest": "<base64url SHA-256>",
  "purpose": "generic.sign",
  "signed_at": 1710000000
}

完了待ち

waitForCompletion の既定と、timeout のあとに読む手順です。

  • timeoutMsnumber任意
    既定は 120000(120 秒)です。登録の寿命に揃えています。
  • intervalMsnumber任意
    既定は 1500 です。GET /status の間隔です。
  • AppClientError.codestring任意
    完了待ちでは timeout と digest_mismatch を見ます。登録 HTTP の失敗は register_failed で、エラーです。
TypeScript(timeout のあと)
import { AppClientError } from "@clavis.nexus/app";

try {
  const status = await app.waitForCompletion(requestId, stored.pollSecret, {
    expectedMessageDigest: stored.messageDigest,
  });
  await storeReceipt({
    request_id: requestId,
    intent_id: status.intentId,
    sub: status.sub,
    message_digest: status.messageDigest,
    purpose: status.purpose,
    signed_at: status.signedAt,
  });
} catch (e) {
  if (e instanceof AppClientError && e.code === "timeout") {
    const again = await app.getStatus(requestId, stored.pollSecret, stored.messageDigest);
    if (again.state === "completed") {
      await storeReceipt(/* again の flatten 項目 */);
      return;
    }
    // pending / intent_issued のときは新しい登録へ進みます
    return;
  }
  throw e;
}
  • timeout のあとは、同じ request_id で getStatus を一度読みます。
  • completed なら保管します。pending または intent_issued なら、新しい登録へ進みます。
  • エンドユーザーが Connect を閉じたときも、同じ読み方です。

回数

プラン面の「残り」と「今月」が指すものです。

  • 数えるのは、Intent の SignProof が初めて成立した成功完了だけです。
  • 失敗、再試行、登録だけの操作は数えません。
  • OIDC(ログイン)は署名の購入に含まれ、人数でも当月の回数でも数えません。
  • 当月は UTC の暦月です。
  • 当月の上限に達すると、新規の Intent 登録は通りません。
  • 購入していても、当月の残りが無いと登録は通りません。
  • 上限に達したあとは、Orgのプラン面で当月の上限を上げるか、翌月(UTC)になるまでお待ちください。

エラー

共通形は error、message、requestId、trace_id です。

Error

{
  "error": "RETURN_URL_NOT_ALLOWED",
  "message": "registration rejected",
  "requestId": "<id>",
  "trace_id": "<id>"
}
  • 404 UNKNOWN_APP
    registration の app_id が未知か、プランの経路では App が Intent を開けない状態です。開発者コンソールで App の状態(有効)、用途の署名、署名の購入、当月の残りを見てください。
  • 403 RETURN_URL_NOT_ALLOWED
    return_url は、ホスト確認済みの親配下の https に置きます。
  • 400 INVALID_RETURN_URL
    return_url は https です。
  • 409 DUPLICATE_JTI
    同じ jti を再登録しました。新しい jti で送ってください。
  • 400 REGISTRATION_EXPIRED
    registration JWT の exp を過ぎています。作り直してください。
  • 400 REGISTRATION_JWT_INVALID
    operate 秘密鍵とコンソールの公開鍵が一致していません。
  • 401 UNAUTHORIZED
    status に X-Intent-Poll-Secret がありません。
  • 403 FORBIDDEN
    poll_secret が一致しません。
  • 404 NOT_FOUND
    request_id が未知か、poll が期限切れです。

Error(開始条件が足りないとき)

{
  "error": "UNKNOWN_APP",
  "message": "registration rejected",
  "requestId": "<id>",
  "trace_id": "<id>"
}

プランが閉じていても、Intent 登録では 404 UNKNOWN_APP になることが多いです。開発者コンソールのプランと App の用途を先に見てください。

切り分け

症状から、次の場所へ進みます。

  • 登録が 404 UNKNOWN_APP
    App 詳細の状態(有効か)、用途の署名、Orgのプラン面の署名 blockers(プランがありません/支払い/上限)を確認してください。手順は クイックスタートです。
  • 登録が上限で通らない
    Orgのプラン面で「残り」と「当月の上限」を見てください。上限を上げるか、翌月(UTC)になるまでお待ちください。
  • 403 RETURN_URL_NOT_ALLOWED
    App 詳細のホストと returnUrl の親を揃えます。ホストが未確認のときは、DNS に TXT レコードを追加して確認します。
  • Connect 前に期限切れ
    登録から 120 秒を過ぎています。新しい jti で登録し直し、すぐに Connect へ進みます。
  • 戻ったが completed でない
    同じ request_id で getStatus を続けます。pending または intent_issued のまま切れたら、新しい登録へ進みます。手順は 完了待ちです。
  • 戻り後に poll_secret が無い
    store の鍵は request_id です。別サーバやセッション消失で読めないときは、新しい登録へ進みます。
  • プランに「支払いが確認できていません」
    支払いが確認できていないときは、Polar の支払い管理画面(Customer Portal)で支払いを確認してください。
  • Org が停止
    Org が停止のときは、署名を購入しても停止は解けません。[email protected]までお問い合わせください。
  • 「購入は開いていません」
    公開 Checkout が閉じているときは、開いてから署名を購入してください。請求・導入は Enterprise プランです。
  • 完了後の保管先が決まらない
    completed の sub/messageDigest/purpose/signedAt をプロダクト側に保管してください。見本は GET /statusです。

参照