palmier 0.9.5 → 0.9.7

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 (258) hide show
  1. package/README.md +30 -13
  2. package/dist/agents/agent.d.ts +0 -1
  3. package/dist/agents/agent.js +0 -1
  4. package/dist/agents/aider.d.ts +0 -1
  5. package/dist/agents/aider.js +0 -1
  6. package/dist/agents/claude.d.ts +0 -1
  7. package/dist/agents/claude.js +0 -1
  8. package/dist/agents/cline.d.ts +0 -1
  9. package/dist/agents/cline.js +0 -1
  10. package/dist/agents/codex.d.ts +0 -1
  11. package/dist/agents/codex.js +0 -1
  12. package/dist/agents/copilot.d.ts +0 -1
  13. package/dist/agents/copilot.js +0 -1
  14. package/dist/agents/cursor.d.ts +0 -1
  15. package/dist/agents/cursor.js +0 -1
  16. package/dist/agents/deepagents.d.ts +0 -1
  17. package/dist/agents/deepagents.js +0 -1
  18. package/dist/agents/droid.d.ts +0 -1
  19. package/dist/agents/droid.js +0 -1
  20. package/dist/agents/gemini.d.ts +0 -1
  21. package/dist/agents/gemini.js +0 -1
  22. package/dist/agents/goose.d.ts +0 -1
  23. package/dist/agents/goose.js +0 -1
  24. package/dist/agents/hermes.d.ts +0 -1
  25. package/dist/agents/hermes.js +0 -1
  26. package/dist/agents/kimi.d.ts +0 -1
  27. package/dist/agents/kimi.js +0 -1
  28. package/dist/agents/kiro.d.ts +0 -1
  29. package/dist/agents/kiro.js +0 -1
  30. package/dist/agents/openclaw.d.ts +0 -1
  31. package/dist/agents/openclaw.js +0 -1
  32. package/dist/agents/opencode.d.ts +0 -1
  33. package/dist/agents/opencode.js +0 -1
  34. package/dist/agents/qoder.d.ts +0 -1
  35. package/dist/agents/qoder.js +0 -1
  36. package/dist/agents/qwen.d.ts +0 -1
  37. package/dist/agents/qwen.js +0 -1
  38. package/dist/agents/shared-prompt.d.ts +0 -1
  39. package/dist/agents/shared-prompt.js +0 -1
  40. package/dist/client-store.d.ts +0 -1
  41. package/dist/client-store.js +0 -1
  42. package/dist/commands/clients.d.ts +0 -1
  43. package/dist/commands/clients.js +0 -1
  44. package/dist/commands/info.d.ts +0 -1
  45. package/dist/commands/info.js +0 -1
  46. package/dist/commands/init.d.ts +0 -1
  47. package/dist/commands/init.js +3 -4
  48. package/dist/commands/pair.d.ts +0 -1
  49. package/dist/commands/pair.js +0 -1
  50. package/dist/commands/restart.d.ts +0 -1
  51. package/dist/commands/restart.js +0 -1
  52. package/dist/commands/run.d.ts +0 -1
  53. package/dist/commands/run.js +0 -1
  54. package/dist/commands/serve.d.ts +0 -1
  55. package/dist/commands/serve.js +0 -1
  56. package/dist/commands/uninstall.d.ts +0 -1
  57. package/dist/commands/uninstall.js +0 -1
  58. package/dist/config.d.ts +0 -1
  59. package/dist/config.js +0 -1
  60. package/dist/event-queues.d.ts +0 -1
  61. package/dist/event-queues.js +0 -1
  62. package/dist/events.d.ts +0 -1
  63. package/dist/events.js +0 -1
  64. package/dist/index.d.ts +0 -1
  65. package/dist/index.js +0 -1
  66. package/dist/linked-device.d.ts +0 -1
  67. package/dist/linked-device.js +0 -1
  68. package/dist/mcp-handler.d.ts +0 -1
  69. package/dist/mcp-handler.js +0 -1
  70. package/dist/mcp-tools.d.ts +0 -1
  71. package/dist/mcp-tools.js +0 -1
  72. package/dist/nats-client.d.ts +0 -1
  73. package/dist/nats-client.js +0 -1
  74. package/dist/network.d.ts +0 -1
  75. package/dist/network.js +0 -1
  76. package/dist/notification-store.d.ts +0 -1
  77. package/dist/notification-store.js +0 -1
  78. package/dist/pending-requests.d.ts +0 -1
  79. package/dist/pending-requests.js +0 -1
  80. package/dist/platform/index.d.ts +0 -1
  81. package/dist/platform/index.js +0 -1
  82. package/dist/platform/linux.d.ts +0 -1
  83. package/dist/platform/linux.js +0 -1
  84. package/dist/platform/macos.d.ts +0 -1
  85. package/dist/platform/macos.js +0 -1
  86. package/dist/platform/platform.d.ts +0 -1
  87. package/dist/platform/platform.js +0 -1
  88. package/dist/platform/windows.d.ts +0 -1
  89. package/dist/platform/windows.js +0 -1
  90. package/dist/pwa/apple-touch-icon.png +0 -0
  91. package/dist/pwa/assets/index-D1bIhEbd.css +1 -0
  92. package/dist/pwa/assets/{index-Cvffaohh.js → index-DWvRAUiy.js} +31 -31
  93. package/dist/pwa/assets/{web-qdLcAD7T.js → web-C4iZbqTC.js} +1 -1
  94. package/dist/pwa/assets/{web-ChtbM4nv.js → web-CBFqJGX6.js} +1 -1
  95. package/dist/pwa/assets/{web-hExASsqW.js → web-DL4uXOpS.js} +1 -1
  96. package/dist/pwa/favicon.ico +0 -0
  97. package/dist/pwa/index.html +3 -3
  98. package/dist/pwa/manifest.webmanifest +1 -1
  99. package/dist/pwa/pwa-192x192.png +0 -0
  100. package/dist/pwa/pwa-512x512.png +0 -0
  101. package/dist/pwa/service-worker.js +1 -1
  102. package/dist/rpc-handler.d.ts +0 -1
  103. package/dist/rpc-handler.js +3 -10
  104. package/dist/sms-store.d.ts +0 -1
  105. package/dist/sms-store.js +0 -1
  106. package/dist/spawn-command.d.ts +0 -1
  107. package/dist/spawn-command.js +0 -1
  108. package/dist/task.d.ts +1 -1
  109. package/dist/task.js +14 -1
  110. package/dist/transports/http-transport.d.ts +0 -1
  111. package/dist/transports/http-transport.js +0 -1
  112. package/dist/transports/nats-transport.d.ts +0 -1
  113. package/dist/transports/nats-transport.js +0 -1
  114. package/dist/types.d.ts +0 -1
  115. package/dist/types.js +0 -1
  116. package/dist/update-checker.d.ts +0 -1
  117. package/dist/update-checker.js +0 -1
  118. package/package.json +5 -1
  119. package/.github/workflows/ci.yml +0 -16
  120. package/.github/workflows/publish.yml +0 -37
  121. package/CLAUDE.md +0 -22
  122. package/dist/pwa/assets/index-DBgOYBrB.css +0 -1
  123. package/palmier-server/.github/workflows/ci.yml +0 -21
  124. package/palmier-server/.github/workflows/deploy.yml +0 -38
  125. package/palmier-server/CLAUDE.md +0 -17
  126. package/palmier-server/PRODUCTION.md +0 -358
  127. package/palmier-server/README.md +0 -231
  128. package/palmier-server/nats.conf +0 -19
  129. package/palmier-server/package.json +0 -15
  130. package/palmier-server/pnpm-lock.yaml +0 -7639
  131. package/palmier-server/pnpm-workspace.yaml +0 -3
  132. package/palmier-server/pwa/index.html +0 -16
  133. package/palmier-server/pwa/logo/logo-prompt.md +0 -28
  134. package/palmier-server/pwa/logo/logo_20260330.png +0 -0
  135. package/palmier-server/pwa/package.json +0 -34
  136. package/palmier-server/pwa/public/apple-touch-icon.png +0 -0
  137. package/palmier-server/pwa/public/favicon.ico +0 -0
  138. package/palmier-server/pwa/public/pwa-192x192.png +0 -0
  139. package/palmier-server/pwa/public/pwa-512x512.png +0 -0
  140. package/palmier-server/pwa/src/App.css +0 -3004
  141. package/palmier-server/pwa/src/App.tsx +0 -59
  142. package/palmier-server/pwa/src/agentLabels.ts +0 -11
  143. package/palmier-server/pwa/src/api.ts +0 -67
  144. package/palmier-server/pwa/src/components/CapabilityToggles.tsx +0 -170
  145. package/palmier-server/pwa/src/components/ConnectionStatusIcon.tsx +0 -114
  146. package/palmier-server/pwa/src/components/HostMenu.tsx +0 -429
  147. package/palmier-server/pwa/src/components/PermissionsDialog.tsx +0 -34
  148. package/palmier-server/pwa/src/components/PullToRefreshIndicator.tsx +0 -46
  149. package/palmier-server/pwa/src/components/RunDetailView.tsx +0 -343
  150. package/palmier-server/pwa/src/components/SessionComposer.tsx +0 -157
  151. package/palmier-server/pwa/src/components/SessionsView.tsx +0 -326
  152. package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +0 -170
  153. package/palmier-server/pwa/src/components/TabBar.tsx +0 -40
  154. package/palmier-server/pwa/src/components/TaskCard.tsx +0 -255
  155. package/palmier-server/pwa/src/components/TaskForm.tsx +0 -764
  156. package/palmier-server/pwa/src/components/TasksView.tsx +0 -179
  157. package/palmier-server/pwa/src/constants.ts +0 -2
  158. package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +0 -432
  159. package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +0 -124
  160. package/palmier-server/pwa/src/draftGuard.ts +0 -24
  161. package/palmier-server/pwa/src/formatTime.ts +0 -44
  162. package/palmier-server/pwa/src/hooks/useBackClose.ts +0 -75
  163. package/palmier-server/pwa/src/hooks/useMediaQuery.ts +0 -17
  164. package/palmier-server/pwa/src/hooks/usePullToRefresh.ts +0 -102
  165. package/palmier-server/pwa/src/hooks/usePushSubscription.ts +0 -77
  166. package/palmier-server/pwa/src/main.tsx +0 -14
  167. package/palmier-server/pwa/src/native/Device.ts +0 -49
  168. package/palmier-server/pwa/src/pages/Dashboard.tsx +0 -542
  169. package/palmier-server/pwa/src/pages/PairHost.tsx +0 -232
  170. package/palmier-server/pwa/src/pages/PairSetup.tsx +0 -134
  171. package/palmier-server/pwa/src/service-worker.ts +0 -142
  172. package/palmier-server/pwa/src/types.ts +0 -75
  173. package/palmier-server/pwa/src/vite-env.d.ts +0 -11
  174. package/palmier-server/pwa/tsconfig.json +0 -21
  175. package/palmier-server/pwa/tsconfig.node.json +0 -19
  176. package/palmier-server/pwa/vite.config.ts +0 -47
  177. package/palmier-server/server/.env.example +0 -20
  178. package/palmier-server/server/package.json +0 -36
  179. package/palmier-server/server/src/db.ts +0 -44
  180. package/palmier-server/server/src/fcm.ts +0 -74
  181. package/palmier-server/server/src/index.ts +0 -688
  182. package/palmier-server/server/src/nats-jwt.ts +0 -299
  183. package/palmier-server/server/src/nats-setup.ts +0 -48
  184. package/palmier-server/server/src/nats.ts +0 -33
  185. package/palmier-server/server/src/notify.ts +0 -34
  186. package/palmier-server/server/src/push.ts +0 -68
  187. package/palmier-server/server/src/routes/device.ts +0 -224
  188. package/palmier-server/server/src/routes/fcm.ts +0 -64
  189. package/palmier-server/server/src/routes/hosts.ts +0 -56
  190. package/palmier-server/server/src/routes/push.ts +0 -101
  191. package/palmier-server/server/tsconfig.json +0 -20
  192. package/palmier-server/spec.md +0 -533
  193. package/src/agents/agent-instructions.md +0 -28
  194. package/src/agents/agent.ts +0 -114
  195. package/src/agents/aider.ts +0 -35
  196. package/src/agents/claude.ts +0 -39
  197. package/src/agents/cline.ts +0 -35
  198. package/src/agents/codex.ts +0 -40
  199. package/src/agents/copilot.ts +0 -37
  200. package/src/agents/cursor.ts +0 -36
  201. package/src/agents/deepagents.ts +0 -36
  202. package/src/agents/droid.ts +0 -35
  203. package/src/agents/gemini.ts +0 -43
  204. package/src/agents/goose.ts +0 -33
  205. package/src/agents/hermes.ts +0 -36
  206. package/src/agents/kimi.ts +0 -35
  207. package/src/agents/kiro.ts +0 -36
  208. package/src/agents/openclaw.ts +0 -29
  209. package/src/agents/opencode.ts +0 -36
  210. package/src/agents/qoder.ts +0 -36
  211. package/src/agents/qwen.ts +0 -32
  212. package/src/agents/shared-prompt.ts +0 -30
  213. package/src/client-store.ts +0 -68
  214. package/src/commands/clients.ts +0 -29
  215. package/src/commands/info.ts +0 -29
  216. package/src/commands/init.ts +0 -165
  217. package/src/commands/pair.ts +0 -137
  218. package/src/commands/restart.ts +0 -6
  219. package/src/commands/run.ts +0 -608
  220. package/src/commands/serve.ts +0 -211
  221. package/src/commands/uninstall.ts +0 -9
  222. package/src/config.ts +0 -36
  223. package/src/cross-spawn.d.ts +0 -5
  224. package/src/event-queues.ts +0 -41
  225. package/src/events.ts +0 -29
  226. package/src/index.ts +0 -111
  227. package/src/linked-device.ts +0 -52
  228. package/src/mcp-handler.ts +0 -200
  229. package/src/mcp-tools.ts +0 -839
  230. package/src/nats-client.ts +0 -19
  231. package/src/network.ts +0 -96
  232. package/src/notification-store.ts +0 -30
  233. package/src/pending-requests.ts +0 -73
  234. package/src/platform/index.ts +0 -20
  235. package/src/platform/linux.ts +0 -296
  236. package/src/platform/macos.ts +0 -329
  237. package/src/platform/platform.ts +0 -31
  238. package/src/platform/windows.ts +0 -299
  239. package/src/rpc-handler.ts +0 -694
  240. package/src/sms-store.ts +0 -28
  241. package/src/spawn-command.ts +0 -123
  242. package/src/task.ts +0 -330
  243. package/src/transports/http-transport.ts +0 -478
  244. package/src/transports/nats-transport.ts +0 -76
  245. package/src/types.ts +0 -89
  246. package/src/update-checker.ts +0 -40
  247. package/test/agent-instructions.test.ts +0 -209
  248. package/test/agent-output-parsing.test.ts +0 -74
  249. package/test/linux-cron.test.ts +0 -41
  250. package/test/macos-plist.test.ts +0 -112
  251. package/test/notification-store.test.ts +0 -57
  252. package/test/pairing.test.ts +0 -35
  253. package/test/result-state.test.ts +0 -110
  254. package/test/task-parsing.test.ts +0 -82
  255. package/test/taskrun-messages.test.ts +0 -224
  256. package/test/tsconfig.json +0 -9
  257. package/test/windows-xml.test.ts +0 -89
  258. 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;