uxnan-bridge 0.0.1-alpha.20260621

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 (255) hide show
  1. package/README.md +150 -0
  2. package/dist/src/account-status.d.ts +13 -0
  3. package/dist/src/account-status.js +78 -0
  4. package/dist/src/account-status.js.map +1 -0
  5. package/dist/src/adapters/base-adapter.d.ts +18 -0
  6. package/dist/src/adapters/base-adapter.js +15 -0
  7. package/dist/src/adapters/base-adapter.js.map +1 -0
  8. package/dist/src/adapters/claude-adapter.d.ts +102 -0
  9. package/dist/src/adapters/claude-adapter.js +486 -0
  10. package/dist/src/adapters/claude-adapter.js.map +1 -0
  11. package/dist/src/adapters/claude-tools.d.ts +25 -0
  12. package/dist/src/adapters/claude-tools.js +146 -0
  13. package/dist/src/adapters/claude-tools.js.map +1 -0
  14. package/dist/src/adapters/codex-adapter.d.ts +116 -0
  15. package/dist/src/adapters/codex-adapter.js +912 -0
  16. package/dist/src/adapters/codex-adapter.js.map +1 -0
  17. package/dist/src/adapters/codex-app-server.d.ts +74 -0
  18. package/dist/src/adapters/codex-app-server.js +225 -0
  19. package/dist/src/adapters/codex-app-server.js.map +1 -0
  20. package/dist/src/adapters/codex-approval.d.ts +88 -0
  21. package/dist/src/adapters/codex-approval.js +160 -0
  22. package/dist/src/adapters/codex-approval.js.map +1 -0
  23. package/dist/src/adapters/codex-tools.d.ts +18 -0
  24. package/dist/src/adapters/codex-tools.js +106 -0
  25. package/dist/src/adapters/codex-tools.js.map +1 -0
  26. package/dist/src/adapters/content-blocks.d.ts +68 -0
  27. package/dist/src/adapters/content-blocks.js +205 -0
  28. package/dist/src/adapters/content-blocks.js.map +1 -0
  29. package/dist/src/adapters/echo-agent-adapter.d.ts +23 -0
  30. package/dist/src/adapters/echo-agent-adapter.js +72 -0
  31. package/dist/src/adapters/echo-agent-adapter.js.map +1 -0
  32. package/dist/src/adapters/gemini-adapter.d.ts +87 -0
  33. package/dist/src/adapters/gemini-adapter.js +594 -0
  34. package/dist/src/adapters/gemini-adapter.js.map +1 -0
  35. package/dist/src/adapters/gemini-tools.d.ts +4 -0
  36. package/dist/src/adapters/gemini-tools.js +48 -0
  37. package/dist/src/adapters/gemini-tools.js.map +1 -0
  38. package/dist/src/adapters/opencode-adapter.d.ts +74 -0
  39. package/dist/src/adapters/opencode-adapter.js +418 -0
  40. package/dist/src/adapters/opencode-adapter.js.map +1 -0
  41. package/dist/src/adapters/opencode-tools.d.ts +2 -0
  42. package/dist/src/adapters/opencode-tools.js +41 -0
  43. package/dist/src/adapters/opencode-tools.js.map +1 -0
  44. package/dist/src/adapters/pi-adapter.d.ts +92 -0
  45. package/dist/src/adapters/pi-adapter.js +467 -0
  46. package/dist/src/adapters/pi-adapter.js.map +1 -0
  47. package/dist/src/adapters/pi-tools.d.ts +10 -0
  48. package/dist/src/adapters/pi-tools.js +72 -0
  49. package/dist/src/adapters/pi-tools.js.map +1 -0
  50. package/dist/src/adapters/process-agent-adapter.d.ts +24 -0
  51. package/dist/src/adapters/process-agent-adapter.js +111 -0
  52. package/dist/src/adapters/process-agent-adapter.js.map +1 -0
  53. package/dist/src/adapters/resolve-claude.d.ts +13 -0
  54. package/dist/src/adapters/resolve-claude.js +57 -0
  55. package/dist/src/adapters/resolve-claude.js.map +1 -0
  56. package/dist/src/adapters/resolve-codex.d.ts +13 -0
  57. package/dist/src/adapters/resolve-codex.js +48 -0
  58. package/dist/src/adapters/resolve-codex.js.map +1 -0
  59. package/dist/src/adapters/resolve-gemini.d.ts +13 -0
  60. package/dist/src/adapters/resolve-gemini.js +47 -0
  61. package/dist/src/adapters/resolve-gemini.js.map +1 -0
  62. package/dist/src/adapters/resolve-opencode.d.ts +11 -0
  63. package/dist/src/adapters/resolve-opencode.js +49 -0
  64. package/dist/src/adapters/resolve-opencode.js.map +1 -0
  65. package/dist/src/adapters/resolve-pi.d.ts +13 -0
  66. package/dist/src/adapters/resolve-pi.js +46 -0
  67. package/dist/src/adapters/resolve-pi.js.map +1 -0
  68. package/dist/src/adapters/run-options.d.ts +22 -0
  69. package/dist/src/adapters/run-options.js +48 -0
  70. package/dist/src/adapters/run-options.js.map +1 -0
  71. package/dist/src/adapters/spawn.d.ts +20 -0
  72. package/dist/src/adapters/spawn.js +16 -0
  73. package/dist/src/adapters/spawn.js.map +1 -0
  74. package/dist/src/agents/agent-manager.d.ts +98 -0
  75. package/dist/src/agents/agent-manager.js +433 -0
  76. package/dist/src/agents/agent-manager.js.map +1 -0
  77. package/dist/src/agents/attachments.d.ts +28 -0
  78. package/dist/src/agents/attachments.js +121 -0
  79. package/dist/src/agents/attachments.js.map +1 -0
  80. package/dist/src/bridge-context.d.ts +45 -0
  81. package/dist/src/bridge-context.js +2 -0
  82. package/dist/src/bridge-context.js.map +1 -0
  83. package/dist/src/bridge-status.d.ts +12 -0
  84. package/dist/src/bridge-status.js +17 -0
  85. package/dist/src/bridge-status.js.map +1 -0
  86. package/dist/src/bridge.d.ts +37 -0
  87. package/dist/src/bridge.js +446 -0
  88. package/dist/src/bridge.js.map +1 -0
  89. package/dist/src/cli.d.ts +2 -0
  90. package/dist/src/cli.js +194 -0
  91. package/dist/src/cli.js.map +1 -0
  92. package/dist/src/conversation/session-history.d.ts +27 -0
  93. package/dist/src/conversation/session-history.js +1082 -0
  94. package/dist/src/conversation/session-history.js.map +1 -0
  95. package/dist/src/conversation/thread-store.d.ts +74 -0
  96. package/dist/src/conversation/thread-store.js +366 -0
  97. package/dist/src/conversation/thread-store.js.map +1 -0
  98. package/dist/src/daemon-config.d.ts +123 -0
  99. package/dist/src/daemon-config.js +64 -0
  100. package/dist/src/daemon-config.js.map +1 -0
  101. package/dist/src/daemon-state.d.ts +27 -0
  102. package/dist/src/daemon-state.js +76 -0
  103. package/dist/src/daemon-state.js.map +1 -0
  104. package/dist/src/git/git-runner.d.ts +24 -0
  105. package/dist/src/git/git-runner.js +63 -0
  106. package/dist/src/git/git-runner.js.map +1 -0
  107. package/dist/src/git/git-service.d.ts +76 -0
  108. package/dist/src/git/git-service.js +435 -0
  109. package/dist/src/git/git-service.js.map +1 -0
  110. package/dist/src/handler-router.d.ts +34 -0
  111. package/dist/src/handler-router.js +67 -0
  112. package/dist/src/handler-router.js.map +1 -0
  113. package/dist/src/handlers/account-handler.d.ts +4 -0
  114. package/dist/src/handlers/account-handler.js +27 -0
  115. package/dist/src/handlers/account-handler.js.map +1 -0
  116. package/dist/src/handlers/agent-handler.d.ts +2 -0
  117. package/dist/src/handlers/agent-handler.js +8 -0
  118. package/dist/src/handlers/agent-handler.js.map +1 -0
  119. package/dist/src/handlers/bridge-control-handler.d.ts +2 -0
  120. package/dist/src/handlers/bridge-control-handler.js +64 -0
  121. package/dist/src/handlers/bridge-control-handler.js.map +1 -0
  122. package/dist/src/handlers/desktop-handler.d.ts +12 -0
  123. package/dist/src/handlers/desktop-handler.js +5 -0
  124. package/dist/src/handlers/desktop-handler.js.map +1 -0
  125. package/dist/src/handlers/git-handler.d.ts +2 -0
  126. package/dist/src/handlers/git-handler.js +82 -0
  127. package/dist/src/handlers/git-handler.js.map +1 -0
  128. package/dist/src/handlers/index.d.ts +8 -0
  129. package/dist/src/handlers/index.js +22 -0
  130. package/dist/src/handlers/index.js.map +1 -0
  131. package/dist/src/handlers/not-implemented.d.ts +10 -0
  132. package/dist/src/handlers/not-implemented.js +21 -0
  133. package/dist/src/handlers/not-implemented.js.map +1 -0
  134. package/dist/src/handlers/notifications-handler.d.ts +2 -0
  135. package/dist/src/handlers/notifications-handler.js +62 -0
  136. package/dist/src/handlers/notifications-handler.js.map +1 -0
  137. package/dist/src/handlers/params.d.ts +11 -0
  138. package/dist/src/handlers/params.js +72 -0
  139. package/dist/src/handlers/params.js.map +1 -0
  140. package/dist/src/handlers/project-handler.d.ts +2 -0
  141. package/dist/src/handlers/project-handler.js +6 -0
  142. package/dist/src/handlers/project-handler.js.map +1 -0
  143. package/dist/src/handlers/thread-context-handler.d.ts +2 -0
  144. package/dist/src/handlers/thread-context-handler.js +211 -0
  145. package/dist/src/handlers/thread-context-handler.js.map +1 -0
  146. package/dist/src/handlers/workspace-handler.d.ts +2 -0
  147. package/dist/src/handlers/workspace-handler.js +101 -0
  148. package/dist/src/handlers/workspace-handler.js.map +1 -0
  149. package/dist/src/hooks/claude-approval-hook.d.ts +7 -0
  150. package/dist/src/hooks/claude-approval-hook.js +95 -0
  151. package/dist/src/hooks/claude-approval-hook.js.map +1 -0
  152. package/dist/src/hooks/gemini-approval-hook.d.ts +7 -0
  153. package/dist/src/hooks/gemini-approval-hook.js +113 -0
  154. package/dist/src/hooks/gemini-approval-hook.js.map +1 -0
  155. package/dist/src/index.d.ts +62 -0
  156. package/dist/src/index.js +65 -0
  157. package/dist/src/index.js.map +1 -0
  158. package/dist/src/keyring-secret-store.d.ts +36 -0
  159. package/dist/src/keyring-secret-store.js +70 -0
  160. package/dist/src/keyring-secret-store.js.map +1 -0
  161. package/dist/src/lock-file.d.ts +18 -0
  162. package/dist/src/lock-file.js +60 -0
  163. package/dist/src/lock-file.js.map +1 -0
  164. package/dist/src/logger.d.ts +28 -0
  165. package/dist/src/logger.js +99 -0
  166. package/dist/src/logger.js.map +1 -0
  167. package/dist/src/pairing/pairing-code-service.d.ts +45 -0
  168. package/dist/src/pairing/pairing-code-service.js +183 -0
  169. package/dist/src/pairing/pairing-code-service.js.map +1 -0
  170. package/dist/src/projects/project-registry.d.ts +14 -0
  171. package/dist/src/projects/project-registry.js +60 -0
  172. package/dist/src/projects/project-registry.js.map +1 -0
  173. package/dist/src/push/push-sender.d.ts +21 -0
  174. package/dist/src/push/push-sender.js +96 -0
  175. package/dist/src/push/push-sender.js.map +1 -0
  176. package/dist/src/push/push-service.d.ts +122 -0
  177. package/dist/src/push/push-service.js +260 -0
  178. package/dist/src/push/push-service.js.map +1 -0
  179. package/dist/src/qr.d.ts +17 -0
  180. package/dist/src/qr.js +31 -0
  181. package/dist/src/qr.js.map +1 -0
  182. package/dist/src/secret-store.d.ts +23 -0
  183. package/dist/src/secret-store.js +27 -0
  184. package/dist/src/secret-store.js.map +1 -0
  185. package/dist/src/secure-device-state.d.ts +16 -0
  186. package/dist/src/secure-device-state.js +63 -0
  187. package/dist/src/secure-device-state.js.map +1 -0
  188. package/dist/src/service-installer.d.ts +57 -0
  189. package/dist/src/service-installer.js +254 -0
  190. package/dist/src/service-installer.js.map +1 -0
  191. package/dist/src/session-state.d.ts +14 -0
  192. package/dist/src/session-state.js +19 -0
  193. package/dist/src/session-state.js.map +1 -0
  194. package/dist/src/transport/crypto.d.ts +43 -0
  195. package/dist/src/transport/crypto.js +78 -0
  196. package/dist/src/transport/crypto.js.map +1 -0
  197. package/dist/src/transport/lan-server.d.ts +33 -0
  198. package/dist/src/transport/lan-server.js +105 -0
  199. package/dist/src/transport/lan-server.js.map +1 -0
  200. package/dist/src/transport/local-hosts.d.ts +17 -0
  201. package/dist/src/transport/local-hosts.js +30 -0
  202. package/dist/src/transport/local-hosts.js.map +1 -0
  203. package/dist/src/transport/mdns-advertiser.d.ts +83 -0
  204. package/dist/src/transport/mdns-advertiser.js +282 -0
  205. package/dist/src/transport/mdns-advertiser.js.map +1 -0
  206. package/dist/src/transport/message-io.d.ts +33 -0
  207. package/dist/src/transport/message-io.js +87 -0
  208. package/dist/src/transport/message-io.js.map +1 -0
  209. package/dist/src/transport/outbound-log.d.ts +24 -0
  210. package/dist/src/transport/outbound-log.js +78 -0
  211. package/dist/src/transport/outbound-log.js.map +1 -0
  212. package/dist/src/transport/relay-client.d.ts +19 -0
  213. package/dist/src/transport/relay-client.js +27 -0
  214. package/dist/src/transport/relay-client.js.map +1 -0
  215. package/dist/src/transport/secure-channel.d.ts +33 -0
  216. package/dist/src/transport/secure-channel.js +81 -0
  217. package/dist/src/transport/secure-channel.js.map +1 -0
  218. package/dist/src/transport/server-handshake.d.ts +49 -0
  219. package/dist/src/transport/server-handshake.js +137 -0
  220. package/dist/src/transport/server-handshake.js.map +1 -0
  221. package/dist/src/transport/session-handler.d.ts +19 -0
  222. package/dist/src/transport/session-handler.js +134 -0
  223. package/dist/src/transport/session-handler.js.map +1 -0
  224. package/dist/src/transport/session-registry.d.ts +58 -0
  225. package/dist/src/transport/session-registry.js +91 -0
  226. package/dist/src/transport/session-registry.js.map +1 -0
  227. package/dist/src/transport/trust-store.d.ts +23 -0
  228. package/dist/src/transport/trust-store.js +33 -0
  229. package/dist/src/transport/trust-store.js.map +1 -0
  230. package/dist/src/transport/ws-adapter.d.ts +7 -0
  231. package/dist/src/transport/ws-adapter.js +16 -0
  232. package/dist/src/transport/ws-adapter.js.map +1 -0
  233. package/dist/src/version.d.ts +1 -0
  234. package/dist/src/version.js +13 -0
  235. package/dist/src/version.js.map +1 -0
  236. package/dist/src/workspace/browse-service.d.ts +10 -0
  237. package/dist/src/workspace/browse-service.js +97 -0
  238. package/dist/src/workspace/browse-service.js.map +1 -0
  239. package/dist/src/workspace/checkpoint-service.d.ts +21 -0
  240. package/dist/src/workspace/checkpoint-service.js +219 -0
  241. package/dist/src/workspace/checkpoint-service.js.map +1 -0
  242. package/dist/src/workspace/path-guard.d.ts +7 -0
  243. package/dist/src/workspace/path-guard.js +51 -0
  244. package/dist/src/workspace/path-guard.js.map +1 -0
  245. package/dist/src/workspace/workspace-service.d.ts +8 -0
  246. package/dist/src/workspace/workspace-service.js +111 -0
  247. package/dist/src/workspace/workspace-service.js.map +1 -0
  248. package/package.json +46 -0
  249. package/scripts/extract-gemini-hook.mjs +16 -0
  250. package/scripts/fake-approval-bridge.mjs +23 -0
  251. package/scripts/install-service-linux.sh +38 -0
  252. package/scripts/install-service-macos.sh +38 -0
  253. package/scripts/install-service-windows.ps1 +26 -0
  254. package/scripts/test-gemini-hook-e2e.mjs +168 -0
  255. package/scripts/write-gemini-settings.mjs +31 -0
@@ -0,0 +1,14 @@
1
+ import { type AgentConfig, type Project } from '@uxnan/shared';
2
+ /** Stable id derived from the absolute path, so it survives restarts. */
3
+ export declare function projectIdFor(cwd: string): string;
4
+ export declare class ProjectRegistry {
5
+ #private;
6
+ constructor(roots: string[], fallbackCwd?: string, projectAgents?: AgentConfig[]);
7
+ list(): Project[];
8
+ /** Find a project by id or by its absolute cwd. Throws if unknown. */
9
+ byId(projectId: string): Project;
10
+ /** Resolve the project that owns `cwd` (exact root match), else synthesize one. */
11
+ resolve(cwd: string): Project;
12
+ /** The pinned agent/model config for the project at `cwd`, if any. */
13
+ agentConfigFor(cwd: string): AgentConfig | undefined;
14
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Resolves the project directories the phone may open. For the MVP the bridge
3
+ * exposes its configured `workspaceRoots` (or its own cwd when none are set) as
4
+ * the list of projects; each project carries the absolute `cwd` that git and
5
+ * agent turns run in.
6
+ *
7
+ * Source: architecture/02a-system-architecture.md §5.8.5 (project resolution).
8
+ */
9
+ import { createHash } from 'node:crypto';
10
+ import { basename, resolve } from 'node:path';
11
+ import { JsonRpcErrorCode, RpcError } from '@uxnan/shared';
12
+ /** Stable id derived from the absolute path, so it survives restarts. */
13
+ export function projectIdFor(cwd) {
14
+ return `proj_${createHash('sha1').update(resolve(cwd)).digest('hex').slice(0, 12)}`;
15
+ }
16
+ export class ProjectRegistry {
17
+ #roots;
18
+ /** Per-project agent/model pins, keyed by the resolved project `cwd`. */
19
+ #agentByCwd;
20
+ constructor(roots, fallbackCwd = process.cwd(), projectAgents = []) {
21
+ const resolved = roots.map((r) => resolve(r)).filter((r) => r.length > 0);
22
+ this.#roots = resolved.length > 0 ? resolved : [resolve(fallbackCwd)];
23
+ this.#agentByCwd = new Map(projectAgents
24
+ .filter((config) => typeof config.cwd === 'string' && config.cwd.length > 0)
25
+ .map((config) => [resolve(config.cwd), config]));
26
+ }
27
+ list() {
28
+ return this.#roots.map((cwd) => this.#toProject(cwd));
29
+ }
30
+ /** Find a project by id or by its absolute cwd. Throws if unknown. */
31
+ byId(projectId) {
32
+ const match = this.#roots.find((cwd) => projectIdFor(cwd) === projectId);
33
+ if (!match) {
34
+ throw new RpcError(JsonRpcErrorCode.ResourceNotFound, `unknown project: ${projectId}`);
35
+ }
36
+ return this.#toProject(match);
37
+ }
38
+ /** Resolve the project that owns `cwd` (exact root match), else synthesize one. */
39
+ resolve(cwd) {
40
+ const target = resolve(cwd);
41
+ const match = this.#roots.find((root) => root === target);
42
+ return this.#toProject(match ?? target);
43
+ }
44
+ /** The pinned agent/model config for the project at `cwd`, if any. */
45
+ agentConfigFor(cwd) {
46
+ return this.#agentByCwd.get(resolve(cwd));
47
+ }
48
+ #toProject(cwd) {
49
+ const resolved = resolve(cwd);
50
+ const pin = this.#agentByCwd.get(resolved);
51
+ return {
52
+ id: projectIdFor(resolved),
53
+ name: basename(resolved) || resolved,
54
+ cwd: resolved,
55
+ ...(pin?.agentId !== undefined ? { agentId: pin.agentId } : {}),
56
+ ...(pin?.model !== undefined ? { model: pin.model } : {}),
57
+ };
58
+ }
59
+ }
60
+ //# sourceMappingURL=project-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-registry.js","sourceRoot":"","sources":["../../../src/projects/project-registry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import type { PushPlatform } from '@uxnan/shared';
2
+ import type { Logger } from '../logger.js';
3
+ export interface PushPayload {
4
+ title: string;
5
+ body: string;
6
+ data?: Record<string, string>;
7
+ }
8
+ /** Delivers a single notification to one device token. */
9
+ export interface PushSender {
10
+ send(token: string, platform: PushPlatform, payload: PushPayload): Promise<void>;
11
+ }
12
+ /** Documented default location for the Firebase service account (bridge/FOR-HUMAN.md). */
13
+ export declare function defaultServiceAccountPath(): string;
14
+ /**
15
+ * Build the bridge's direct FCM sender. Returns a {@link PushSender} when a
16
+ * Firebase service account is present and `firebase-admin` loads; returns `null`
17
+ * (no direct path) when the credential is missing or init fails — the caller then
18
+ * falls back to the relay. Kept async + dynamic so the bridge never hard-depends
19
+ * on `firebase-admin`.
20
+ */
21
+ export declare function createBridgePushSender(logger: Logger): Promise<PushSender | null>;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Bridge-side direct push delivery (FOR-DEV → *Direct FCM from the bridge*).
3
+ *
4
+ * Background push is sent **by the bridge itself** so it works on ANY transport —
5
+ * direct LAN, Tailscale, or relay — not only when a hosted relay is in the loop.
6
+ * Delivery goes through a {@link PushSender} seam so {@link PushService} can be
7
+ * unit-tested with a fake sender (no Firebase credentials required).
8
+ *
9
+ * The real FCM sender is loaded lazily and only when a Firebase service account is
10
+ * available (`UXNAN_FCM_SERVICE_ACCOUNT`, falling back to the documented
11
+ * `~/.uxnan/firebase-service-account.json`); without it the factory returns
12
+ * `null` and the bridge degrades to the relay fallback — or, with neither, a
13
+ * silent no-op (foreground local notifications still work, relay-free).
14
+ *
15
+ * Same trust model as the relay owning the credential today: a local, gitignored
16
+ * JSON the user provides (see bridge/FOR-HUMAN.md). Push payloads stay minimal —
17
+ * title + short body + thread/turn ids — no conversation plaintext beyond the
18
+ * already-truncated turn summary the relay path also carries.
19
+ */
20
+ import { homedir } from 'node:os';
21
+ import { join } from 'node:path';
22
+ import { readFile } from 'node:fs/promises';
23
+ /** Documented default location for the Firebase service account (bridge/FOR-HUMAN.md). */
24
+ export function defaultServiceAccountPath() {
25
+ return join(homedir(), '.uxnan', 'firebase-service-account.json');
26
+ }
27
+ /**
28
+ * Resolve the Firebase service-account path: the explicit `UXNAN_FCM_SERVICE_ACCOUNT`
29
+ * env var first, then the documented `~/.uxnan/firebase-service-account.json`. The
30
+ * default keeps the bridge plug-and-play — drop the JSON in place and push works
31
+ * without setting an env var.
32
+ */
33
+ function resolveServiceAccountPath() {
34
+ const fromEnv = process.env['UXNAN_FCM_SERVICE_ACCOUNT'];
35
+ return fromEnv && fromEnv.trim() ? fromEnv.trim() : defaultServiceAccountPath();
36
+ }
37
+ /**
38
+ * Build the bridge's direct FCM sender. Returns a {@link PushSender} when a
39
+ * Firebase service account is present and `firebase-admin` loads; returns `null`
40
+ * (no direct path) when the credential is missing or init fails — the caller then
41
+ * falls back to the relay. Kept async + dynamic so the bridge never hard-depends
42
+ * on `firebase-admin`.
43
+ */
44
+ export async function createBridgePushSender(logger) {
45
+ const serviceAccountPath = resolveServiceAccountPath();
46
+ let credentialRaw;
47
+ try {
48
+ credentialRaw = await readFile(serviceAccountPath, 'utf-8');
49
+ }
50
+ catch {
51
+ logger.info(`push: no Firebase service account at ${serviceAccountPath} — direct FCM disabled (relay fallback only)`);
52
+ return null;
53
+ }
54
+ try {
55
+ const sender = await loadFcmSender(credentialRaw, logger);
56
+ logger.info('push: direct FCM sender ready');
57
+ return sender;
58
+ }
59
+ catch (err) {
60
+ logger.warn(`push: failed to init direct FCM (${errorMessage(err)}) — falling back to relay/noop`);
61
+ return null;
62
+ }
63
+ }
64
+ async function loadFcmSender(credentialRaw, logger) {
65
+ // Dynamic import via a non-literal specifier so the optional `firebase-admin`
66
+ // dependency is not statically resolved at build time (it may not be installed).
67
+ // FOR-HUMAN: install `firebase-admin` + provide the service account.
68
+ const moduleName = 'firebase-admin';
69
+ // firebase-admin is CommonJS: under ESM dynamic import its API lands on the
70
+ // `.default` interop key, so reach through it (falling back to the namespace
71
+ // should a bundler ever hoist the named exports). Without this the admin object
72
+ // is undefined and FCM init silently degrades.
73
+ const imported = (await import(moduleName));
74
+ const admin = imported.default ?? imported;
75
+ const credential = JSON.parse(credentialRaw);
76
+ // Named app so this never collides with any other firebase-admin init in-process.
77
+ const app = admin.initializeApp({ credential: admin.credential.cert(credential) }, 'uxnan-bridge');
78
+ const messaging = admin.messaging(app);
79
+ return {
80
+ async send(token, platform, payload) {
81
+ await messaging.send({
82
+ token,
83
+ notification: { title: payload.title, body: payload.body },
84
+ ...(payload.data ? { data: payload.data } : {}),
85
+ // High priority so the phone wakes promptly while backgrounded.
86
+ android: { priority: 'high' },
87
+ apns: { headers: { 'apns-priority': '10' } },
88
+ });
89
+ logger.info(`push: delivered "${payload.title}" via direct FCM (${platform})`);
90
+ },
91
+ };
92
+ }
93
+ function errorMessage(err) {
94
+ return err instanceof Error ? err.message : String(err);
95
+ }
96
+ //# sourceMappingURL=push-sender.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"push-sender.js","sourceRoot":"","sources":["../../../src/push/push-sender.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAe5C,0FAA0F;AAC1F,MAAM,UAAU,yBAAyB;IACvC,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,+BAA+B,CAAC,CAAC;AACpE,CAAC;AAED;;;;;GAKG;AACH,SAAS,yBAAyB;IAChC,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC;IACzD,OAAO,OAAO,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,yBAAyB,EAAE,CAAC;AAClF,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,MAAc;IACzD,MAAM,kBAAkB,GAAG,yBAAyB,EAAE,CAAC;IACvD,IAAI,aAAqB,CAAC;IAC1B,IAAI,CAAC;QACH,aAAa,GAAG,MAAM,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC,CAAC;IAC9D,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,CAAC,IAAI,CACT,wCAAwC,kBAAkB,8CAA8C,CACzG,CAAC;QACF,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;QAC1D,MAAM,CAAC,IAAI,CAAC,+BAA+B,CAAC,CAAC;QAC7C,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,IAAI,CACT,oCAAoC,YAAY,CAAC,GAAG,CAAC,gCAAgC,CACtF,CAAC;QACF,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,aAAqB,EAAE,MAAc;IAChE,8EAA8E;IAC9E,iFAAiF;IACjF,qEAAqE;IACrE,MAAM,UAAU,GAAG,gBAAgB,CAAC;IACpC,4EAA4E;IAC5E,6EAA6E;IAC7E,gFAAgF;IAChF,+CAA+C;IAC/C,MAAM,QAAQ,GAAG,CAAC,MAAM,MAAM,CAAC,UAAU,CAAC,CAErB,CAAC;IACtB,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,QAAQ,CAAC;IAC3C,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,aAAa,CAAW,CAAC;IACvD,kFAAkF;IAClF,MAAM,GAAG,GAAG,KAAK,CAAC,aAAa,CAC7B,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,EACjD,cAAc,CACf,CAAC;IACF,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IACvC,OAAO;QACL,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO;YACjC,MAAM,SAAS,CAAC,IAAI,CAAC;gBACnB,KAAK;gBACL,YAAY,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE;gBAC1D,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC/C,gEAAgE;gBAChE,OAAO,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE;gBAC7B,IAAI,EAAE,EAAE,OAAO,EAAE,EAAE,eAAe,EAAE,IAAI,EAAE,EAAE;aAC7C,CAAC,CAAC;YACH,MAAM,CAAC,IAAI,CAAC,oBAAoB,OAAO,CAAC,KAAK,qBAAqB,QAAQ,GAAG,CAAC,CAAC;QACjF,CAAC;KACF,CAAC;AACJ,CAAC;AAiBD,SAAS,YAAY,CAAC,GAAY;IAChC,OAAO,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;AAC1D,CAAC"}
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Bridge-side push coordination (architecture/02a §5.10.2; FOR-DEV → *Direct FCM
3
+ * from the bridge*).
4
+ *
5
+ * The phone registers its FCM/APNs token over the live session
6
+ * (`notifications/register`). The bridge keeps the real token and, when a turn
7
+ * ends with push enabled, delivers a background notification via two paths, in
8
+ * priority order:
9
+ *
10
+ * 1. **Direct FCM (PRIMARY)** — when a Firebase service account is present the
11
+ * bridge sends straight to FCM via {@link PushSender}. Works on ANY transport
12
+ * (direct LAN, Tailscale, or relay) — no hosted relay required.
13
+ * 2. **Relay fallback** — with no local credential (or the relay explicitly
14
+ * enabled), the bridge forwards the token to the relay (`POST /push/register`),
15
+ * keeps the returned `notificationSecret`, and asks the relay to deliver
16
+ * (`POST /push/notify`). For setups that keep the credential on a hosted relay.
17
+ *
18
+ * Everything here is GATED: with neither a direct FCM sender nor a reachable relay,
19
+ * background push is a silent no-op (foreground local notifications still work).
20
+ * Without a registered token the bridge simply skips pushing. Direct delivery needs
21
+ * the user's Firebase service account (bridge/FOR-HUMAN.md); the relay path needs it
22
+ * on the relay (relay/FOR-HUMAN.md) — plus a real device to validate either.
23
+ *
24
+ * Persistence: registrations are keyed by `sessionId` and persisted to
25
+ * `~/.uxnan/push-state.json` (atomic write), so background push survives a
26
+ * bridge restart WITHOUT waiting for the phone to reconnect and re-register. The
27
+ * persisted entry carries the device token + platform (for the direct path) and,
28
+ * when used, the relay `notificationSecret` (for the fallback). Multiple
29
+ * registrations are kept, so several paired phones each receive background push;
30
+ * a turn-end pushes to all of them.
31
+ *
32
+ * Note: `register`/`updatePreferences`/`unregister` act on the *active* session
33
+ * (the one whose request is being served). With the MVP default
34
+ * `maxConcurrentSessions: 1` this is exact; with several concurrent sessions the
35
+ * "active" one is the most recently established — per-request session identity
36
+ * would be needed to disambiguate (FOR-DEV).
37
+ */
38
+ import type { NotificationPreferences, PushPlatform, RegisterNotificationsResult } from '@uxnan/shared';
39
+ import type { DaemonConfig } from '../daemon-config.js';
40
+ import { type DaemonState } from '../daemon-state.js';
41
+ import type { Logger } from '../logger.js';
42
+ import type { PushSender } from './push-sender.js';
43
+ export interface TurnEndInfo {
44
+ threadId: string;
45
+ turnId: string;
46
+ status: 'completed' | 'error';
47
+ /** Assistant text (completed) or error message, used to build the body. */
48
+ text?: string;
49
+ }
50
+ type FetchFn = (url: string, init: {
51
+ method: string;
52
+ headers: Record<string, string>;
53
+ body: string;
54
+ }) => Promise<{
55
+ ok: boolean;
56
+ status: number;
57
+ json(): Promise<unknown>;
58
+ }>;
59
+ /** Parameters for {@link PushService.register} — identifies the requesting phone. */
60
+ export interface RegisterPushParams {
61
+ /** Relay session id of the phone making the request (its registration key). */
62
+ sessionId: string;
63
+ /** Trusted-device id of that phone, when known (enables prune-on-untrust). */
64
+ deviceId?: string;
65
+ pushToken: string;
66
+ platform: PushPlatform;
67
+ preferences?: NotificationPreferences;
68
+ }
69
+ export interface PushServiceOptions {
70
+ relayUrl: string;
71
+ config: DaemonConfig;
72
+ logger: Logger;
73
+ fetchFn?: FetchFn;
74
+ /** Daemon state for persisting registrations; omitted in unit tests (no-op). */
75
+ state?: DaemonState;
76
+ /**
77
+ * Direct FCM sender (PRIMARY push path). Present when a Firebase service account
78
+ * is configured (see {@link createBridgePushSender}); `undefined` → the bridge
79
+ * uses the relay fallback only. Injected by tests with a fake sender.
80
+ */
81
+ pushSender?: PushSender;
82
+ }
83
+ export declare class PushService {
84
+ #private;
85
+ constructor(options: PushServiceOptions);
86
+ /** True when the bridge can deliver push directly via FCM (credential present). */
87
+ get directPushAvailable(): boolean;
88
+ /**
89
+ * Load persisted registrations from `push-state.json`. Call once at startup so
90
+ * background push keeps working across a bridge restart. Best-effort: a missing
91
+ * or malformed file leaves the service empty.
92
+ */
93
+ load(): Promise<void>;
94
+ /** Called when a phone session is established. */
95
+ setActiveSession(sessionId: string): void;
96
+ /** Called when a session closes; the registration persists for background push. */
97
+ clearActiveSession(sessionId: string): void;
98
+ get activeSessionId(): string | undefined;
99
+ /**
100
+ * Handle `notifications/register` for a SPECIFIC phone session. Always stores the
101
+ * real device token locally (the direct FCM path needs it); additionally registers
102
+ * with the relay when the relay is enabled OR there is no direct sender, keeping
103
+ * the returned secret for the fallback path. Keyed by `sessionId`, so several
104
+ * concurrent phones each get their own registration. `registered` is true when at
105
+ * least one delivery path exists.
106
+ */
107
+ register(params: RegisterPushParams): Promise<RegisterNotificationsResult>;
108
+ /** Update a specific session's notification preferences. */
109
+ updatePreferences(sessionId: string, preferences: NotificationPreferences): void;
110
+ /** Drop a specific session's registration (its phone asked to stop pushes). */
111
+ unregister(sessionId: string): void;
112
+ /**
113
+ * Drop every registration owned by a trusted device — called when the device is
114
+ * removed via `bridge/removeTrustedDevice`, so a revoked phone stops receiving
115
+ * background push instead of lingering until it re-registers or is overwritten.
116
+ * Returns the number of registrations removed.
117
+ */
118
+ unregisterDevice(deviceId: string): number;
119
+ /** Fire-and-forget: push a turn-ended notification if enabled and registered. */
120
+ onTurnEnd(info: TurnEndInfo): void;
121
+ }
122
+ export {};
@@ -0,0 +1,260 @@
1
+ import { DAEMON_FILES } from '../daemon-state.js';
2
+ const DEFAULT_PREFERENCES = { turnCompleted: true, turnError: true };
3
+ export class PushService {
4
+ #httpBase;
5
+ #config;
6
+ #logger;
7
+ #fetch;
8
+ #state;
9
+ #pushSender;
10
+ #activeSessionId;
11
+ /** Registrations keyed by relay `sessionId` (one per paired phone). */
12
+ #registrations = new Map();
13
+ constructor(options) {
14
+ this.#httpBase = toHttpBase(options.relayUrl);
15
+ this.#config = options.config;
16
+ this.#logger = options.logger;
17
+ this.#fetch = options.fetchFn ?? globalThis.fetch;
18
+ this.#state = options.state;
19
+ this.#pushSender = options.pushSender;
20
+ }
21
+ /** True when the bridge can deliver push directly via FCM (credential present). */
22
+ get directPushAvailable() {
23
+ return this.#pushSender !== undefined;
24
+ }
25
+ /**
26
+ * Load persisted registrations from `push-state.json`. Call once at startup so
27
+ * background push keeps working across a bridge restart. Best-effort: a missing
28
+ * or malformed file leaves the service empty.
29
+ */
30
+ async load() {
31
+ if (!this.#state)
32
+ return;
33
+ try {
34
+ const persisted = await this.#state.readJson(DAEMON_FILES.pushState);
35
+ const registrations = persisted?.registrations;
36
+ if (!Array.isArray(registrations))
37
+ return;
38
+ for (const reg of registrations) {
39
+ if (isRegistration(reg))
40
+ this.#registrations.set(reg.sessionId, reg);
41
+ }
42
+ if (this.#registrations.size > 0) {
43
+ this.#logger.info(`loaded ${this.#registrations.size} push registration(s)`);
44
+ }
45
+ }
46
+ catch (err) {
47
+ this.#logger.warn(`push-state load failed: ${errorMessage(err)}`);
48
+ }
49
+ }
50
+ /** Called when a phone session is established. */
51
+ setActiveSession(sessionId) {
52
+ this.#activeSessionId = sessionId;
53
+ }
54
+ /** Called when a session closes; the registration persists for background push. */
55
+ clearActiveSession(sessionId) {
56
+ if (this.#activeSessionId === sessionId)
57
+ this.#activeSessionId = undefined;
58
+ }
59
+ get activeSessionId() {
60
+ return this.#activeSessionId;
61
+ }
62
+ /**
63
+ * Handle `notifications/register` for a SPECIFIC phone session. Always stores the
64
+ * real device token locally (the direct FCM path needs it); additionally registers
65
+ * with the relay when the relay is enabled OR there is no direct sender, keeping
66
+ * the returned secret for the fallback path. Keyed by `sessionId`, so several
67
+ * concurrent phones each get their own registration. `registered` is true when at
68
+ * least one delivery path exists.
69
+ */
70
+ async register(params) {
71
+ const { sessionId, deviceId, pushToken, platform, preferences } = params;
72
+ const reg = {
73
+ sessionId,
74
+ ...(deviceId !== undefined ? { deviceId } : {}),
75
+ pushToken,
76
+ platform,
77
+ preferences: preferences ?? DEFAULT_PREFERENCES,
78
+ };
79
+ // Register with the relay only when it's the wanted/only path: the user enabled
80
+ // it, or there is no direct FCM sender to deliver. Best-effort — a relay that is
81
+ // down does not fail registration when direct FCM can still deliver.
82
+ if (this.#config.relayEnabled || !this.#pushSender) {
83
+ const secret = await this.#registerWithRelay(sessionId, pushToken, platform);
84
+ if (secret)
85
+ reg.notificationSecret = secret;
86
+ }
87
+ this.#registrations.set(sessionId, reg);
88
+ await this.#persist();
89
+ const direct = this.#pushSender !== undefined;
90
+ const viaRelay = reg.notificationSecret !== undefined;
91
+ if (direct || viaRelay) {
92
+ this.#logger.info(`push token registered (${direct ? 'direct FCM' : 'relay'})`);
93
+ return { registered: true };
94
+ }
95
+ this.#logger.warn('push token stored but no delivery path (no FCM creds, relay unavailable)');
96
+ return { registered: false };
97
+ }
98
+ /** Forward a token to the relay; returns the notify secret, or undefined on failure. */
99
+ async #registerWithRelay(sessionId, pushToken, platform) {
100
+ try {
101
+ const res = await this.#fetch(`${this.#httpBase}/push/register`, {
102
+ method: 'POST',
103
+ headers: { 'content-type': 'application/json' },
104
+ body: JSON.stringify({ sessionId, pushToken, platform }),
105
+ });
106
+ if (!res.ok) {
107
+ this.#logger.warn(`push register rejected by relay (${res.status})`);
108
+ return undefined;
109
+ }
110
+ const data = (await res.json());
111
+ return data.notificationSecret ?? undefined;
112
+ }
113
+ catch (err) {
114
+ this.#logger.warn(`push relay register failed: ${errorMessage(err)}`);
115
+ return undefined;
116
+ }
117
+ }
118
+ /** Update a specific session's notification preferences. */
119
+ updatePreferences(sessionId, preferences) {
120
+ const reg = this.#registrations.get(sessionId);
121
+ if (!reg)
122
+ return;
123
+ reg.preferences = preferences;
124
+ void this.#persist();
125
+ }
126
+ /** Drop a specific session's registration (its phone asked to stop pushes). */
127
+ unregister(sessionId) {
128
+ if (this.#registrations.delete(sessionId))
129
+ void this.#persist();
130
+ }
131
+ /**
132
+ * Drop every registration owned by a trusted device — called when the device is
133
+ * removed via `bridge/removeTrustedDevice`, so a revoked phone stops receiving
134
+ * background push instead of lingering until it re-registers or is overwritten.
135
+ * Returns the number of registrations removed.
136
+ */
137
+ unregisterDevice(deviceId) {
138
+ let removed = 0;
139
+ for (const [sessionId, reg] of this.#registrations) {
140
+ if (reg.deviceId === deviceId) {
141
+ this.#registrations.delete(sessionId);
142
+ removed += 1;
143
+ }
144
+ }
145
+ if (removed > 0) {
146
+ void this.#persist();
147
+ this.#logger.info(`pruned ${removed} push registration(s) for removed device`);
148
+ }
149
+ return removed;
150
+ }
151
+ /** Fire-and-forget: push a turn-ended notification if enabled and registered. */
152
+ onTurnEnd(info) {
153
+ void this.#maybePush(info).catch((err) => this.#logger.warn(`push notify failed: ${errorMessage(err)}`));
154
+ }
155
+ async #maybePush(info) {
156
+ if (!this.#config.pushEnabled)
157
+ return;
158
+ if (this.#registrations.size === 0)
159
+ return;
160
+ const { title, body } = buildNotification(info);
161
+ // Notify every registered phone whose preferences opt into this event.
162
+ await Promise.all([...this.#registrations.values()]
163
+ .filter((reg) => this.#wantsPush(info.status, reg.preferences))
164
+ .map((reg) => this.#notifyOne(reg, info, title, body)));
165
+ }
166
+ #wantsPush(status, prefs) {
167
+ if (status === 'completed')
168
+ return this.#config.pushOnAgentDone && prefs.turnCompleted;
169
+ return this.#config.pushOnAgentError && prefs.turnError;
170
+ }
171
+ async #notifyOne(reg, info, title, body) {
172
+ const data = { threadId: info.threadId, turnId: info.turnId };
173
+ // PRIMARY: deliver straight to FCM when a sender + token are available. Works
174
+ // on any transport; on failure we log rather than retry via the relay (the
175
+ // direct path has no dedupe, so a fallback could double-deliver).
176
+ if (this.#pushSender && reg.pushToken && reg.platform) {
177
+ try {
178
+ await this.#pushSender.send(reg.pushToken, reg.platform, { title, body, data });
179
+ }
180
+ catch (err) {
181
+ this.#logger.warn(`direct push delivery failed: ${errorMessage(err)}`);
182
+ }
183
+ return;
184
+ }
185
+ // FALLBACK: ask the relay to deliver (it holds the token + dedupes by turn).
186
+ if (reg.notificationSecret) {
187
+ const res = await this.#fetch(`${this.#httpBase}/push/notify`, {
188
+ method: 'POST',
189
+ headers: { 'content-type': 'application/json' },
190
+ body: JSON.stringify({
191
+ sessionId: reg.sessionId,
192
+ notificationSecret: reg.notificationSecret,
193
+ ...data,
194
+ title,
195
+ body,
196
+ }),
197
+ });
198
+ if (!res.ok)
199
+ this.#logger.warn(`push notify rejected by relay (${res.status})`);
200
+ return;
201
+ }
202
+ this.#logger.warn(`push skipped for ${reg.sessionId}: no delivery path`);
203
+ }
204
+ /** Atomically persist the current registrations (best-effort). */
205
+ async #persist() {
206
+ if (!this.#state)
207
+ return;
208
+ try {
209
+ const state = {
210
+ version: 1,
211
+ registrations: [...this.#registrations.values()],
212
+ };
213
+ await this.#state.writeJson(DAEMON_FILES.pushState, state);
214
+ }
215
+ catch (err) {
216
+ this.#logger.warn(`push-state persist failed: ${errorMessage(err)}`);
217
+ }
218
+ }
219
+ }
220
+ function isRegistration(value) {
221
+ if (!value || typeof value !== 'object')
222
+ return false;
223
+ const reg = value;
224
+ // A usable registration needs at least one delivery path: a device token (direct
225
+ // FCM) or a relay secret (fallback). Older persisted entries had only the secret.
226
+ const hasPath = typeof reg['pushToken'] === 'string' || typeof reg['notificationSecret'] === 'string';
227
+ return (typeof reg['sessionId'] === 'string' &&
228
+ hasPath &&
229
+ typeof reg['preferences'] === 'object' &&
230
+ reg['preferences'] !== null);
231
+ }
232
+ function buildNotification(info) {
233
+ if (info.status === 'error') {
234
+ return { title: 'Turn failed', body: truncate(info.text) ?? 'The agent reported an error.' };
235
+ }
236
+ return { title: 'Turn completed', body: truncate(info.text) ?? 'Your agent finished a turn.' };
237
+ }
238
+ function truncate(text, max = 120) {
239
+ if (!text)
240
+ return undefined;
241
+ const trimmed = text.trim();
242
+ if (!trimmed)
243
+ return undefined;
244
+ return trimmed.length > max ? `${trimmed.slice(0, max - 1)}…` : trimmed;
245
+ }
246
+ /** Convert a relay ws(s):// URL into its http(s):// origin for the REST endpoints. */
247
+ function toHttpBase(relayUrl) {
248
+ try {
249
+ const url = new URL(relayUrl);
250
+ const protocol = url.protocol === 'wss:' ? 'https:' : url.protocol === 'ws:' ? 'http:' : url.protocol;
251
+ return `${protocol}//${url.host}`;
252
+ }
253
+ catch {
254
+ return relayUrl.replace(/^ws/, 'http').replace(/\/$/, '');
255
+ }
256
+ }
257
+ function errorMessage(err) {
258
+ return err instanceof Error ? err.message : String(err);
259
+ }
260
+ //# sourceMappingURL=push-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"push-service.js","sourceRoot":"","sources":["../../../src/push/push-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import { type PairingPayload } from '@uxnan/shared';
2
+ export interface GeneratePairingOptions {
3
+ /** Relay URL (remote fallback). Omit for a LAN/Tailscale-only QR. */
4
+ relayUrl?: string;
5
+ /** Direct `host:port` addresses the phone should try first (LAN/Tailscale). */
6
+ hosts?: string[];
7
+ macDeviceId: string;
8
+ macIdentityPublicKey: string;
9
+ displayName: string;
10
+ /** Current time in epoch ms (injected for testability). */
11
+ now: number;
12
+ /** Optional explicit session id; a random UUID is used otherwise. */
13
+ sessionId?: string;
14
+ }
15
+ export declare function generatePairingPayload(options: GeneratePairingOptions): PairingPayload;
16
+ /** Render a pairing payload as an ASCII QR code (for terminal display). */
17
+ export declare function renderPairingQr(payload: PairingPayload): Promise<string>;
package/dist/src/qr.js ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Pairing QR generation.
3
+ *
4
+ * Source: architecture/02a-system-architecture.md §5.8.2 (qr) and §5.9.1 (Phase 1).
5
+ */
6
+ import { randomUUID } from 'node:crypto';
7
+ import qrcode from 'qrcode-terminal';
8
+ import { PAIRING_QR_VERSION, defaultPairingExpiry, encodePairingQr, } from '@uxnan/shared';
9
+ export function generatePairingPayload(options) {
10
+ const payload = {
11
+ v: PAIRING_QR_VERSION,
12
+ sessionId: options.sessionId ?? randomUUID(),
13
+ macDeviceId: options.macDeviceId,
14
+ macIdentityPublicKey: options.macIdentityPublicKey,
15
+ expiresAt: defaultPairingExpiry(options.now),
16
+ displayName: options.displayName,
17
+ };
18
+ if (options.relayUrl)
19
+ payload.relay = options.relayUrl;
20
+ if (options.hosts && options.hosts.length > 0)
21
+ payload.hosts = options.hosts;
22
+ return payload;
23
+ }
24
+ /** Render a pairing payload as an ASCII QR code (for terminal display). */
25
+ export function renderPairingQr(payload) {
26
+ const data = encodePairingQr(payload);
27
+ return new Promise((resolve) => {
28
+ qrcode.generate(data, { small: true }, (output) => resolve(output));
29
+ });
30
+ }
31
+ //# sourceMappingURL=qr.js.map