diff --git a/docs/infra/smalruby-api.md b/docs/infra/smalruby-api.md index 51cbe64f82..925b05c24d 100644 --- a/docs/infra/smalruby-api.md +++ b/docs/infra/smalruby-api.md @@ -8,10 +8,10 @@ | エンドポイント | Lambda | 用途 | |---|---|---| -| `GET /cors-proxy` | `smalruby-api-cors-proxy{stageSuffix}` | 任意 URL の CORS フリーフェッチ + Google Drive URL 変換 + バイナリ Base64 化。音声合成 (text2speech) もこの汎用プロキシ経由で `synthesis-service.scratch.mit.edu` を叩く | +| `GET /cors-proxy` | `smalruby-api-cors-proxy{stageSuffix}` | 任意 URL の CORS フリーフェッチ + Google Drive URL 変換 + バイナリ Base64 化。音声合成 (text2speech) と翻訳 (translate) もこの汎用プロキシ経由で `synthesis-service.scratch.mit.edu` / `translate-service.scratch.mit.edu` を叩く | | `GET /mesh-domain` | `smalruby-api-mesh-zone{stageSuffix}` | クライアント IP から Mesh ドメイン (CRC32 ハッシュ) を生成 | | `GET /scratch-api-proxy/projects/{projectId}` | `smalruby-api-scratch-projects{stageSuffix}` | Scratch 公式 API (project info) のステータス透過プロキシ | -| `GET /scratch-api-proxy/translate` | `smalruby-api-scratch-translate{stageSuffix}` | Scratch 公式翻訳サービスのプロキシ | +| `GET /scratch-api-proxy/translate` | `smalruby-api-scratch-translate{stageSuffix}` | **obsolete**。translate は共通 `cors-proxy` 経由に統一した (#862、text2speech #861 と同方式)。新規開発で使わない。Lambda/ルートは deploy を伴うため今は残置 | `OPTIONS` (preflight) は HTTP API v2 の **built-in CORS** が自動処理。旧 SAM の `cors-for-smalruby` Lambda は不要。 @@ -60,10 +60,12 @@ Scratch Foundation 公式 API (`https://api.scratch.mit.edu/projects/{projectId} 実装: `infra/smalruby-api/lambda/scratch-api-projects.ts` -### `GET /scratch-api-proxy/translate` +### `GET /scratch-api-proxy/translate` (obsolete) Scratch translate サービス (`https://translate-service.scratch.mit.edu/translate`) のプロキシ。 +> ⚠️ **obsolete — 新規開発で使わない**: 翻訳 (translate) は text2speech (#861) と同じく**汎用 `GET /cors-proxy?url=`** 経由に統一した (#862)。`cors-proxy` は翻訳のテキスト応答をそのまま返すため、この専用 Lambda は不要になった。フロント側の実装は `packages/scratch-vm/src/extensions/scratch3_translate/index.js` を参照。Lambda コード (`lambda/scratch-api-translate.ts`) と CDK ルート定義は **deploy を伴うため今は削除せず残置**している (別作業で撤去予定)。 + 実装: `infra/smalruby-api/lambda/scratch-api-translate.ts` > **音声合成 (text2speech) について**: 音声合成サービス (`https://synthesis-service.scratch.mit.edu/synth`) も同じ CORS 制約があるが、専用 Lambda は作らず**汎用 `GET /cors-proxy?url=`** を再利用する。`cors-proxy` は `audio/*` をバイナリと判定して Base64 返却 (`isBase64Encoded: true`) するため、API Gateway 側でバイト列にデコードされ、拡張の `arrayBuffer()` がそのまま音声を得られる。フロント側の実装は `packages/scratch-vm/src/extensions/scratch3_text2speech/index.js` を参照 (#859)。 diff --git a/docs/maintenance/smalruby-markers-vm.md b/docs/maintenance/smalruby-markers-vm.md index ac712c36ba..7f0d98ed44 100644 --- a/docs/maintenance/smalruby-markers-vm.md +++ b/docs/maintenance/smalruby-markers-vm.md @@ -20,7 +20,7 @@ scratch-vm の **upstream ファイルに埋め込んだ Smalruby マーカー** | `src/engine/runtime.js` | BEFORE_STEP event | upstream v13.7.2 が削除した `BEFORE_STEP` イベント(getter + `_step` での emit)を維持。Mesh v2 (broadcast-receiver.js / mesh-service.js) が毎フレーム queued remote events を流すために購読している | | `src/engine/blocks.js` | XML coords guard | `blockToXML` で x/y が finite number のときだけ XML 属性を出力。Ruby → blocks 変換の x/y 未指定 (undefined) を scratch-blocks v2 に正しく伝え、`fromRuby` 再レイアウト経路を維持する | | `src/engine/blocks.js` | orphaned-parent guard | `getTopLevelScript` で `block.parent` が this._blocks に存在しない場合に停止。Ruby → blocks 変換中の孤立 parent id で `undefined.parent` 参照クラッシュを防ぐ | -| `src/extensions/scratch3_translate/index.js` | translate CORS proxy | `serverURL` を Smalruby プロキシ (`https://api.smalruby.app/scratch-api-proxy/`) に上書き。Scratch の翻訳サービスは CORS を scratch.mit.edu 限定にしたため smalruby.app からの直叩きが失敗する。マーカー無しで上書きしていた過去の版が v13.7.2 upstream マージで静かに revert された (#857) ため、次回以降のマージで検知できるようマーカーで囲む | +| `src/extensions/scratch3_translate/index.js` | translate CORS proxy | 翻訳 URL を Smalruby の**汎用** CORS プロキシ (`https://api.smalruby.app/cors-proxy?url=`) で包む。Scratch の翻訳サービスは CORS を scratch.mit.edu 限定にしたため smalruby.app からの直叩きが失敗する。汎用 cors-proxy はテキスト応答をそのまま返すので専用 Lambda (`/scratch-api-proxy/translate`) は不要 (obsolete)。`serverURL` は upstream の値 (`https://translate-service.scratch.mit.edu/`) のまま維持し、URL 組み立て箇所だけラップするので upstream 差分が最小。text2speech (#859) と同じ方式に統一 (#862)。過去にマーカー無しで上書きした版が v13.7.2 upstream マージで静かに revert された (#857) ため、マーカーで囲んで次回以降のマージで検知できるようにする | | `src/extensions/scratch3_text2speech/index.js` | synthesis CORS proxy | 音声合成 URL を Smalruby の**汎用** CORS プロキシ (`https://api.smalruby.app/cors-proxy?url=`) で包む。Scratch の音声合成サービスは CORS を scratch.mit.edu 限定にしたため smalruby.app からの直叩きが失敗する。汎用 cors-proxy はバイナリ音声を Base64 で返却する (API Gateway がバイト列にデコード) ので専用 Lambda は不要。`SERVER_HOST` は upstream の値のまま維持し、URL 組み立て箇所だけラップするので upstream 差分が最小。translate (#857) と同じ根本原因 (#859) | ## 関連ファイル diff --git a/packages/scratch-vm/src/extensions/scratch3_translate/index.js b/packages/scratch-vm/src/extensions/scratch3_translate/index.js index 2791a0120b..31440ab923 100644 --- a/packages/scratch-vm/src/extensions/scratch3_translate/index.js +++ b/packages/scratch-vm/src/extensions/scratch3_translate/index.js @@ -24,14 +24,19 @@ const blockIconURI = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABQCAYA * The url of the translate server. * @type {string} */ +const serverURL = 'https://translate-service.scratch.mit.edu/'; + // === Smalruby: Start of translate CORS proxy === // Scratch's translate service is CORS-locked to scratch.mit.edu, so calling it // directly from smalruby.app fails ('Access-Control-Allow-Origin' mismatch). -// Route through the Smalruby proxy (infra/smalruby-api scratch-api-proxy/translate) -// which forwards to translate-service.scratch.mit.edu server-side and returns the -// response with permissive CORS headers. Keep this override across upstream merges -// (it was silently reverted during the v13.7.2 merge — see issue #857). -const serverURL = 'https://api.smalruby.app/scratch-api-proxy/'; +// Instead of a dedicated per-service Lambda, we reuse Smalruby's *generic* CORS +// proxy (infra/smalruby-api `GET /cors-proxy?url=`). It fetches +// the target server-side and returns the (text) response with permissive CORS +// headers. See where the translate request URL is built (search for CORS_PROXY_HOST). +// serverURL stays at the upstream value so upstream merges only touch the URL-build +// site (guarded by test/unit/extension_translate_proxy.js). Same root cause as +// text2speech (#857/#859), which the dedicated translate Lambda now replaces (#862). +const CORS_PROXY_HOST = 'https://api.smalruby.app/cors-proxy'; // === Smalruby: End of translate CORS proxy === /** @@ -272,6 +277,13 @@ class Scratch3TranslateBlocks { urlBase += '&text='; urlBase += encodeURIComponent(args.WORDS); + // === Smalruby: Start of translate CORS proxy === + // Wrap the translate URL in the generic Smalruby CORS proxy so smalruby.app + // is not blocked by the CORS-locked Scratch service. The whole translate URL + // (including its query string) becomes the encoded `url` param. + urlBase = `${CORS_PROXY_HOST}?url=${encodeURIComponent(urlBase)}`; + // === Smalruby: End of translate CORS proxy === + const tempThis = this; const translatePromise = fetchWithTimeout(urlBase, {}, serverTimeoutMs) .then(response => response.text()) diff --git a/packages/scratch-vm/test/unit/extension_translate_proxy.js b/packages/scratch-vm/test/unit/extension_translate_proxy.js index 67f8e39fb0..0c00062300 100644 --- a/packages/scratch-vm/test/unit/extension_translate_proxy.js +++ b/packages/scratch-vm/test/unit/extension_translate_proxy.js @@ -5,10 +5,11 @@ const extPath = require.resolve('../../src/extensions/scratch3_translate'); // The Translate extension is an upstream Scratch file whose translate service is // CORS-locked to scratch.mit.edu. Smalruby must route requests through its own -// proxy (api.smalruby.app/scratch-api-proxy/) so smalruby.app is not blocked by -// CORS. This test guards that the override is not silently reverted by an -// upstream merge (as happened during the v13.7.2 merge; see issue #857). -test('translate extension routes fetch through the Smalruby CORS proxy', (t) => { +// generic CORS proxy (api.smalruby.app/cors-proxy?url=) so +// smalruby.app is not blocked by CORS. This test guards that the proxy wrapping is +// not silently reverted by an upstream merge (same root cause as text2speech; see +// issue #862 / #859 / #857). +test('translate extension routes fetch through the generic Smalruby CORS proxy', (t) => { // Stub fetchWithTimeout before the extension captures it via destructuring at // module load time, then fresh-require the extension so it picks up the stub. const fetchModule = require(fetchModulePath); @@ -31,13 +32,19 @@ test('translate extension routes fetch through the Smalruby CORS proxy', (t) => t.ok(capturedUrl, 'fetchWithTimeout was called'); t.match( capturedUrl, - /^https:\/\/api\.smalruby\.app\/scratch-api-proxy\/translate\?/, - 'serverURL points to the Smalruby CORS proxy', + /^https:\/\/api\.smalruby\.app\/cors-proxy\?url=/, + 'request goes to the generic Smalruby CORS proxy', + ); + // The translate URL (with its query) is carried as the encoded `url` param. + t.match( + capturedUrl, + /url=https%3A%2F%2Ftranslate-service\.scratch\.mit\.edu%2Ftranslate/, + 'the CORS-locked translate URL is wrapped as the proxy `url` param', ); t.notMatch( capturedUrl, - /translate-service\.scratch\.mit\.edu/, - 'does not call the CORS-locked Scratch translate service directly', + /^https:\/\/translate-service\.scratch\.mit\.edu/, + 'the browser does not call the CORS-locked Scratch translate service directly', ); t.end(); });