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.
- package/README.md +150 -0
- package/dist/src/account-status.d.ts +13 -0
- package/dist/src/account-status.js +78 -0
- package/dist/src/account-status.js.map +1 -0
- package/dist/src/adapters/base-adapter.d.ts +18 -0
- package/dist/src/adapters/base-adapter.js +15 -0
- package/dist/src/adapters/base-adapter.js.map +1 -0
- package/dist/src/adapters/claude-adapter.d.ts +102 -0
- package/dist/src/adapters/claude-adapter.js +486 -0
- package/dist/src/adapters/claude-adapter.js.map +1 -0
- package/dist/src/adapters/claude-tools.d.ts +25 -0
- package/dist/src/adapters/claude-tools.js +146 -0
- package/dist/src/adapters/claude-tools.js.map +1 -0
- package/dist/src/adapters/codex-adapter.d.ts +116 -0
- package/dist/src/adapters/codex-adapter.js +912 -0
- package/dist/src/adapters/codex-adapter.js.map +1 -0
- package/dist/src/adapters/codex-app-server.d.ts +74 -0
- package/dist/src/adapters/codex-app-server.js +225 -0
- package/dist/src/adapters/codex-app-server.js.map +1 -0
- package/dist/src/adapters/codex-approval.d.ts +88 -0
- package/dist/src/adapters/codex-approval.js +160 -0
- package/dist/src/adapters/codex-approval.js.map +1 -0
- package/dist/src/adapters/codex-tools.d.ts +18 -0
- package/dist/src/adapters/codex-tools.js +106 -0
- package/dist/src/adapters/codex-tools.js.map +1 -0
- package/dist/src/adapters/content-blocks.d.ts +68 -0
- package/dist/src/adapters/content-blocks.js +205 -0
- package/dist/src/adapters/content-blocks.js.map +1 -0
- package/dist/src/adapters/echo-agent-adapter.d.ts +23 -0
- package/dist/src/adapters/echo-agent-adapter.js +72 -0
- package/dist/src/adapters/echo-agent-adapter.js.map +1 -0
- package/dist/src/adapters/gemini-adapter.d.ts +87 -0
- package/dist/src/adapters/gemini-adapter.js +594 -0
- package/dist/src/adapters/gemini-adapter.js.map +1 -0
- package/dist/src/adapters/gemini-tools.d.ts +4 -0
- package/dist/src/adapters/gemini-tools.js +48 -0
- package/dist/src/adapters/gemini-tools.js.map +1 -0
- package/dist/src/adapters/opencode-adapter.d.ts +74 -0
- package/dist/src/adapters/opencode-adapter.js +418 -0
- package/dist/src/adapters/opencode-adapter.js.map +1 -0
- package/dist/src/adapters/opencode-tools.d.ts +2 -0
- package/dist/src/adapters/opencode-tools.js +41 -0
- package/dist/src/adapters/opencode-tools.js.map +1 -0
- package/dist/src/adapters/pi-adapter.d.ts +92 -0
- package/dist/src/adapters/pi-adapter.js +467 -0
- package/dist/src/adapters/pi-adapter.js.map +1 -0
- package/dist/src/adapters/pi-tools.d.ts +10 -0
- package/dist/src/adapters/pi-tools.js +72 -0
- package/dist/src/adapters/pi-tools.js.map +1 -0
- package/dist/src/adapters/process-agent-adapter.d.ts +24 -0
- package/dist/src/adapters/process-agent-adapter.js +111 -0
- package/dist/src/adapters/process-agent-adapter.js.map +1 -0
- package/dist/src/adapters/resolve-claude.d.ts +13 -0
- package/dist/src/adapters/resolve-claude.js +57 -0
- package/dist/src/adapters/resolve-claude.js.map +1 -0
- package/dist/src/adapters/resolve-codex.d.ts +13 -0
- package/dist/src/adapters/resolve-codex.js +48 -0
- package/dist/src/adapters/resolve-codex.js.map +1 -0
- package/dist/src/adapters/resolve-gemini.d.ts +13 -0
- package/dist/src/adapters/resolve-gemini.js +47 -0
- package/dist/src/adapters/resolve-gemini.js.map +1 -0
- package/dist/src/adapters/resolve-opencode.d.ts +11 -0
- package/dist/src/adapters/resolve-opencode.js +49 -0
- package/dist/src/adapters/resolve-opencode.js.map +1 -0
- package/dist/src/adapters/resolve-pi.d.ts +13 -0
- package/dist/src/adapters/resolve-pi.js +46 -0
- package/dist/src/adapters/resolve-pi.js.map +1 -0
- package/dist/src/adapters/run-options.d.ts +22 -0
- package/dist/src/adapters/run-options.js +48 -0
- package/dist/src/adapters/run-options.js.map +1 -0
- package/dist/src/adapters/spawn.d.ts +20 -0
- package/dist/src/adapters/spawn.js +16 -0
- package/dist/src/adapters/spawn.js.map +1 -0
- package/dist/src/agents/agent-manager.d.ts +98 -0
- package/dist/src/agents/agent-manager.js +433 -0
- package/dist/src/agents/agent-manager.js.map +1 -0
- package/dist/src/agents/attachments.d.ts +28 -0
- package/dist/src/agents/attachments.js +121 -0
- package/dist/src/agents/attachments.js.map +1 -0
- package/dist/src/bridge-context.d.ts +45 -0
- package/dist/src/bridge-context.js +2 -0
- package/dist/src/bridge-context.js.map +1 -0
- package/dist/src/bridge-status.d.ts +12 -0
- package/dist/src/bridge-status.js +17 -0
- package/dist/src/bridge-status.js.map +1 -0
- package/dist/src/bridge.d.ts +37 -0
- package/dist/src/bridge.js +446 -0
- package/dist/src/bridge.js.map +1 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +194 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/conversation/session-history.d.ts +27 -0
- package/dist/src/conversation/session-history.js +1082 -0
- package/dist/src/conversation/session-history.js.map +1 -0
- package/dist/src/conversation/thread-store.d.ts +74 -0
- package/dist/src/conversation/thread-store.js +366 -0
- package/dist/src/conversation/thread-store.js.map +1 -0
- package/dist/src/daemon-config.d.ts +123 -0
- package/dist/src/daemon-config.js +64 -0
- package/dist/src/daemon-config.js.map +1 -0
- package/dist/src/daemon-state.d.ts +27 -0
- package/dist/src/daemon-state.js +76 -0
- package/dist/src/daemon-state.js.map +1 -0
- package/dist/src/git/git-runner.d.ts +24 -0
- package/dist/src/git/git-runner.js +63 -0
- package/dist/src/git/git-runner.js.map +1 -0
- package/dist/src/git/git-service.d.ts +76 -0
- package/dist/src/git/git-service.js +435 -0
- package/dist/src/git/git-service.js.map +1 -0
- package/dist/src/handler-router.d.ts +34 -0
- package/dist/src/handler-router.js +67 -0
- package/dist/src/handler-router.js.map +1 -0
- package/dist/src/handlers/account-handler.d.ts +4 -0
- package/dist/src/handlers/account-handler.js +27 -0
- package/dist/src/handlers/account-handler.js.map +1 -0
- package/dist/src/handlers/agent-handler.d.ts +2 -0
- package/dist/src/handlers/agent-handler.js +8 -0
- package/dist/src/handlers/agent-handler.js.map +1 -0
- package/dist/src/handlers/bridge-control-handler.d.ts +2 -0
- package/dist/src/handlers/bridge-control-handler.js +64 -0
- package/dist/src/handlers/bridge-control-handler.js.map +1 -0
- package/dist/src/handlers/desktop-handler.d.ts +12 -0
- package/dist/src/handlers/desktop-handler.js +5 -0
- package/dist/src/handlers/desktop-handler.js.map +1 -0
- package/dist/src/handlers/git-handler.d.ts +2 -0
- package/dist/src/handlers/git-handler.js +82 -0
- package/dist/src/handlers/git-handler.js.map +1 -0
- package/dist/src/handlers/index.d.ts +8 -0
- package/dist/src/handlers/index.js +22 -0
- package/dist/src/handlers/index.js.map +1 -0
- package/dist/src/handlers/not-implemented.d.ts +10 -0
- package/dist/src/handlers/not-implemented.js +21 -0
- package/dist/src/handlers/not-implemented.js.map +1 -0
- package/dist/src/handlers/notifications-handler.d.ts +2 -0
- package/dist/src/handlers/notifications-handler.js +62 -0
- package/dist/src/handlers/notifications-handler.js.map +1 -0
- package/dist/src/handlers/params.d.ts +11 -0
- package/dist/src/handlers/params.js +72 -0
- package/dist/src/handlers/params.js.map +1 -0
- package/dist/src/handlers/project-handler.d.ts +2 -0
- package/dist/src/handlers/project-handler.js +6 -0
- package/dist/src/handlers/project-handler.js.map +1 -0
- package/dist/src/handlers/thread-context-handler.d.ts +2 -0
- package/dist/src/handlers/thread-context-handler.js +211 -0
- package/dist/src/handlers/thread-context-handler.js.map +1 -0
- package/dist/src/handlers/workspace-handler.d.ts +2 -0
- package/dist/src/handlers/workspace-handler.js +101 -0
- package/dist/src/handlers/workspace-handler.js.map +1 -0
- package/dist/src/hooks/claude-approval-hook.d.ts +7 -0
- package/dist/src/hooks/claude-approval-hook.js +95 -0
- package/dist/src/hooks/claude-approval-hook.js.map +1 -0
- package/dist/src/hooks/gemini-approval-hook.d.ts +7 -0
- package/dist/src/hooks/gemini-approval-hook.js +113 -0
- package/dist/src/hooks/gemini-approval-hook.js.map +1 -0
- package/dist/src/index.d.ts +62 -0
- package/dist/src/index.js +65 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/keyring-secret-store.d.ts +36 -0
- package/dist/src/keyring-secret-store.js +70 -0
- package/dist/src/keyring-secret-store.js.map +1 -0
- package/dist/src/lock-file.d.ts +18 -0
- package/dist/src/lock-file.js +60 -0
- package/dist/src/lock-file.js.map +1 -0
- package/dist/src/logger.d.ts +28 -0
- package/dist/src/logger.js +99 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/pairing/pairing-code-service.d.ts +45 -0
- package/dist/src/pairing/pairing-code-service.js +183 -0
- package/dist/src/pairing/pairing-code-service.js.map +1 -0
- package/dist/src/projects/project-registry.d.ts +14 -0
- package/dist/src/projects/project-registry.js +60 -0
- package/dist/src/projects/project-registry.js.map +1 -0
- package/dist/src/push/push-sender.d.ts +21 -0
- package/dist/src/push/push-sender.js +96 -0
- package/dist/src/push/push-sender.js.map +1 -0
- package/dist/src/push/push-service.d.ts +122 -0
- package/dist/src/push/push-service.js +260 -0
- package/dist/src/push/push-service.js.map +1 -0
- package/dist/src/qr.d.ts +17 -0
- package/dist/src/qr.js +31 -0
- package/dist/src/qr.js.map +1 -0
- package/dist/src/secret-store.d.ts +23 -0
- package/dist/src/secret-store.js +27 -0
- package/dist/src/secret-store.js.map +1 -0
- package/dist/src/secure-device-state.d.ts +16 -0
- package/dist/src/secure-device-state.js +63 -0
- package/dist/src/secure-device-state.js.map +1 -0
- package/dist/src/service-installer.d.ts +57 -0
- package/dist/src/service-installer.js +254 -0
- package/dist/src/service-installer.js.map +1 -0
- package/dist/src/session-state.d.ts +14 -0
- package/dist/src/session-state.js +19 -0
- package/dist/src/session-state.js.map +1 -0
- package/dist/src/transport/crypto.d.ts +43 -0
- package/dist/src/transport/crypto.js +78 -0
- package/dist/src/transport/crypto.js.map +1 -0
- package/dist/src/transport/lan-server.d.ts +33 -0
- package/dist/src/transport/lan-server.js +105 -0
- package/dist/src/transport/lan-server.js.map +1 -0
- package/dist/src/transport/local-hosts.d.ts +17 -0
- package/dist/src/transport/local-hosts.js +30 -0
- package/dist/src/transport/local-hosts.js.map +1 -0
- package/dist/src/transport/mdns-advertiser.d.ts +83 -0
- package/dist/src/transport/mdns-advertiser.js +282 -0
- package/dist/src/transport/mdns-advertiser.js.map +1 -0
- package/dist/src/transport/message-io.d.ts +33 -0
- package/dist/src/transport/message-io.js +87 -0
- package/dist/src/transport/message-io.js.map +1 -0
- package/dist/src/transport/outbound-log.d.ts +24 -0
- package/dist/src/transport/outbound-log.js +78 -0
- package/dist/src/transport/outbound-log.js.map +1 -0
- package/dist/src/transport/relay-client.d.ts +19 -0
- package/dist/src/transport/relay-client.js +27 -0
- package/dist/src/transport/relay-client.js.map +1 -0
- package/dist/src/transport/secure-channel.d.ts +33 -0
- package/dist/src/transport/secure-channel.js +81 -0
- package/dist/src/transport/secure-channel.js.map +1 -0
- package/dist/src/transport/server-handshake.d.ts +49 -0
- package/dist/src/transport/server-handshake.js +137 -0
- package/dist/src/transport/server-handshake.js.map +1 -0
- package/dist/src/transport/session-handler.d.ts +19 -0
- package/dist/src/transport/session-handler.js +134 -0
- package/dist/src/transport/session-handler.js.map +1 -0
- package/dist/src/transport/session-registry.d.ts +58 -0
- package/dist/src/transport/session-registry.js +91 -0
- package/dist/src/transport/session-registry.js.map +1 -0
- package/dist/src/transport/trust-store.d.ts +23 -0
- package/dist/src/transport/trust-store.js +33 -0
- package/dist/src/transport/trust-store.js.map +1 -0
- package/dist/src/transport/ws-adapter.d.ts +7 -0
- package/dist/src/transport/ws-adapter.js +16 -0
- package/dist/src/transport/ws-adapter.js.map +1 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +13 -0
- package/dist/src/version.js.map +1 -0
- package/dist/src/workspace/browse-service.d.ts +10 -0
- package/dist/src/workspace/browse-service.js +97 -0
- package/dist/src/workspace/browse-service.js.map +1 -0
- package/dist/src/workspace/checkpoint-service.d.ts +21 -0
- package/dist/src/workspace/checkpoint-service.js +219 -0
- package/dist/src/workspace/checkpoint-service.js.map +1 -0
- package/dist/src/workspace/path-guard.d.ts +7 -0
- package/dist/src/workspace/path-guard.js +51 -0
- package/dist/src/workspace/path-guard.js.map +1 -0
- package/dist/src/workspace/workspace-service.d.ts +8 -0
- package/dist/src/workspace/workspace-service.js +111 -0
- package/dist/src/workspace/workspace-service.js.map +1 -0
- package/package.json +46 -0
- package/scripts/extract-gemini-hook.mjs +16 -0
- package/scripts/fake-approval-bridge.mjs +23 -0
- package/scripts/install-service-linux.sh +38 -0
- package/scripts/install-service-macos.sh +38 -0
- package/scripts/install-service-windows.ps1 +26 -0
- package/scripts/test-gemini-hook-e2e.mjs +168 -0
- 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":"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"}
|
|
@@ -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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"}
|
package/dist/src/qr.d.ts
ADDED
|
@@ -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
|