APIキー

OpenAIやAnthropicのコンソールのような、AIプロダクトのAPIキーのページです。スコープ付きの権限と有効期限を持つキーを作成し、シークレットは一度だけ表示され、コピーで確認でき、取り消しは元に戻せ、その場で名前を変更でき、猶予期間つきでローテーションでき、キーごとの使用量を確認できます。

OpenAI、Anthropic、Vercelはどれも同じAPIキーのページを提供しています。末尾しか見えないキーのリスト、新しいキーを作成するダイアログ、シークレットをコピーできる一度きりの機会です。API keysは、そのページを、どの設定セクションにも組み込める1つのブロックにしたものです。ダイアログを閉じてシークレットを失う、誤ってキーを取り消す、何かが消えるとリストが跳ねるといった、起こりがちな問題に対処します。

各行には、キーの名前、hx_live_…a3F9のようなマスクされたシークレット、権限、プロジェクト、作成日、最終使用日(未使用ならNever used)が表示されます。1週間以内に期限切れになるキーには警告バッジが付き、期限切れのキーにはその旨が表示されます。usageを渡すと、広い画面ではキーの横に日別リクエストの小さなチャートが表示されます。

Create keyは、名前、プロジェクト、権限(すべて、読み取り専用、チェックしたリソースのみに制限)、有効期限(30日、90日、無期限、または選んだ日付)を持つダイアログを開きます。その後、同じダイアログが表示画面に変わり、等幅のボックスに完全なキー、チェックで確認が示されるCopyボタン、Doneが動作する前にチェックするボックスが表示されます。コピーする前に閉じようとすると、警告とClose anywayボタンとともに開いたままになります。タブを閉じる前にも確認します。

取り消す前に、キーの名前を示して確認します。その後、行が折りたたまれて消え、トーストが5秒間Undoを提示します。onRevokeはその時間が過ぎてから実行されるため、Undoにサーバーは必要ありません。ページを離れたり別のキーを作成したりすると、待機中の取り消しはすぐに送信されます。Rotate keyは新しいシークレットに切り替え、古いものを1時間、1日、1週間のあいだ使えるままにできます。名前はF2またはダブルクリックでその場で変更でき、Enterで保存、Escapeでキャンセルします。

  1. Proレジストリをcomponents.jsonに追加する

    components.json
    {
      "registries": {
        "@hextaui-pro": {
          "url": "https://hextaui.com/r/pro/{name}.json",
          "headers": {
            "Authorization": "Bearer ${HEXTAUI_PRO_TOKEN}"
          }
        }
      }
    }
  2. トークンを追加する

    アカウントページでトークンを作成し、.env.local に HEXTAUI_PRO_TOKEN として設定してください。

  3. ブロックを追加する

    pnpm dlx shadcn@latest add @hextaui-pro/api-keys

APIに接続する

ApiKeySettingsは、渡されたキーを表示し、作成、取り消し、名前変更のために呼び出しを行います。どのコールバックからエラーを投げても、入力内容を保ったまま理由が表示されます。リストの読み込み中はApiKeySettingsSkeletonを表示してください。

"use client"

import * as React from "react"
import { IconKey } from "@tabler/icons-react"

import { SettingsSection, SettingsShell, type SettingsSectionItem } from "../settings/settings"
import {
  ApiKeySettings,
  ApiKeySettingsSkeleton,
  type ApiKey,
  type ApiKeyInput,
  type ApiKeySecret,
} from "@/components/blocks/api-keys/api-keys"

const sections: SettingsSectionItem[] = [
  { id: "api-keys", label: "API keys", icon: <IconKey /> },
]

async function request<T>(input: string, init?: RequestInit) {
  const response = await fetch(input, {
    ...init,
    headers: { "content-type": "application/json", ...init?.headers },
  })
  if (!response.ok) {
    const body = (await response.json().catch(() => ({}))) as { error?: string }
    throw new Error(body.error ?? "Something went wrong.")
  }
  return (response.status === 204 ? null : await response.json()) as T
}

export function ApiKeysPage() {
  const [keys, setKeys] = React.useState<ApiKey[] | null>(null)
  const [failed, setFailed] = React.useState(false)

  const load = React.useCallback(() => {
    setFailed(false)
    request<{ keys: ApiKey[] }>("/api/keys")
      .then((data) => setKeys(data.keys))
      .catch(() => setFailed(true))
  }, [])

  React.useEffect(load, [load])

  const create = async (input: ApiKeyInput) => {
    const result = await request<ApiKeySecret>("/api/keys", {
      method: "POST",
      body: JSON.stringify(input),
    })
    setKeys((current) => [result.key, ...(current ?? [])])
    return result
  }

  const revoke = async (key: ApiKey) => {
    await request(`/api/keys/${key.id}`, { method: "DELETE" })
    setKeys((current) => current?.filter((item) => item.id !== key.id) ?? null)
  }

  const rename = async (key: ApiKey, name: string) => {
    const updated = await request<ApiKey>(`/api/keys/${key.id}`, {
      method: "PATCH",
      body: JSON.stringify({ name }),
    })
    setKeys((current) => current?.map((item) => (item.id === key.id ? updated : item)) ?? null)
  }

  return (
    <SettingsShell sections={sections} className="h-svh">
      <SettingsSection
        id="api-keys"
        status={failed ? "error" : keys ? "ready" : "loading"}
        skeleton={<ApiKeySettingsSkeleton />}
        onRetry={load}
      >
        <ApiKeySettings
          keys={keys ?? []}
          limit={10}
          onCreate={create}
          onRevoke={revoke}
          onRename={rename}
        />
      </SettingsSection>
    </SettingsShell>
  )
}

サーバー上でキーを作成する

generateApiKeyはcrypto.getRandomValuesを使うため、Node、エッジランタイム、Workersで動作します。ハッシュと末尾4文字を保存し、シークレットは一度だけ返してください。

import { generateApiKey, keyHint, type ApiKey, type ApiKeyInput } from "@/components/blocks/api-keys/api-keys"

async function sha256(value: string) {
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value))
  return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")
}

export async function createKey(
  input: ApiKeyInput,
  save: (record: ApiKey & { hash: string }) => Promise<void>
) {
  const secret = generateApiKey("hx_live_")
  const key: ApiKey = {
    id: crypto.randomUUID(),
    name: input.name.slice(0, 60),
    hint: keyHint(secret),
    permission: input.permission,
    resources: input.resources,
    project: input.project,
    createdAt: new Date().toISOString(),
    lastUsedAt: null,
    expiresAt: input.expiresAt ? new Date(input.expiresAt).toISOString() : null,
  }
  await save({ ...key, hash: await sha256(secret) })
  return { key, secret }
}

ローテーション、独自のスコープ、元に戻せない取り消し

古いシークレットに猶予期間を設けるRotate keyを追加するには、onRotateを渡します。確認した瞬間に取り消すには、undoTimeoutを0にします。APIに合わせるには、resources、prefix、snippetを渡します。

"use client"

import { ApiKeySettings, type ApiKey, type ApiKeyOption, type ApiKeySecret } from "@/components/blocks/api-keys/api-keys"

const resources: ApiKeyOption[] = [
  { value: "chat", label: "Chat", description: "Send messages and read replies." },
  { value: "search", label: "Search", description: "Query your indexed documents." },
]

export function RotatingKeys({
  keys,
  api,
}: {
  keys: ApiKey[]
  api: {
    create: (input: unknown) => Promise<ApiKeySecret>
    revoke: (id: string) => Promise<void>
    rotate: (id: string, expireOldInHours: number) => Promise<ApiKeySecret>
  }
}) {
  return (
    <ApiKeySettings
      keys={keys}
      prefix="sk_test_"
      resources={resources}
      undoTimeout={0}
      expiringSoon={14}
      snippet={`curl https://api.example.com/v1/chat \\\n  -H "Authorization: Bearer $EXAMPLE_KEY"`}
      onCreate={(input) => api.create(input)}
      onRevoke={(key) => api.revoke(key.id)}
      onRotate={(key, { expireOldIn }) => api.rotate(key.id, expireOldIn)}
    />
  )
}

構造

外側から内側へ組み合わせるパーツ。

パーツ説明
ApiKeySettingsリスト、Create keyを含むヘッダー、上限のメモ、空の状態、すべてのダイアログ。
ApiKeySettingsSkeletonSettingsSectionのskeletonプロップ向けの、リストの形をした読み込みプレースホルダー。
generateApiKeycrypto.getRandomValuesを使って、プレフィックス付きのランダムなキーを作成します。
keyHintシークレットの末尾の文字。マスクされたキーとして保存・表示するためのものです。

ApiKeySettings

すべてのdivプロップも受け付けます。

プロパティ型デフォルト
keys{ id, name, hint, permission, resources?, project?, createdAt, lastUsedAt?, expiresAt?, usage? }。日付にはDate、ISO文字列、タイムスタンプを渡せます。usageは1日あたりのリクエスト数で、古いものが先頭です。
ApiKey[]–
onCreateキーを作成し、一度だけ表示されるシークレットとともに返します。エラーを投げると、そのメッセージがダイアログに表示されます。
(input: ApiKeyInput) => Promise<{ key, secret }>–
onRevokeキーを取り消します。元に戻せる時間が過ぎてから実行されます。エラーを投げるとキーが戻り、Try againが表示されます。
(key) => void | Promise<void>–
onRenameRename、F2、ダブルクリックを追加します。新しい名前はすぐに表示され、これがエラーを投げた場合は元に戻ります。
(key, name) => void | Promise<void>–
onRotateRotate keyを追加します。expireOldInは、古いシークレットが引き続き使える時間数で、0なら即時です。
(key, { expireOldIn }) => Promise<{ key, secret }>–
limit許可されるキーの最大数。上限に達するとCreate keyが無効になり、メモがその理由を示します。
number–
resources制限付きキーに使用を許可できるもの。デフォルトは、models、responses、embeddings、files、agents、usageです。
{ value, label, description? }[]–
projects作成ダイアログにProjectピッカーを追加し、各キーにプロジェクトを表示します。
{ value, label }[]–
prefixキー自身のプレフィックスがない限り、マスクされたキーのヒントの前に表示されます。
string"hx_live_"
snippet空の状態に表示されるコマンドです。Copyボタンが付きます。
string–
undoTimeout取り消し後にUndoを提示する時間(ms)。0にすると、確認された時点で取り消します。
number5000
expiringSoonキーに警告バッジが付く、有効期限までの日数。
number7
nowテストやスクリーンショット用に、相対日付に使う時刻を固定します。
Date–
プロパティ型デフォルト
rows表示するプレースホルダーのキーの数。
number3

generateApiKey

プレフィックスに、ランダムな英字と数字を加えたものを返します。

プロパティ型デフォルト
prefixキーの先頭に付けます。
string"hx_live_"
lengthプレフィックスの後ろに続くランダムな文字。8から256文字です。
number40
キーアクション
F2フォーカスのある行のキーの名前を変更します。
Enter名前の変更中は、名前を保存します。作成ダイアログでは、キーを作成します。
Escape名前の変更中は、古い名前を保持します。表示画面では、キーがコピーされていない場合に一度だけ警告します。
Tab各キーの操作、続いてダイアログのフィールドとボタンの順に移動します。
  • キーが作成されると、フォーカスがCopyに移り、二度と表示されないので今コピーするようにというステータスメッセージが表示されます。
  • 確認ではキーとマスクされたシークレットが示され、フォーカスはCancelから始まります。
  • 取り消し後は、フォーカスが次のキー、前のキー、またはCreate keyへ移ります。Undo後は、復元されたキーに戻ります。名前の変更後は、そのキーの操作に戻ります。
  • 制限付きバッジはそのリソース名を示し、使用量チャートは合計として読み上げられ、名前変更と復元は読み上げられます。
  • モーション軽減時は、行は折りたたまれずに表示され、消えます。

使用技術

API keys を構成する無料のHextaUIコンポーネントです。それぞれ単独でインストールできます。

コード

4 個のファイルを components/blocks/api-keys に追加しました。