クイックスタート
署名の依頼(Intent)をサーバで登録します。エンドユーザーは確認画面(Connect)で Passkey を確認し、アプリケーションの URL に戻ります。完了は、サーバが status.state の completed を読んだときです。
開発者コンソール
登録の前に用意する組織(Org)、App、ホスト、秘密鍵です。
Intent が通るとき
次が揃った Org の App だけが、署名の Intent を登録できます。
- 契約者の Passkey があります(Org のプラン面)。
- 署名を購入しています(Org のプラン面)。
- App のホスト確認が通っています(App 詳細の状態が「有効」)。
- App の用途に署名があります。
- 当月の上限に達していません(プラン面の残り)。
CLI / エージェント
同じ開始を @clavis.nexus/setup の CLI または MCP で進めます。Passkey・カード決済・DNS レコードの追加は人間が行います。ツール応答の needs_human を見ます。
1.
npm install -g @clavis.nexus/setupのあとclavisを呼びます(またはnpx -y @clavis.nexus/setup)。2.
clavis loginのあと、ブラウザで Passkey を確認します。3.
clavis statusで足りない段を見ます。4. 足りない段を
org-anchor-*、card-checkout、app-register、app-host-*、declaration-setの順で埋めます。5. MCP は
clavis mcpです。完了後、clavis emit-envで Intent 用の環境変数を出します。
手順
サーバで登録し、ブラウザで Connect を開き、戻ったあと completed を読みます。
1. 検証済みホスト上のサーバに
npm install @clavis.nexus/appを入れます。2. ブラウザに
npm install @clavis.nexus/app-shellを入れます。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_IDApp の id です。OPERATE_PRIVATE_JWKEd25519 秘密 JWK の JSON です。サーバだけが持ちます。MASTER_PUBLIC_JWK_JSONhttps://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. ブラウザはサーバへ 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. エンドユーザーは Connect で Passkey を確認します。
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-4466554400007.
completedのsub、messageDigest、purpose、signedAtをプロダクト側に保管します。保管の JSON 見本は 署名の GET /status、timeout のあとは 完了待ちです。HTTP のpayload.sign_proofの形は 認証です。
参照
資格と HTTP の参照です。