署名
サーバで依頼を登録し、検証済みの https URL へ戻します。完了はサーバが読みます。
Base URL は https://api.clavis.nexus です。パスは /app/intent-requestsです。手順は クイックスタートです。
前提
1. 組織(Org)を作成します。
2. Org を開き、プランの署名で購入します(プラン面は
/developers/{orgId}です)。3. App を登録し、ホストを確認します(クイックスタート)。
4. operate 秘密鍵をサーバに置きます。Master 公開 JWK の環境変数への保存は 認証です。
SDK
サーバは npm install @clavis.nexus/app です。ブラウザの待ちは npm install @clavis.nexus/app-shell です。
手順
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_IDApp の id です。OPERATE_PRIVATE_JWKEd25519 秘密 JWK(OKP、crv Ed25519、d あり)の JSON です。サーバだけが持ちます。MASTER_PUBLIC_JWK_JSON集約 JWKS の master オブジェクトを JSON 文字列にして保存します。returnUrl検証済みホスト配下の https です。storePollSecretpoll_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. ブラウザは
@clavis.nexus/app-shellのあとconsume_urlへ遷移します。CSS はimport "@clavis.nexus/app-shell/styles.css"です。手順は クイックスタートです。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-446655440000requestId戻りクエリの request_id です。store を読む鍵です。pollSecret登録時にサーバが保管した値です。statecompleted のとき完了です。sub / messageDigest / purpose / signedAtcompleted の署名結果です。プロダクトが保管し、提示します。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"
}
}vnumberJSON の形の版です。形を変えるときに上げます。operationobject操作の本文です。display_hintで見せる操作と同じものを入れます。identity_bindingobject任意プロダクトが身元確認を行ったときだけ入れます。identity_binding.providerstring確認の手段または事業者のラベルです。値はプロダクトが決めます。identity_binding.refstring確認先で結果を引ける参照です。氏名、番号、画像は入れないでください。identity_binding.assurancestring確認の水準です。値はプロダクトが決めます。identity_binding.verified_atstring (RFC 3339)身元確認を行った時刻です。
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任意
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_APPregistration のapp_idが未知か、プランの経路では App が Intent を開けない状態です。開発者コンソールで App の状態(有効)、用途の署名、署名の購入、当月の残りを見てください。403 RETURN_URL_NOT_ALLOWEDreturn_urlは、ホスト確認済みの親配下の https に置きます。400 INVALID_RETURN_URLreturn_url は https です。409 DUPLICATE_JTI同じ jti を再登録しました。新しい jti で送ってください。400 REGISTRATION_EXPIREDregistration JWT の exp を過ぎています。作り直してください。400 REGISTRATION_JWT_INVALIDoperate 秘密鍵とコンソールの公開鍵が一致していません。401 UNAUTHORIZEDstatus に X-Intent-Poll-Secret がありません。403 FORBIDDENpoll_secret が一致しません。404 NOT_FOUNDrequest_id が未知か、poll が期限切れです。
Error(開始条件が足りないとき)
{
"error": "UNKNOWN_APP",
"message": "registration rejected",
"requestId": "<id>",
"trace_id": "<id>"
}プランが閉じていても、Intent 登録では 404 UNKNOWN_APP になることが多いです。開発者コンソールのプランと App の用途を先に見てください。
切り分け
症状から、次の場所へ進みます。
登録が 404 UNKNOWN_APP登録が上限で通らないOrgのプラン面で「残り」と「当月の上限」を見てください。上限を上げるか、翌月(UTC)になるまでお待ちください。403 RETURN_URL_NOT_ALLOWEDApp 詳細のホストと returnUrl の親を揃えます。ホストが未確認のときは、DNS に TXT レコードを追加して確認します。Connect 前に期限切れ登録から 120 秒を過ぎています。新しいjtiで登録し直し、すぐに Connect へ進みます。戻ったが completed でない戻り後に poll_secret が無いstore の鍵はrequest_idです。別サーバやセッション消失で読めないときは、新しい登録へ進みます。プランに「支払いが確認できていません」支払いが確認できていないときは、Polar の支払い管理画面(Customer Portal)で支払いを確認してください。Org が停止Org が停止のときは、署名を購入しても停止は解けません。[email protected]までお問い合わせください。「購入は開いていません」公開 Checkout が閉じているときは、開いてから署名を購入してください。請求・導入は Enterprise プランです。完了後の保管先が決まらない