palmier 0.9.6 → 0.9.8
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 +28 -13
- package/dist/agents/agent.d.ts +0 -1
- package/dist/agents/agent.js +0 -1
- package/dist/agents/aider.d.ts +0 -1
- package/dist/agents/aider.js +0 -1
- package/dist/agents/claude.d.ts +0 -1
- package/dist/agents/claude.js +0 -1
- package/dist/agents/cline.d.ts +0 -1
- package/dist/agents/cline.js +0 -1
- package/dist/agents/codex.d.ts +0 -1
- package/dist/agents/codex.js +0 -1
- package/dist/agents/copilot.d.ts +0 -1
- package/dist/agents/copilot.js +0 -1
- package/dist/agents/cursor.d.ts +0 -1
- package/dist/agents/cursor.js +0 -1
- package/dist/agents/deepagents.d.ts +0 -1
- package/dist/agents/deepagents.js +0 -1
- package/dist/agents/droid.d.ts +0 -1
- package/dist/agents/droid.js +0 -1
- package/dist/agents/gemini.d.ts +0 -1
- package/dist/agents/gemini.js +0 -1
- package/dist/agents/goose.d.ts +0 -1
- package/dist/agents/goose.js +0 -1
- package/dist/agents/hermes.d.ts +0 -1
- package/dist/agents/hermes.js +0 -1
- package/dist/agents/kimi.d.ts +0 -1
- package/dist/agents/kimi.js +0 -1
- package/dist/agents/kiro.d.ts +0 -1
- package/dist/agents/kiro.js +0 -1
- package/dist/agents/openclaw.d.ts +0 -1
- package/dist/agents/openclaw.js +0 -1
- package/dist/agents/opencode.d.ts +0 -1
- package/dist/agents/opencode.js +0 -1
- package/dist/agents/qoder.d.ts +0 -1
- package/dist/agents/qoder.js +0 -1
- package/dist/agents/qwen.d.ts +0 -1
- package/dist/agents/qwen.js +0 -1
- package/dist/agents/shared-prompt.d.ts +0 -1
- package/dist/agents/shared-prompt.js +0 -1
- package/dist/client-store.d.ts +0 -1
- package/dist/client-store.js +0 -1
- package/dist/commands/clients.d.ts +0 -1
- package/dist/commands/clients.js +0 -1
- package/dist/commands/info.d.ts +0 -1
- package/dist/commands/info.js +0 -1
- package/dist/commands/init.d.ts +0 -1
- package/dist/commands/init.js +1 -2
- package/dist/commands/pair.d.ts +0 -1
- package/dist/commands/pair.js +0 -1
- package/dist/commands/restart.d.ts +0 -1
- package/dist/commands/restart.js +0 -1
- package/dist/commands/run.d.ts +0 -1
- package/dist/commands/run.js +19 -3
- package/dist/commands/serve.d.ts +0 -1
- package/dist/commands/serve.js +0 -1
- package/dist/commands/uninstall.d.ts +0 -1
- package/dist/commands/uninstall.js +0 -1
- package/dist/config.d.ts +0 -1
- package/dist/config.js +0 -1
- package/dist/event-queues.d.ts +0 -1
- package/dist/event-queues.js +0 -1
- package/dist/events.d.ts +0 -1
- package/dist/events.js +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/linked-device.d.ts +0 -1
- package/dist/linked-device.js +0 -1
- package/dist/mcp-handler.d.ts +0 -1
- package/dist/mcp-handler.js +0 -1
- package/dist/mcp-tools.d.ts +0 -1
- package/dist/mcp-tools.js +0 -1
- package/dist/nats-client.d.ts +0 -1
- package/dist/nats-client.js +0 -1
- package/dist/network.d.ts +0 -1
- package/dist/network.js +0 -1
- package/dist/notification-store.d.ts +0 -1
- package/dist/notification-store.js +0 -1
- package/dist/pending-requests.d.ts +0 -1
- package/dist/pending-requests.js +0 -1
- package/dist/platform/index.d.ts +0 -1
- package/dist/platform/index.js +0 -1
- package/dist/platform/linux.d.ts +0 -1
- package/dist/platform/linux.js +0 -1
- package/dist/platform/macos.d.ts +0 -1
- package/dist/platform/macos.js +0 -1
- package/dist/platform/platform.d.ts +0 -1
- package/dist/platform/platform.js +0 -1
- package/dist/platform/windows.d.ts +0 -1
- package/dist/platform/windows.js +0 -1
- package/dist/pwa/assets/{index-MLEFUP3r.js → index-DWvRAUiy.js} +31 -31
- package/dist/pwa/assets/{web-B1sKCc7e.js → web-C4iZbqTC.js} +1 -1
- package/dist/pwa/assets/{web-ETD-8ZHd.js → web-CBFqJGX6.js} +1 -1
- package/dist/pwa/assets/{web-B4xEa6WO.js → web-DL4uXOpS.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/rpc-handler.d.ts +0 -1
- package/dist/rpc-handler.js +0 -1
- package/dist/sms-store.d.ts +0 -1
- package/dist/sms-store.js +0 -1
- package/dist/spawn-command.d.ts +0 -1
- package/dist/spawn-command.js +0 -1
- package/dist/task.d.ts +0 -1
- package/dist/task.js +0 -1
- package/dist/transports/http-transport.d.ts +0 -1
- package/dist/transports/http-transport.js +0 -1
- package/dist/transports/nats-transport.d.ts +0 -1
- package/dist/transports/nats-transport.js +0 -1
- package/dist/types.d.ts +0 -1
- package/dist/types.js +0 -1
- package/dist/update-checker.d.ts +0 -1
- package/dist/update-checker.js +0 -1
- package/package.json +11 -1
- package/.github/workflows/ci.yml +0 -16
- package/.github/workflows/publish.yml +0 -37
- package/CLAUDE.md +0 -22
- package/dist/pwa/apple-touch-icon.png +0 -0
- package/dist/pwa/manifest.webmanifest +0 -1
- package/dist/pwa/pwa-192x192.png +0 -0
- package/dist/pwa/pwa-512x512.png +0 -0
- package/dist/pwa/registerSW.js +0 -1
- package/dist/pwa/service-worker.js +0 -2
- package/palmier-server/.github/workflows/ci.yml +0 -21
- package/palmier-server/.github/workflows/deploy.yml +0 -38
- package/palmier-server/CLAUDE.md +0 -17
- package/palmier-server/PRODUCTION.md +0 -358
- package/palmier-server/README.md +0 -231
- package/palmier-server/nats.conf +0 -19
- package/palmier-server/package.json +0 -15
- package/palmier-server/pnpm-lock.yaml +0 -7639
- package/palmier-server/pnpm-workspace.yaml +0 -3
- package/palmier-server/pwa/index.html +0 -16
- package/palmier-server/pwa/logo/logo_20260421.png +0 -0
- package/palmier-server/pwa/package.json +0 -34
- package/palmier-server/pwa/public/apple-touch-icon.png +0 -0
- package/palmier-server/pwa/public/favicon.ico +0 -0
- package/palmier-server/pwa/public/pwa-192x192.png +0 -0
- package/palmier-server/pwa/public/pwa-512x512.png +0 -0
- package/palmier-server/pwa/src/App.css +0 -3012
- package/palmier-server/pwa/src/App.tsx +0 -59
- package/palmier-server/pwa/src/agentLabels.ts +0 -11
- package/palmier-server/pwa/src/api.ts +0 -67
- package/palmier-server/pwa/src/components/CapabilityToggles.tsx +0 -170
- package/palmier-server/pwa/src/components/ConnectionStatusIcon.tsx +0 -113
- package/palmier-server/pwa/src/components/HostMenu.tsx +0 -429
- package/palmier-server/pwa/src/components/PermissionsDialog.tsx +0 -34
- package/palmier-server/pwa/src/components/PullToRefreshIndicator.tsx +0 -46
- package/palmier-server/pwa/src/components/RunDetailView.tsx +0 -343
- package/palmier-server/pwa/src/components/SessionComposer.tsx +0 -157
- package/palmier-server/pwa/src/components/SessionsView.tsx +0 -326
- package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +0 -170
- package/palmier-server/pwa/src/components/TabBar.tsx +0 -40
- package/palmier-server/pwa/src/components/TaskCard.tsx +0 -255
- package/palmier-server/pwa/src/components/TaskForm.tsx +0 -766
- package/palmier-server/pwa/src/components/TasksView.tsx +0 -179
- package/palmier-server/pwa/src/constants.ts +0 -2
- package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +0 -432
- package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +0 -124
- package/palmier-server/pwa/src/draftGuard.ts +0 -24
- package/palmier-server/pwa/src/formatTime.ts +0 -44
- package/palmier-server/pwa/src/hooks/useBackClose.ts +0 -75
- package/palmier-server/pwa/src/hooks/useMediaQuery.ts +0 -17
- package/palmier-server/pwa/src/hooks/usePullToRefresh.ts +0 -102
- package/palmier-server/pwa/src/hooks/usePushSubscription.ts +0 -77
- package/palmier-server/pwa/src/main.tsx +0 -14
- package/palmier-server/pwa/src/native/Device.ts +0 -49
- package/palmier-server/pwa/src/pages/Dashboard.tsx +0 -542
- package/palmier-server/pwa/src/pages/PairHost.tsx +0 -232
- package/palmier-server/pwa/src/pages/PairSetup.tsx +0 -134
- package/palmier-server/pwa/src/service-worker.ts +0 -142
- package/palmier-server/pwa/src/types.ts +0 -75
- package/palmier-server/pwa/src/vite-env.d.ts +0 -11
- package/palmier-server/pwa/tsconfig.json +0 -21
- package/palmier-server/pwa/tsconfig.node.json +0 -19
- package/palmier-server/pwa/vite.config.ts +0 -47
- package/palmier-server/server/.env.example +0 -20
- package/palmier-server/server/package.json +0 -36
- package/palmier-server/server/src/db.ts +0 -44
- package/palmier-server/server/src/fcm.ts +0 -74
- package/palmier-server/server/src/index.ts +0 -688
- package/palmier-server/server/src/nats-jwt.ts +0 -299
- package/palmier-server/server/src/nats-setup.ts +0 -48
- package/palmier-server/server/src/nats.ts +0 -33
- package/palmier-server/server/src/notify.ts +0 -34
- package/palmier-server/server/src/push.ts +0 -68
- package/palmier-server/server/src/routes/device.ts +0 -224
- package/palmier-server/server/src/routes/fcm.ts +0 -64
- package/palmier-server/server/src/routes/hosts.ts +0 -56
- package/palmier-server/server/src/routes/push.ts +0 -101
- package/palmier-server/server/tsconfig.json +0 -20
- package/palmier-server/spec.md +0 -533
- package/src/agents/agent-instructions.md +0 -28
- package/src/agents/agent.ts +0 -114
- package/src/agents/aider.ts +0 -35
- package/src/agents/claude.ts +0 -39
- package/src/agents/cline.ts +0 -35
- package/src/agents/codex.ts +0 -40
- package/src/agents/copilot.ts +0 -37
- package/src/agents/cursor.ts +0 -36
- package/src/agents/deepagents.ts +0 -36
- package/src/agents/droid.ts +0 -35
- package/src/agents/gemini.ts +0 -43
- package/src/agents/goose.ts +0 -33
- package/src/agents/hermes.ts +0 -36
- package/src/agents/kimi.ts +0 -35
- package/src/agents/kiro.ts +0 -36
- package/src/agents/openclaw.ts +0 -29
- package/src/agents/opencode.ts +0 -36
- package/src/agents/qoder.ts +0 -36
- package/src/agents/qwen.ts +0 -32
- package/src/agents/shared-prompt.ts +0 -30
- package/src/client-store.ts +0 -68
- package/src/commands/clients.ts +0 -29
- package/src/commands/info.ts +0 -29
- package/src/commands/init.ts +0 -165
- package/src/commands/pair.ts +0 -137
- package/src/commands/restart.ts +0 -6
- package/src/commands/run.ts +0 -608
- package/src/commands/serve.ts +0 -211
- package/src/commands/uninstall.ts +0 -9
- package/src/config.ts +0 -36
- package/src/cross-spawn.d.ts +0 -5
- package/src/event-queues.ts +0 -41
- package/src/events.ts +0 -29
- package/src/index.ts +0 -111
- package/src/linked-device.ts +0 -52
- package/src/mcp-handler.ts +0 -200
- package/src/mcp-tools.ts +0 -839
- package/src/nats-client.ts +0 -19
- package/src/network.ts +0 -96
- package/src/notification-store.ts +0 -30
- package/src/pending-requests.ts +0 -73
- package/src/platform/index.ts +0 -20
- package/src/platform/linux.ts +0 -296
- package/src/platform/macos.ts +0 -329
- package/src/platform/platform.ts +0 -31
- package/src/platform/windows.ts +0 -299
- package/src/rpc-handler.ts +0 -691
- package/src/sms-store.ts +0 -28
- package/src/spawn-command.ts +0 -123
- package/src/task.ts +0 -343
- package/src/transports/http-transport.ts +0 -478
- package/src/transports/nats-transport.ts +0 -76
- package/src/types.ts +0 -89
- package/src/update-checker.ts +0 -40
- package/test/agent-instructions.test.ts +0 -209
- package/test/agent-output-parsing.test.ts +0 -74
- package/test/linux-cron.test.ts +0 -41
- package/test/macos-plist.test.ts +0 -112
- package/test/notification-store.test.ts +0 -57
- package/test/pairing.test.ts +0 -35
- package/test/result-state.test.ts +0 -110
- package/test/task-parsing.test.ts +0 -82
- package/test/taskrun-messages.test.ts +0 -224
- package/test/tsconfig.json +0 -9
- package/test/windows-xml.test.ts +0 -89
- package/tsconfig.json +0 -19
|
@@ -1,299 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* NATS JWT/NKey utilities for decentralized authentication.
|
|
3
|
-
*
|
|
4
|
-
* NATS supports a decentralized auth model where authorization is embedded in
|
|
5
|
-
* signed JWTs rather than managed in server config files. This means new hosts
|
|
6
|
-
* can be onboarded without restarting or reconfiguring the NATS server.
|
|
7
|
-
*
|
|
8
|
-
* Key hierarchy:
|
|
9
|
-
*
|
|
10
|
-
* Operator (root of trust)
|
|
11
|
-
* └─ signs Account JWT → embedded in NATS server config (one-time)
|
|
12
|
-
* └─ Account (runtime signing key, held by palmier-server)
|
|
13
|
-
* ├─ signs Host User JWTs → scoped to host.{id}.* subjects
|
|
14
|
-
* ├─ signs PWA User JWTs → scoped to RPC + pairing subjects
|
|
15
|
-
* └─ signs Server User JWT → full access for relaying
|
|
16
|
-
*
|
|
17
|
-
* JWT format (NATS-specific, not standard JWT):
|
|
18
|
-
* Header: {"typ":"JWT","alg":"ed25519-nkey"}
|
|
19
|
-
* Payload: NATS claims (issuer, subject, permissions)
|
|
20
|
-
* Signature: Ed25519(header.payload, signing_key)
|
|
21
|
-
*
|
|
22
|
-
* Subject permission model:
|
|
23
|
-
*
|
|
24
|
-
* | Role | Publish | Subscribe |
|
|
25
|
-
* |-------------------|--------------------------|------------------------|
|
|
26
|
-
* | Host (X) | host-event.X.>, host.X.> | host.X.>, pair.* |
|
|
27
|
-
* | PWA (pairing) | pair.* | (none) |
|
|
28
|
-
* | PWA (connected X) | host.X.rpc.> | host-event.X.> |
|
|
29
|
-
* | Server | > (full) | > (full) |
|
|
30
|
-
*
|
|
31
|
-
* Request/reply inbox subjects (_INBOX.>) are allowed automatically via the
|
|
32
|
-
* `resp` field in the JWT claims.
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
import { createOperator, createAccount, createUser, fromSeed, type KeyPair } from "nkeys.js";
|
|
36
|
-
import crypto from "crypto";
|
|
37
|
-
|
|
38
|
-
// ---------------------------------------------------------------------------
|
|
39
|
-
// Base64url helpers
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
|
|
42
|
-
function base64urlEncode(data: Uint8Array): string {
|
|
43
|
-
return Buffer.from(data).toString("base64url");
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function base64urlEncodeStr(str: string): string {
|
|
47
|
-
return Buffer.from(str, "utf-8").toString("base64url");
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
// NKey seed ↔ string helpers
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
|
|
54
|
-
export function seedToString(seed: Uint8Array): string {
|
|
55
|
-
return new TextDecoder().decode(seed);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function seedFromString(seed: string): Uint8Array {
|
|
59
|
-
return new TextEncoder().encode(seed);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
63
|
-
// JWT signing
|
|
64
|
-
// ---------------------------------------------------------------------------
|
|
65
|
-
|
|
66
|
-
/** Sign a NATS JWT: base64url(header).base64url(claims).base64url(ed25519_sig). */
|
|
67
|
-
function signJwt(claims: Record<string, unknown>, signingKey: KeyPair): string {
|
|
68
|
-
const header = base64urlEncodeStr(JSON.stringify({ typ: "JWT", alg: "ed25519-nkey" }));
|
|
69
|
-
const payload = base64urlEncodeStr(JSON.stringify(claims));
|
|
70
|
-
const sigInput = new TextEncoder().encode(`${header}.${payload}`);
|
|
71
|
-
const signature = base64urlEncode(signingKey.sign(sigInput));
|
|
72
|
-
return `${header}.${payload}.${signature}`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// ---------------------------------------------------------------------------
|
|
76
|
-
// Account JWT
|
|
77
|
-
// ---------------------------------------------------------------------------
|
|
78
|
-
|
|
79
|
-
/** Create an Operator JWT (self-signed). Used in the NATS server `operator` field. */
|
|
80
|
-
export function createOperatorJwt(operatorSeed: string): string {
|
|
81
|
-
const operatorKp = fromSeed(seedFromString(operatorSeed));
|
|
82
|
-
const claims = {
|
|
83
|
-
jti: crypto.randomUUID().replace(/-/g, ""),
|
|
84
|
-
iat: Math.floor(Date.now() / 1000),
|
|
85
|
-
iss: operatorKp.getPublicKey(),
|
|
86
|
-
name: "palmier-operator",
|
|
87
|
-
sub: operatorKp.getPublicKey(),
|
|
88
|
-
nats: {
|
|
89
|
-
type: "operator",
|
|
90
|
-
version: 2,
|
|
91
|
-
},
|
|
92
|
-
};
|
|
93
|
-
const jwt = signJwt(claims, operatorKp);
|
|
94
|
-
operatorKp.clear();
|
|
95
|
-
return jwt;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Create an Account JWT signed by the operator. Embedded in NATS server config. */
|
|
99
|
-
export function createAccountJwt(operatorSeed: string, accountPublicKey: string): string {
|
|
100
|
-
const operatorKp = fromSeed(seedFromString(operatorSeed));
|
|
101
|
-
const claims = {
|
|
102
|
-
jti: crypto.randomUUID().replace(/-/g, ""),
|
|
103
|
-
iat: Math.floor(Date.now() / 1000),
|
|
104
|
-
iss: operatorKp.getPublicKey(),
|
|
105
|
-
name: "palmier",
|
|
106
|
-
sub: accountPublicKey,
|
|
107
|
-
nats: {
|
|
108
|
-
limits: {
|
|
109
|
-
subs: -1,
|
|
110
|
-
data: -1,
|
|
111
|
-
payload: -1,
|
|
112
|
-
imports: -1,
|
|
113
|
-
exports: -1,
|
|
114
|
-
wildcards: true,
|
|
115
|
-
conn: -1,
|
|
116
|
-
leaf: -1,
|
|
117
|
-
},
|
|
118
|
-
type: "account",
|
|
119
|
-
version: 2,
|
|
120
|
-
},
|
|
121
|
-
};
|
|
122
|
-
const jwt = signJwt(claims, operatorKp);
|
|
123
|
-
operatorKp.clear();
|
|
124
|
-
return jwt;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// ---------------------------------------------------------------------------
|
|
128
|
-
// User JWT (generic)
|
|
129
|
-
// ---------------------------------------------------------------------------
|
|
130
|
-
|
|
131
|
-
interface UserPermissions {
|
|
132
|
-
pub: { allow: string[] };
|
|
133
|
-
sub: { allow: string[] };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Create a User JWT signed by the account key.
|
|
138
|
-
* _INBOX.> is added to subscribe permissions so request/reply works.
|
|
139
|
-
* Unlimited values (-1) for subs/data/payload mean no artificial caps.
|
|
140
|
-
*/
|
|
141
|
-
function createUserJwt(
|
|
142
|
-
accountSeed: string,
|
|
143
|
-
userPublicKey: string,
|
|
144
|
-
name: string,
|
|
145
|
-
permissions: UserPermissions,
|
|
146
|
-
): string {
|
|
147
|
-
const accountKp = fromSeed(seedFromString(accountSeed));
|
|
148
|
-
const claims = {
|
|
149
|
-
jti: crypto.randomUUID().replace(/-/g, ""),
|
|
150
|
-
iat: Math.floor(Date.now() / 1000),
|
|
151
|
-
iss: accountKp.getPublicKey(),
|
|
152
|
-
name,
|
|
153
|
-
sub: userPublicKey,
|
|
154
|
-
nats: {
|
|
155
|
-
pub: { allow: [...permissions.pub.allow, "_INBOX.>"] },
|
|
156
|
-
sub: { allow: [...permissions.sub.allow, "_INBOX.>"] },
|
|
157
|
-
subs: -1,
|
|
158
|
-
data: -1,
|
|
159
|
-
payload: -1,
|
|
160
|
-
type: "user",
|
|
161
|
-
version: 2,
|
|
162
|
-
},
|
|
163
|
-
};
|
|
164
|
-
const jwt = signJwt(claims, accountKp);
|
|
165
|
-
accountKp.clear();
|
|
166
|
-
return jwt;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// ---------------------------------------------------------------------------
|
|
170
|
-
// Role-specific JWT generators
|
|
171
|
-
// ---------------------------------------------------------------------------
|
|
172
|
-
|
|
173
|
-
export interface NatsCredentials {
|
|
174
|
-
jwt: string;
|
|
175
|
-
nkeySeed: string;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Create credentials for a host daemon, scoped to the host's own subjects.
|
|
180
|
-
*
|
|
181
|
-
* A host with id X can only:
|
|
182
|
-
* - Publish to: host-event.X.> (task events), host.X.> (FCM requests, push)
|
|
183
|
-
* - Subscribe to: host.X.> (RPC, device responses), pair.* (during pairing)
|
|
184
|
-
*
|
|
185
|
-
* It cannot access any other host's subjects.
|
|
186
|
-
*/
|
|
187
|
-
export function createHostCredentials(accountSeed: string, hostId: string): NatsCredentials {
|
|
188
|
-
const userKp = createUser();
|
|
189
|
-
const jwt = createUserJwt(accountSeed, userKp.getPublicKey(), `host-${hostId}`, {
|
|
190
|
-
pub: { allow: [`host-event.${hostId}.>`, `host.${hostId}.>`] },
|
|
191
|
-
sub: { allow: [`host.${hostId}.>`, "pair.*"] },
|
|
192
|
-
});
|
|
193
|
-
const nkeySeed = seedToString(userKp.getSeed());
|
|
194
|
-
userKp.clear();
|
|
195
|
-
return { jwt, nkeySeed };
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Create pairing-only credentials for the PWA. Used during the pairing flow
|
|
200
|
-
* before the PWA knows which host it will connect to.
|
|
201
|
-
*
|
|
202
|
-
* Minimal permissions:
|
|
203
|
-
* - Publish to: pair.* (send pairing request)
|
|
204
|
-
* - Subscribe to: (none — reply inbox handled by `resp`)
|
|
205
|
-
*/
|
|
206
|
-
export function createPairingCredentials(accountSeed: string): NatsCredentials {
|
|
207
|
-
const userKp = createUser();
|
|
208
|
-
const jwt = createUserJwt(accountSeed, userKp.getPublicKey(), "pwa-pairing", {
|
|
209
|
-
pub: { allow: ["pair.*"] },
|
|
210
|
-
sub: { allow: [] },
|
|
211
|
-
});
|
|
212
|
-
const nkeySeed = seedToString(userKp.getSeed());
|
|
213
|
-
userKp.clear();
|
|
214
|
-
return { jwt, nkeySeed };
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Create host-scoped credentials for a PWA client after pairing.
|
|
219
|
-
*
|
|
220
|
-
* Scoped to a single host:
|
|
221
|
-
* - Publish to: host.{hostId}.rpc.> (send RPC to paired host)
|
|
222
|
-
* - Subscribe to: host-event.{hostId}.> (receive task events from paired host)
|
|
223
|
-
*
|
|
224
|
-
* A PWA client cannot see events or send RPC to any other host.
|
|
225
|
-
*/
|
|
226
|
-
export function createPwaCredentials(accountSeed: string, hostId: string): NatsCredentials {
|
|
227
|
-
const userKp = createUser();
|
|
228
|
-
const jwt = createUserJwt(accountSeed, userKp.getPublicKey(), `pwa-${hostId}`, {
|
|
229
|
-
pub: { allow: [`host.${hostId}.rpc.>`] },
|
|
230
|
-
sub: { allow: [`host-event.${hostId}.>`] },
|
|
231
|
-
});
|
|
232
|
-
const nkeySeed = seedToString(userKp.getSeed());
|
|
233
|
-
userKp.clear();
|
|
234
|
-
return { jwt, nkeySeed };
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Create credentials for the palmier server itself (full access for relaying).
|
|
239
|
-
*/
|
|
240
|
-
export function createServerCredentials(accountSeed: string): NatsCredentials {
|
|
241
|
-
const userKp = createUser();
|
|
242
|
-
const jwt = createUserJwt(accountSeed, userKp.getPublicKey(), "server", {
|
|
243
|
-
pub: { allow: [">"] },
|
|
244
|
-
sub: { allow: [">"] },
|
|
245
|
-
});
|
|
246
|
-
const nkeySeed = seedToString(userKp.getSeed());
|
|
247
|
-
userKp.clear();
|
|
248
|
-
return { jwt, nkeySeed };
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// ---------------------------------------------------------------------------
|
|
252
|
-
// Setup: generate operator + account keys and NATS config
|
|
253
|
-
// ---------------------------------------------------------------------------
|
|
254
|
-
|
|
255
|
-
export interface NatsSetupResult {
|
|
256
|
-
operatorSeed: string;
|
|
257
|
-
operatorPublicKey: string;
|
|
258
|
-
accountSeed: string;
|
|
259
|
-
accountPublicKey: string;
|
|
260
|
-
operatorJwt: string;
|
|
261
|
-
accountJwt: string;
|
|
262
|
-
natsConfig: string;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Generate all keys and the NATS server config snippet for JWT auth.
|
|
267
|
-
* Run once during initial deployment, then store seeds as env vars.
|
|
268
|
-
*
|
|
269
|
-
* The `operator` field in nats.conf requires a full JWT (not a public key) —
|
|
270
|
-
* NATS treats non-JWT strings as file paths.
|
|
271
|
-
*/
|
|
272
|
-
export function generateNatsSetup(): NatsSetupResult {
|
|
273
|
-
const operatorKp = createOperator();
|
|
274
|
-
const accountKp = createAccount();
|
|
275
|
-
|
|
276
|
-
const operatorSeed = seedToString(operatorKp.getSeed());
|
|
277
|
-
const operatorPublicKey = operatorKp.getPublicKey();
|
|
278
|
-
const accountSeed = seedToString(accountKp.getSeed());
|
|
279
|
-
const accountPublicKey = accountKp.getPublicKey();
|
|
280
|
-
|
|
281
|
-
const operatorJwt = createOperatorJwt(operatorSeed);
|
|
282
|
-
const accountJwt = createAccountJwt(operatorSeed, accountPublicKey);
|
|
283
|
-
|
|
284
|
-
const natsConfig = [
|
|
285
|
-
`# NATS JWT auth configuration`,
|
|
286
|
-
`# Generated by palmier nats-setup`,
|
|
287
|
-
``,
|
|
288
|
-
`operator: ${operatorJwt}`,
|
|
289
|
-
`resolver: MEMORY`,
|
|
290
|
-
`resolver_preload: {`,
|
|
291
|
-
` ${accountPublicKey}: ${accountJwt}`,
|
|
292
|
-
`}`,
|
|
293
|
-
].join("\n");
|
|
294
|
-
|
|
295
|
-
operatorKp.clear();
|
|
296
|
-
accountKp.clear();
|
|
297
|
-
|
|
298
|
-
return { operatorSeed, operatorPublicKey, accountSeed, accountPublicKey, operatorJwt, accountJwt, natsConfig };
|
|
299
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* One-time setup script for NATS JWT authentication.
|
|
3
|
-
*
|
|
4
|
-
* Run this once when deploying Palmier for the first time (or when rotating keys).
|
|
5
|
-
* It generates the cryptographic keys and config needed for NATS to enforce
|
|
6
|
-
* per-host subject isolation.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* cd server && pnpm nats-setup
|
|
10
|
-
*
|
|
11
|
-
* What it generates:
|
|
12
|
-
*
|
|
13
|
-
* Operator NKey pair
|
|
14
|
-
* └─ Signs the Account JWT (one-time). Store the seed securely as a backup —
|
|
15
|
-
* you only need it again if you regenerate the account JWT.
|
|
16
|
-
*
|
|
17
|
-
* Account NKey pair
|
|
18
|
-
* └─ Signs User JWTs at runtime. The server uses NATS_ACCOUNT_SEED to create
|
|
19
|
-
* scoped credentials for each host (during registration) and each PWA session.
|
|
20
|
-
*
|
|
21
|
-
* Account JWT
|
|
22
|
-
* └─ Embedded in the NATS server config (resolver_preload). Tells the NATS server
|
|
23
|
-
* to trust User JWTs signed by this account key.
|
|
24
|
-
*
|
|
25
|
-
* NATS config snippet
|
|
26
|
-
* └─ Drop-in replacement for the authorization block in nats.conf.
|
|
27
|
-
*
|
|
28
|
-
* See PRODUCTION.md for the full deployment flow.
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
import { generateNatsSetup } from "./nats-jwt.js";
|
|
32
|
-
|
|
33
|
-
const setup = generateNatsSetup();
|
|
34
|
-
|
|
35
|
-
console.log("=== NATS JWT Auth Setup ===\n");
|
|
36
|
-
|
|
37
|
-
console.log("NATS_ACCOUNT_SEED (add to server/.env):\n");
|
|
38
|
-
console.log(` ${setup.accountSeed}\n`);
|
|
39
|
-
|
|
40
|
-
console.log("nats.conf auth section (replace any existing auth block):\n");
|
|
41
|
-
console.log(setup.natsConfig);
|
|
42
|
-
console.log();
|
|
43
|
-
|
|
44
|
-
console.log("Keys (store securely, do not commit):\n");
|
|
45
|
-
console.log(` Operator seed: ${setup.operatorSeed}`);
|
|
46
|
-
console.log(` Operator public key: ${setup.operatorPublicKey}`);
|
|
47
|
-
console.log(` Account seed: ${setup.accountSeed}`);
|
|
48
|
-
console.log(` Account public key: ${setup.accountPublicKey}`);
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { connect, jwtAuthenticator, NatsConnection } from "nats";
|
|
2
|
-
import { createServerCredentials, seedFromString } from "./nats-jwt.js";
|
|
3
|
-
|
|
4
|
-
let nc: NatsConnection | null = null;
|
|
5
|
-
|
|
6
|
-
export async function connectNats(): Promise<NatsConnection> {
|
|
7
|
-
if (nc) return nc;
|
|
8
|
-
|
|
9
|
-
const url = process.env.NATS_URL || "nats://localhost:4222";
|
|
10
|
-
const accountSeed = process.env.NATS_ACCOUNT_SEED;
|
|
11
|
-
|
|
12
|
-
if (!accountSeed) {
|
|
13
|
-
throw new Error("NATS_ACCOUNT_SEED is required for JWT auth");
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// Generate server credentials with full access
|
|
17
|
-
const creds = createServerCredentials(accountSeed);
|
|
18
|
-
|
|
19
|
-
nc = await connect({
|
|
20
|
-
servers: url,
|
|
21
|
-
authenticator: jwtAuthenticator(creds.jwt, seedFromString(creds.nkeySeed)),
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
console.log(`Connected to NATS at ${url} (JWT auth)`);
|
|
25
|
-
return nc;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export async function getNatsConnection(): Promise<NatsConnection> {
|
|
29
|
-
if (!nc) {
|
|
30
|
-
return connectNats();
|
|
31
|
-
}
|
|
32
|
-
return nc;
|
|
33
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { sendPushToClients } from "./push.js";
|
|
2
|
-
import { sendFcmToClients } from "./fcm.js";
|
|
3
|
-
|
|
4
|
-
export interface NotificationPayload {
|
|
5
|
-
type: string;
|
|
6
|
-
host_id: string;
|
|
7
|
-
title?: string;
|
|
8
|
-
body?: string;
|
|
9
|
-
task_id?: string;
|
|
10
|
-
session_id?: string;
|
|
11
|
-
run_id?: string;
|
|
12
|
-
result_file?: string;
|
|
13
|
-
agent_name?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function stringifyPayload(payload: NotificationPayload): Record<string, string> {
|
|
17
|
-
const result: Record<string, string> = {};
|
|
18
|
-
for (const [key, value] of Object.entries(payload)) {
|
|
19
|
-
if (value !== undefined && value !== null) {
|
|
20
|
-
result[key] = String(value);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
return result;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export async function notifyClients(
|
|
27
|
-
hostId: string,
|
|
28
|
-
payload: NotificationPayload
|
|
29
|
-
): Promise<void> {
|
|
30
|
-
await Promise.allSettled([
|
|
31
|
-
sendPushToClients(hostId, payload),
|
|
32
|
-
sendFcmToClients(hostId, stringifyPayload(payload)),
|
|
33
|
-
]);
|
|
34
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import webPush from "web-push";
|
|
2
|
-
import { pool } from "./db.js";
|
|
3
|
-
|
|
4
|
-
let configured = false;
|
|
5
|
-
|
|
6
|
-
function ensureConfigured(): void {
|
|
7
|
-
if (configured) return;
|
|
8
|
-
|
|
9
|
-
const publicKey = process.env.VAPID_PUBLIC_KEY;
|
|
10
|
-
const privateKey = process.env.VAPID_PRIVATE_KEY;
|
|
11
|
-
const mailto = process.env.VAPID_MAILTO || "mailto:admin@example.com";
|
|
12
|
-
|
|
13
|
-
if (!publicKey || !privateKey) {
|
|
14
|
-
console.warn("VAPID keys not configured. Web Push will not work.");
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
webPush.setVapidDetails(mailto, publicKey, privateKey);
|
|
19
|
-
configured = true;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export async function sendPushToClients(
|
|
23
|
-
hostId: string,
|
|
24
|
-
payload: object | string
|
|
25
|
-
): Promise<void> {
|
|
26
|
-
ensureConfigured();
|
|
27
|
-
if (!configured) {
|
|
28
|
-
console.warn("[Push] VAPID not configured, skipping push for host", hostId);
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const result = await pool.query(
|
|
33
|
-
"SELECT endpoint, p256dh, auth FROM push_subscriptions WHERE host_id = $1",
|
|
34
|
-
[hostId]
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
const body = typeof payload === "string" ? payload : JSON.stringify(payload);
|
|
38
|
-
|
|
39
|
-
const sendPromises = result.rows.map(async (row) => {
|
|
40
|
-
const subscription: webPush.PushSubscription = {
|
|
41
|
-
endpoint: row.endpoint,
|
|
42
|
-
keys: {
|
|
43
|
-
p256dh: row.p256dh,
|
|
44
|
-
auth: row.auth,
|
|
45
|
-
},
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
await webPush.sendNotification(subscription, body);
|
|
50
|
-
} catch (err: any) {
|
|
51
|
-
if (err.statusCode === 410 || err.statusCode === 404) {
|
|
52
|
-
// Subscription expired or invalid, remove it
|
|
53
|
-
await pool.query(
|
|
54
|
-
"DELETE FROM push_subscriptions WHERE host_id = $1 AND endpoint = $2",
|
|
55
|
-
[hostId, row.endpoint]
|
|
56
|
-
);
|
|
57
|
-
console.log(`Removed stale push subscription for host ${hostId}`);
|
|
58
|
-
} else {
|
|
59
|
-
console.error(
|
|
60
|
-
`Failed to send push to ${row.endpoint}:`,
|
|
61
|
-
err.message
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
await Promise.allSettled(sendPromises);
|
|
68
|
-
}
|
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
import { Router, Request, Response } from "express";
|
|
2
|
-
import type { Router as RouterType } from "express";
|
|
3
|
-
import { getNatsConnection } from "../nats.js";
|
|
4
|
-
import { StringCodec } from "nats";
|
|
5
|
-
|
|
6
|
-
const router: RouterType = Router();
|
|
7
|
-
|
|
8
|
-
// POST /api/device/notifications - Receive a notification from Android, relay to host via NATS
|
|
9
|
-
router.post("/notifications", async (req: Request, res: Response) => {
|
|
10
|
-
try {
|
|
11
|
-
const { hostId, notification } = req.body;
|
|
12
|
-
|
|
13
|
-
if (!hostId || !notification?.id) {
|
|
14
|
-
res.status(400).json({ error: "hostId and notification with id are required" });
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const conn = await getNatsConnection();
|
|
19
|
-
const sc = StringCodec();
|
|
20
|
-
conn.publish(
|
|
21
|
-
`host.${hostId}.device.notifications`,
|
|
22
|
-
sc.encode(JSON.stringify(notification)),
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
res.json({ ok: true });
|
|
26
|
-
} catch (err) {
|
|
27
|
-
console.error("Device notification relay error:", err);
|
|
28
|
-
res.status(500).json({ error: "Internal server error" });
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
// POST /api/device/sms - Receive an SMS from Android, relay to host via NATS
|
|
33
|
-
router.post("/sms", async (req: Request, res: Response) => {
|
|
34
|
-
try {
|
|
35
|
-
const { hostId, sms } = req.body;
|
|
36
|
-
|
|
37
|
-
if (!hostId || !sms?.id) {
|
|
38
|
-
res.status(400).json({ error: "hostId and sms with id are required" });
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const conn = await getNatsConnection();
|
|
43
|
-
const sc = StringCodec();
|
|
44
|
-
conn.publish(
|
|
45
|
-
`host.${hostId}.device.sms`,
|
|
46
|
-
sc.encode(JSON.stringify(sms)),
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
res.json({ ok: true });
|
|
50
|
-
} catch (err) {
|
|
51
|
-
console.error("Device SMS relay error:", err);
|
|
52
|
-
res.status(500).json({ error: "Internal server error" });
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
// POST /api/device/contacts-response - Receive contacts response from Android, relay to host via NATS
|
|
57
|
-
router.post("/contacts-response", async (req: Request, res: Response) => {
|
|
58
|
-
try {
|
|
59
|
-
const { requestId, hostId, result } = req.body;
|
|
60
|
-
|
|
61
|
-
if (!requestId || !hostId) {
|
|
62
|
-
res.status(400).json({ error: "requestId and hostId are required" });
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const conn = await getNatsConnection();
|
|
67
|
-
const sc = StringCodec();
|
|
68
|
-
conn.publish(
|
|
69
|
-
`host.${hostId}.contacts.${requestId}`,
|
|
70
|
-
sc.encode(JSON.stringify(result)),
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
res.json({ ok: true });
|
|
74
|
-
} catch (err) {
|
|
75
|
-
console.error("Device contacts response relay error:", err);
|
|
76
|
-
res.status(500).json({ error: "Internal server error" });
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// POST /api/device/calendar-response - Receive calendar response from Android, relay to host via NATS
|
|
81
|
-
router.post("/calendar-response", async (req: Request, res: Response) => {
|
|
82
|
-
try {
|
|
83
|
-
const { requestId, hostId, result } = req.body;
|
|
84
|
-
|
|
85
|
-
if (!requestId || !hostId) {
|
|
86
|
-
res.status(400).json({ error: "requestId and hostId are required" });
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const conn = await getNatsConnection();
|
|
91
|
-
const sc = StringCodec();
|
|
92
|
-
conn.publish(
|
|
93
|
-
`host.${hostId}.calendar.${requestId}`,
|
|
94
|
-
sc.encode(JSON.stringify(result)),
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
res.json({ ok: true });
|
|
98
|
-
} catch (err) {
|
|
99
|
-
console.error("Device calendar response relay error:", err);
|
|
100
|
-
res.status(500).json({ error: "Internal server error" });
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
// POST /api/device/sms-response - Receive SMS send response from Android, relay to host via NATS
|
|
105
|
-
router.post("/sms-response", async (req: Request, res: Response) => {
|
|
106
|
-
try {
|
|
107
|
-
const { requestId, hostId, result } = req.body;
|
|
108
|
-
|
|
109
|
-
if (!requestId || !hostId) {
|
|
110
|
-
res.status(400).json({ error: "requestId and hostId are required" });
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const conn = await getNatsConnection();
|
|
115
|
-
const sc = StringCodec();
|
|
116
|
-
conn.publish(
|
|
117
|
-
`host.${hostId}.sms.${requestId}`,
|
|
118
|
-
sc.encode(JSON.stringify(result)),
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
res.json({ ok: true });
|
|
122
|
-
} catch (err) {
|
|
123
|
-
console.error("Device SMS response relay error:", err);
|
|
124
|
-
res.status(500).json({ error: "Internal server error" });
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// POST /api/device/alarm-response - Receive alarm response from Android, relay to host via NATS
|
|
129
|
-
router.post("/alarm-response", async (req: Request, res: Response) => {
|
|
130
|
-
try {
|
|
131
|
-
const { requestId, hostId, result } = req.body;
|
|
132
|
-
|
|
133
|
-
if (!requestId || !hostId) {
|
|
134
|
-
res.status(400).json({ error: "requestId and hostId are required" });
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const conn = await getNatsConnection();
|
|
139
|
-
const sc = StringCodec();
|
|
140
|
-
conn.publish(
|
|
141
|
-
`host.${hostId}.alarm.${requestId}`,
|
|
142
|
-
sc.encode(JSON.stringify(result)),
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
res.json({ ok: true });
|
|
146
|
-
} catch (err) {
|
|
147
|
-
console.error("Device alarm response relay error:", err);
|
|
148
|
-
res.status(500).json({ error: "Internal server error" });
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
// POST /api/device/battery-response - Receive battery response from Android, relay to host via NATS
|
|
153
|
-
router.post("/battery-response", async (req: Request, res: Response) => {
|
|
154
|
-
try {
|
|
155
|
-
const { requestId, hostId, result } = req.body;
|
|
156
|
-
|
|
157
|
-
if (!requestId || !hostId) {
|
|
158
|
-
res.status(400).json({ error: "requestId and hostId are required" });
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const conn = await getNatsConnection();
|
|
163
|
-
const sc = StringCodec();
|
|
164
|
-
conn.publish(
|
|
165
|
-
`host.${hostId}.battery.${requestId}`,
|
|
166
|
-
sc.encode(JSON.stringify(result)),
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
res.json({ ok: true });
|
|
170
|
-
} catch (err) {
|
|
171
|
-
console.error("Device battery response relay error:", err);
|
|
172
|
-
res.status(500).json({ error: "Internal server error" });
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
// POST /api/device/ringer-response - Receive ringer mode response from Android, relay to host via NATS
|
|
177
|
-
router.post("/ringer-response", async (req: Request, res: Response) => {
|
|
178
|
-
try {
|
|
179
|
-
const { requestId, hostId, result } = req.body;
|
|
180
|
-
|
|
181
|
-
if (!requestId || !hostId) {
|
|
182
|
-
res.status(400).json({ error: "requestId and hostId are required" });
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const conn = await getNatsConnection();
|
|
187
|
-
const sc = StringCodec();
|
|
188
|
-
conn.publish(
|
|
189
|
-
`host.${hostId}.ringer.${requestId}`,
|
|
190
|
-
sc.encode(JSON.stringify(result)),
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
res.json({ ok: true });
|
|
194
|
-
} catch (err) {
|
|
195
|
-
console.error("Device ringer response relay error:", err);
|
|
196
|
-
res.status(500).json({ error: "Internal server error" });
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
// POST /api/device/email-response - Receive email response from Android, relay to host via NATS
|
|
201
|
-
router.post("/email-response", async (req: Request, res: Response) => {
|
|
202
|
-
try {
|
|
203
|
-
const { requestId, hostId, result } = req.body;
|
|
204
|
-
|
|
205
|
-
if (!requestId || !hostId) {
|
|
206
|
-
res.status(400).json({ error: "requestId and hostId are required" });
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const conn = await getNatsConnection();
|
|
211
|
-
const sc = StringCodec();
|
|
212
|
-
conn.publish(
|
|
213
|
-
`host.${hostId}.email.${requestId}`,
|
|
214
|
-
sc.encode(JSON.stringify(result)),
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
res.json({ ok: true });
|
|
218
|
-
} catch (err) {
|
|
219
|
-
console.error("Device email response relay error:", err);
|
|
220
|
-
res.status(500).json({ error: "Internal server error" });
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
export default router;
|