@kubohiroya/turbowarp-extension-security-dialog 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/NOTICE.md CHANGED
@@ -32,3 +32,9 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32
32
  `assets/screenshots/required-modes-{en,ja}-20261025.png` are unmodified browser captures of the public Editor on 25 October 2026, showing Camera Source 0.19.3, Asset Cache 0.3.1 and Bubble 0.13.4. Their origin, Editor revisions and hashes are recorded in `assets/screenshots/capture-20261025.json`. The captured UI, icons and banners retain their respective rights holders and applicable licenses; this package does not relicense those elements.
33
33
 
34
34
  `assets/screenshots/localized-descriptions-{en,ja}-20261025.png` are unmodified captures of the updated public Editor on 25 October 2026, showing localized descriptions for Camera Source 0.19.4, Asset Cache 0.3.2 and Bubble 0.13.5. Their origin, revisions and hashes are recorded in `assets/screenshots/localized-descriptions-20261025.json`. The captured UI, icons and banners retain their respective rights holders and applicable licenses.
35
+
36
+ `assets/screenshots/required-approval-{en,ja}-20261028.png` are unmodified captures of the public Editor on 28 October 2026 after the required-mode approval update, showing Camera Source 0.19.4, Asset Cache 0.3.2 and Bubble 0.13.5 with Deny and With sandbox disabled. Their origin, verified distribution hashes, Editor revisions and image hashes are recorded in `assets/screenshots/required-approval-20261028.json`. The captured UI, icons and banners retain their respective rights holders and applicable licenses.
37
+
38
+ `assets/screenshots/preselected-required-{en,ja}-20261028.png` are unmodified captures of the public Editor on 28 October 2026 after automatic preselection was added when all listed extensions require unsandboxed execution. Camera Source 0.19.4, Asset Cache 0.3.2 and Bubble 0.13.5 have Without sandbox preselected and still require OK before execution. Origin, verified distribution hashes, Editor revisions and image hashes are recorded in `assets/screenshots/preselected-required-20261028.json`. The captured UI, icons and banners retain their respective rights holders and applicable licenses.
39
+
40
+ `assets/screenshots/hidden-bulk-required-{en,ja}-20261028.png` are unmodified captures of the public Editor on 28 October 2026 with Without sandbox preselected for all three required extensions and the redundant bulk selection section hidden. Camera Source 0.19.4, Asset Cache 0.3.2 and Bubble 0.13.5 still require OK before execution. Origin, verified distribution hashes, Editor revisions and image hashes are recorded in `assets/screenshots/hidden-bulk-required-20261028.json`. The captured UI, icons and banners retain their respective rights holders and applicable licenses.
package/README.ja.md CHANGED
@@ -5,17 +5,99 @@
5
5
  TurboWarpの複数の機能拡張を、1つの一覧で確認するための共通パッケージです。
6
6
  関連タスクと受け入れ基準は [scratch-gui #33](https://github.com/kubohiroya/scratch-gui/issues/33) に記録しています。
7
7
 
8
+ ## Pagesのカタログから機能拡張を選択する
9
+
10
+ `ExtensionSelectionList` は既存のバナー・メタデータ・詳細表示を共有し、各行の右端へチェックボックスを1つ表示します。
11
+ TurboWarp Plus一覧の対応範囲は [#28](https://github.com/kubohiroya/turbowarp-extension-security-dialog/issues/28) に記録しています。
12
+ ホスト側でGitHub PagesのJSONをHTTPS取得し、このコンポーネントへ渡します。
13
+
14
+ ```jsx
15
+ import {loadExtensionCatalog} from '@kubohiroya/turbowarp-extension-security-dialog';
16
+ import {ExtensionSelectionList} from '@kubohiroya/turbowarp-extension-security-dialog/react';
17
+
18
+ const controller = new AbortController();
19
+ const entries = await loadExtensionCatalog(
20
+ 'https://kubohiroya.github.io/turbowarp-plus-packages/extensions-v1.json',
21
+ controller.signal
22
+ );
23
+
24
+ const list = <ExtensionSelectionList entries={entries} locale="ja"
25
+ filterQuery={searchText} loadedIds={loadedExtensionIds} busy={loading}
26
+ onCancel={closeLibrary}
27
+ onComplete={ordered => host.reviewAndLoadInOrder(ordered)} />;
28
+ ```
29
+
30
+ 例の `host.reviewAndLoadInOrder` はホスト側で実装する実行許可確認・VM読み込み処理です。
31
+ `onComplete` は依存先を先に並べた重複のない配列を返し、`loadedIds` は除外します。
32
+ ある拡張を選ぶと推移的な依存先すべてがchecked・disabledになります。
33
+ 親を解除すると依存関係を再計算し、ほかの選択やユーザー自身の明示選択が必要としている拡張は選択を保ちます。
34
+ 読み込み済みの行はchecked・disabledです。検索と表示言語を切り替えても選択を保ちます。
35
+ `busy` で選択・フッター操作を無効にします。取得失敗・再試行・カタログの取得中止・VMエラーの表示と、
36
+ 読み込み成功に応じた `loadedIds` 更新はホスト側の担当です。
37
+
38
+ 各行は「手動で選択」(青)・「依存関係で自動選択」(紫)・「未選択」(中立色)を、背景・左枠・
39
+ アイコン・状態名で区別します。「読み込み済み」も独立した表示です。一覧の上部には凡例を表示します。
40
+ 自動選択した行には「Bubble、Sprite に必要」のように、必要としている明示選択元すべてを表示します。
41
+ 手動で選んだ拡張がほかの選択元の依存先になった場合も、「手動で選択」を維持し、依存元を併記して
42
+ チェックボックスを解除不可にします。DOMの `data-selection-state` は順に `selected` / `dependency` /
43
+ `unselected` / `loaded` です。
44
+
45
+ 任意のカテゴリ定義がある場合、ダッシュボードと同じ順序でカテゴリ見出し(`h2`)を表示します。
46
+ 日本語は `titleJa` があれば使用し、なければ `title` にフォールバックします。
47
+ 検索で空になったカテゴリは非表示になりますが、選択状態と「詳細」の対象は維持します。
48
+ カテゴリのない旧JSONは従来の平坦な一覧です。カテゴリ付きと未指定の項目が混在する場合、未指定分は
49
+ 末尾の「その他の機能拡張」へ表示します。配色はホストのモーダル背景色・文字色に従います。
50
+ 強調色は `--twextsec-manual-accent` / `--twextsec-dependency-accent` /
51
+ `--twextsec-unselected-accent` / `--twextsec-loaded-accent` で調整できます。
52
+
53
+ 一覧の表示・選択だけでは機能拡張のコードを読み込み・実行しません。
54
+ JSONのメタデータを直ちに表示し、「詳細」を開いたときだけソースを文字列として取得します。
55
+ 取得に失敗してもカタログのメタデータと選択は利用できます。
56
+ 実行許可確認には `ExtensionSecurityDialog context="catalog"` を使用でき、キャンセル文言が選択一覧へ戻る案内になります。
57
+ 既定の `context="project"` はプロジェクト読み込み用の文言を維持します。
58
+ ホストは確定された実行モードと独自のセキュリティ方針を尊重して読み込んでください。
59
+
60
+ JSON契約は `schemaVersion: 1` と `extensions` 配列です。各要素の必須項目は次のとおりです。
61
+
62
+ | 項目 | 内容 |
63
+ | --- | --- |
64
+ | `id` | ASCII英数字の機能拡張ID |
65
+ | `name` | 表示名 |
66
+ | `version` | 配布バージョン |
67
+ | `url` | バージョン固定の公開済み機能拡張JSへのHTTPS URL |
68
+ | `dependencies` | 同じJSONに含まれる依存先の機能拡張ID配列。npmパッケージ名ではない |
69
+
70
+ `package`・`description`・`descriptions`(`ja`等の小文字言語タグをキーとする説明)・`author`・`license`・
71
+ `banner`・`repository`・`unsandboxed`(例: `required`)は任意のメタデータです。
72
+ 完全なJSON例は[英語版のAPI例](README.md#selecting-extensions-from-a-pages-catalog)を参照してください。
73
+ 生成側でパッケージ依存を拡張IDへ変換し、ビルド専用パッケージを除外します。
74
+
75
+ カテゴリはトップレベルの任意項目 `categories: [{id, title, titleJa?}]` としてダッシュボード順に定義し、
76
+ 各 `extensions[].category` にそのIDを指定します。カテゴリIDの重複・不正な定義・未定義IDの参照は拒否します。
77
+ `parseExtensionCatalog` の返り値は従来どおり配列で、カテゴリ参照は各要素の
78
+ `category: {id, title, titleJa?, order}` へ解決します。`order` は定義の配列順です。
79
+
80
+ `parseExtensionCatalog` は未対応schema、ID/URL重複、存在しない依存先、循環依存、HTTPS以外や認証情報付きのURLを拒否します。
81
+ `resolveExtensionSelection(entries, explicitIds, loadedIds)` はReactなしでも同じ選択集合・依存順を計算します。
82
+ 返り値の `requiredBy` は依存先IDをキー、必要としている明示選択元IDの集合を値とするMapです。
83
+ 推移的依存・共有依存を含め、選択を解除すると依存元の集合も再計算します。
84
+ `loadExtensionCatalog` は任意のAbortSignalを受け付け、認証情報・Refererを送らずに取得し、HTTP失敗や不正なJSON契約を例外として返します。
85
+
86
+ 切り戻しはホストの起動時固定・既定OFFのフラグをOFFにして再読み込みするか、カタログ接続差分をrevertします。
87
+ 既存の3つの実行モードを持つセキュリティ確認は、そのまま利用できます。
88
+
8
89
  ## 公開Editorのスクリーンショット
9
90
 
10
91
  [英語版](./README.md)と日本語版は、同じダイアログの表示言語を切り替え、ラベルと拡張の説明文を各言語で表示しています。
11
- 2026年10月25日の[公開Editor](https://kubohiroya.github.io/turbowarp-extension-data-editor/editor.html?lang=ja)で、
12
- Camera Source 0.19.4・Asset Cache 0.3.2・Bubble 0.13.5を開いた実画面です。
92
+ 全件必須時の自動選択と一括選択欄の非表示に対応した[公開Editor](https://kubohiroya.github.io/turbowarp-extension-data-editor/editor.html?lang=ja)で、
93
+ 2026年10月28日にCamera Source 0.19.4・Asset Cache 0.3.2・Bubble 0.13.5を開いた実画面です。
13
94
 
14
- ![Camera Source・Asset Cache・Bubbleの説明が日本語で表示された確認画面。サンドボックスありは非対応で無効、サンドボックスなしは動作に必要。サンドボックス内では動作しない理由を表示](https://raw.githubusercontent.com/kubohiroya/turbowarp-extension-security-dialog/main/assets/screenshots/localized-descriptions-ja-20261025.png)
95
+ ![Camera Source・Asset Cache・Bubbleの説明が日本語で表示された修正後の確認画面。拒否とサンドボックスありは無効、サンドボックスなしを選択済みで動作に必要と表示。一括選択欄は非表示。OKで読み込むかキャンセルで中止できる](https://raw.githubusercontent.com/kubohiroya/turbowarp-extension-security-dialog/main/assets/screenshots/hidden-bulk-required-ja-20261028.png)
15
96
 
16
- 3拡張とも`Unsandboxed: required`を宣言しています。「サンドボックスあり」は無効化され、
17
- 「この拡張はサンドボックス内では動作しません。」と理由が表示されます。
18
- 「サンドボックスなし」には「動作に必要」と表示します。拒否は選択でき、実行には自分での選択とOKが必要です。
97
+ 3拡張とも`Unsandboxed: required`を宣言しています。「拒否」と「サンドボックスあり」は無効化され、
98
+ 理由が表示されます。「サンドボックスなし」だけが有効で、「動作に必要」と表示します。
99
+ 全拡張がこのモードを必要とする場合、全件のメタデータ取得後に選択済みになり、一括選択欄は非表示になります。
100
+ 「OK」で続行するか、「キャンセル」でプロジェクトの読み込みを中止します。
19
101
  必要なモードの表示は、安全性や配布元の信頼性の認定ではありません。
20
102
 
21
103
  | 機能拡張 | サンドボックスなしが必要な理由 |
@@ -25,7 +107,7 @@ Camera Source 0.19.4・Asset Cache 0.3.2・Bubble 0.13.5を開いた実画面で
25
107
  | Bubble | ホストの実行環境と描画機能へ直接アクセスして、ステージ上に重ねた吹き出しや立ち絵を描画・更新します。 |
26
108
 
27
109
  画像はダイアログ部分をブラウザーで直接撮影したもので、加工していません。
28
- 取得元の版・Editor build・画像hashは[撮影台帳](./assets/screenshots/localized-descriptions-20261025.json)に記録しています。
110
+ 取得元の版・Editor build・画像hashは[撮影台帳](./assets/screenshots/hidden-bulk-required-20261028.json)に記録しています。
29
111
 
30
112
  ## 連動するパッケージ
31
113
 
@@ -207,12 +289,15 @@ HTTP/HTTPSの出所URLは完全なURLを表示し、別タブで開きます。
207
289
  個別選択・一括選択・一覧の見出しは、禁止アイコンの「拒否」、盾の「サンドボックスあり」、
208
290
  警告の「サンドボックスなし」の順です。アイコンと色は文字ラベルを補助し、安全性の認定には使いません。
209
291
 
210
- 先頭の宣言コメントに `// Unsandboxed: required` があれば、「サンドボックスあり」を無効化して
211
- 動作しない理由を表示し、「サンドボックスなし」に「動作に必要」を付けます。拒否は選択できます。
292
+ 先頭の宣言コメントに `// Unsandboxed: required` があれば、「拒否」と「サンドボックスあり」を無効化して
293
+ 理由を表示し、選択できる唯一のモード「サンドボックスなし」に「動作に必要」を付けます。
294
+ 選択したモードを「OK」で承認するか、「キャンセル」でプロジェクトの読み込みを中止します。
212
295
  この宣言は作者の自己申告であり、未宣言・取得失敗から必須モードを推測しません。
213
296
 
214
- 実行する選択の確定はメタデータ取得の完了または失敗後です。遅れて取得した宣言に対応しない選択は、
215
- 保存済みの復元値も含めて解除し、選び直しを案内します。権限の広いモードへ自動で変更しません。
297
+ 「拒否」を含む選択の確定はメタデータ取得の完了または失敗後です。全拡張の取得が成功し、すべてが
298
+ この必須モードを宣言していれば、非対応の保存済み選択も含めて「サンドボックスなし」を選択済みにします。
299
+ 「OK」を押すまで拡張は実行せず、「キャンセル」で中止できます。取得中・未宣言・任意指定・取得失敗が
300
+ 1件でもあれば、この一括初期選択は行いません。混在する一覧では、非対応の選択を解除して選び直しを案内します。
216
301
  一括選択は対応する拡張だけに適用し、全件が非対応の一括ボタンは無効化します。
217
302
 
218
303
  ## 接続例
package/README.md CHANGED
@@ -12,14 +12,15 @@ Related tasks and acceptance criteria are tracked in
12
12
  [English](./README.md) and [Japanese](./README.ja.md) use the same dialog with localized labels and extension descriptions.
13
13
  This capture shows Camera Source 0.19.4, Asset Cache 0.3.2 and Bubble 0.13.5 in the
14
14
  [public Editor](https://kubohiroya.github.io/turbowarp-extension-data-editor/editor.html?lang=en)
15
- on 25 October 2026.
15
+ on 28 October 2026, with **Without sandbox** already selected for all three extensions and the unnecessary bulk selection section hidden.
16
16
 
17
- ![English descriptions for Camera Source, Asset Cache and Bubble: With sandbox is disabled as Unsupported for all three extensions; Without sandbox is marked Required to run, with the reason This extension cannot run inside the sandbox.](https://raw.githubusercontent.com/kubohiroya/turbowarp-extension-security-dialog/main/assets/screenshots/localized-descriptions-en-20261025.png)
17
+ ![English descriptions for Camera Source, Asset Cache and Bubble: Deny and With sandbox are disabled for all three extensions; Without sandbox is preselected and marked Required to run. The bulk selection section is hidden. Choose OK to load the project or Cancel to stop.](https://raw.githubusercontent.com/kubohiroya/turbowarp-extension-security-dialog/main/assets/screenshots/hidden-bulk-required-en-20261028.png)
18
18
 
19
- All three extensions declare `Unsandboxed: required`. **With sandbox** is disabled, with
20
- **This extension cannot run inside the sandbox.** shown as the reason. **Without sandbox**
21
- is marked **Required to run**. Deny remains available, and running still requires an explicit
22
- choice and OK. These labels describe execution requirements, not a safety certification.
19
+ All three extensions declare `Unsandboxed: required`. **Deny** and **With sandbox** are
20
+ disabled, with the reason shown. **Without sandbox** is enabled and marked **Required to run**.
21
+ When every listed extension requires this mode, it is preselected after all metadata loads, and the bulk selection section is hidden.
22
+ Press OK to continue, or **Cancel** to stop loading the project.
23
+ These labels describe execution requirements, not a safety certification.
23
24
 
24
25
  | Extension | Why it needs to run without a sandbox |
25
26
  | --- | --- |
@@ -28,7 +29,7 @@ choice and OK. These labels describe execution requirements, not a safety certif
28
29
  | Bubble | Accesses the host runtime and renderer directly to draw and update layered speech bubbles and portraits on the stage. |
29
30
 
30
31
  The PNG is an unmodified, modal-only browser capture. Source versions, Editor build and image
31
- hashes are recorded in [the capture manifest](./assets/screenshots/localized-descriptions-20261025.json).
32
+ hashes are recorded in [the capture manifest](./assets/screenshots/hidden-bulk-required-20261028.json).
32
33
 
33
34
  ## How it fits together
34
35
 
@@ -222,15 +223,19 @@ Per-row choices, the bulk choices and the list heading are ordered: **Deny** (pr
222
223
  icon), **With sandbox** (shield), **Without sandbox** (warning). Icons and colour support the
223
224
  text labels; they are not a safety certification.
224
225
 
225
- If the leading declaration comment contains `// Unsandboxed: required`, *With sandbox* is
226
- disabled with the reason shown, and *Without sandbox* is marked *Required to run*. Deny stays
227
- selectable. This declaration is the author's own claim; a required mode is never inferred from
226
+ If the leading declaration comment contains `// Unsandboxed: required`, *Deny* and *With sandbox*
227
+ are disabled with the reason shown, and *Without sandbox* is the only available mode, marked
228
+ *Required to run*. Press OK to approve the selected modes, or Cancel to stop loading
229
+ the project. This declaration is the author's own claim; a required mode is never inferred from
228
230
  a missing declaration or a failed fetch.
229
231
 
230
- A choice to run is only committed once metadata has loaded or failed. A selection that
231
- contradicts a late-arriving declaration is cleared — including a restored saved value — and
232
- the user is asked to choose again. The mode is never silently widened. A bulk choice applies
233
- only to extensions that support it, and a bulk button is disabled when no listed extension does.
232
+ A choice, including Deny, is only committed once metadata has loaded or failed. When every
233
+ listed extension has loaded metadata declaring this requirement, *Without sandbox* is
234
+ preselected for all entries, including entries with incompatible saved choices. Nothing runs
235
+ until the user presses OK; Cancel still stops loading the project. Pending, missing, optional
236
+ or failed declarations prevent this batch default. In mixed batches, incompatible choices
237
+ are cleared and the user is asked to choose again. A bulk choice applies only to extensions
238
+ that support it, and a bulk button is disabled when no listed extension does.
234
239
 
235
240
  ## Wiring it up
236
241
 
@@ -251,6 +256,107 @@ surfacing as a VM error is identified with `isExtensionSecurityCancelled(error)`
251
256
  Under Webpack 4 with CSS Modules, load `webpack4.css` instead of `styles.css`; root-level
252
257
  compatibility entries resolve for older Webpack 4 setups that do not read package exports.
253
258
 
259
+ ## Selecting extensions from a Pages catalog
260
+
261
+ `ExtensionSelectionList` shares the security dialog's banners, metadata and Details view,
262
+ with one checkbox at the right of each row. This API supports the TurboWarp Plus library
263
+ tracked in [#28](https://github.com/kubohiroya/turbowarp-extension-security-dialog/issues/28).
264
+ The host fetches a versioned JSON catalog over HTTPS:
265
+
266
+ ```jsx
267
+ import {loadExtensionCatalog} from '@kubohiroya/turbowarp-extension-security-dialog';
268
+ import {ExtensionSelectionList} from '@kubohiroya/turbowarp-extension-security-dialog/react';
269
+
270
+ const controller = new AbortController();
271
+ const entries = await loadExtensionCatalog(
272
+ 'https://kubohiroya.github.io/turbowarp-plus-packages/extensions-v1.json',
273
+ controller.signal
274
+ );
275
+
276
+ const list = <ExtensionSelectionList entries={entries} locale="en"
277
+ filterQuery={searchText} loadedIds={loadedExtensionIds} busy={loading}
278
+ onCancel={closeLibrary}
279
+ onComplete={ordered => host.reviewAndLoadInOrder(ordered)} />;
280
+ ```
281
+
282
+ `host.reviewAndLoadInOrder` represents the host's permission review and VM loading path.
283
+ The callback receives dependencies before their dependants, each once, excluding
284
+ `loadedIds`. Selecting an extension checks and disables all transitive dependencies.
285
+ Removing its selection recomputes the closure; dependencies remain checked if another
286
+ selection needs them or the user selected them explicitly. Loaded rows are checked and
287
+ disabled. Search and locale changes preserve selection. `busy` disables selection and the
288
+ footer controls. The host owns fetch errors, retry, catalog cancellation, VM errors, and
289
+ updating `loadedIds` after each successful load.
290
+
291
+ Rows distinguish **Selected by you** (blue), **Selected automatically** (purple), and
292
+ **Not selected** (neutral), using a background, left border, icon and label together.
293
+ Already loaded rows have their own label and accent. A legend explains the states above
294
+ the list. Automatically selected dependencies name every selected root that needs them,
295
+ for example “Required by Bubble, Sprite”. An explicitly selected dependency keeps its
296
+ “Selected by you” state while another root requires it; its checkbox stays disabled and
297
+ the dependency reason is also shown. The row's `data-selection-state` is `selected`,
298
+ `dependency`, `unselected` or `loaded`.
299
+
300
+ Optional catalog categories render as semantic `h2` sections in dashboard order. Japanese
301
+ uses `titleJa` when available, otherwise `title`; other locales use `title`. Search hides
302
+ empty sections without changing selection or source controls. Catalogs without categories
303
+ keep a flat list. If categorized and uncategorized entries are mixed, the latter appear
304
+ under “Other extensions” at the end. Theme colors use the host's modal background and
305
+ foreground; accent overrides are `--twextsec-manual-accent`, `--twextsec-dependency-accent`,
306
+ `--twextsec-unselected-accent`, and `--twextsec-loaded-accent`.
307
+
308
+ The selection list never loads or runs extension code. Catalog metadata renders immediately;
309
+ only Details fetches the source as text. Source failures leave catalog metadata and selection
310
+ usable. `ExtensionSecurityDialog context="catalog"` provides permission review with cancel
311
+ wording that returns to this list; its default `context="project"` retains project wording.
312
+ The host must respect the committed execution modes and its own security policy.
313
+
314
+ The Pages document has the following contract (`schemaVersion: 1`):
315
+
316
+ ```json
317
+ {
318
+ "schemaVersion": 1,
319
+ "categories": [{"id": "camera", "title": "Camera", "titleJa": "カメラ"}],
320
+ "extensions": [
321
+ {
322
+ "id": "cameraSource",
323
+ "name": "Camera Source",
324
+ "category": "camera",
325
+ "version": "1.2.3",
326
+ "package": "@example/camera-source",
327
+ "url": "https://cdn.jsdelivr.net/npm/@example/camera-source@1.2.3/dist/extension.js",
328
+ "description": "Share a camera stream.",
329
+ "descriptions": {"ja": "カメラ映像を共有します。"},
330
+ "repository": "https://github.com/example/camera-source",
331
+ "banner": "https://example.com/camera-source.svg",
332
+ "unsandboxed": "required",
333
+ "dependencies": []
334
+ }
335
+ ]
336
+ }
337
+ ```
338
+
339
+ IDs contain ASCII letters and digits. `name`, `version`, `url`, and `dependencies` are
340
+ required. Dependencies are extension IDs in this document, not npm package names;
341
+ the generator resolves package dependencies to these IDs. `package`, descriptions, author,
342
+ license, banner, repository and execution requirement are optional metadata. The generator
343
+ uses version-pinned, published distribution URLs and excludes build-only packages.
344
+ `parseExtensionCatalog` rejects unsupported schemas, duplicate IDs/URLs, missing dependencies,
345
+ cycles, and non-HTTPS or credential-bearing URLs before the UI or VM receives entries.
346
+ `resolveExtensionSelection(entries, explicitIds, loadedIds)` exposes the same closure and
347
+ load order independently of React. Its `requiredBy` map associates each dependency ID with
348
+ a set of the explicit root IDs that require it, including transitive and shared dependencies.
349
+ Optional `categories: [{id, title, titleJa?}]` are declared once, in dashboard order;
350
+ `extensions[].category` references one of those IDs. Duplicate category IDs, invalid
351
+ definitions and unknown references are rejected. `parseExtensionCatalog` retains its array
352
+ return type and resolves each reference to `entry.category: {id, title, titleJa?, order}`.
353
+ `loadExtensionCatalog` accepts an optional AbortSignal,
354
+ omits credentials/referrers, and rejects failed HTTP responses or invalid JSON contracts.
355
+
356
+ To roll back catalog integration, turn off the host's startup flag (off by default) and
357
+ reload, or revert the catalog wiring. The existing three-way security dialog remains
358
+ available without the catalog API.
359
+
254
360
  ## Stored preferences
255
361
 
256
362
  Preferences are saved only when OK was pressed **and** the project then opened successfully.
package/dist/index.cjs CHANGED
@@ -50,12 +50,15 @@ __export(index_exports, {
50
50
  getExtensionDescription: () => getExtensionDescription,
51
51
  getGitHubRepository: () => getGitHubRepository,
52
52
  getPreviewCandidates: () => getPreviewCandidates,
53
+ loadExtensionCatalog: () => loadExtensionCatalog,
53
54
  loadExtensionInfo: () => loadExtensionInfo,
54
55
  messages: () => messages,
56
+ parseExtensionCatalog: () => parseExtensionCatalog,
55
57
  parseExtensionMetadata: () => parseExtensionMetadata,
56
58
  preferences: () => preferences_default,
57
59
  projectFingerprint: () => projectFingerprint,
58
60
  readPreferences: () => readPreferences,
61
+ resolveExtensionSelection: () => resolveExtensionSelection,
59
62
  restoreChoices: () => restoreChoices,
60
63
  writePreferences: () => writePreferences
61
64
  });
@@ -334,8 +337,28 @@ var preferences_default = preferences;
334
337
  // src/messages.js
335
338
  var messages = {
336
339
  "en": {
340
+ "selectionIntro": "Choose extensions to add. Required extensions are selected automatically.",
341
+ "selectionRequired": "Requires running without a sandbox.",
342
+ "selectedState": "Selected by you",
343
+ "dependencyState": "Selected automatically",
344
+ "unselectedState": "Not selected",
345
+ "loadedState": "Already loaded",
346
+ "selectionLegend": "Selection states",
347
+ "requiredBy": "Required by {names}",
348
+ "otherCategory": "Other extensions",
349
+ "catalogRequiredIntro": "All selected extensions require Without sandbox, which is already selected. Choose OK to load them, or Cancel to return to the selection list.",
350
+ "catalogRequired": "This extension cannot run inside the sandbox. Choose Without sandbox, or Cancel to return to the selection list.",
351
+ "catalogCancelQuestion": "Cancel loading the selected extensions and return to the selection list?",
352
+ "selectExtension": "Select",
353
+ "dependency": "Required by another selection",
354
+ "alreadyLoaded": "Already loaded",
355
+ "noResults": "No matching extensions",
356
+ "selectedSummary": "{count} extensions to load",
357
+ "loadSelected": "Load selected extensions",
358
+ "loadingExtensions": "Loading\u2026",
337
359
  "title": "Extension Security",
338
360
  "intro": "Choose how to run each extension (you can choose while metadata loads).",
361
+ "allRequiredIntro": "All extensions require Without sandbox, which is already selected. Choose OK to load the project, or Cancel to stop.",
339
362
  "progress": "Extension information: {done}/{total}",
340
363
  "bulk": "Apply to all listed extensions",
341
364
  "extension": "Extension",
@@ -350,7 +373,7 @@ var messages = {
350
373
  "embedded": "Embedded in this project",
351
374
  "pending": "Getting metadata\u2026",
352
375
  "failed": "Could not retrieve metadata",
353
- "required": "This extension cannot run inside the sandbox.",
376
+ "required": "This extension cannot run inside the sandbox. Choose Without sandbox, or Cancel to stop loading the project.",
354
377
  "requiredToRun": "Required to run",
355
378
  "unsupported": "Unsupported",
356
379
  "choiceReset": "The incompatible selection was cleared. Choose how to run this extension again.",
@@ -371,8 +394,28 @@ var messages = {
371
394
  "warning": "Extensions without a sandbox can access your project and browser information."
372
395
  },
373
396
  "ja": {
397
+ "selectionIntro": "\u8FFD\u52A0\u3059\u308B\u6A5F\u80FD\u62E1\u5F35\u3092\u9078\u3093\u3067\u304F\u3060\u3055\u3044\u3002\u4F9D\u5B58\u3059\u308B\u6A5F\u80FD\u62E1\u5F35\u3082\u81EA\u52D5\u3067\u9078\u629E\u3055\u308C\u307E\u3059\u3002",
398
+ "selectionRequired": "\u30B5\u30F3\u30C9\u30DC\u30C3\u30AF\u30B9\u306A\u3057\u3067\u306E\u5B9F\u884C\u304C\u5FC5\u8981\u3067\u3059\u3002",
399
+ "selectedState": "\u624B\u52D5\u3067\u9078\u629E",
400
+ "dependencyState": "\u4F9D\u5B58\u95A2\u4FC2\u3067\u81EA\u52D5\u9078\u629E",
401
+ "unselectedState": "\u672A\u9078\u629E",
402
+ "loadedState": "\u8AAD\u307F\u8FBC\u307F\u6E08\u307F",
403
+ "selectionLegend": "\u9078\u629E\u72B6\u614B\u306E\u51E1\u4F8B",
404
+ "requiredBy": "{names} \u306B\u5FC5\u8981",
405
+ "otherCategory": "\u305D\u306E\u4ED6\u306E\u6A5F\u80FD\u62E1\u5F35",
406
+ "catalogRequiredIntro": "\u9078\u629E\u3057\u305F\u3059\u3079\u3066\u306E\u6A5F\u80FD\u62E1\u5F35\u3067\u5FC5\u8981\u306A\u300C\u30B5\u30F3\u30C9\u30DC\u30C3\u30AF\u30B9\u306A\u3057\u300D\u3092\u9078\u629E\u6E08\u307F\u3067\u3059\u3002\u300COK\u300D\u3067\u8AAD\u307F\u8FBC\u3080\u304B\u3001\u300C\u30AD\u30E3\u30F3\u30BB\u30EB\u300D\u3067\u9078\u629E\u4E00\u89A7\u306B\u623B\u308A\u307E\u3059\u3002",
407
+ "catalogRequired": "\u3053\u306E\u62E1\u5F35\u306F\u30B5\u30F3\u30C9\u30DC\u30C3\u30AF\u30B9\u5185\u3067\u306F\u52D5\u4F5C\u3057\u307E\u305B\u3093\u3002\u300C\u30B5\u30F3\u30C9\u30DC\u30C3\u30AF\u30B9\u306A\u3057\u300D\u3092\u9078\u3076\u304B\u3001\u300C\u30AD\u30E3\u30F3\u30BB\u30EB\u300D\u3067\u9078\u629E\u4E00\u89A7\u306B\u623B\u308A\u307E\u3059\u3002",
408
+ "catalogCancelQuestion": "\u9078\u629E\u3057\u305F\u6A5F\u80FD\u62E1\u5F35\u306E\u8AAD\u307F\u8FBC\u307F\u3092\u53D6\u308A\u6D88\u3057\u3066\u3001\u9078\u629E\u4E00\u89A7\u306B\u623B\u308A\u307E\u3059\u304B\uFF1F",
409
+ "selectExtension": "\u9078\u629E",
410
+ "dependency": "\u9078\u629E\u3057\u305F\u62E1\u5F35\u306B\u5FC5\u8981",
411
+ "alreadyLoaded": "\u8AAD\u307F\u8FBC\u307F\u6E08\u307F",
412
+ "noResults": "\u4E00\u81F4\u3059\u308B\u6A5F\u80FD\u62E1\u5F35\u304C\u3042\u308A\u307E\u305B\u3093",
413
+ "selectedSummary": "\u8AAD\u307F\u8FBC\u3080\u6A5F\u80FD\u62E1\u5F35 {count} \u4EF6",
414
+ "loadSelected": "\u9078\u629E\u3057\u305F\u62E1\u5F35\u3092\u8AAD\u307F\u8FBC\u3080",
415
+ "loadingExtensions": "\u8AAD\u307F\u8FBC\u307F\u4E2D\u2026",
374
416
  "title": "\u6A5F\u80FD\u62E1\u5F35\u30BB\u30AD\u30E5\u30EA\u30C6\u30A3",
375
417
  "intro": "\u5B9F\u884C\u65B9\u6CD5\u3092\u9078\u3093\u3067\u304F\u3060\u3055\u3044\uFF08\u53D6\u5F97\u4E2D\u3067\u3082\u9078\u629E\u3067\u304D\u307E\u3059\uFF09",
418
+ "allRequiredIntro": "\u3059\u3079\u3066\u306E\u6A5F\u80FD\u62E1\u5F35\u3067\u5FC5\u8981\u306A\u300C\u30B5\u30F3\u30C9\u30DC\u30C3\u30AF\u30B9\u306A\u3057\u300D\u3092\u9078\u629E\u6E08\u307F\u3067\u3059\u3002\u300COK\u300D\u3067\u8AAD\u307F\u8FBC\u3080\u304B\u3001\u300C\u30AD\u30E3\u30F3\u30BB\u30EB\u300D\u3067\u4E2D\u6B62\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
376
419
  "progress": "\u6A5F\u80FD\u62E1\u5F35\u306E\u60C5\u5831\uFF1A{done}/{total} \u4EF6\u3092\u78BA\u8A8D",
377
420
  "bulk": "\u8868\u793A\u4E2D\u306E\u3059\u3079\u3066\u306B\u9069\u7528",
378
421
  "extension": "\u6A5F\u80FD\u62E1\u5F35",
@@ -387,7 +430,7 @@ var messages = {
387
430
  "embedded": "\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u5185\u306B\u57CB\u3081\u8FBC\u307F",
388
431
  "pending": "\u30E1\u30BF\u30C7\u30FC\u30BF\u3092\u53D6\u5F97\u4E2D\u2026",
389
432
  "failed": "\u30E1\u30BF\u30C7\u30FC\u30BF\u3092\u53D6\u5F97\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F",
390
- "required": "\u3053\u306E\u62E1\u5F35\u306F\u30B5\u30F3\u30C9\u30DC\u30C3\u30AF\u30B9\u5185\u3067\u306F\u52D5\u4F5C\u3057\u307E\u305B\u3093\u3002",
433
+ "required": "\u3053\u306E\u62E1\u5F35\u306F\u30B5\u30F3\u30C9\u30DC\u30C3\u30AF\u30B9\u5185\u3067\u306F\u52D5\u4F5C\u3057\u307E\u305B\u3093\u3002\u300C\u30B5\u30F3\u30C9\u30DC\u30C3\u30AF\u30B9\u306A\u3057\u300D\u3092\u9078\u3076\u304B\u3001\u300C\u30AD\u30E3\u30F3\u30BB\u30EB\u300D\u3067\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u306E\u8AAD\u307F\u8FBC\u307F\u3092\u4E2D\u6B62\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
391
434
  "requiredToRun": "\u52D5\u4F5C\u306B\u5FC5\u8981",
392
435
  "unsupported": "\u975E\u5BFE\u5FDC",
393
436
  "choiceReset": "\u975E\u5BFE\u5FDC\u306E\u9078\u629E\u3092\u89E3\u9664\u3057\u307E\u3057\u305F\u3002\u5B9F\u884C\u65B9\u6CD5\u3092\u9078\u3073\u76F4\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
@@ -413,3 +456,112 @@ var formatMessage = (locale, key, values = {}) => {
413
456
  const template = messages[language][key] || messages.en[key] || key;
414
457
  return template.replace(/\{(\w+)\}/g, (match, name) => String(values[name] === void 0 ? match : values[name]));
415
458
  };
459
+
460
+ // src/selection.js
461
+ var httpsURL = (value) => {
462
+ if (typeof value !== "string" || !value.trim()) throw new Error("Expected an HTTPS URL");
463
+ const url = new URL(value);
464
+ if (url.protocol !== "https:" || url.username || url.password) throw new Error("Expected an HTTPS URL");
465
+ return url.href;
466
+ };
467
+ var resolveExtensionSelection = (entries, explicitIds = [], loadedIds = []) => {
468
+ const byId = /* @__PURE__ */ new Map();
469
+ for (const entry of entries) {
470
+ if (!entry || typeof entry.id !== "string" || !entry.id || byId.has(entry.id)) {
471
+ throw new Error(`Duplicate or missing extension ID: ${entry && entry.id}`);
472
+ }
473
+ if (entry.dependencies !== void 0 && (!Array.isArray(entry.dependencies) || entry.dependencies.some((id) => typeof id !== "string" || !id))) {
474
+ throw new Error(`Invalid extension dependencies: ${entry.id}`);
475
+ }
476
+ byId.set(entry.id, entry);
477
+ }
478
+ const selected = /* @__PURE__ */ new Set();
479
+ const required = /* @__PURE__ */ new Set();
480
+ const requiredBy = /* @__PURE__ */ new Map();
481
+ const visiting = /* @__PURE__ */ new Set();
482
+ const ordered = [];
483
+ const visit = (id) => {
484
+ const entry = byId.get(id);
485
+ if (!entry) throw new Error(`Missing extension dependency: ${id}`);
486
+ if (visiting.has(id)) throw new Error(`Cyclic extension dependency: ${id}`);
487
+ if (selected.has(id)) return;
488
+ visiting.add(id);
489
+ for (const dependency of entry.dependencies || []) {
490
+ required.add(dependency);
491
+ visit(dependency);
492
+ }
493
+ visiting.delete(id);
494
+ selected.add(id);
495
+ ordered.push(entry);
496
+ };
497
+ const roots = [...explicitIds].filter((id) => byId.has(id));
498
+ for (const id of roots) visit(id);
499
+ for (const root of roots) {
500
+ const seen = /* @__PURE__ */ new Set();
501
+ const describeDependencies = (id) => {
502
+ for (const dependency of byId.get(id).dependencies || []) {
503
+ if (seen.has(dependency)) continue;
504
+ seen.add(dependency);
505
+ if (!requiredBy.has(dependency)) requiredBy.set(dependency, /* @__PURE__ */ new Set());
506
+ requiredBy.get(dependency).add(root);
507
+ describeDependencies(dependency);
508
+ }
509
+ };
510
+ describeDependencies(root);
511
+ }
512
+ const loaded = new Set(loadedIds);
513
+ return { selected, required, requiredBy, loaded, ordered: ordered.filter((entry) => !loaded.has(entry.id)) };
514
+ };
515
+ var parseExtensionCatalog = (data) => {
516
+ if (!data || data.schemaVersion !== 1 || !Array.isArray(data.extensions)) {
517
+ throw new Error("Unsupported extension catalog");
518
+ }
519
+ const categories = /* @__PURE__ */ new Map();
520
+ if (data.categories !== void 0) {
521
+ if (!Array.isArray(data.categories)) throw new Error("Invalid extension catalog categories");
522
+ data.categories.forEach((category, order) => {
523
+ if (!category || typeof category.id !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(category.id) || typeof category.title !== "string" || !category.title.trim() || category.titleJa !== void 0 && (typeof category.titleJa !== "string" || !category.titleJa.trim())) {
524
+ throw new Error("Invalid extension catalog category");
525
+ }
526
+ if (categories.has(category.id)) throw new Error(`Duplicate extension category: ${category.id}`);
527
+ categories.set(category.id, __spreadValues({
528
+ id: category.id,
529
+ title: category.title,
530
+ order
531
+ }, category.titleJa !== void 0 ? { titleJa: category.titleJa } : {}));
532
+ });
533
+ }
534
+ const urls = /* @__PURE__ */ new Set();
535
+ const entries = data.extensions.map((item) => {
536
+ if (!item || typeof item.id !== "string" || !/^[a-zA-Z0-9]+$/.test(item.id) || typeof item.name !== "string" || !item.name.trim() || typeof item.version !== "string" || !item.version || !Array.isArray(item.dependencies) || item.dependencies.some((id) => typeof id !== "string" || !/^[a-zA-Z0-9]+$/.test(id))) {
537
+ throw new Error("Invalid extension catalog entry");
538
+ }
539
+ const url = httpsURL(item.url);
540
+ if (urls.has(url)) throw new Error(`Duplicate extension URL: ${url}`);
541
+ urls.add(url);
542
+ const metadata = { name: item.name, id: item.id, version: item.version };
543
+ for (const field of ["description", "author", "license", "updatedAt", "unsandboxed"]) {
544
+ if (typeof item[field] === "string") metadata[field] = item[field];
545
+ }
546
+ if (item.descriptions && typeof item.descriptions === "object") {
547
+ metadata.descriptions = Object.fromEntries(Object.entries(item.descriptions).filter(([locale, text]) => /^[a-z]{2,8}(?:-[a-z0-9]{1,8})*$/.test(locale) && typeof text === "string"));
548
+ }
549
+ if (item.banner) metadata.banner = httpsURL(item.banner);
550
+ if (item.repository) metadata.repository = httpsURL(item.repository);
551
+ const entry = { id: item.id, ids: [item.id], url, dependencies: [...new Set(item.dependencies)], metadata };
552
+ if (item.category !== void 0) {
553
+ if (typeof item.category !== "string" || !categories.has(item.category)) {
554
+ throw new Error(`Missing extension category: ${item.category}`);
555
+ }
556
+ entry.category = categories.get(item.category);
557
+ }
558
+ return entry;
559
+ });
560
+ resolveExtensionSelection(entries, entries.map((entry) => entry.id));
561
+ return entries;
562
+ };
563
+ var loadExtensionCatalog = async (url, signal) => {
564
+ const response = await fetch(httpsURL(url), { signal, credentials: "omit", referrerPolicy: "no-referrer" });
565
+ if (!response.ok) throw new Error(`Extension catalog HTTP ${response.status}`);
566
+ return parseExtensionCatalog(await response.json());
567
+ };
package/dist/index.d.ts CHANGED
@@ -24,3 +24,17 @@ export const preferences: {
24
24
  };
25
25
  export const messages: Record<string, Record<string, string>>;
26
26
  export function formatMessage(locale: string, key: string, values?: Record<string, string | number>): string;
27
+
28
+ export interface ExtensionCategory {
29
+ id: string; title: string; titleJa?: string; order?: number;
30
+ }
31
+ export interface SelectableExtension extends ExtensionEntry {
32
+ id: string; dependencies: string[]; metadata: ExtensionMetadata; category?: ExtensionCategory;
33
+ }
34
+ export interface ExtensionSelection {
35
+ selected: Set<string>; required: Set<string>; requiredBy: Map<string, Set<string>>;
36
+ loaded: Set<string>; ordered: SelectableExtension[];
37
+ }
38
+ export function resolveExtensionSelection(entries: SelectableExtension[], explicitIds?: Iterable<string>, loadedIds?: Iterable<string>): ExtensionSelection;
39
+ export function parseExtensionCatalog(data: unknown): SelectableExtension[];
40
+ export function loadExtensionCatalog(url: string, signal?: AbortSignal): Promise<SelectableExtension[]>;