@kubohiroya/turbowarp-webrtc-qrcode-pairing 0.1.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.
Files changed (46) hide show
  1. package/LICENSE +373 -0
  2. package/README.ja.md +122 -0
  3. package/README.md +414 -0
  4. package/dist/extension-manifest.json +318 -0
  5. package/dist/webrtc-qrcode-pairing.js +4064 -0
  6. package/docs/architecture.ja.md +57 -0
  7. package/docs/architecture.md +65 -0
  8. package/docs/design/qr-pairing/README.md +16 -0
  9. package/docs/design/qr-pairing/architecture.md +191 -0
  10. package/docs/design/qr-pairing/block-api.md +123 -0
  11. package/docs/design/qr-pairing/dataflow.md +202 -0
  12. package/docs/design/qr-pairing/extraction-plan.md +134 -0
  13. package/docs/design/qr-pairing/interfaces.ts +473 -0
  14. package/docs/design/qr-pairing/requirements.md +152 -0
  15. package/docs/design/qr-pairing/test-plan.md +126 -0
  16. package/docs/index.html +34 -0
  17. package/docs/initialization-issue.ja.md +34 -0
  18. package/docs/integration-guide.ja.md +245 -0
  19. package/docs/integration-guide.md +258 -0
  20. package/docs/migration.ja.md +110 -0
  21. package/docs/migration.md +115 -0
  22. package/package.json +90 -0
  23. package/schemas/extension-manifest.schema.json +49 -0
  24. package/src/block-definitions.json +264 -0
  25. package/src/clock.ts +18 -0
  26. package/src/config/feature-flags.ts +18 -0
  27. package/src/config/qr-config.ts +22 -0
  28. package/src/config.ts +12 -0
  29. package/src/errors.ts +51 -0
  30. package/src/extension-manifest.ts +149 -0
  31. package/src/extension.ts +281 -0
  32. package/src/globals.d.ts +54 -0
  33. package/src/index.ts +8 -0
  34. package/src/pairing/controller.ts +836 -0
  35. package/src/pairing/limits.ts +12 -0
  36. package/src/pairing/types.ts +96 -0
  37. package/src/ports/display.ts +136 -0
  38. package/src/ports/qr-scan.ts +151 -0
  39. package/src/ports/webrtc.ts +61 -0
  40. package/src/qr/courier.ts +267 -0
  41. package/src/qr/envelope.ts +0 -0
  42. package/src/qr/hash.ts +13 -0
  43. package/src/qr/limits.ts +44 -0
  44. package/src/qr/svg.ts +34 -0
  45. package/templates/README.ja.md +183 -0
  46. package/templates/README.md +198 -0
@@ -0,0 +1,134 @@
1
+ # 移設計画(turbowarp-realtime-motion-capture からの選別)
2
+
3
+ 対応要求: requirements.md §7 / Issue #1「既存コードの抽出範囲」
4
+
5
+ 抽出元: `/Users/hiroya/Dev/turbowarp-realtime-motion-capture`(`@kubohiroya/turbowarp-realtime-motion-capture`)。
6
+ 抽出元の削除は本Issueの完了条件ではない(NFR-5)。ここでは**コピー元と改変内容**だけを決める。
7
+
8
+ ## 0. 移設の区分
9
+
10
+ | 区分 | 意味 |
11
+ |---|---|
12
+ | **A: ほぼそのまま** | import路とエラー文言のみ調整。ロジックは変更しない |
13
+ | **B: 一般化して移設** | motion-capture固有の型・命名・前提を外す。ロジックの骨格は維持 |
14
+ | **C: 分解して再設計** | 概念は流用するが、往復・多重session・取消のために書き直す |
15
+ | **D: 新規** | 移設元に存在しない |
16
+ | **E: 移設しない** | motion-capture固有の関心事 |
17
+
18
+ ## 1. 実装ファイル
19
+
20
+ | 移設元 | 行数 | 移設先 | 区分 | 具体的な作業 |
21
+ |---|---|---|---|---|
22
+ | `src/qr-svg.ts` | 39 | `src/qr/svg.ts` | A | `import type {QrErrorCorrectionLevel} from "./qr-courier.js"` → `"./envelope.js"`。それ以外は同一 |
23
+ | `src/sprite-skin.ts` | 112 | `src/ports/display.ts` | A | クラス名は`TemporarySpriteSkinManager`のまま。エラー文言の`Offer QR`→`Pairing QR`。`DisplayPort`をimplementsする宣言を追加 |
24
+ | `src/qr-courier.ts` の定数群 | (冒頭6行) | `src/qr/limits.ts` | B | `MAX_MESSAGE_LENGTH` / `MAX_PART_COUNT` / `MAX_CHUNK_LENGTH`をそのまま移し、`MAX_PART_TEXT_LENGTH` / `MAX_ACTIVE_SESSIONS` / 期限定数を追加。単位と境界条件をコメントで明記(FR-2.7) |
25
+ | `src/qr-courier.ts` の`QrCourierPartV1` / `validatePart` / `serializeQrCourierPart` / `parseQrCourierPart` / `partIdentity` / `requireIdentifier` / `requireTimestamp` / `requirePairingCode` | ~140 | `src/qr/envelope.ts` | B | ①`protocol`を`twqr/1`へ ②`peerId`→`senderPeerId`+`targetPeerId` ③`replyTo`追加(offerは空文字、answerは非空を要求) ④`QrCourierKind`→`QrMessageKind` ⑤エラーを`QrPairingError(code, message)`へ置換 ⑥`Offer pairing code is empty.`のようなOffer前提の文言を種別非依存に ⑦`createdAt`が期限判定に使われないことをコメントで明記 ⑧payloadの印字可能ASCII検査をparse時にも行う |
26
+ | `src/qr-courier.ts` の`sha256Base64Url` | ~8 | `src/qr/hash.ts` | A | envelopeとcourierの双方が使うため単独モジュールへ |
27
+ | — | — | `src/errors.ts` | D | `PairingErrorCode`と`QrPairingError`。`qr/`と`pairing/`の双方が参照するので、依存を持たない葉モジュールとして`src/`直下に置く |
28
+ | `src/qr-courier.ts` の`createQrCourierParts` / `maximumPayloadLength` | ~75 | `src/qr/courier.ts` | B | 二分探索とpart数収束ループはそのまま。`options`を`CreatePartsOptions`(sender/target/replyTo/sessionId)へ。戻り値に`sessionId`/`messageId`を追加 |
29
+ | `src/qr-courier.ts` の`QrCourierAssembler` | ~45 | `src/qr/courier.ts`(`PartAssembler`) | B | ①入力を`string`から`QrEnvelopeV1`へ(parseは呼び出し側=controllerが行い、session照合をparse直後に挟めるようにする) ②`missingParts()` / `receivedCount()` / `requiredCount()` / `isComplete()` / `clear()`を追加(FR-2.3) ③エラーを`QrPairingError`へ |
30
+ | `src/webrtc-capability.ts` | 38 | `src/ports/webrtc.ts` | B | `WebRtcOfferCapabilityV2`→`WebRtcPairingPort`。要求versionを3へ。`acceptOffer` / `getAnswer` / `acceptAnswer` / `connectionState` / `closePeer`の存在検査を追加。エラーコード`webrtc-capability-missing` |
31
+ | `src/extension.ts` L200-310(`prepareOfferQr` / `showOfferQrPart` / `offerQrPartCount` / `offerQrCurrentPart` / `showNextOfferQrPart` / `createAndShowOfferQr` / `endOfferQrDisplay` / `offerQrState` / `offerQrError`) | ~110 | `src/pairing/controller.ts` + `src/pairing/session.ts` | C | 流用する概念: `operation`カウンタによる古い非同期結果の破棄(→`epoch`)、`state`/`lastError`の保持(→`phase`/`errorCode`)、part表示と巡回、`endOfferQrDisplay`での一括解放。書き直す理由: 単一session固定・Offer専用・camera側なし・期限なし・受信なしのため |
32
+ | `src/extension.ts` のruntimeリスナ設定(L172-177, `runStopListener`/`stopListener`/`disposeListener`/`targetRemovedListener`) | ~25 | `src/extension.ts` | B | motion-capture固有の`pose`/`calibration`/`fusion`停止を除去し、本拡張の`stopTransient()`/`dispose()`へ束ねる |
33
+ | `src/extension.ts` の`toScratchBlock`/`blockEnabled`/`requireEnabled` | ~40 | `src/extension.ts` | B | 既存scaffoldの`toScratchBlock`とほぼ同型。`feature`フィールドによるフィルタを`qrCodePairing`単独で復元 |
34
+ | `config/feature-flags.ts` | 27 | `src/config/feature-flags.ts` | B | フラグを`qrCodePairing`1件へ。グローバル名`__TWMP_FEATURE_FLAGS__`→`__TWQP_FEATURE_FLAGS__`。Issueの指定どおり`src/`配下へ置く |
35
+ | `config/qr-config.ts` | 19 | `src/config/qr-config.ts` | A | グローバル名`__TWMP_QR_CONFIG__`→`__TWQP_QR_CONFIG__` |
36
+ | `src/globals.d.ts` の`TurboWarpRenderer` / `TurboWarpTarget` / `TurboWarpBlockUtility` / `TurboWarpRuntime` / `Scratch.vm` | ~30 | `src/globals.d.ts`(既存へ追記) | A | scaffoldの`globals.d.ts`にはrenderer/target/runtimeの宣言が無いので追加。`declare module "@kubohiroya/turbowarp-jsqr/jsqr.js"`も必要 |
37
+ | — | — | `src/qr/courier.ts`のAnswer側分割呼出し | D | `kind='answer'`+`replyTo`+`sessionId`継承。移設元に相当物なし |
38
+ | — | — | `src/ports/qr-scan.ts` | D | camera-sourceのlease保持+jsqr`scanFrame`ポーリング。移設元は読取り側を持たない |
39
+ | — | — | `src/pairing/types.ts` | D | phase/errorCode/portの定義 |
40
+ | — | — | `src/clock.ts`(`MonotonicClock`) | D | 単調時計。移設元は`Date.now()`のみ |
41
+ | `src/pose/**`, `src/fusion/**`, `src/calibration/**`, `src/avatar/**`, `src/frame-sync/**`, `src/markers/**`, `src/protocol/**` | 約8,900 | — | E | motion-capture固有。本拡張のスコープ外 |
42
+ | `src/extension-manifest.ts`, `src/config.ts`, `src/index.ts` | — | — | E | scaffold側に同等物が既にある |
43
+
44
+ 移設対象の実コード合計はおよそ **620行**(うちAが約190行、Bが約320行、Cが約110行)。
45
+
46
+ ## 2. テスト
47
+
48
+ | 移設元 | 行数 | 移設先 | 区分 | 作業 |
49
+ |---|---|---|---|---|
50
+ | `tests/qr-courier.test.ts` | 116 | `tests/qr-courier.test.ts` | B | 4件のitを維持。`peerId`→`senderPeerId`/`targetPeerId`、protocol名、`QrCourierAssembler`→`PartAssembler`(入力がenvelope)に合わせて更新。「順不同・重複・混在identity・欠落part・範囲外index・過大入力」の観点はそのまま使える(DoD 3/4に直結) |
51
+ | `tests/turbowarp-jsqr-integration.test.ts` | 80 | `tests/qr-jsqr-integration.test.ts` | A | `createQrCourierParts`→`createParts`の呼び替えのみ。**`feat/pairing/optical-workflow`へ延期**: このテストは`@kubohiroya/turbowarp-jsqr`拡張の`scanFrame`経路を検証するもので、走査ポートを実装するPRで一緒に入れるほうが依存の追加が1回で済む。生成QRの実デコード検証自体は`qr-courier.test.ts`が`jsqr`パッケージ直接利用で担保する |
52
+ | `tests/extension.test.ts` L85-130, L485-693(offer QRブロック関連) | ~250 | `tests/pairing-hub.test.ts` ほか | C | 流用する観点: フラグOFFでブロックが出ない/`disabled`を返す、peer名のtrim、単一part/複数part、次part巡回、表示終了で復元、webrtc未ロード時のエラー、進行中の二重起動拒否、スプライト削除時の復元。往復・camera側・取消・期限・複数peerは新規 |
53
+ | — | — | `tests/pairing-camera.test.ts` | D | Offer受理→Answer生成→表示 |
54
+ | — | — | `tests/pairing-roundtrip.test.ts` | D | hub↔cameraを同一プロセス内で結線した往復(DoD 1/3/6) |
55
+ | — | — | `tests/pairing-lifecycle.test.ts` | D | 取消・期限・再試行・古い非同期応答・古いQR(DoD 5) |
56
+ | — | — | `tests/extension-manifest.test.ts` | B | 既存を新opcode一覧に合わせて更新 |
57
+
58
+ ## 3. 依存パッケージの移設
59
+
60
+ `package.json`へ追加する。
61
+
62
+ ```jsonc
63
+ "dependencies": {
64
+ "qrcode": "1.5.4" // 移設元と同一版に固定
65
+ },
66
+ "peerDependencies": { // すべて optional
67
+ "@kubohiroya/turbowarp-webrtc": ">=0.4.0", // capability v3 を含む版(後述)
68
+ "@kubohiroya/turbowarp-jsqr": "0.3.0",
69
+ "@kubohiroya/turbowarp-camera-source": "0.5.0"
70
+ },
71
+ "devDependencies": {
72
+ "@types/qrcode": "1.5.6",
73
+ "jsqr": "^1.4.0" // テストでの実デコード検証用
74
+ }
75
+ ```
76
+
77
+ `pnpm-workspace.yaml`の`minimumReleaseAgeExclude`に上記の`@kubohiroya/*`を追記する(移設元と同じ運用)。
78
+
79
+ **注意点**:
80
+
81
+ - 本リポジトリは`vitest ^5.0.0`、移設元は`vitest ^4.1.10`。移設テストがvitest 5で動くことを最初のPRで確認する
82
+ (`vi.stubGlobal`/`vi.unstubAllGlobals`は5系でも同APIだが、タイマ系APIの差異は要確認)。
83
+ - 本リポジトリは`@kubohiroya/vite-plugin-turbowarp-extension` 0.3.0、移設元は0.4.0。
84
+ `qrcode`をバンドルできることは`refactor/pairing/qr-courier`で確認済み(`src/qr/courier.ts`を入口に
85
+ vite単体ビルドすると、外部importなし・node組込みなしの単一チャンク約88 KBになる)。
86
+ `src/extension.ts`が`qr/`を参照するのは`feat/pairing/optical-workflow`以降なので、
87
+ `pnpm run check:dist`での最終確認はそのPRで行う。
88
+ - 移設元はprettierを使うが本リポジトリは使わない。移設したコードは本リポジトリのeslint設定に合わせる
89
+ (移設元はダブルクォート、本リポジトリのscaffoldはシングルクォート。`eslint.config.mjs`の実際の規則に従う)。
90
+
91
+ ## 4. 前提となる他リポジトリの変更(ブロッカー)
92
+
93
+ ### 4.1 `turbowarp-webrtc`: runtime capability v3
94
+
95
+ 現状のv3不在が**FR-1の実装を直接ブロックする**。
96
+
97
+ - 現状(`src/runtime-capability.ts`): `version` / `requireVersion` / `createOffer` / `getOffer` /
98
+ `setLatestDataEnabled` / `configureLatestDataChannel` / `sendLatestData` / `latestDataStats`。
99
+ - 追加が必要: `acceptOffer(peer, code)` / `getAnswer(peer)` / `acceptAnswer(peer, code)` /
100
+ `connectionState(peer)` / `closePeer(peer)`。いずれも`PeerSessionPort`(`src/manual-peer-session.ts:36-59`)に
101
+ 既に実装済みで、capabilityへ委譲を追加するだけで足りる。
102
+ - 併せて`runtimeCapabilityVersion`を3にし、`requireVersion`が1/2/3を受理するようにする。
103
+ - これは`turbowarp-webrtc`側の別Issue/PRとして起票し、本Issue #1からリンクする。
104
+
105
+ **この作業が完了するまで**、本リポジトリでは`WebRtcPairingPort`をinterfaceとして定義し、
106
+ テストではフェイクを注入して往復ロジックを完成させられる。実機検証だけがブロックされる。
107
+
108
+ ### 4.2 `turbowarp-jsqr` / `turbowarp-camera-source`
109
+
110
+ 変更不要。`ext_kubohiroyajsqr.scanFrame`と`ext_kubohiroyacamerasource.acquireCamera`をそのまま使う。
111
+ ただし両者とも`runtime.ext_*`という非versioned な公開面なので、本拡張側で存在検査とエラーコード化を行う。
112
+ 将来的にversioned capabilityへ移行する提案は、それぞれのリポジトリの別Issueとする。
113
+
114
+ ## 5. PR分割(Issue #1の表を本設計で具体化)
115
+
116
+ | # | ブランチ | 内容 | 依存 | 検証 |
117
+ |---|---|---|---|---|
118
+ | 0 | `turbowarp-webrtc` 側 `feat/runtime-capability/v3` | capability v3の追加 | なし | 当該リポジトリのcheck |
119
+ | 1 | `feat/pairing/contracts` | 本設計文書一式、`src/pairing/types.ts`、`src/config/feature-flags.ts`、`src/config/qr-config.ts`、`globals.d.ts`の拡張 | なし | typecheck / lint |
120
+ | 2 | `refactor/pairing/qr-courier` | `src/qr/**`(limits・envelope・courier・svg)と移設テスト、依存パッケージ追加 | #1 | `pnpm run check`、jsQR実デコード、部分・重複・欠落 |
121
+ | 3 | `feat/pairing/offer-answer` | `src/ports/webrtc.ts`、`src/clock.ts`、`src/pairing/{limits,types,controller}.ts`、往復・取消・期限・複数peerのテスト | #1,#2(実機は#0) | フェイクWebRTCでの往復テスト |
122
+ | 4 | `feat/pairing/optical-workflow` | `src/ports/display.ts`、`src/ports/qr-scan.ts`、ブロック定義とextension.ts、実機検証 | #3、#0、jsqr/camera-source | 実機での投影→読取り→搬送→接続 |
123
+ | 5 | `docs/pairing/integration` | README(日英)、docs/index.html、利用ガイド、移行・切戻し手順 | #4 | `pnpm run docs:check`、DoD一覧の確認 |
124
+
125
+ #2と#0は並行可能。#3は#0未完でもフェイク注入で進められるが、#4の実機検証は#0の完了を待つ。
126
+
127
+ ## 6. 移設時に必ず落とすもの(一般化チェックリスト)
128
+
129
+ - [ ] `twmp` / `multiview` / `motion capture` を含む識別子・文字列・グローバル名が残っていない
130
+ - [ ] `Offer`固有の文言(`Offer pairing code`, `Offer QR must be displayed…`)が種別非依存になっている
131
+ - [ ] `pose` / `calibration` / `fusion` / `avatar` / `frame-sync` / `markers` への参照が無い
132
+ - [ ] `qr/`配下がDOM・Scratch・runtimeに依存していない(importが`qrcode`と自モジュールのみ)
133
+ - [ ] エラーが素のErrorではなく`QrPairingError`でコード付き
134
+ - [ ] SDP・pairing code・payloadが例外メッセージへ混入していない
@@ -0,0 +1,473 @@
1
+ /**
2
+ * QR搬送WebRTCペアリング 型契約(設計文書)
3
+ *
4
+ * 対応要求: requirements.md / Issue #1
5
+ * 本ファイルは設計の規範であり、ビルド対象ではない(tsconfig の include は src/tests/scripts)。
6
+ * 実装時は各セクションを src/ 配下の該当モジュールへ分割する(architecture.md 参照)。
7
+ */
8
+
9
+ /* ============================================================================
10
+ * src/qr/limits.ts — 上限・単位・境界条件(FR-2.7)
11
+ * ==========================================================================*/
12
+
13
+ /**
14
+ * メッセージ(pairing code)の最大長。
15
+ * 単位: 文字数。payload は印字可能ASCII `[\x20-\x7E]` に限定されるため、
16
+ * 1文字 = 1 UTF-16 code unit = 1 バイトであり、文字数とバイト数は一致する。
17
+ * 境界: 1 <= messageLength <= MAX_MESSAGE_LENGTH。0 は不正。
18
+ *
19
+ * 注意(実効上限): 実際に搬送できる長さは MAX_PART_COUNT × chunk長 で頭打ちになる。
20
+ * chunk長 = (QR v40 の該当ECCレベルのバイト容量) - (envelopeヘッダ長 <= 362)。
21
+ * L: 2953 - 362 = 2591 → 64part で 165,824 → MAX_MESSAGE_LENGTH が binding
22
+ * M: 2331 - 362 = 1969 → 64part で 126,016 → part数が binding
23
+ * Q: 1663 - 362 = 1301 → 64part で 83,264 → part数が binding
24
+ * H: 1273 - 362 = 911 → 64part で 58,304 → part数が binding
25
+ * 上限超過は分割時に `message-too-large` もしくは `too-many-parts` で失敗する。
26
+ */
27
+ export const MAX_MESSAGE_LENGTH = 128 * 1024; // 131072
28
+
29
+ /** 1メッセージあたりの最大part数。境界: 1 <= partCount <= 64、0 <= partIndex < partCount。*/
30
+ export const MAX_PART_COUNT = 64;
31
+
32
+ /** 1partのpayload最大長(文字数)。QR容量による実効上限より常に大きい安全弁。*/
33
+ export const MAX_CHUNK_LENGTH = 4096;
34
+
35
+ /** 受理するQRテキストの最大長(文字数)。envelopeヘッダ + payload の上限。*/
36
+ export const MAX_PART_TEXT_LENGTH = 8192;
37
+
38
+ /** 同時に保持できるsession数(受信状態の保持上限、FR-2.7)。*/
39
+ export const MAX_ACTIVE_SESSIONS = 8;
40
+
41
+ /** 期限の既定値と範囲(秒、FR-4.6)。*/
42
+ export const DEFAULT_TIMEOUT_SECONDS = 600;
43
+ export const MIN_TIMEOUT_SECONDS = 1;
44
+ export const MAX_TIMEOUT_SECONDS = 3600;
45
+
46
+ /** connectionState のポーリング間隔(ミリ秒)。*/
47
+ export const CONNECTION_POLL_INTERVAL_MS = 250;
48
+
49
+ /** カメラ走査のポーリング間隔(ミリ秒)。jsqr の既定と揃える。*/
50
+ export const SCAN_POLL_INTERVAL_MS = 150;
51
+
52
+ /* ============================================================================
53
+ * src/qr/envelope.ts — 搬送形式 twqr/1(FR-2.1, FR-2.8)
54
+ * ==========================================================================*/
55
+
56
+ /**
57
+ * protocol識別子。
58
+ * 移設元 `twmp-qr/1` に peer対応情報(senderPeerId/targetPeerId)と replyTo を追加した
59
+ * 非互換形式のため、新しい名前空間とversionを与える。
60
+ * `twmp-qr/1` は本拡張では受理しない(`unsupported-protocol`)。相互運用しない理由と
61
+ * motion-capture側の移行方針は block-api.md「形式互換性」を参照。
62
+ */
63
+ export const QR_PROTOCOL = 'twqr/1' as const;
64
+
65
+ export type QrMessageKind = 'offer' | 'answer';
66
+ export type QrErrorCorrectionLevel = 'L' | 'M' | 'Q' | 'H';
67
+
68
+ export interface QrEnvelopeV1 {
69
+ /** 固定値 'twqr/1'。*/
70
+ readonly protocol: typeof QR_PROTOCOL;
71
+ /** 1回のOffer→Answer往復を識別する。hubがOffer生成時にUUIDで採番し、Answerが反映する。*/
72
+ readonly sessionId: string;
73
+ /** 送信側が自分を呼ぶ名前。受信側はこれを自分のWebRTC peerキーに使う。*/
74
+ readonly senderPeerId: string;
75
+ /** 送信側が受信側を呼ぶ名前。受信側は自分の自称としてこれを採用(または照合)する。*/
76
+ readonly targetPeerId: string;
77
+ readonly kind: QrMessageKind;
78
+ /** `<sessionId>.<messageHashの先頭12文字>`。内容が変われば必ず変わる。*/
79
+ readonly messageId: string;
80
+ /** kind='answer' のとき、応答対象のOfferの messageId。kind='offer' のときは空文字。*/
81
+ readonly replyTo: string;
82
+ /** 送信端末のローカル壁時計(ミリ秒)。診断・表示用。期限判定には使わない(FR-4.6)。*/
83
+ readonly createdAt: number;
84
+ /** 0 <= partIndex < partCount。*/
85
+ readonly partIndex: number;
86
+ /** 1 <= partCount <= MAX_PART_COUNT。*/
87
+ readonly partCount: number;
88
+ /** 1 <= messageLength <= MAX_MESSAGE_LENGTH(文字数)。*/
89
+ readonly messageLength: number;
90
+ /** メッセージ全体の SHA-256、base64url無パディング(43文字)。破損検出用で認証ではない(FR-2.6)。*/
91
+ readonly messageHash: string;
92
+ /** メッセージの一部。印字可能ASCII、長さ <= MAX_CHUNK_LENGTH。*/
93
+ readonly payload: string;
94
+ }
95
+
96
+ /** JSONへ直列化する。検証を通らないenvelopeは直列化しない。*/
97
+ export declare function serializeEnvelope(envelope: QrEnvelopeV1): string;
98
+
99
+ /** QRテキストをparseし全フィールドを検証する。失敗時は QrPairingError を投げる。*/
100
+ export declare function parseEnvelope(text: string): QrEnvelopeV1;
101
+
102
+ /**
103
+ * 同一メッセージの part であることを判定するための同一性キー。
104
+ * payload と partIndex を除く全フィールドを連結する。
105
+ */
106
+ export declare function envelopeIdentity(envelope: QrEnvelopeV1): string;
107
+
108
+ /* ============================================================================
109
+ * src/qr/courier.ts — 分割と再構成(FR-2.2, FR-2.3, FR-2.4)
110
+ * ==========================================================================*/
111
+
112
+ export interface CreatePartsOptions {
113
+ readonly senderPeerId: string;
114
+ readonly targetPeerId: string;
115
+ readonly kind: QrMessageKind;
116
+ /** kind='answer' のとき必須。Offerの messageId。*/
117
+ readonly replyTo?: string;
118
+ /** 省略時は crypto.randomUUID()。Answerでは必ずOfferの sessionId を渡す。*/
119
+ readonly sessionId?: string;
120
+ readonly errorCorrectionLevel?: QrErrorCorrectionLevel;
121
+ /** 省略時は Date.now()。テストのため注入可能。*/
122
+ readonly createdAt?: number;
123
+ }
124
+
125
+ export interface QrParts {
126
+ readonly parts: readonly QrEnvelopeV1[];
127
+ /** parts と同順の QRテキスト。*/
128
+ readonly texts: readonly string[];
129
+ readonly sessionId: string;
130
+ readonly messageId: string;
131
+ }
132
+
133
+ /**
134
+ * メッセージをQR容量に合わせて分割する。
135
+ * chunk長は「ヘッダ込みでQR v40に収まる最大payload長」を二分探索で決め、
136
+ * part数が確定するまで反復する(移設元の手順を維持)。
137
+ */
138
+ export declare function createParts(
139
+ message: string,
140
+ options: CreatePartsOptions
141
+ ): Promise<QrParts>;
142
+
143
+ export interface PartAcceptResult {
144
+ readonly received: number;
145
+ readonly total: number;
146
+ /** 同一indexの同一payloadを再受理した場合 true。エラーではない。*/
147
+ readonly duplicate: boolean;
148
+ }
149
+
150
+ /** 再構成器。1つの PartAssembler は1メッセージ分の part しか受け付けない。*/
151
+ export declare class PartAssembler {
152
+ add(envelope: QrEnvelopeV1): PartAcceptResult;
153
+ /** 未取得の partIndex を昇順で返す(FR-2.3)。*/
154
+ missingParts(): readonly number[];
155
+ receivedCount(): number;
156
+ /** 最初のpartを受けるまでは 0。*/
157
+ requiredCount(): number;
158
+ isComplete(): boolean;
159
+ /** 長さとhashを検証して結合結果を返す。未完成・不一致は QrPairingError。*/
160
+ assemble(): Promise<string>;
161
+ /** 受信状態を破棄する。*/
162
+ clear(): void;
163
+ }
164
+
165
+ /* ============================================================================
166
+ * src/qr/svg.ts — 表示用のQR画像生成(FR-3.1)
167
+ * ==========================================================================*/
168
+
169
+ export declare function createQrSvg(
170
+ text: string,
171
+ errorCorrectionLevel?: QrErrorCorrectionLevel
172
+ ): string;
173
+
174
+ /** 診断用。実機検証記録の「QR表示条件」に使う。*/
175
+ export declare function qrVersion(
176
+ text: string,
177
+ errorCorrectionLevel?: QrErrorCorrectionLevel
178
+ ): number;
179
+
180
+ /* ============================================================================
181
+ * src/pairing/types.ts — ロール・状態・エラー(FR-4.1, FR-4.2)
182
+ * ==========================================================================*/
183
+
184
+ export type PairingRole = 'hub' | 'camera';
185
+
186
+ /**
187
+ * 公開する状態名。hub と camera で到達する集合が異なる(dataflow.md 参照)。
188
+ * 'offer-received' / 'answer-received' が「QR読取り完了」、
189
+ * 'connected' が「WebRTC接続成立」であり、両者は必ず区別される(FR-4.2)。
190
+ */
191
+ export type PairingPhase =
192
+ | 'disabled' // フィーチャーフラグOFF
193
+ | 'idle' // sessionが存在しない
194
+ | 'creating-offer' // hub: createOffer 実行中
195
+ | 'offer-ready' // hub: Offer QR 準備完了・表示可能
196
+ | 'awaiting-answer' // hub: Answer part を部分受信中
197
+ | 'answer-received' // hub: Answer 検証完了(未引渡し/引渡し直後)
198
+ | 'awaiting-offer' // camera: Offer part 未受信
199
+ | 'receiving' // camera: Offer part を部分受信中
200
+ | 'offer-received' // camera: Offer 検証完了
201
+ | 'creating-answer' // camera: acceptOffer 実行中
202
+ | 'answer-ready' // camera: Answer QR 準備完了・表示可能
203
+ | 'connecting' // WebRTCへ引渡し済み、接続待ち
204
+ | 'connected' // 接続成立
205
+ | 'cancelled'
206
+ | 'expired'
207
+ | 'failed';
208
+
209
+ export type PairingErrorCode =
210
+ // フラグ・依存
211
+ | 'feature-disabled'
212
+ | 'webrtc-capability-missing'
213
+ | 'qr-decoder-missing'
214
+ | 'camera-unavailable'
215
+ | 'renderer-unavailable'
216
+ // 形式・検証(FR-2.5)
217
+ | 'invalid-json'
218
+ | 'unsupported-protocol'
219
+ | 'invalid-envelope'
220
+ | 'part-too-large'
221
+ | 'index-out-of-range'
222
+ | 'message-too-large'
223
+ | 'too-many-parts'
224
+ | 'message-mismatch'
225
+ | 'conflicting-part'
226
+ | 'missing-parts'
227
+ | 'length-mismatch'
228
+ | 'hash-mismatch'
229
+ // 交換の同一性(FR-1.4, FR-4.5)
230
+ | 'unknown-session'
231
+ | 'stale-exchange'
232
+ | 'unexpected-kind'
233
+ | 'peer-mismatch'
234
+ | 'reply-mismatch'
235
+ | 'already-accepted'
236
+ // ライフサイクル
237
+ | 'session-limit'
238
+ | 'session-exists'
239
+ | 'no-session'
240
+ | 'invalid-argument'
241
+ | 'timeout'
242
+ | 'cancelled'
243
+ | 'webrtc-rejected';
244
+
245
+ /**
246
+ * 公開エラー。message は必ずサニタイズ済みで、SDP・ICE・pairing code・payload を含まない(FR-4.9)。
247
+ */
248
+ export declare class QrPairingError extends Error {
249
+ readonly code: PairingErrorCode;
250
+ constructor(code: PairingErrorCode, message: string, options?: { cause?: unknown });
251
+ }
252
+
253
+ export interface PairingProgress {
254
+ readonly phase: PairingPhase;
255
+ readonly role: PairingRole;
256
+ readonly sessionKey: string;
257
+ readonly exchangeId: string; // = envelope の sessionId
258
+ readonly localPeerId: string;
259
+ readonly remotePeerId: string;
260
+ /** 表示中QRの総part数。未準備なら0。*/
261
+ readonly outgoingPartCount: number;
262
+ /** 1始まり。未表示なら0。*/
263
+ readonly outgoingCurrentPart: number;
264
+ readonly receivedParts: number;
265
+ /** 最初のpartを受けるまでは0。*/
266
+ readonly requiredParts: number;
267
+ readonly missingParts: readonly number[];
268
+ readonly connectionState: string;
269
+ readonly errorCode: PairingErrorCode | '';
270
+ readonly errorMessage: string;
271
+ /** 残り秒数。期限なし・終了済みは0。*/
272
+ readonly remainingSeconds: number;
273
+ }
274
+
275
+ /* ============================================================================
276
+ * src/ports/*.ts — 外部依存の抽象(architecture.md §4)
277
+ * ==========================================================================*/
278
+
279
+ /**
280
+ * turbowarp-webrtc runtime capability v3。
281
+ * v2 は createOffer/getOffer しか公開していないため、本拡張は v3 を必須とする。
282
+ * 取得: runtime['kubohiroyaWebRtcCapability'] → requireVersion(3)
283
+ */
284
+ export const WEBRTC_CAPABILITY_KEY = 'kubohiroyaWebRtcCapability';
285
+ export const REQUIRED_WEBRTC_CAPABILITY_VERSION = 3;
286
+
287
+ export interface WebRtcPairingPort {
288
+ readonly version: number;
289
+ createOffer(peer: string): Promise<string>;
290
+ getOffer(peer: string): string;
291
+ /** Offerを受理しAnswerコードを返す。*/
292
+ acceptOffer(peer: string, code: string): Promise<string>;
293
+ getAnswer(peer: string): string;
294
+ acceptAnswer(peer: string, code: string): Promise<void>;
295
+ /** RTCPeerConnectionState 相当の文字列。未知のpeerは 'new' 以外の既定値を返してよい。*/
296
+ connectionState(peer: string): string;
297
+ closePeer(peer: string): void;
298
+ }
299
+
300
+ export declare function requireWebRtcPairingPort(
301
+ runtime: TurboWarpRuntime
302
+ ): WebRtcPairingPort;
303
+
304
+ /** turbowarp-jsqr のデコード能力。画像処理は本拡張で実装しない(FR-3.3)。*/
305
+ export interface QrDecodePort {
306
+ scanFrame(frame: CameraFrameSource): string | null;
307
+ }
308
+
309
+ /** turbowarp-camera-source のlease規則を尊重する最小面(FR-3.4)。*/
310
+ export interface CameraFrameSource {
311
+ readonly element: HTMLVideoElement | HTMLCanvasElement;
312
+ readonly width: number;
313
+ readonly height: number;
314
+ }
315
+
316
+ export interface CameraLease {
317
+ getFrameSource(): CameraFrameSource;
318
+ release(): Promise<void>;
319
+ }
320
+
321
+ export interface CameraSourcePort {
322
+ acquireCamera(options: { owner: string; cameraId: string }): Promise<CameraLease>;
323
+ }
324
+
325
+ /** session中に1つのleaseを保持し、jsqrのscanFrameでポーリングする走査器。*/
326
+ export interface QrScanPort {
327
+ /**
328
+ * 1件のQRテキストを取得する。abort されたら AbortError を投げる。
329
+ * 同じテキストを連続で返さないための抑制は行わない(重複はassemblerが吸収する)。
330
+ */
331
+ scanOnce(options: {
332
+ cameraId: string;
333
+ signal: AbortSignal;
334
+ intervalMilliseconds?: number;
335
+ }): Promise<string>;
336
+ /** session終了時に呼ぶ。保持中のleaseを解放する。*/
337
+ release(): Promise<void>;
338
+ }
339
+
340
+ /** 一時skinの退避・復元(FR-3.5)。移設元 TemporarySpriteSkinManager と同一契約。*/
341
+ export interface DisplayPort {
342
+ validateTarget(target: TurboWarpTarget | undefined): TurboWarpTarget;
343
+ show(target: TurboWarpTarget | undefined, svg: string): void;
344
+ releaseTarget(target: TurboWarpTarget): void;
345
+ releaseAll(): void;
346
+ isDisplaying(target: TurboWarpTarget): boolean;
347
+ }
348
+
349
+ /** 単調時計。壁時計に依存しない期限判定に使う(FR-4.6)。*/
350
+ export interface MonotonicClock {
351
+ nowMilliseconds(): number;
352
+ }
353
+
354
+ /* ============================================================================
355
+ * src/pairing/session.ts — 1 exchange の状態
356
+ * ==========================================================================*/
357
+
358
+ export interface PairingSessionOptions {
359
+ readonly sessionKey: string;
360
+ readonly role: PairingRole;
361
+ readonly localPeerId: string;
362
+ /** hub では必須。camera では空でよく、Offer受理時に確定する。*/
363
+ readonly remotePeerId: string;
364
+ readonly timeoutSeconds: number;
365
+ readonly errorCorrectionLevel: QrErrorCorrectionLevel;
366
+ }
367
+
368
+ export interface PairingSessionState {
369
+ readonly options: PairingSessionOptions;
370
+ phase: PairingPhase;
371
+ /** 世代番号。cancel / retry / expire / dispose で加算される(FR-4.4)。*/
372
+ epoch: number;
373
+ exchangeId: string;
374
+ /** 自分が送出したメッセージのID。hubならOffer、cameraならAnswer。*/
375
+ outgoingMessageId: string;
376
+ /** 受理した相手メッセージのID。*/
377
+ incomingMessageId: string;
378
+ outgoing: QrParts | undefined;
379
+ outgoingSvgs: readonly string[];
380
+ outgoingCurrentIndex: number;
381
+ assembler: PartAssembler | undefined;
382
+ /** WebRTCへ引き渡し済みか。二重引渡しを防ぐ(FR-1.2)。*/
383
+ delivered: boolean;
384
+ startedAtMonotonic: number;
385
+ errorCode: PairingErrorCode | '';
386
+ errorMessage: string;
387
+ }
388
+
389
+ /* ============================================================================
390
+ * src/pairing/controller.ts — 公開API(extension.ts はこれを薄く包む)
391
+ * ==========================================================================*/
392
+
393
+ export interface PairingControllerOptions {
394
+ readonly runtime: TurboWarpRuntime;
395
+ readonly enabled?: boolean;
396
+ readonly errorCorrectionLevel?: QrErrorCorrectionLevel;
397
+ readonly webrtc?: WebRtcPairingPort;
398
+ readonly scan?: QrScanPort;
399
+ readonly display?: DisplayPort;
400
+ readonly clock?: MonotonicClock;
401
+ }
402
+
403
+ export declare class PairingController {
404
+ constructor(options: PairingControllerOptions);
405
+
406
+ // --- 開始・終了(FR-4.3) ---
407
+ startOfferPairing(input: {
408
+ sessionKey: string;
409
+ localPeerId: string;
410
+ remotePeerId: string;
411
+ }): Promise<void>;
412
+ startAnswerPairing(input: {
413
+ sessionKey: string;
414
+ /** 空なら Offer の targetPeerId を採用。非空なら一致検証する。*/
415
+ expectedLocalPeerId: string;
416
+ }): void;
417
+ cancelPairing(sessionKey: string): void;
418
+ retryPairing(sessionKey: string): Promise<void>;
419
+ setTimeoutSeconds(sessionKey: string, seconds: number): void;
420
+
421
+ // --- 受信(FR-2, FR-3.3) ---
422
+ /** デコード済み文字列の正規入口。完了時は内部でWebRTCへ1回だけ引き渡す。*/
423
+ ingestQrText(sessionKey: string, text: string): Promise<void>;
424
+ /** カメラ走査。全part受信・取消・期限のいずれかまでブロックする。*/
425
+ scanFromCamera(sessionKey: string, cameraId: string): Promise<void>;
426
+
427
+ // --- 表示(FR-3.1) ---
428
+ showPart(sessionKey: string, oneBasedIndex: number, target: TurboWarpTarget | undefined): void;
429
+ showNextPart(sessionKey: string, target: TurboWarpTarget | undefined): void;
430
+ partSvg(sessionKey: string, oneBasedIndex: number): string;
431
+ endDisplay(sessionKey: string): void;
432
+
433
+ // --- 観測(FR-4.1, FR-4.2) ---
434
+ progress(sessionKey: string): PairingProgress;
435
+ sessionKeys(): readonly string[];
436
+ isConnected(sessionKey: string): boolean;
437
+ /** 接続成立・失敗・取消・期限まで待つ。*/
438
+ waitUntilConnected(sessionKey: string): Promise<void>;
439
+
440
+ // --- ライフサイクル(FR-4.8) ---
441
+ /** PROJECT_RUN_STOP 相当。表示とleaseを解放し、進行中sessionを取り消す。*/
442
+ stopTransient(): void;
443
+ /** RUNTIME_DISPOSED 相当。全解放とリスナ解除。*/
444
+ dispose(): void;
445
+ }
446
+
447
+ /* ============================================================================
448
+ * src/config/feature-flags.ts(NFR-3)
449
+ * ==========================================================================*/
450
+
451
+ export interface QrPairingFeatureFlags {
452
+ readonly qrCodePairing: boolean;
453
+ }
454
+
455
+ interface FeatureFlagGlobal {
456
+ readonly __TWQP_FEATURE_FLAGS__?: Partial<QrPairingFeatureFlags>;
457
+ }
458
+
459
+ /** 起動時に一度だけ評価し freeze する。既定 OFF。*/
460
+ export declare const featureFlags: QrPairingFeatureFlags;
461
+
462
+ /* ============================================================================
463
+ * src/config/qr-config.ts
464
+ * ==========================================================================*/
465
+
466
+ interface QrConfigGlobal {
467
+ readonly __TWQP_QR_CONFIG__?: {
468
+ readonly errorCorrectionLevel?: QrErrorCorrectionLevel;
469
+ };
470
+ }
471
+
472
+ /** 起動時固定。既定 'M'。*/
473
+ export declare const qrConfig: { readonly errorCorrectionLevel: QrErrorCorrectionLevel };