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