tailii-host 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 (141) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -0
  3. package/dist/apnsConfigStore.d.ts +22 -0
  4. package/dist/apnsConfigStore.js +50 -0
  5. package/dist/apnsConfigStore.js.map +1 -0
  6. package/dist/apnsJwtProvider.d.ts +50 -0
  7. package/dist/apnsJwtProvider.js +158 -0
  8. package/dist/apnsJwtProvider.js.map +1 -0
  9. package/dist/apnsSender.d.ts +69 -0
  10. package/dist/apnsSender.js +155 -0
  11. package/dist/apnsSender.js.map +1 -0
  12. package/dist/approvalPushNotifier.d.ts +100 -0
  13. package/dist/approvalPushNotifier.js +236 -0
  14. package/dist/approvalPushNotifier.js.map +1 -0
  15. package/dist/broker.d.ts +24 -0
  16. package/dist/broker.js +197 -0
  17. package/dist/broker.js.map +1 -0
  18. package/dist/chatTailController.d.ts +87 -0
  19. package/dist/chatTailController.js +292 -0
  20. package/dist/chatTailController.js.map +1 -0
  21. package/dist/claudeSessionStore.d.ts +15 -0
  22. package/dist/claudeSessionStore.js +201 -0
  23. package/dist/claudeSessionStore.js.map +1 -0
  24. package/dist/cli.d.ts +2 -0
  25. package/dist/cli.js +53 -0
  26. package/dist/cli.js.map +1 -0
  27. package/dist/codexRolloutTailer.d.ts +56 -0
  28. package/dist/codexRolloutTailer.js +303 -0
  29. package/dist/codexRolloutTailer.js.map +1 -0
  30. package/dist/codexSessionStore.d.ts +23 -0
  31. package/dist/codexSessionStore.js +274 -0
  32. package/dist/codexSessionStore.js.map +1 -0
  33. package/dist/codexUsage.d.ts +16 -0
  34. package/dist/codexUsage.js +144 -0
  35. package/dist/codexUsage.js.map +1 -0
  36. package/dist/deviceTokenStore.d.ts +38 -0
  37. package/dist/deviceTokenStore.js +73 -0
  38. package/dist/deviceTokenStore.js.map +1 -0
  39. package/dist/dirLister.d.ts +9 -0
  40. package/dist/dirLister.js +98 -0
  41. package/dist/dirLister.js.map +1 -0
  42. package/dist/doctor.d.ts +36 -0
  43. package/dist/doctor.js +162 -0
  44. package/dist/doctor.js.map +1 -0
  45. package/dist/engine.d.ts +85 -0
  46. package/dist/engine.js +962 -0
  47. package/dist/engine.js.map +1 -0
  48. package/dist/hook.d.ts +67 -0
  49. package/dist/hook.js +511 -0
  50. package/dist/hook.js.map +1 -0
  51. package/dist/hookSettings.d.ts +40 -0
  52. package/dist/hookSettings.js +128 -0
  53. package/dist/hookSettings.js.map +1 -0
  54. package/dist/idleReaper.d.ts +13 -0
  55. package/dist/idleReaper.js +30 -0
  56. package/dist/idleReaper.js.map +1 -0
  57. package/dist/imageService.d.ts +53 -0
  58. package/dist/imageService.js +280 -0
  59. package/dist/imageService.js.map +1 -0
  60. package/dist/kick.d.ts +8 -0
  61. package/dist/kick.js +60 -0
  62. package/dist/kick.js.map +1 -0
  63. package/dist/launch.d.ts +107 -0
  64. package/dist/launch.js +396 -0
  65. package/dist/launch.js.map +1 -0
  66. package/dist/legacyHomeMigration.d.ts +1 -0
  67. package/dist/legacyHomeMigration.js +23 -0
  68. package/dist/legacyHomeMigration.js.map +1 -0
  69. package/dist/lineWriter.d.ts +9 -0
  70. package/dist/lineWriter.js +19 -0
  71. package/dist/lineWriter.js.map +1 -0
  72. package/dist/mdnsAdvertiser.d.ts +46 -0
  73. package/dist/mdnsAdvertiser.js +268 -0
  74. package/dist/mdnsAdvertiser.js.map +1 -0
  75. package/dist/observationLog.d.ts +54 -0
  76. package/dist/observationLog.js +108 -0
  77. package/dist/observationLog.js.map +1 -0
  78. package/dist/pairingCode.d.ts +60 -0
  79. package/dist/pairingCode.js +282 -0
  80. package/dist/pairingCode.js.map +1 -0
  81. package/dist/paths.d.ts +13 -0
  82. package/dist/paths.js +46 -0
  83. package/dist/paths.js.map +1 -0
  84. package/dist/permissionMode.d.ts +8 -0
  85. package/dist/permissionMode.js +32 -0
  86. package/dist/permissionMode.js.map +1 -0
  87. package/dist/planUsageFetcher.d.ts +35 -0
  88. package/dist/planUsageFetcher.js +172 -0
  89. package/dist/planUsageFetcher.js.map +1 -0
  90. package/dist/protocol.d.ts +329 -0
  91. package/dist/protocol.js +544 -0
  92. package/dist/protocol.js.map +1 -0
  93. package/dist/pushTokenCommand.d.ts +9 -0
  94. package/dist/pushTokenCommand.js +85 -0
  95. package/dist/pushTokenCommand.js.map +1 -0
  96. package/dist/pushTypes.d.ts +41 -0
  97. package/dist/pushTypes.js +68 -0
  98. package/dist/pushTypes.js.map +1 -0
  99. package/dist/sendLog.d.ts +14 -0
  100. package/dist/sendLog.js +64 -0
  101. package/dist/sendLog.js.map +1 -0
  102. package/dist/sessionActivityProvider.d.ts +7 -0
  103. package/dist/sessionActivityProvider.js +44 -0
  104. package/dist/sessionActivityProvider.js.map +1 -0
  105. package/dist/sessionIdleTracker.d.ts +19 -0
  106. package/dist/sessionIdleTracker.js +42 -0
  107. package/dist/sessionIdleTracker.js.map +1 -0
  108. package/dist/sessionListService.d.ts +27 -0
  109. package/dist/sessionListService.js +85 -0
  110. package/dist/sessionListService.js.map +1 -0
  111. package/dist/sessionMetadataStore.d.ts +33 -0
  112. package/dist/sessionMetadataStore.js +122 -0
  113. package/dist/sessionMetadataStore.js.map +1 -0
  114. package/dist/sessionSearch.d.ts +29 -0
  115. package/dist/sessionSearch.js +139 -0
  116. package/dist/sessionSearch.js.map +1 -0
  117. package/dist/setup.d.ts +83 -0
  118. package/dist/setup.js +412 -0
  119. package/dist/setup.js.map +1 -0
  120. package/dist/sleep.d.ts +4 -0
  121. package/dist/sleep.js +25 -0
  122. package/dist/sleep.js.map +1 -0
  123. package/dist/socketPath.d.ts +12 -0
  124. package/dist/socketPath.js +51 -0
  125. package/dist/socketPath.js.map +1 -0
  126. package/dist/subagentTailer.d.ts +15 -0
  127. package/dist/subagentTailer.js +399 -0
  128. package/dist/subagentTailer.js.map +1 -0
  129. package/dist/tmux.d.ts +62 -0
  130. package/dist/tmux.js +163 -0
  131. package/dist/tmux.js.map +1 -0
  132. package/dist/transcriptTailer.d.ts +61 -0
  133. package/dist/transcriptTailer.js +606 -0
  134. package/dist/transcriptTailer.js.map +1 -0
  135. package/dist/usageAggregator.d.ts +12 -0
  136. package/dist/usageAggregator.js +55 -0
  137. package/dist/usageAggregator.js.map +1 -0
  138. package/dist/version.d.ts +10 -0
  139. package/dist/version.js +45 -0
  140. package/dist/version.js.map +1 -0
  141. package/package.json +50 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Namasugi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # tailii-host
2
+
3
+ Tailii is an iPhone app for controlling Claude Code and Codex sessions running on your own Mac.
4
+
5
+ `tailii-host` は、Tailii iPhone アプリと Mac 上の Claude Code / Codex セッションをつなぐホスト側ヘルパーです。Mac 側で `npx tailii-host setup` を実行してペアリングします。
6
+
7
+ ## Requirements
8
+
9
+ - Node.js 20 以上
10
+ - macOS
11
+ - tmux
12
+ - `claude` CLI (Claude Code) がインストール済みでログイン済みであること
13
+ - Tailii iPhone アプリ
14
+
15
+ ## Quick start
16
+
17
+ Mac で次のコマンドを実行します。
18
+
19
+ ```sh
20
+ npx tailii-host setup
21
+ ```
22
+
23
+ 表示された QR コードを Tailii アプリで読み取るか、表示されたコードをアプリに入力してペアリングします。
24
+
25
+ `setup` は、アプリが SSH 経由で実行するランチャー(`~/.local/bin/tailii-host`)を自動生成し、あわせて環境診断(tmux / claude CLI / リモートログイン)を表示します。診断だけを再実行するには:
26
+
27
+ ```sh
28
+ npx tailii-host doctor
29
+ ```
30
+
31
+ ## How it works
32
+
33
+ Tailii はサーバーレス構成で動作し、リレーサーバーは使いません。Mac への到達性は自宅 LAN や Tailscale など、ユーザー自身のネットワーク環境で確保します。
34
+
35
+ 承認フローでは Claude Code の PreToolUse hook を使い、ツール実行を一時停止して構造化イベントを iPhone に転送します。ユーザーは iPhone アプリ上でネイティブに承認または拒否できます。セッションは tmux により維持されるため、接続が切れても継続できます。バックグラウンド通知は Mac から APNs に直接送信されます。
36
+
37
+ 実行エンジンは、サブスクリプション認証済みの対話型 `claude` CLI です。Agent SDK やヘッドレス実行は使いません。
38
+
39
+ ## Commands
40
+
41
+ - `setup`: メインのユーザー向けコマンドです。Mac と Tailii iPhone アプリをペアリングします。
42
+ - `engine`: Tailii が利用する実行エンジン関連の処理を行います。
43
+ - `serve`: Mac 側ホストのローカルサーバーを起動します。
44
+ - `hook`: Claude Code の hook から呼び出され、承認イベントを処理します。
45
+ - `launch`: tmux-backed セッションを起動します。
46
+ - `kick`: セッションやホスト側処理を再開・通知するために使われます。
47
+ - `push-token`: iPhone アプリのプッシュ通知トークンを登録します。
48
+
49
+ ## License
50
+
51
+ MIT
@@ -0,0 +1,22 @@
1
+ /** APNs 送信に必要な認証設定(config.json のデコード結果)。 */
2
+ export interface ApnsConfig {
3
+ /** Apple Developer Team ID(JWT の `iss` クレーム)。 */
4
+ teamId: string;
5
+ /** APNs 認証キーの Key ID(JWT ヘッダの `kid`)。 */
6
+ keyId: string;
7
+ /** `.p8` 認証キーファイルへのパス(0600 で保存された秘密鍵)。 */
8
+ keyPath: string;
9
+ /** `apns-topic`(= bundle id)。 */
10
+ topic: string;
11
+ }
12
+ /**
13
+ * `~/.tailii/apns/config.json` から APNs 認証設定を読み込むストア。
14
+ * 未設定(ファイル欠落)・不整合(不正 JSON/フィールド欠落)はいずれも `load()` が
15
+ * `null` を返す(Requirement 6.4)。テスト容易性のため base を注入できる。
16
+ */
17
+ export declare class ApnsConfigStore {
18
+ private readonly base;
19
+ constructor(base?: string);
20
+ /** `config.json` を読み込む。未設定・不整合なら `null`(Requirement 6.4)。 */
21
+ load(): ApnsConfig | null;
22
+ }
@@ -0,0 +1,50 @@
1
+ // apnsConfigStore.ts
2
+ // tailii (TS host) — APNs 認証設定の読込・未設定判別
3
+ // Swift 版 ApnsConfigStore.swift の移植。
4
+ //
5
+ // `~/.tailii/apns/config.json` を読み、APNs 送信に必要な認証設定
6
+ // (teamId / keyId / keyPath / topic)を提供する。欠落・不整合は null で表現し、
7
+ // 呼び出し側が「未設定=送信スキップ」を判別できる(Requirement 6.4)。
8
+ // 秘密(.p8)そのものは保持せず keyPath 参照のみを扱う(Requirement 6.5)。
9
+ import * as fs from "node:fs";
10
+ import * as path from "node:path";
11
+ import { defaultApnsBase } from "./pushTypes.js";
12
+ /**
13
+ * `~/.tailii/apns/config.json` から APNs 認証設定を読み込むストア。
14
+ * 未設定(ファイル欠落)・不整合(不正 JSON/フィールド欠落)はいずれも `load()` が
15
+ * `null` を返す(Requirement 6.4)。テスト容易性のため base を注入できる。
16
+ */
17
+ export class ApnsConfigStore {
18
+ base;
19
+ constructor(base) {
20
+ this.base = base ?? defaultApnsBase();
21
+ }
22
+ /** `config.json` を読み込む。未設定・不整合なら `null`(Requirement 6.4)。 */
23
+ load() {
24
+ let raw;
25
+ try {
26
+ raw = JSON.parse(fs.readFileSync(path.join(this.base, "config.json"), "utf8"));
27
+ }
28
+ catch {
29
+ // ファイル欠落 / 不正 JSON = 未設定(6.4)
30
+ return null;
31
+ }
32
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw))
33
+ return null;
34
+ const obj = raw;
35
+ if (typeof obj["teamId"] !== "string" ||
36
+ typeof obj["keyId"] !== "string" ||
37
+ typeof obj["keyPath"] !== "string" ||
38
+ typeof obj["topic"] !== "string") {
39
+ // フィールド欠落 = 未設定扱い(6.4)
40
+ return null;
41
+ }
42
+ return {
43
+ teamId: obj["teamId"],
44
+ keyId: obj["keyId"],
45
+ keyPath: obj["keyPath"],
46
+ topic: obj["topic"],
47
+ };
48
+ }
49
+ }
50
+ //# sourceMappingURL=apnsConfigStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apnsConfigStore.js","sourceRoot":"","sources":["../src/apnsConfigStore.ts"],"names":[],"mappings":"AAAA,qBAAqB;AACrB,wCAAwC;AACxC,qCAAqC;AACrC,EAAE;AACF,mDAAmD;AACnD,6DAA6D;AAC7D,6CAA6C;AAC7C,qDAAqD;AAErD,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAcjD;;;;GAIG;AACH,MAAM,OAAO,eAAe;IACT,IAAI,CAAS;IAE9B,YAAY,IAAa;QACvB,IAAI,CAAC,IAAI,GAAG,IAAI,IAAI,eAAe,EAAE,CAAC;IACxC,CAAC;IAED,6DAA6D;IAC7D,IAAI;QACF,IAAI,GAAY,CAAC;QACjB,IAAI,CAAC;YACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,aAAa,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;QACjF,CAAC;QAAC,MAAM,CAAC;YACP,8BAA8B;YAC9B,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC;QAC/E,MAAM,GAAG,GAAG,GAA8B,CAAC;QAC3C,IACE,OAAO,GAAG,CAAC,QAAQ,CAAC,KAAK,QAAQ;YACjC,OAAO,GAAG,CAAC,OAAO,CAAC,KAAK,QAAQ;YAChC,OAAO,GAAG,CAAC,SAAS,CAAC,KAAK,QAAQ;YAClC,OAAO,GAAG,CAAC,OAAO,CAAC,KAAK,QAAQ,EAChC,CAAC;YACD,uBAAuB;YACvB,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO;YACL,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAC;YACrB,KAAK,EAAE,GAAG,CAAC,OAAO,CAAC;YACnB,OAAO,EAAE,GAAG,CAAC,SAAS,CAAC;YACvB,KAAK,EAAE,GAAG,CAAC,OAAO,CAAC;SACpB,CAAC;IACJ,CAAC;CACF"}
@@ -0,0 +1,50 @@
1
+ import { ApnsConfigStore } from "./apnsConfigStore.js";
2
+ /** 既定の再利用窓(秒)。50 分 = 3000 秒(毎時再発行の目安より短く保ち 429 を回避, 6.1)。 */
3
+ export declare const DEFAULT_JWT_REUSE_WINDOW_SECONDS = 3000;
4
+ /** JWT 生成/設定に関するエラー理由。 */
5
+ export type JWTCacheErrorReason = "config-missing" | "key-load-failed" | "encoding-failed";
6
+ /** JWT 生成/設定に関する型付きエラー(6.4)。 */
7
+ export declare class JWTCacheError extends Error {
8
+ readonly reason: JWTCacheErrorReason;
9
+ constructor(reason: JWTCacheErrorReason, detail?: string);
10
+ }
11
+ /** APNs プロバイダ JWT の供給契約(再利用窓内はキャッシュ、超過で再署名)。 */
12
+ export interface APNsJWTProviding {
13
+ /** 再利用窓内はキャッシュを返し、超過で ES256 再署名した JWT を返す(config 未設定は throw, 6.4)。 */
14
+ currentToken(): string;
15
+ /** キャッシュを無効化し、次回 currentToken() で再署名を強制する(6.2)。 */
16
+ invalidate(): void;
17
+ }
18
+ export interface APNsJWTProviderOptions {
19
+ /** jwt-cache.json を保存する base ディレクトリ。省略時は `~/.tailii/apns`。 */
20
+ base?: string;
21
+ /** 再利用窓(秒)。省略時は 3000(50 分)。 */
22
+ reuseWindow?: number;
23
+ /** 現在時刻(Unix 秒)を返す関数。テストで決定化するため注入可能。 */
24
+ now?: () => number;
25
+ }
26
+ /**
27
+ * ES256 で APNs プロバイダ JWT を署名し、再利用窓内はファイルキャッシュを流用する実体。
28
+ * キャッシュは `~/.tailii/apns/jwt-cache.json` に永続する(hook プロセスは短命かつ
29
+ * 複数起動しうるため、跨プロセスで再利用が効く)。base / now / reuseWindow を注入できる。
30
+ */
31
+ export declare class APNsJWTProvider implements APNsJWTProviding {
32
+ private readonly configStore;
33
+ private readonly base;
34
+ private readonly reuseWindow;
35
+ private readonly now;
36
+ constructor(config: ApnsConfigStore, options?: APNsJWTProviderOptions);
37
+ private get cacheURL();
38
+ /** 再利用窓内はキャッシュを返し、超過で ES256 再署名する(Requirement 6.1)。 */
39
+ currentToken(): string;
40
+ /** キャッシュファイルを削除し、次回 currentToken() で再署名を強制する(6.2)。 */
41
+ invalidate(): void;
42
+ /** `.p8`(PKCS#8 PEM)を読み込み ES256 JWT を署名する。 */
43
+ private sign;
44
+ /** `.p8`(PKCS#8 PEM)ファイルから P256 署名鍵を読み込む(秘密鍵は非ログ, 6.5)。 */
45
+ private loadPrivateKey;
46
+ /** jwt-cache.json を読み込む。欠落・不正 JSON なら `null`。 */
47
+ private loadCache;
48
+ /** jwt-cache.json を 0600 で保存する(ディレクトリは 0700)。保存失敗は握り潰す。 */
49
+ private saveCache;
50
+ }
@@ -0,0 +1,158 @@
1
+ // apnsJwtProvider.ts
2
+ // tailii (TS host) — ES256 JWT 署名+再利用キャッシュ
3
+ // Swift 版 APNsJWTProvider.swift の移植。
4
+ //
5
+ // `.p8`(P256 秘密鍵)から APNs プロバイダ JWT を ES256 で署名する。
6
+ // ヘッダ `{alg:"ES256", kid:<keyId>}`、クレーム `{iss:<teamId>, iat:<now>}` を
7
+ // base64url(padding 無し)で符号化し、署名は `header.payload` に対する P256 ECDSA の
8
+ // raw 64 バイト(r||s)を base64url 化する(DER ではなく raw 表現, Requirement 6.1)。
9
+ //
10
+ // 再利用キャッシュ: `~/.tailii/apns/jwt-cache.json`(`{jwt, issuedAt}`)。
11
+ // 再利用窓(既定 50 分)内は流用し、超過で再署名する(6.1 / 429 回避)。
12
+ // invalidate() はキャッシュを破棄し次回再署名を強制する(ExpiredProviderToken 受領時, 6.2)。
13
+ //
14
+ // 署名は Node 組み込みの `node:crypto`(P256 ECDSA)で行い、外部 JWT ライブラリは不採用。
15
+ // `.p8` 秘密鍵はログに出さない(6.5)。鍵情報は ApnsConfigStore から取得する。
16
+ import * as crypto from "node:crypto";
17
+ import * as fs from "node:fs";
18
+ import * as path from "node:path";
19
+ import { defaultApnsBase, ensureApnsDirectory } from "./pushTypes.js";
20
+ /** 既定の再利用窓(秒)。50 分 = 3000 秒(毎時再発行の目安より短く保ち 429 を回避, 6.1)。 */
21
+ export const DEFAULT_JWT_REUSE_WINDOW_SECONDS = 3000;
22
+ /** JWT 生成/設定に関する型付きエラー(6.4)。 */
23
+ export class JWTCacheError extends Error {
24
+ reason;
25
+ constructor(reason, detail = "") {
26
+ super(detail ? `${reason}: ${detail}` : reason);
27
+ this.reason = reason;
28
+ this.name = "JWTCacheError";
29
+ }
30
+ }
31
+ /**
32
+ * ES256 で APNs プロバイダ JWT を署名し、再利用窓内はファイルキャッシュを流用する実体。
33
+ * キャッシュは `~/.tailii/apns/jwt-cache.json` に永続する(hook プロセスは短命かつ
34
+ * 複数起動しうるため、跨プロセスで再利用が効く)。base / now / reuseWindow を注入できる。
35
+ */
36
+ export class APNsJWTProvider {
37
+ configStore;
38
+ base;
39
+ reuseWindow;
40
+ now;
41
+ constructor(config, options = {}) {
42
+ this.configStore = config;
43
+ this.base = options.base ?? defaultApnsBase();
44
+ this.reuseWindow = options.reuseWindow ?? DEFAULT_JWT_REUSE_WINDOW_SECONDS;
45
+ this.now = options.now ?? (() => Math.floor(Date.now() / 1000));
46
+ }
47
+ get cacheURL() {
48
+ return path.join(this.base, "jwt-cache.json");
49
+ }
50
+ /** 再利用窓内はキャッシュを返し、超過で ES256 再署名する(Requirement 6.1)。 */
51
+ currentToken() {
52
+ const config = this.configStore.load();
53
+ if (config === null) {
54
+ // 未設定なら送信側が判別できるよう明示 throw(6.4)。
55
+ throw new JWTCacheError("config-missing");
56
+ }
57
+ const currentTime = this.now();
58
+ // 有効なキャッシュがあれば流用(跨インスタンス/跨プロセスでも効く)。
59
+ const cached = this.loadCache();
60
+ if (cached !== null && currentTime - cached.issuedAt < this.reuseWindow && currentTime >= cached.issuedAt) {
61
+ return cached.jwt;
62
+ }
63
+ // 再署名して永続化。
64
+ const jwt = this.sign(config, currentTime);
65
+ this.saveCache({ jwt, issuedAt: currentTime });
66
+ return jwt;
67
+ }
68
+ /** キャッシュファイルを削除し、次回 currentToken() で再署名を強制する(6.2)。 */
69
+ invalidate() {
70
+ try {
71
+ fs.rmSync(this.cacheURL);
72
+ }
73
+ catch {
74
+ // ファイル欠落など削除失敗は無視(次回は再署名できる)。
75
+ }
76
+ }
77
+ // MARK: - 署名
78
+ /** `.p8`(PKCS#8 PEM)を読み込み ES256 JWT を署名する。 */
79
+ sign(config, iat) {
80
+ const key = this.loadPrivateKey(config.keyPath);
81
+ // header: {alg, kid} を辞書順で決定的に符号化。
82
+ const header = { alg: "ES256", kid: config.keyId };
83
+ // claims: {iat, iss}。iat は整数で載せる(辞書順で iat < iss)。
84
+ const claims = { iat, iss: config.teamId };
85
+ const signingInput = `${base64url(canonicalJSON(header))}.${base64url(canonicalJSON(claims))}`;
86
+ // ES256: SHA256(header.payload) を P256 ECDSA 署名。raw 64 バイト(r||s)を用いる
87
+ //(APNs/JWT では DER ではなく raw 表現が必須 → dsaEncoding: "ieee-p1363")。
88
+ let signature;
89
+ try {
90
+ signature = crypto.sign("sha256", Buffer.from(signingInput, "utf8"), {
91
+ key,
92
+ dsaEncoding: "ieee-p1363",
93
+ });
94
+ }
95
+ catch (error) {
96
+ throw new JWTCacheError("encoding-failed", String(error));
97
+ }
98
+ return `${signingInput}.${base64url(signature)}`;
99
+ }
100
+ /** `.p8`(PKCS#8 PEM)ファイルから P256 署名鍵を読み込む(秘密鍵は非ログ, 6.5)。 */
101
+ loadPrivateKey(keyPath) {
102
+ let pem;
103
+ try {
104
+ pem = fs.readFileSync(keyPath, "utf8");
105
+ }
106
+ catch {
107
+ throw new JWTCacheError("key-load-failed", "鍵ファイル読込失敗");
108
+ }
109
+ try {
110
+ return crypto.createPrivateKey({ key: pem, format: "pem" });
111
+ }
112
+ catch {
113
+ throw new JWTCacheError("key-load-failed", "PKCS#8 PEM パース失敗");
114
+ }
115
+ }
116
+ // MARK: - キャッシュ I/O
117
+ /** jwt-cache.json を読み込む。欠落・不正 JSON なら `null`。 */
118
+ loadCache() {
119
+ let raw;
120
+ try {
121
+ raw = JSON.parse(fs.readFileSync(this.cacheURL, "utf8"));
122
+ }
123
+ catch {
124
+ return null;
125
+ }
126
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw))
127
+ return null;
128
+ const obj = raw;
129
+ if (typeof obj["jwt"] !== "string" || typeof obj["issuedAt"] !== "number")
130
+ return null;
131
+ return { jwt: obj["jwt"], issuedAt: obj["issuedAt"] };
132
+ }
133
+ /** jwt-cache.json を 0600 で保存する(ディレクトリは 0700)。保存失敗は握り潰す。 */
134
+ saveCache(entry) {
135
+ try {
136
+ ensureApnsDirectory(this.base);
137
+ // 辞書順(issuedAt < jwt)で符号化(Swift 版 .sortedKeys と同一)。
138
+ const payload = JSON.stringify({ issuedAt: entry.issuedAt, jwt: entry.jwt });
139
+ fs.writeFileSync(this.cacheURL, payload, { mode: 0o600 });
140
+ fs.chmodSync(this.cacheURL, 0o600);
141
+ }
142
+ catch {
143
+ // 保存失敗は致命ではない(次回は再署名できる)。
144
+ }
145
+ }
146
+ }
147
+ /** base64url(padding 無し)符号化。`+`→`-`, `/`→`_`, `=` を除去。 */
148
+ function base64url(data) {
149
+ return data.toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
150
+ }
151
+ /** 与えたオブジェクトを、キー辞書順で決定的に JSON 符号化した Buffer を返す。 */
152
+ function canonicalJSON(obj) {
153
+ const sorted = {};
154
+ for (const key of Object.keys(obj).sort())
155
+ sorted[key] = obj[key];
156
+ return Buffer.from(JSON.stringify(sorted), "utf8");
157
+ }
158
+ //# sourceMappingURL=apnsJwtProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apnsJwtProvider.js","sourceRoot":"","sources":["../src/apnsJwtProvider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,69 @@
1
+ import { type ApnsHost, type PushError } from "./pushTypes.js";
2
+ /** HTTP POST の結果(成功時は status+body、転送層失敗時は error)。 */
3
+ export type HttpPostResult = {
4
+ ok: true;
5
+ status: number;
6
+ body: Buffer;
7
+ } | {
8
+ ok: false;
9
+ error: Error;
10
+ };
11
+ /** APNs への HTTP POST を抽象化する注入可能クライアント契約。 */
12
+ export interface HTTPPosting {
13
+ post(url: string, headers: Record<string, string>, body: Buffer): Promise<HttpPostResult>;
14
+ }
15
+ /** APNs リクエスト1件分の宛先・認証・トピック・dedup・期限・本文。 */
16
+ export interface ApnsRequest {
17
+ /** 送信先環境(production / sandbox)。送信ホスト選択に使う。 */
18
+ host: ApnsHost;
19
+ /** 宛先 device token(hex)。URL パスに用いる。 */
20
+ deviceToken: string;
21
+ /** `authorization: bearer <jwt>` に用いる APNs プロバイダ JWT。 */
22
+ bearerJWT: string;
23
+ /** `apns-topic`(= bundle id)。 */
24
+ topic: string;
25
+ /** `apns-collapse-id`(= approvalId)。同一承認の重複表示を抑止する(7.2/7.3)。 */
26
+ collapseId: string;
27
+ /** `apns-expiration`(Unix 秒 or 相対秒。awareness 用に数分)。 */
28
+ expiration: number;
29
+ /** リクエスト本文(aps + 最小カスタムキー)。 */
30
+ body: Buffer;
31
+ }
32
+ /** APNs 送信の結果(200 なら ok、それ以外は写像した PushError)。 */
33
+ export type ApnsSendResult = {
34
+ ok: true;
35
+ } | {
36
+ ok: false;
37
+ error: PushError;
38
+ };
39
+ /** APNs 送信の契約。応答を PushError に写像する。 */
40
+ export interface ApnsSending {
41
+ send(req: ApnsRequest): Promise<ApnsSendResult>;
42
+ }
43
+ /**
44
+ * 注入した HTTPPosting で APNs へ POST し、応答を PushError へ写像する実体。
45
+ * URL・method・ヘッダ組み立てと応答分類のみを担い、実 I/O と HTTP/2 は注入クライアントに委譲する。
46
+ */
47
+ export declare class APNsSender implements ApnsSending {
48
+ private readonly client;
49
+ constructor(client: HTTPPosting);
50
+ send(req: ApnsRequest): Promise<ApnsSendResult>;
51
+ }
52
+ /**
53
+ * HTTP ステータスとレスポンス本文(`{"reason":"..."}`)を ApnsSendResult に写像する。
54
+ * - 200 → ok / 403 ExpiredProviderToken → expiredProviderToken /
55
+ * 410・400 BadDeviceToken → unregistered / 429 → tooManyProviderTokenUpdates /
56
+ * その他 → http(status, reason)。
57
+ */
58
+ export declare function mapResponse(status: number, body: Buffer): ApnsSendResult;
59
+ /**
60
+ * `node:http2` を用いる本番 HTTPPosting 実体。
61
+ * https 宛の APNs は HTTP/2 が必須のため、Node の fetch(HTTP/1.1 のみ)ではなく
62
+ * http2.connect で送る。テストではネットワークに触れないよう mock を注入し、本実体は実機/E2E で用いる。
63
+ */
64
+ export declare class Http2HttpClient implements HTTPPosting {
65
+ /** リクエストごとの時間上限(ms)。到達で転送層失敗に畳む。 */
66
+ private readonly requestTimeoutMs;
67
+ constructor(requestTimeoutMs?: number);
68
+ post(url: string, headers: Record<string, string>, body: Buffer): Promise<HttpPostResult>;
69
+ }
@@ -0,0 +1,155 @@
1
+ // apnsSender.ts
2
+ // tailii (TS host) — APNs HTTP/2 送信・応答写像
3
+ // Swift 版 APNsSender.swift の移植。
4
+ //
5
+ // 保存済み JWT / device token 宛に APNs へ HTTP/2 で直送する。
6
+ // `POST https://{host}/3/device/{deviceToken}` に、ヘッダ
7
+ // `authorization: bearer <jwt>`, `apns-topic: <topic>`, `apns-push-type: alert`,
8
+ // `apns-priority: 10`, `apns-expiration: <int>`, `apns-collapse-id: <collapseId>`
9
+ // を載せ、最小 payload の本文を POST する(Requirement 2.2, 7.3)。
10
+ //
11
+ // 応答写像(Requirement 6.2, 6.3):
12
+ // 200 → success
13
+ // 403 reason=ExpiredProviderToken → expiredProviderToken(JWT 再発行, 6.2)
14
+ // 410 / 400 BadDeviceToken → unregistered(宛先削除・停止, 6.3)
15
+ // 429 (TooManyProviderTokenUpdates) → tooManyProviderTokenUpdates
16
+ // その他非200 → http(status, reason)
17
+ // 転送層失敗(ネットワーク/timeout) → transport(String)
18
+ //
19
+ // HTTP クライアントは注入(HTTPPosting)。テストは canned (status, body) を返す mock を注入し、
20
+ // 本番は `node:http2` を用いる Http2HttpClient を注入する(Node の fetch は HTTP/2 非対応)。
21
+ // 秘密(JWT/token)はログに出さない(6.5)。
22
+ import * as http2 from "node:http2";
23
+ import { apnsSendHost } from "./pushTypes.js";
24
+ /**
25
+ * 注入した HTTPPosting で APNs へ POST し、応答を PushError へ写像する実体。
26
+ * URL・method・ヘッダ組み立てと応答分類のみを担い、実 I/O と HTTP/2 は注入クライアントに委譲する。
27
+ */
28
+ export class APNsSender {
29
+ client;
30
+ constructor(client) {
31
+ this.client = client;
32
+ }
33
+ async send(req) {
34
+ // URL: https://{host}/3/device/{deviceToken}
35
+ const url = `https://${apnsSendHost(req.host)}/3/device/${req.deviceToken}`;
36
+ // 必須ヘッダ一式。
37
+ const headers = {
38
+ authorization: `bearer ${req.bearerJWT}`,
39
+ "apns-topic": req.topic,
40
+ "apns-push-type": "alert",
41
+ "apns-priority": "10",
42
+ "apns-expiration": String(req.expiration),
43
+ "apns-collapse-id": req.collapseId,
44
+ };
45
+ const result = await this.client.post(url, headers, req.body);
46
+ if (!result.ok) {
47
+ // 転送層の失敗(ネットワーク / timeout)。
48
+ return { ok: false, error: { kind: "transport", detail: result.error.message } };
49
+ }
50
+ return mapResponse(result.status, result.body);
51
+ }
52
+ }
53
+ /**
54
+ * HTTP ステータスとレスポンス本文(`{"reason":"..."}`)を ApnsSendResult に写像する。
55
+ * - 200 → ok / 403 ExpiredProviderToken → expiredProviderToken /
56
+ * 410・400 BadDeviceToken → unregistered / 429 → tooManyProviderTokenUpdates /
57
+ * その他 → http(status, reason)。
58
+ */
59
+ export function mapResponse(status, body) {
60
+ if (status === 200)
61
+ return { ok: true };
62
+ const reason = parseReason(body);
63
+ if (status === 403 && reason === "ExpiredProviderToken") {
64
+ return { ok: false, error: { kind: "expiredProviderToken" } };
65
+ }
66
+ if (status === 410) {
67
+ // 410 は宛先未登録(Unregistered)。reason 有無に関わらず宛先除外。
68
+ return { ok: false, error: { kind: "unregistered" } };
69
+ }
70
+ if (status === 400 && reason === "BadDeviceToken") {
71
+ return { ok: false, error: { kind: "unregistered" } };
72
+ }
73
+ if (status === 429) {
74
+ // TooManyProviderTokenUpdates(reason 有無に関わらず)。
75
+ return { ok: false, error: { kind: "tooManyProviderTokenUpdates" } };
76
+ }
77
+ return { ok: false, error: { kind: "http", status, reason } };
78
+ }
79
+ /** APNs エラー JSON(`{"reason":"..."}`)から reason を取り出す(不能・欠落時は空文字)。 */
80
+ function parseReason(body) {
81
+ try {
82
+ const obj = JSON.parse(body.toString("utf8"));
83
+ if (typeof obj === "object" && obj !== null && !Array.isArray(obj)) {
84
+ const reason = obj["reason"];
85
+ if (typeof reason === "string")
86
+ return reason;
87
+ }
88
+ }
89
+ catch {
90
+ // パース不能 → 空文字(分類は status に委ねる)。
91
+ }
92
+ return "";
93
+ }
94
+ /**
95
+ * `node:http2` を用いる本番 HTTPPosting 実体。
96
+ * https 宛の APNs は HTTP/2 が必須のため、Node の fetch(HTTP/1.1 のみ)ではなく
97
+ * http2.connect で送る。テストではネットワークに触れないよう mock を注入し、本実体は実機/E2E で用いる。
98
+ */
99
+ export class Http2HttpClient {
100
+ /** リクエストごとの時間上限(ms)。到達で転送層失敗に畳む。 */
101
+ requestTimeoutMs;
102
+ constructor(requestTimeoutMs = 10_000) {
103
+ this.requestTimeoutMs = requestTimeoutMs;
104
+ }
105
+ post(url, headers, body) {
106
+ return new Promise((resolve) => {
107
+ let settled = false;
108
+ const finish = (result) => {
109
+ if (settled)
110
+ return;
111
+ settled = true;
112
+ resolve(result);
113
+ };
114
+ let parsed;
115
+ try {
116
+ parsed = new URL(url);
117
+ }
118
+ catch (error) {
119
+ finish({ ok: false, error: error instanceof Error ? error : new Error(String(error)) });
120
+ return;
121
+ }
122
+ const session = http2.connect(parsed.origin);
123
+ session.on("error", (error) => {
124
+ finish({ ok: false, error });
125
+ session.close();
126
+ });
127
+ const req = session.request({
128
+ ":method": "POST",
129
+ ":path": parsed.pathname + parsed.search,
130
+ ...headers,
131
+ });
132
+ req.setTimeout(this.requestTimeoutMs, () => {
133
+ finish({ ok: false, error: new Error("APNs request timeout") });
134
+ req.close(http2.constants.NGHTTP2_CANCEL);
135
+ session.close();
136
+ });
137
+ let status = 0;
138
+ req.on("response", (responseHeaders) => {
139
+ status = Number(responseHeaders[":status"]) || 0;
140
+ });
141
+ const chunks = [];
142
+ req.on("data", (chunk) => chunks.push(chunk));
143
+ req.on("end", () => {
144
+ finish({ ok: true, status, body: Buffer.concat(chunks) });
145
+ session.close();
146
+ });
147
+ req.on("error", (error) => {
148
+ finish({ ok: false, error });
149
+ session.close();
150
+ });
151
+ req.end(body);
152
+ });
153
+ }
154
+ }
155
+ //# sourceMappingURL=apnsSender.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apnsSender.js","sourceRoot":"","sources":["../src/apnsSender.ts"],"names":[],"mappings":"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"}