happy-coder 0.10.0-3 → 0.10.0

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.
@@ -7,7 +7,7 @@ import { join, basename } from 'node:path';
7
7
  import { readFile, open, stat, unlink, mkdir, writeFile, rename } from 'node:fs/promises';
8
8
  import * as z from 'zod';
9
9
  import { z as z$1 } from 'zod';
10
- import { randomBytes, randomUUID } from 'node:crypto';
10
+ import { randomBytes, createCipheriv, createDecipheriv, randomUUID } from 'node:crypto';
11
11
  import tweetnacl from 'tweetnacl';
12
12
  import { EventEmitter } from 'node:events';
13
13
  import { io } from 'socket.io-client';
@@ -21,7 +21,7 @@ import { platform } from 'os';
21
21
  import { Expo } from 'expo-server-sdk';
22
22
 
23
23
  var name = "happy-coder";
24
- var version = "0.10.0-3";
24
+ var version = "0.10.0";
25
25
  var description = "Mobile and Web client for Claude Code and Codex";
26
26
  var author = "Kirill Dubovitskiy";
27
27
  var license = "MIT";
@@ -81,7 +81,7 @@ var scripts = {
81
81
  build: "shx rm -rf dist && npx tsc --noEmit && pkgroll",
82
82
  test: "yarn build && tsx --env-file .env.integration-test node_modules/.bin/vitest run",
83
83
  start: "yarn build && ./bin/happy.mjs",
84
- dev: "yarn build && tsx --env-file .env.dev src/index.ts",
84
+ dev: "tsx --env-file .env.dev src/index.ts",
85
85
  "dev:local-server": "yarn build && tsx --env-file .env.dev-local-server src/index.ts",
86
86
  "dev:integration-test-env": "yarn build && tsx --env-file .env.integration-test src/index.ts",
87
87
  prepublishOnly: "yarn build && yarn test",
@@ -93,11 +93,13 @@ var dependencies = {
93
93
  "@anthropic-ai/sdk": "^0.56.0",
94
94
  "@modelcontextprotocol/sdk": "^1.15.1",
95
95
  "@stablelib/base64": "^2.0.1",
96
+ "@stablelib/hex": "^2.0.1",
96
97
  "@types/cross-spawn": "^6.0.6",
97
98
  "@types/http-proxy": "^1.17.16",
98
99
  "@types/ps-list": "^6.2.1",
99
100
  "@types/qrcode-terminal": "^0.12.2",
100
101
  "@types/react": "^19.1.9",
102
+ "@types/tmp": "^0.2.6",
101
103
  axios: "^1.10.0",
102
104
  chalk: "^5.4.1",
103
105
  "cross-spawn": "^7.0.6",
@@ -113,6 +115,7 @@ var dependencies = {
113
115
  react: "^19.1.1",
114
116
  "socket.io-client": "^4.8.1",
115
117
  tar: "^7.4.3",
118
+ tmp: "^0.2.5",
116
119
  tweetnacl: "^1.0.3",
117
120
  zod: "^3.23.8"
118
121
  };
@@ -166,6 +169,7 @@ var packageJson = {
166
169
 
167
170
  class Configuration {
168
171
  serverUrl;
172
+ webappUrl;
169
173
  isDaemonProcess;
170
174
  // Directories and paths (from persistence)
171
175
  happyHomeDir;
@@ -178,6 +182,7 @@ class Configuration {
178
182
  isExperimentalEnabled;
179
183
  constructor() {
180
184
  this.serverUrl = process.env.HAPPY_SERVER_URL || "https://api.cluster-fluster.com";
185
+ this.webappUrl = process.env.HAPPY_WEBAPP_URL || "https://app.happy.engineering";
181
186
  const args = process.argv.slice(2);
182
187
  this.isDaemonProcess = args.length >= 2 && args[0] === "daemon" && args[1] === "start-sync";
183
188
  if (process.env.HAPPY_HOME_DIR) {
@@ -222,7 +227,17 @@ function decodeBase64(base64, variant = "base64") {
222
227
  function getRandomBytes(size) {
223
228
  return new Uint8Array(randomBytes(size));
224
229
  }
225
- function encrypt(data, secret) {
230
+ function libsodiumEncryptForPublicKey(data, recipientPublicKey) {
231
+ const ephemeralKeyPair = tweetnacl.box.keyPair();
232
+ const nonce = getRandomBytes(tweetnacl.box.nonceLength);
233
+ const encrypted = tweetnacl.box(data, nonce, recipientPublicKey, ephemeralKeyPair.secretKey);
234
+ const result = new Uint8Array(ephemeralKeyPair.publicKey.length + nonce.length + encrypted.length);
235
+ result.set(ephemeralKeyPair.publicKey, 0);
236
+ result.set(nonce, ephemeralKeyPair.publicKey.length);
237
+ result.set(encrypted, ephemeralKeyPair.publicKey.length + nonce.length);
238
+ return result;
239
+ }
240
+ function encryptLegacy(data, secret) {
226
241
  const nonce = getRandomBytes(tweetnacl.secretbox.nonceLength);
227
242
  const encrypted = tweetnacl.secretbox(new TextEncoder().encode(JSON.stringify(data)), nonce, secret);
228
243
  const result = new Uint8Array(nonce.length + encrypted.length);
@@ -230,7 +245,7 @@ function encrypt(data, secret) {
230
245
  result.set(encrypted, nonce.length);
231
246
  return result;
232
247
  }
233
- function decrypt(data, secret) {
248
+ function decryptLegacy(data, secret) {
234
249
  const nonce = data.slice(0, tweetnacl.secretbox.nonceLength);
235
250
  const encrypted = data.slice(tweetnacl.secretbox.nonceLength);
236
251
  const decrypted = tweetnacl.secretbox.open(encrypted, nonce, secret);
@@ -239,6 +254,61 @@ function decrypt(data, secret) {
239
254
  }
240
255
  return JSON.parse(new TextDecoder().decode(decrypted));
241
256
  }
257
+ function encryptWithDataKey(data, dataKey) {
258
+ const nonce = getRandomBytes(12);
259
+ const cipher = createCipheriv("aes-256-gcm", dataKey, nonce);
260
+ const plaintext = new TextEncoder().encode(JSON.stringify(data));
261
+ const encrypted = Buffer.concat([
262
+ cipher.update(plaintext),
263
+ cipher.final()
264
+ ]);
265
+ const authTag = cipher.getAuthTag();
266
+ const bundle = new Uint8Array(12 + encrypted.length + 16 + 1);
267
+ bundle.set([0], 0);
268
+ bundle.set(nonce, 1);
269
+ bundle.set(new Uint8Array(encrypted), 13);
270
+ bundle.set(new Uint8Array(authTag), 13 + encrypted.length);
271
+ return bundle;
272
+ }
273
+ function decryptWithDataKey(bundle, dataKey) {
274
+ if (bundle.length < 1) {
275
+ return null;
276
+ }
277
+ if (bundle[0] !== 0) {
278
+ return null;
279
+ }
280
+ if (bundle.length < 12 + 16 + 1) {
281
+ return null;
282
+ }
283
+ const nonce = bundle.slice(1, 13);
284
+ const authTag = bundle.slice(bundle.length - 16);
285
+ const ciphertext = bundle.slice(13, bundle.length - 16);
286
+ try {
287
+ const decipher = createDecipheriv("aes-256-gcm", dataKey, nonce);
288
+ decipher.setAuthTag(authTag);
289
+ const decrypted = Buffer.concat([
290
+ decipher.update(ciphertext),
291
+ decipher.final()
292
+ ]);
293
+ return JSON.parse(new TextDecoder().decode(decrypted));
294
+ } catch (error) {
295
+ return null;
296
+ }
297
+ }
298
+ function encrypt(key, variant, data) {
299
+ if (variant === "legacy") {
300
+ return encryptLegacy(data, key);
301
+ } else {
302
+ return encryptWithDataKey(data, key);
303
+ }
304
+ }
305
+ function decrypt(key, variant, data) {
306
+ if (variant === "legacy") {
307
+ return decryptLegacy(data, key);
308
+ } else {
309
+ return decryptWithDataKey(data, key);
310
+ }
311
+ }
242
312
 
243
313
  const defaultSettings = {
244
314
  onboardingCompleted: false
@@ -302,8 +372,13 @@ async function updateSettings(updater) {
302
372
  }
303
373
  }
304
374
  const credentialsSchema = z.object({
305
- secret: z.string().base64(),
306
- token: z.string()
375
+ token: z.string(),
376
+ secret: z.string().base64().nullish(),
377
+ // Legacy
378
+ encryption: z.object({
379
+ publicKey: z.string().base64(),
380
+ machineKey: z.string().base64()
381
+ }).nullish()
307
382
  });
308
383
  async function readCredentials() {
309
384
  if (!existsSync(configuration.privateKeyFile)) {
@@ -312,15 +387,30 @@ async function readCredentials() {
312
387
  try {
313
388
  const keyBase64 = await readFile(configuration.privateKeyFile, "utf8");
314
389
  const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
315
- return {
316
- secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
317
- token: credentials.token
318
- };
390
+ if (credentials.secret) {
391
+ return {
392
+ token: credentials.token,
393
+ encryption: {
394
+ type: "legacy",
395
+ secret: new Uint8Array(Buffer.from(credentials.secret, "base64"))
396
+ }
397
+ };
398
+ } else if (credentials.encryption) {
399
+ return {
400
+ token: credentials.token,
401
+ encryption: {
402
+ type: "dataKey",
403
+ publicKey: new Uint8Array(Buffer.from(credentials.encryption.publicKey, "base64")),
404
+ machineKey: new Uint8Array(Buffer.from(credentials.encryption.machineKey, "base64"))
405
+ }
406
+ };
407
+ }
319
408
  } catch {
320
409
  return null;
321
410
  }
411
+ return null;
322
412
  }
323
- async function writeCredentials(credentials) {
413
+ async function writeCredentialsLegacy(credentials) {
324
414
  if (!existsSync(configuration.happyHomeDir)) {
325
415
  await mkdir(configuration.happyHomeDir, { recursive: true });
326
416
  }
@@ -329,6 +419,15 @@ async function writeCredentials(credentials) {
329
419
  token: credentials.token
330
420
  }, null, 2));
331
421
  }
422
+ async function writeCredentialsDataKey(credentials) {
423
+ if (!existsSync(configuration.happyHomeDir)) {
424
+ await mkdir(configuration.happyHomeDir, { recursive: true });
425
+ }
426
+ await writeFile(configuration.privateKeyFile, JSON.stringify({
427
+ encryption: { publicKey: encodeBase64(credentials.publicKey), machineKey: encodeBase64(credentials.machineKey) },
428
+ token: credentials.token
429
+ }, null, 2));
430
+ }
332
431
  async function clearCredentials() {
333
432
  if (existsSync(configuration.privateKeyFile)) {
334
433
  await unlink(configuration.privateKeyFile);
@@ -663,32 +762,6 @@ z$1.object({
663
762
  ]),
664
763
  createdAt: z$1.number()
665
764
  });
666
- z$1.object({
667
- createdAt: z$1.number(),
668
- id: z$1.string(),
669
- seq: z$1.number(),
670
- updatedAt: z$1.number(),
671
- metadata: z$1.any(),
672
- metadataVersion: z$1.number(),
673
- agentState: z$1.any().nullable(),
674
- agentStateVersion: z$1.number(),
675
- // Connectivity tracking (from server)
676
- connectivityStatus: z$1.union([
677
- z$1.enum(["neverConnected", "online", "offline"]),
678
- z$1.string()
679
- // Forward compatibility
680
- ]).optional(),
681
- connectivityStatusSince: z$1.number().optional(),
682
- connectivityStatusReason: z$1.string().optional(),
683
- // State tracking (from server)
684
- state: z$1.union([
685
- z$1.enum(["running", "archiveRequested", "archived"]),
686
- z$1.string()
687
- // Forward compatibility
688
- ]).optional(),
689
- stateSince: z$1.number().optional(),
690
- stateReason: z$1.string().optional()
691
- });
692
765
  z$1.object({
693
766
  host: z$1.string(),
694
767
  platform: z$1.string(),
@@ -713,37 +786,6 @@ z$1.object({
713
786
  // Forward compatibility
714
787
  ]).optional()
715
788
  });
716
- z$1.object({
717
- id: z$1.string(),
718
- metadata: z$1.any(),
719
- // Decrypted MachineMetadata
720
- metadataVersion: z$1.number(),
721
- daemonState: z$1.any().nullable(),
722
- // Decrypted DaemonState
723
- daemonStateVersion: z$1.number(),
724
- // We don't really care about these on the CLI for now
725
- // ApiMachineClient will not sync these
726
- active: z$1.boolean(),
727
- activeAt: z$1.number(),
728
- createdAt: z$1.number(),
729
- updatedAt: z$1.number(),
730
- // Connectivity tracking (from server)
731
- connectivityStatus: z$1.union([
732
- z$1.enum(["neverConnected", "online", "offline"]),
733
- z$1.string()
734
- // Forward compatibility
735
- ]).optional(),
736
- connectivityStatusSince: z$1.number().optional(),
737
- connectivityStatusReason: z$1.string().optional(),
738
- // State tracking (from server)
739
- state: z$1.union([
740
- z$1.enum(["running", "archiveRequested", "archived"]),
741
- z$1.string()
742
- // Forward compatibility
743
- ]).optional(),
744
- stateSince: z$1.number().optional(),
745
- stateReason: z$1.string().optional()
746
- });
747
789
  z$1.object({
748
790
  content: SessionMessageContentSchema,
749
791
  createdAt: z$1.number(),
@@ -867,12 +909,14 @@ class AsyncLock {
867
909
  class RpcHandlerManager {
868
910
  handlers = /* @__PURE__ */ new Map();
869
911
  scopePrefix;
870
- secret;
912
+ encryptionKey;
913
+ encryptionVariant;
871
914
  logger;
872
915
  socket = null;
873
916
  constructor(config) {
874
917
  this.scopePrefix = config.scopePrefix;
875
- this.secret = config.secret;
918
+ this.encryptionKey = config.encryptionKey;
919
+ this.encryptionVariant = config.encryptionVariant;
876
920
  this.logger = config.logger || ((msg, data) => logger.debug(msg, data));
877
921
  }
878
922
  /**
@@ -898,20 +942,19 @@ class RpcHandlerManager {
898
942
  if (!handler) {
899
943
  this.logger("[RPC] [ERROR] Method not found", { method: request.method });
900
944
  const errorResponse = { error: "Method not found" };
901
- const encryptedError = encodeBase64(encrypt(errorResponse, this.secret));
945
+ const encryptedError = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, errorResponse));
902
946
  return encryptedError;
903
947
  }
904
- const decryptedParams = decrypt(decodeBase64(request.params), this.secret);
948
+ const decryptedParams = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(request.params));
905
949
  const result = await handler(decryptedParams);
906
- const encryptedResponse = encodeBase64(encrypt(result, this.secret));
950
+ const encryptedResponse = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, result));
907
951
  return encryptedResponse;
908
952
  } catch (error) {
909
953
  this.logger("[RPC] [ERROR] Error handling request", { error });
910
954
  const errorResponse = {
911
955
  error: error instanceof Error ? error.message : "Unknown error"
912
956
  };
913
- const encryptedError = encodeBase64(encrypt(errorResponse, this.secret));
914
- return encryptedError;
957
+ return encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, errorResponse));
915
958
  }
916
959
  }
917
960
  onSocketConnect(socket) {
@@ -1256,7 +1299,6 @@ function registerCommonHandlers(rpcHandlerManager) {
1256
1299
 
1257
1300
  class ApiSessionClient extends EventEmitter {
1258
1301
  token;
1259
- secret;
1260
1302
  sessionId;
1261
1303
  metadata;
1262
1304
  metadataVersion;
@@ -1268,18 +1310,22 @@ class ApiSessionClient extends EventEmitter {
1268
1310
  rpcHandlerManager;
1269
1311
  agentStateLock = new AsyncLock();
1270
1312
  metadataLock = new AsyncLock();
1271
- constructor(token, secret, session) {
1313
+ encryptionKey;
1314
+ encryptionVariant;
1315
+ constructor(token, session) {
1272
1316
  super();
1273
1317
  this.token = token;
1274
- this.secret = secret;
1275
1318
  this.sessionId = session.id;
1276
1319
  this.metadata = session.metadata;
1277
1320
  this.metadataVersion = session.metadataVersion;
1278
1321
  this.agentState = session.agentState;
1279
1322
  this.agentStateVersion = session.agentStateVersion;
1323
+ this.encryptionKey = session.encryptionKey;
1324
+ this.encryptionVariant = session.encryptionVariant;
1280
1325
  this.rpcHandlerManager = new RpcHandlerManager({
1281
1326
  scopePrefix: this.sessionId,
1282
- secret: this.secret,
1327
+ encryptionKey: this.encryptionKey,
1328
+ encryptionVariant: this.encryptionVariant,
1283
1329
  logger: (msg, data) => logger.debug(msg, data)
1284
1330
  });
1285
1331
  registerCommonHandlers(this.rpcHandlerManager);
@@ -1321,7 +1367,7 @@ class ApiSessionClient extends EventEmitter {
1321
1367
  return;
1322
1368
  }
1323
1369
  if (data.body.t === "new-message" && data.body.message.content.t === "encrypted") {
1324
- const body = decrypt(decodeBase64(data.body.message.content.c), this.secret);
1370
+ const body = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(data.body.message.content.c));
1325
1371
  logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", body);
1326
1372
  const userResult = UserMessageSchema.safeParse(body);
1327
1373
  if (userResult.success) {
@@ -1335,11 +1381,11 @@ class ApiSessionClient extends EventEmitter {
1335
1381
  }
1336
1382
  } else if (data.body.t === "update-session") {
1337
1383
  if (data.body.metadata && data.body.metadata.version > this.metadataVersion) {
1338
- this.metadata = decrypt(decodeBase64(data.body.metadata.value), this.secret);
1384
+ this.metadata = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(data.body.metadata.value));
1339
1385
  this.metadataVersion = data.body.metadata.version;
1340
1386
  }
1341
1387
  if (data.body.agentState && data.body.agentState.version > this.agentStateVersion) {
1342
- this.agentState = data.body.agentState.value ? decrypt(decodeBase64(data.body.agentState.value), this.secret) : null;
1388
+ this.agentState = data.body.agentState.value ? decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(data.body.agentState.value)) : null;
1343
1389
  this.agentStateVersion = data.body.agentState.version;
1344
1390
  }
1345
1391
  } else if (data.body.t === "update-machine") {
@@ -1393,7 +1439,7 @@ class ApiSessionClient extends EventEmitter {
1393
1439
  };
1394
1440
  }
1395
1441
  logger.debugLargeJson("[SOCKET] Sending message through socket:", content);
1396
- const encrypted = encodeBase64(encrypt(content, this.secret));
1442
+ const encrypted = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, content));
1397
1443
  this.socket.emit("message", {
1398
1444
  sid: this.sessionId,
1399
1445
  message: encrypted
@@ -1427,7 +1473,7 @@ class ApiSessionClient extends EventEmitter {
1427
1473
  sentFrom: "cli"
1428
1474
  }
1429
1475
  };
1430
- const encrypted = encodeBase64(encrypt(content, this.secret));
1476
+ const encrypted = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, content));
1431
1477
  this.socket.emit("message", {
1432
1478
  sid: this.sessionId,
1433
1479
  message: encrypted
@@ -1442,7 +1488,7 @@ class ApiSessionClient extends EventEmitter {
1442
1488
  data: event
1443
1489
  }
1444
1490
  };
1445
- const encrypted = encodeBase64(encrypt(content, this.secret));
1491
+ const encrypted = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, content));
1446
1492
  this.socket.emit("message", {
1447
1493
  sid: this.sessionId,
1448
1494
  message: encrypted
@@ -1502,14 +1548,14 @@ class ApiSessionClient extends EventEmitter {
1502
1548
  this.metadataLock.inLock(async () => {
1503
1549
  await backoff(async () => {
1504
1550
  let updated = handler(this.metadata);
1505
- const answer = await this.socket.emitWithAck("update-metadata", { sid: this.sessionId, expectedVersion: this.metadataVersion, metadata: encodeBase64(encrypt(updated, this.secret)) });
1551
+ const answer = await this.socket.emitWithAck("update-metadata", { sid: this.sessionId, expectedVersion: this.metadataVersion, metadata: encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, updated)) });
1506
1552
  if (answer.result === "success") {
1507
- this.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
1553
+ this.metadata = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(answer.metadata));
1508
1554
  this.metadataVersion = answer.version;
1509
1555
  } else if (answer.result === "version-mismatch") {
1510
1556
  if (answer.version > this.metadataVersion) {
1511
1557
  this.metadataVersion = answer.version;
1512
- this.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
1558
+ this.metadata = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(answer.metadata));
1513
1559
  }
1514
1560
  throw new Error("Metadata version mismatch");
1515
1561
  } else if (answer.result === "error") ;
@@ -1525,15 +1571,15 @@ class ApiSessionClient extends EventEmitter {
1525
1571
  this.agentStateLock.inLock(async () => {
1526
1572
  await backoff(async () => {
1527
1573
  let updated = handler(this.agentState || {});
1528
- const answer = await this.socket.emitWithAck("update-state", { sid: this.sessionId, expectedVersion: this.agentStateVersion, agentState: updated ? encodeBase64(encrypt(updated, this.secret)) : null });
1574
+ const answer = await this.socket.emitWithAck("update-state", { sid: this.sessionId, expectedVersion: this.agentStateVersion, agentState: updated ? encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, updated)) : null });
1529
1575
  if (answer.result === "success") {
1530
- this.agentState = answer.agentState ? decrypt(decodeBase64(answer.agentState), this.secret) : null;
1576
+ this.agentState = answer.agentState ? decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(answer.agentState)) : null;
1531
1577
  this.agentStateVersion = answer.version;
1532
1578
  logger.debug("Agent state updated", this.agentState);
1533
1579
  } else if (answer.result === "version-mismatch") {
1534
1580
  if (answer.version > this.agentStateVersion) {
1535
1581
  this.agentStateVersion = answer.version;
1536
- this.agentState = answer.agentState ? decrypt(decodeBase64(answer.agentState), this.secret) : null;
1582
+ this.agentState = answer.agentState ? decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(answer.agentState)) : null;
1537
1583
  }
1538
1584
  throw new Error("Agent state version mismatch");
1539
1585
  } else if (answer.result === "error") ;
@@ -1563,13 +1609,13 @@ class ApiSessionClient extends EventEmitter {
1563
1609
  }
1564
1610
 
1565
1611
  class ApiMachineClient {
1566
- constructor(token, secret, machine) {
1612
+ constructor(token, machine) {
1567
1613
  this.token = token;
1568
- this.secret = secret;
1569
1614
  this.machine = machine;
1570
1615
  this.rpcHandlerManager = new RpcHandlerManager({
1571
1616
  scopePrefix: this.machine.id,
1572
- secret: this.secret,
1617
+ encryptionKey: this.machine.encryptionKey,
1618
+ encryptionVariant: this.machine.encryptionVariant,
1573
1619
  logger: (msg, data) => logger.debug(msg, data)
1574
1620
  });
1575
1621
  registerCommonHandlers(this.rpcHandlerManager);
@@ -1583,11 +1629,12 @@ class ApiMachineClient {
1583
1629
  requestShutdown
1584
1630
  }) {
1585
1631
  this.rpcHandlerManager.registerHandler("spawn-happy-session", async (params) => {
1586
- const { directory, sessionId, machineId, approvedNewDirectoryCreation } = params || {};
1632
+ const { directory, sessionId, machineId, approvedNewDirectoryCreation, agent, token } = params || {};
1633
+ logger.debug(`[API MACHINE] Spawning session with params: ${JSON.stringify(params)}`);
1587
1634
  if (!directory) {
1588
1635
  throw new Error("Directory is required");
1589
1636
  }
1590
- const result = await spawnSession({ directory, sessionId, machineId, approvedNewDirectoryCreation });
1637
+ const result = await spawnSession({ directory, sessionId, machineId, approvedNewDirectoryCreation, agent, token });
1591
1638
  switch (result.type) {
1592
1639
  case "success":
1593
1640
  logger.debug(`[API MACHINE] Spawned session ${result.sessionId}`);
@@ -1630,17 +1677,17 @@ class ApiMachineClient {
1630
1677
  const updated = handler(this.machine.metadata);
1631
1678
  const answer = await this.socket.emitWithAck("machine-update-metadata", {
1632
1679
  machineId: this.machine.id,
1633
- metadata: encodeBase64(encrypt(updated, this.secret)),
1680
+ metadata: encodeBase64(encrypt(this.machine.encryptionKey, this.machine.encryptionVariant, updated)),
1634
1681
  expectedVersion: this.machine.metadataVersion
1635
1682
  });
1636
1683
  if (answer.result === "success") {
1637
- this.machine.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
1684
+ this.machine.metadata = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(answer.metadata));
1638
1685
  this.machine.metadataVersion = answer.version;
1639
1686
  logger.debug("[API MACHINE] Metadata updated successfully");
1640
1687
  } else if (answer.result === "version-mismatch") {
1641
1688
  if (answer.version > this.machine.metadataVersion) {
1642
1689
  this.machine.metadataVersion = answer.version;
1643
- this.machine.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
1690
+ this.machine.metadata = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(answer.metadata));
1644
1691
  }
1645
1692
  throw new Error("Metadata version mismatch");
1646
1693
  }
@@ -1655,17 +1702,17 @@ class ApiMachineClient {
1655
1702
  const updated = handler(this.machine.daemonState);
1656
1703
  const answer = await this.socket.emitWithAck("machine-update-state", {
1657
1704
  machineId: this.machine.id,
1658
- daemonState: encodeBase64(encrypt(updated, this.secret)),
1705
+ daemonState: encodeBase64(encrypt(this.machine.encryptionKey, this.machine.encryptionVariant, updated)),
1659
1706
  expectedVersion: this.machine.daemonStateVersion
1660
1707
  });
1661
1708
  if (answer.result === "success") {
1662
- this.machine.daemonState = decrypt(decodeBase64(answer.daemonState), this.secret);
1709
+ this.machine.daemonState = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(answer.daemonState));
1663
1710
  this.machine.daemonStateVersion = answer.version;
1664
1711
  logger.debug("[API MACHINE] Daemon state updated successfully");
1665
1712
  } else if (answer.result === "version-mismatch") {
1666
1713
  if (answer.version > this.machine.daemonStateVersion) {
1667
1714
  this.machine.daemonStateVersion = answer.version;
1668
- this.machine.daemonState = decrypt(decodeBase64(answer.daemonState), this.secret);
1715
+ this.machine.daemonState = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(answer.daemonState));
1669
1716
  }
1670
1717
  throw new Error("Daemon state version mismatch");
1671
1718
  }
@@ -1712,12 +1759,12 @@ class ApiMachineClient {
1712
1759
  const update = data.body;
1713
1760
  if (update.metadata) {
1714
1761
  logger.debug("[API MACHINE] Received external metadata update");
1715
- this.machine.metadata = decrypt(decodeBase64(update.metadata.value), this.secret);
1762
+ this.machine.metadata = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(update.metadata.value));
1716
1763
  this.machine.metadataVersion = update.metadata.version;
1717
1764
  }
1718
1765
  if (update.daemonState) {
1719
1766
  logger.debug("[API MACHINE] Received external daemon state update");
1720
- this.machine.daemonState = decrypt(decodeBase64(update.daemonState.value), this.secret);
1767
+ this.machine.daemonState = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(update.daemonState.value));
1721
1768
  this.machine.daemonStateVersion = update.daemonState.version;
1722
1769
  }
1723
1770
  } else {
@@ -1889,46 +1936,62 @@ class PushNotificationClient {
1889
1936
  }
1890
1937
 
1891
1938
  class ApiClient {
1892
- token;
1893
- secret;
1939
+ static async create(credential) {
1940
+ return new ApiClient(credential);
1941
+ }
1942
+ credential;
1894
1943
  pushClient;
1895
- constructor(token, secret) {
1896
- this.token = token;
1897
- this.secret = secret;
1898
- this.pushClient = new PushNotificationClient(token);
1944
+ constructor(credential) {
1945
+ this.credential = credential;
1946
+ this.pushClient = new PushNotificationClient(credential.token, configuration.serverUrl);
1899
1947
  }
1900
1948
  /**
1901
1949
  * Create a new session or load existing one with the given tag
1902
1950
  */
1903
1951
  async getOrCreateSession(opts) {
1952
+ let dataEncryptionKey = null;
1953
+ let encryptionKey;
1954
+ let encryptionVariant;
1955
+ if (this.credential.encryption.type === "dataKey") {
1956
+ encryptionKey = getRandomBytes(32);
1957
+ encryptionVariant = "dataKey";
1958
+ let encryptedDataKey = libsodiumEncryptForPublicKey(encryptionKey, this.credential.encryption.publicKey);
1959
+ dataEncryptionKey = new Uint8Array(encryptedDataKey.length + 1);
1960
+ dataEncryptionKey.set([0], 0);
1961
+ dataEncryptionKey.set(encryptedDataKey, 1);
1962
+ } else {
1963
+ encryptionKey = this.credential.encryption.secret;
1964
+ encryptionVariant = "legacy";
1965
+ }
1904
1966
  try {
1905
1967
  const response = await axios.post(
1906
1968
  `${configuration.serverUrl}/v1/sessions`,
1907
1969
  {
1908
1970
  tag: opts.tag,
1909
- metadata: encodeBase64(encrypt(opts.metadata, this.secret)),
1910
- agentState: opts.state ? encodeBase64(encrypt(opts.state, this.secret)) : null
1971
+ metadata: encodeBase64(encrypt(encryptionKey, encryptionVariant, opts.metadata)),
1972
+ agentState: opts.state ? encodeBase64(encrypt(encryptionKey, encryptionVariant, opts.state)) : null,
1973
+ dataEncryptionKey: dataEncryptionKey ? encodeBase64(dataEncryptionKey) : null
1911
1974
  },
1912
1975
  {
1913
1976
  headers: {
1914
- "Authorization": `Bearer ${this.token}`,
1977
+ "Authorization": `Bearer ${this.credential.token}`,
1915
1978
  "Content-Type": "application/json"
1916
1979
  },
1917
- timeout: 5e3
1918
- // 5 second timeout
1980
+ timeout: 6e4
1981
+ // 1 minute timeout for very bad network connections
1919
1982
  }
1920
1983
  );
1921
1984
  logger.debug(`Session created/loaded: ${response.data.session.id} (tag: ${opts.tag})`);
1922
1985
  let raw = response.data.session;
1923
1986
  let session = {
1924
1987
  id: raw.id,
1925
- createdAt: raw.createdAt,
1926
- updatedAt: raw.updatedAt,
1927
1988
  seq: raw.seq,
1928
- metadata: decrypt(decodeBase64(raw.metadata), this.secret),
1989
+ metadata: decrypt(encryptionKey, encryptionVariant, decodeBase64(raw.metadata)),
1929
1990
  metadataVersion: raw.metadataVersion,
1930
- agentState: raw.agentState ? decrypt(decodeBase64(raw.agentState), this.secret) : null,
1931
- agentStateVersion: raw.agentStateVersion
1991
+ agentState: raw.agentState ? decrypt(encryptionKey, encryptionVariant, decodeBase64(raw.agentState)) : null,
1992
+ agentStateVersion: raw.agentStateVersion,
1993
+ encryptionKey,
1994
+ encryptionVariant
1932
1995
  };
1933
1996
  return session;
1934
1997
  } catch (error) {
@@ -1936,54 +1999,40 @@ class ApiClient {
1936
1999
  throw new Error(`Failed to get or create session: ${error instanceof Error ? error.message : "Unknown error"}`);
1937
2000
  }
1938
2001
  }
1939
- /**
1940
- * Get machine by ID from the server
1941
- * Returns the current machine state from the server with decrypted metadata and daemonState
1942
- */
1943
- async getMachine(machineId) {
1944
- const response = await axios.get(`${configuration.serverUrl}/v1/machines/${machineId}`, {
1945
- headers: {
1946
- "Authorization": `Bearer ${this.token}`,
1947
- "Content-Type": "application/json"
1948
- },
1949
- timeout: 2e3
1950
- });
1951
- const raw = response.data.machine;
1952
- if (!raw) {
1953
- return null;
1954
- }
1955
- logger.debug(`[API] Machine ${machineId} fetched from server`);
1956
- const machine = {
1957
- id: raw.id,
1958
- metadata: raw.metadata ? decrypt(decodeBase64(raw.metadata), this.secret) : null,
1959
- metadataVersion: raw.metadataVersion || 0,
1960
- daemonState: raw.daemonState ? decrypt(decodeBase64(raw.daemonState), this.secret) : null,
1961
- daemonStateVersion: raw.daemonStateVersion || 0,
1962
- active: raw.active,
1963
- activeAt: raw.activeAt,
1964
- createdAt: raw.createdAt,
1965
- updatedAt: raw.updatedAt
1966
- };
1967
- return machine;
1968
- }
1969
2002
  /**
1970
2003
  * Register or update machine with the server
1971
2004
  * Returns the current machine state from the server with decrypted metadata and daemonState
1972
2005
  */
1973
2006
  async getOrCreateMachine(opts) {
2007
+ let dataEncryptionKey = null;
2008
+ let encryptionKey;
2009
+ let encryptionVariant;
2010
+ if (this.credential.encryption.type === "dataKey") {
2011
+ encryptionVariant = "dataKey";
2012
+ encryptionKey = this.credential.encryption.machineKey;
2013
+ let encryptedDataKey = libsodiumEncryptForPublicKey(this.credential.encryption.machineKey, this.credential.encryption.publicKey);
2014
+ dataEncryptionKey = new Uint8Array(encryptedDataKey.length + 1);
2015
+ dataEncryptionKey.set([0], 0);
2016
+ dataEncryptionKey.set(encryptedDataKey, 1);
2017
+ } else {
2018
+ encryptionKey = this.credential.encryption.secret;
2019
+ encryptionVariant = "legacy";
2020
+ }
1974
2021
  const response = await axios.post(
1975
2022
  `${configuration.serverUrl}/v1/machines`,
1976
2023
  {
1977
2024
  id: opts.machineId,
1978
- metadata: encodeBase64(encrypt(opts.metadata, this.secret)),
1979
- daemonState: opts.daemonState ? encodeBase64(encrypt(opts.daemonState, this.secret)) : void 0
2025
+ metadata: encodeBase64(encrypt(encryptionKey, encryptionVariant, opts.metadata)),
2026
+ daemonState: opts.daemonState ? encodeBase64(encrypt(encryptionKey, encryptionVariant, opts.daemonState)) : void 0,
2027
+ dataEncryptionKey: dataEncryptionKey ? encodeBase64(dataEncryptionKey) : void 0
1980
2028
  },
1981
2029
  {
1982
2030
  headers: {
1983
- "Authorization": `Bearer ${this.token}`,
2031
+ "Authorization": `Bearer ${this.credential.token}`,
1984
2032
  "Content-Type": "application/json"
1985
2033
  },
1986
- timeout: 5e3
2034
+ timeout: 6e4
2035
+ // 1 minute timeout for very bad network connections
1987
2036
  }
1988
2037
  );
1989
2038
  if (response.status !== 200) {
@@ -1995,22 +2044,20 @@ class ApiClient {
1995
2044
  logger.debug(`[API] Machine ${opts.machineId} registered/updated with server`);
1996
2045
  const machine = {
1997
2046
  id: raw.id,
1998
- metadata: raw.metadata ? decrypt(decodeBase64(raw.metadata), this.secret) : null,
2047
+ encryptionKey,
2048
+ encryptionVariant,
2049
+ metadata: raw.metadata ? decrypt(encryptionKey, encryptionVariant, decodeBase64(raw.metadata)) : null,
1999
2050
  metadataVersion: raw.metadataVersion || 0,
2000
- daemonState: raw.daemonState ? decrypt(decodeBase64(raw.daemonState), this.secret) : null,
2001
- daemonStateVersion: raw.daemonStateVersion || 0,
2002
- active: raw.active,
2003
- activeAt: raw.activeAt,
2004
- createdAt: raw.createdAt,
2005
- updatedAt: raw.updatedAt
2051
+ daemonState: raw.daemonState ? decrypt(encryptionKey, encryptionVariant, decodeBase64(raw.daemonState)) : null,
2052
+ daemonStateVersion: raw.daemonStateVersion || 0
2006
2053
  };
2007
2054
  return machine;
2008
2055
  }
2009
2056
  sessionSyncClient(session) {
2010
- return new ApiSessionClient(this.token, this.secret, session);
2057
+ return new ApiSessionClient(this.credential.token, session);
2011
2058
  }
2012
2059
  machineSyncClient(machine) {
2013
- return new ApiMachineClient(this.token, this.secret, machine);
2060
+ return new ApiMachineClient(this.credential.token, machine);
2014
2061
  }
2015
2062
  push() {
2016
2063
  return this.pushClient;
@@ -2028,7 +2075,7 @@ class ApiClient {
2028
2075
  },
2029
2076
  {
2030
2077
  headers: {
2031
- "Authorization": `Bearer ${this.token}`,
2078
+ "Authorization": `Bearer ${this.credential.token}`,
2032
2079
  "Content-Type": "application/json"
2033
2080
  },
2034
2081
  timeout: 5e3
@@ -2093,4 +2140,4 @@ const RawJSONLinesSchema = z$1.discriminatedUnion("type", [
2093
2140
  }).passthrough()
2094
2141
  ]);
2095
2142
 
2096
- export { ApiClient as A, RawJSONLinesSchema as R, ApiSessionClient as a, packageJson as b, configuration as c, backoff as d, delay as e, AsyncLock as f, readDaemonState as g, clearDaemonState as h, readCredentials as i, encodeBase64 as j, encodeBase64Url as k, logger as l, decodeBase64 as m, acquireDaemonLock as n, writeDaemonState as o, projectPath as p, releaseDaemonLock as q, readSettings as r, clearCredentials as s, clearMachineId as t, updateSettings as u, getLatestDaemonLog as v, writeCredentials as w };
2143
+ export { ApiClient as A, RawJSONLinesSchema as R, ApiSessionClient as a, packageJson as b, configuration as c, backoff as d, delay as e, AsyncLock as f, readDaemonState as g, clearDaemonState as h, readCredentials as i, encodeBase64 as j, encodeBase64Url as k, logger as l, decodeBase64 as m, writeCredentialsDataKey as n, acquireDaemonLock as o, projectPath as p, writeDaemonState as q, readSettings as r, releaseDaemonLock as s, clearCredentials as t, updateSettings as u, clearMachineId as v, writeCredentialsLegacy as w, getLatestDaemonLog as x };