# 関数

サーバーなしでリクエストを1つ受け取って処理するコードです。決済サービスの Webhook の受信、シークレットキーが必要な計算、定期ジョブの本体に使います。

```
list_functions        デプロイした関数と呼び出しアドレス
get_function          現在のコード （変更する前にまず読んでください）
deploy_function       デプロイ （同じ名前ならまるごと置き換え、次の呼び出しから適用）
set_function_public   公開かどうかだけを変更
set_function_limits   時間の制限を変更（呼び出し 1 回、SQL 1 文）
remove_function       削除
list_function_secrets   関数シークレットの名前（値は返しません）
set_function_secrets    関数シークレットを保存（同じ名前は上書き）
copy_schema_from_project      別のプロジェクトのテーブルと構造（関数・ビュー・ポリシー含む）をここに作る
copy_data_from_project        別のプロジェクトのテーブルの行をここへコピー（無い行のみ、または変更分も）
copy_users_from_project       別のプロジェクトのログインユーザーをコピー（ハッシュごと、サーバー内で）
compare_projects_data       二つのプロジェクトのテーブルを行数とハッシュで比較（コピーが完全か）
get_project_forward         別のプロジェクトがこのプロジェクトの公開関数の呼び出しを代わりに受けているか確認
set_project_forward         公開関数の呼び出しを別のプロジェクトに代わりに受けさせる（分割後も Webhook が途切れない）
set_project_write_lock      プロジェクトを一時的に読み取り専用にする（分割の最後のコピー前。自動で解除されます）
get_project_write_lock      書き込みロックを見る
copy_functions_from_project  別のプロジェクトの関数をコードごとコピー（非公開で）
copy_function_secrets   別のプロジェクトの関数シークレットを名前でコピー
delete_function_secret  関数シークレットを1つ削除
```

コードはファイル1つです:

```ts
export default async (request, { sql }) => {
  const body = request.json() as { id?: string } | null
  const rows = await sql('select * from orders where id = $1', [body?.id])
  return { order: rows[0] ?? null }
}
```

- `request`: method、headers、claims（呼び出したキーのクレーム。`role` は `anon`・`authenticated`・`service_role`。キーなしで呼んだ公開関数では null）、text()、json()
- `sql(text, values)`: このプロジェクトの service_role 権限で1文を実行。RLS を通らないため、claims で自分で確認してください。
- レスポンス: オブジェクト（JSON）、文字列、または `{ status, headers, body }`
- **ネットワークは既定で無効です。** `set_function_egress`（またはコンソールの関数画面の `外部リクエスト`）で有効にすると、`fetch()` で公開 HTTPS アドレスを呼び出せます（サーバーが代わりに送信し、内部アドレスはブロック、プランの1分あたりの上限まで（Free 300・Pro 3,000・Enterprise 10,000回、`list_functions`で確認できます）、すべて関数のログに残ります）。
- **接続（ソケット）**: `Deno.connect`、`Deno.connectTls`、`Deno.startTls` も使えるので、メールライブラリ（denomailer）やデータベースドライバがそのまま動きます。サーバーが代わりに開き、外部リクエストと同じルールに従います。ネットワークを有効にする必要があり、接続1回は1分あたりの上限に1回として数え、公開アドレスのみ許可されます。ポートは1024以上と、Web（80・443）およびメール（110・143・465・587・993・995）のみ開いており、25番などそれ以外の低いポートは遮断します。TLS証明書はサーバーが名前で検証します。1回の呼び出しで同時に4つまで、60秒使われない接続は閉じ、呼び出しが終わると一緒に閉じます。
- このプロジェクトのAPI(`https://<ref>.<zone>`)はネットワークを有効にしなくても呼び出せ、自動で入る変数(`SUPABASE_URL`、`SUPABASE_ANON_KEY`、`SUPABASE_SERVICE_ROLE_KEY`、blackwall://env)が入っているため、Supabaseの関数の`createClient(Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!)`がそのまま動きます。この呼び出しは1分あたりの上限に数えず、ログには残ります。Supabaseの新しい名前 `SUPABASE_SECRET_KEYS`・`SUPABASE_PUBLISHABLE_KEYS`（`{"default": "<キー>"}` の形のJSON）も入ります。`SUPABASE_DB_URL` はありません。関数はネットワークのない場所で動きデータベースに直接接続できないため、`createClient` か2番目の引数の `sql` を使ってください。
- **npm パッケージ**: `deploy_function` に `dependencies`（`{"zod": "^3.23.8"}`）または `package_json`（+ `package_lock`）を一緒に送ると、デプロイ時にインストールし、コードと一緒に1つの ESM ファイルにまとめます（最大5MB）。インストールスクリプトは実行せず、ネイティブモジュール（.node）は使えません。複数のファイルは `files`（`{"lib/util.ts": "..."}`）で送り、相対パスで import します。バンドルされた関数を変更するときは、`get_function` が返した `package_json` と `files` も一緒に送り直してください。
- **インストール時間**: 一度インストールしたパッケージの組み合わせ（package.json とロックファイル）は再利用するので、2回目からのデプロイは数秒で終わります。MCP の `deploy_function` は40秒以内に終わらないと、すぐに `status: "building"` で応答し、サーバーでデプロイを続けます。`get_function` の `build.status` が `deployed` になれば新しいバージョンが呼び出しを受け、`failed` なら `build.error` に理由があります。
- **フォルダーごとのデプロイ**: `index.ts` とほかのモジュール（任意で `deno.json`）が入ったフォルダーを `oneend functions deploy <名前> <フォルダー>` で、または `deploy_function` の `files` に `index.ts` を含むすべてを入れて送ります。Supabase と同じように `deno.json` の `imports` と `npm:zod@3` のような import を使え、デプロイ時に npm パッケージに変換してインストールします。
- **jsr: と URL の import**: `jsr:@supabase/supabase-js@2` は npm の同じパッケージとして、ほかの `jsr:` パッケージは JSR の npm 互換レジストリからインストールします。`https://deno.land/std@0.177.0/http/server.ts` や `https://esm.sh/...` のような URL の import は**デプロイ時に一度だけ**取得して一緒にまとめます（取得したアドレス、最終的なアドレス、SHA-256 が `get_function` の `remote_imports` に残り、`deno.lock` があればハッシュが一致しないとデプロイできません）。関数が実行中に取得することはありません。アドレスに届かなければ、そのアドレスと理由を示してデプロイが失敗します。
- **_shared フォルダー**: Supabase と同じように `supabase/functions/<名前>/index.ts` が `../_shared/cors.ts` を import していれば、`oneend functions deploy <名前>` がそのフォルダーも一緒に送ります。MCP では `files` に `../_shared/cors.ts` のパスのまま入れます。
- **時間の制限**: 呼び出し 1 回は既定で 150 秒（Supabase のエッジ関数と同じ）、関数の中の SQL 1 文は既定で 8 秒です。`set_function_limits`（または `oneend functions limits`、コンソールの関数画面）で関数ごとに変えられ、最大は `get_function` の `timeout_cap_seconds` です。超えると 504 で打ち切られます。
- 本番の環境変数（blackwall://env）が `process.env` で渡されます。値を変更すると、次の呼び出しから新しい値になります。
- **関数シークレット**: Webhook の署名キーやメールサービスの API キーのように関数だけが使う値は `set_function_secrets { ref, secrets: { "RESEND_API_KEY": "re_…" } }` で保存します（コンソールの関数シークレット、`oneend secrets set`、REST `POST /v1/projects/<ref>/function-secrets`）。サイトのビルドには入らず、同じ名前の環境変数より優先して `process.env` に入り、次の呼び出しから使われます。値は保存できますが、どのツールや API でも読み返せません。`list_function_secrets` は名前と変更日時だけを返し、削除は `delete_function_secret`（`DELETE /v1/projects/<ref>/function-secrets/<name>`）です。別のプロジェクトにすでに保存したキーは貼り直さず、`copy_function_secrets { ref, from: "<別のプロジェクトの ref>", names: ["ANTHROPIC_API_KEY"] }` でコピーします（`oneend secrets copy --from <ref> 名前...`、REST `POST /v1/projects/<ref>/function-secrets/copy`）。値はサーバーの中だけで移り、すでにある名前は `overwrite: true` のときだけ置き換え、応答は `copied`・`skipped`・`missing` の名前だけです。両方のプロジェクトでオーナーか管理者である必要があります。表示・変更できるのはオーナーと管理者のみです。
- 関数は本番のシークレットを読めるため、**オーナーと管理者だけ**がデプロイできます。

呼び出し:

```
POST https://<ref>.<zone>/functions/v1/<name>
Authorization: Bearer <公開キー、ユーザートークンまたはシークレットキー>
```

呼び出せる人:

| 関数 | キーなし | 公開キー | ユーザートークン・シークレットキー |
|---|---|---|---|
| 非公開（既定） | 401 | 可（`claims.role` = `anon`） | 可（`authenticated`・`service_role`） |
| 公開（`public: true`） | 可（`claims` = null） | 可 | 可 |

非公開は Supabase の `verify_jwt` と同じなので、公開キーを付けた `fetch` と `supabase.functions.invoke` はそのまま動きます。
公開キーはアプリに入っていて誰でも見られるため、ログインしたユーザーだけを受け付けるには関数の中で `request.claims.role === 'authenticated'` を確認してください。
呼び出し元の `Authorization` と `apikey` ヘッダーはそのまま関数に届きます（Supabase と同じ）。そのため関数の中で `createClient(url, anonKey, { global: { headers: { Authorization: req.headers.get('Authorization') } } })` とすれば、そのユーザーの権限で問い合わせられます。このプロジェクトのキーとトークンで確認できた値だけが届き、確認できない `apikey`、Cookie、プラットフォームの認証情報は届きません。
キーなしで届く決済サービスの Webhook だけを `public: true` にし、その中で署名を自分で検証してください。出力（console.log）は get_logs の source "functions" で確認します。

# 定期ジョブ

```
save_job          作成・変更 （同じ名前なら置き換え）
list_jobs         一覧、次回の実行、最後の結果
run_job           今すぐ1回実行 （1分以内）
list_job_runs     実行履歴とエラー
set_job_enabled   無効化・有効化
remove_job        削除
set_job_retention 実行履歴を残す日数（1 | 3 | 7 | 14 | 30 | 90、null は既定値）
```

- スケジュールは5つの欄の cron で、**UTC** です。韓国時間は UTC + 9時間（日本時間も同じです）: 韓国の午前0時は `0 15 * * *`、韓国の午前9時は `0 0 * * *`。
- ジョブが行うのは次のどちらかです: `kind: "function"`（デプロイした関数を本文 {} の POST で呼び出す）または `kind: "sql"`（サービスロールで1文）。
- `save_job` では外部の URL は呼び出せません（SQL からは `net.http_post`）。同じ時刻の実行は、サーバーが複数台あっても1回だけです。SQL ジョブは `timeout_seconds`（既定120秒）以内に終わる必要があります。ジョブの SQL が `set statement_timeout to '15min'` のように自分で延ばすとその分待ち、どちらでもプランの最大（Free 300秒、Pro・Enterprise 3600秒）を超えると止まります。
- プロジェクトあたりのジョブ数はプランごとに異なります（Free 50、Pro 200、Enterprise 500、SQL で作ったジョブを含む）。`list_jobs` の `limit`・`used` で確認します。あるプロジェクトのジョブが集中しても、ほかのプロジェクトのジョブは遅れません（プロジェクトごとに同時に最大4つ）。

# Supabase の SQL をそのまま: スキーマ、pg_cron、pg_net、Vault

Supabase で使っていた SQL を変えずに実行します。SQL エディター、`run_sql`、`apply_migrations`、`oneend db push`、SQL 予約ジョブはすべて同じ名前変換を通ります。

```
create schema if not exists blackquant;          -- app_<ref>_blackquant として作られる
create table blackquant.prices (...);            -- SQL では書いた名前のまま
select cron.schedule('collect', '* * * * *', $$ select net.http_post(url := 'https://<ref>.<zone>/functions/v1/collect') $$);
select decrypted_secret from vault.decrypted_secrets where name = 'service_role_key';
```

- **スキーマ**: `create schema blackquant` はこのプロジェクトだけの `app_<ref>_blackquant` になります。新しいテーブルは public と同じくアクセスルール（RLS）が有効の状態で作られ、anon・authenticated・service_role の権限も public と同じです。ほかのプロジェクトのスキーマは名前も作れず、読むこともできません。
- **公開スキーマ**: データ API は public だけに応答します。`set_exposed_schemas`（コンソールのデータベース → スキーマ、`oneend db schemas expose`）で公開すると、`Accept-Profile`・`Content-Profile` ヘッダー、supabase-js の `db: { schema: 'blackquant' }` で使えます。一覧は `list_schemas`。公開していないスキーマは PGRST106（406）です。
- **検索パスと最大行数**: `get_data_api_settings` で見て、`set_data_api_settings` で変えます（コンソールの設定 → データ API）。`search_path` に入れたスキーマ（`create schema` で作ったもの、extensions）は要求したスキーマの後ろに付き、関数とビューがスキーマ名なしでその中を見つけます。
- **接続数の上限**: `get_database_settings` で見て、`set_database_settings` で変えます（コンソールの設定 → データベース）。定期ジョブや関数が `too many connections for role` で失敗するときは、API ログイン（`<ref>_authenticator`）が上限に止まっています。`enforced.api_connections` が `limits.api_connections` より小さければ、`api_connections` をその値まで上げます。すぐに反映され、開いている接続は切りません。サーバーを独占するプロジェクトは、サーバーの最大接続数から直接ログイン（既定30）と予約10を引いた数まで上げられ、他のプロジェクトと共有するサーバーではサーバーの最大接続数の10分の1（20〜80）です。定期ジョブは上限に当たると最大1分待ってから接続し直します。
- **pg_cron**: `cron.schedule(name, schedule, command)`、`cron.unschedule`、`cron.alter_job`、`cron.job`、`cron.job_run_details`（7日保存）を使います。One Server のスケジューラーがサービスロールで実行し、`list_jobs` に `source: "sql"` で表示されます。最短1分間隔（`30 seconds` のような秒単位は拒否）、`@hourly`・`@daily` などの略記は使えます。`create extension pg_cron` はエラーなく通ります。
- **pg_net**: `net.http_post(url, body, params, headers, timeout_milliseconds)`、`net.http_get`、`net.http_delete` はリクエストをキューに入れて id を返します。トランザクションが終わってから数秒以内に送信し、応答は `net._http_response` に6時間残ります。https のみ、内部アドレスは拒否、タイムアウトは指定した`timeout_milliseconds`（既定5秒）を関数の時間上限の最大（既定150秒、運営者が900秒まで）まで守り、応答を待つのをやめても呼び出された関数は最後まで動きます。関数の外部リクエストと同じ1分あたりの上限を共有します。自分のプロジェクトのアドレス（`https://<ref>.<zone>`）は関数の外部リクエストが無効でも使えます。ほかのアドレスは `set_function_egress` で有効にする必要があります。
- **http 拡張（同期の `http_get`・`http_post`）はありません。** 同じトランザクションで応答を待つ呼び出しは、`net.http_get` で送って後から `net._http_response` で読むか、関数の `fetch()` に置き換えてください。応答を使わない呼び出しは `net.http_post` に置き換えればそのまま動きます。データベースサーバーが直接外部に送ると内部アドレスの遮断と1分あたりの上限を通らないため、入れていません。移行ツールがこの拡張を呼ぶ関数と予約ジョブを名前で示します。
- **Vault**: `vault.create_secret`、`vault.decrypted_secrets` はサービスロール（SQL エディター、`run_sql`、シークレットキー、SQL 予約ジョブ）でだけ復号されます。専用データベースのプロジェクトで初めて使うときに有効になります。

# データベース Webhook

テーブルの行が変わると通知します。関数の外部リクエストを有効にしていないなら、**外部サービス（Slack、メール送信 API など）への通知にはこれを使います。**

```
create_db_webhook            作成: テーブル、イベント（insert/update/delete）、送信先（url または function）
list_db_webhooks             一覧と最近の結果（署名キーは末尾4文字だけ）
list_db_webhook_deliveries   配信履歴と失敗の理由
set_db_webhook_enabled       無効化・有効化
remove_db_webhook            削除
```

- 本文: `{ type: "INSERT"|"UPDATE"|"DELETE", table, schema: "public", record, old_record, hook, delivery, occurred_at }`
- コミットされた変更だけを送ります。サーバーが再起動中だった場合も、後で送ります。順序は保証しないため、`delivery` で重複を取り除いてください。
- 2xx 以外、または10秒以内に応答がなければ失敗です。10秒後、1分後、5分後、30分後に再送し、5回目で破棄します。
- url は公開された https アドレスだけが使えます。プライベートや内部のアドレス、リダイレクトは拒否します。
- 受信側での検証: `x-blackwall-signature: t=<秒>,v1=<署名>`。署名は HMAC-SHA256(signing_secret, "<t>.<本文>") の16進数です。
- signing_secret は Webhook の作成時に一度だけ表示します。一覧（`list_db_webhooks`）には `signing_secret_set` と末尾4文字（`signing_secret_last4`）だけが出ます。なくした場合は Webhook を削除して作り直すと新しいキーが発行されます。

## Web 標準の Response、Deno.serve

```
Deno.serve(async (req) => {
  const { name } = await req.json()
  return new Response(JSON.stringify({ hello: name }), { headers: { "content-type": "application/json" } })
})
```

```
export default {
  async fetch(req, ctx) {
    const hits = await ctx.kv.incr("hits")
    const on = await ctx.flags.isEnabled("new-checkout", req.claims?.sub ?? null)
    return Response.json({ hits, on })
  },
}
```

Supabase 向けに書いた関数もそのまま動きます。`new Response(...)` (ストリームを含む)、`Deno.serve(handler)`、`export default { fetch }`、`Deno.env.get()` が使えます。関数名のあとのパス (`/functions/v1/<name>/a/b`) は `req.url` で受け取ります。`EdgeRuntime.waitUntil(promise)` に渡した処理は、応答を先に返したあとも関数の時間上限の中で最後まで動き、その間もクエリと外部へのリクエストができます。
ストリームから作った Response は書いたとおりに送ります (サーバー送信イベント、モデルの回答)。`ctx.kv`、`ctx.config`、`ctx.flags` で KV とエッジ設定を使います (blackwall://edge)。

---

https://end.blackall.ai/docs/functions?lang=ja
