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.
@@ -42,7 +42,7 @@ function _interopNamespaceDefault(e) {
42
42
  var z__namespace = /*#__PURE__*/_interopNamespaceDefault(z);
43
43
 
44
44
  var name = "happy-coder";
45
- var version = "0.10.0-3";
45
+ var version = "0.10.0";
46
46
  var description = "Mobile and Web client for Claude Code and Codex";
47
47
  var author = "Kirill Dubovitskiy";
48
48
  var license = "MIT";
@@ -102,7 +102,7 @@ var scripts = {
102
102
  build: "shx rm -rf dist && npx tsc --noEmit && pkgroll",
103
103
  test: "yarn build && tsx --env-file .env.integration-test node_modules/.bin/vitest run",
104
104
  start: "yarn build && ./bin/happy.mjs",
105
- dev: "yarn build && tsx --env-file .env.dev src/index.ts",
105
+ dev: "tsx --env-file .env.dev src/index.ts",
106
106
  "dev:local-server": "yarn build && tsx --env-file .env.dev-local-server src/index.ts",
107
107
  "dev:integration-test-env": "yarn build && tsx --env-file .env.integration-test src/index.ts",
108
108
  prepublishOnly: "yarn build && yarn test",
@@ -114,11 +114,13 @@ var dependencies = {
114
114
  "@anthropic-ai/sdk": "^0.56.0",
115
115
  "@modelcontextprotocol/sdk": "^1.15.1",
116
116
  "@stablelib/base64": "^2.0.1",
117
+ "@stablelib/hex": "^2.0.1",
117
118
  "@types/cross-spawn": "^6.0.6",
118
119
  "@types/http-proxy": "^1.17.16",
119
120
  "@types/ps-list": "^6.2.1",
120
121
  "@types/qrcode-terminal": "^0.12.2",
121
122
  "@types/react": "^19.1.9",
123
+ "@types/tmp": "^0.2.6",
122
124
  axios: "^1.10.0",
123
125
  chalk: "^5.4.1",
124
126
  "cross-spawn": "^7.0.6",
@@ -134,6 +136,7 @@ var dependencies = {
134
136
  react: "^19.1.1",
135
137
  "socket.io-client": "^4.8.1",
136
138
  tar: "^7.4.3",
139
+ tmp: "^0.2.5",
137
140
  tweetnacl: "^1.0.3",
138
141
  zod: "^3.23.8"
139
142
  };
@@ -187,6 +190,7 @@ var packageJson = {
187
190
 
188
191
  class Configuration {
189
192
  serverUrl;
193
+ webappUrl;
190
194
  isDaemonProcess;
191
195
  // Directories and paths (from persistence)
192
196
  happyHomeDir;
@@ -199,6 +203,7 @@ class Configuration {
199
203
  isExperimentalEnabled;
200
204
  constructor() {
201
205
  this.serverUrl = process.env.HAPPY_SERVER_URL || "https://api.cluster-fluster.com";
206
+ this.webappUrl = process.env.HAPPY_WEBAPP_URL || "https://app.happy.engineering";
202
207
  const args = process.argv.slice(2);
203
208
  this.isDaemonProcess = args.length >= 2 && args[0] === "daemon" && args[1] === "start-sync";
204
209
  if (process.env.HAPPY_HOME_DIR) {
@@ -243,7 +248,17 @@ function decodeBase64(base64, variant = "base64") {
243
248
  function getRandomBytes(size) {
244
249
  return new Uint8Array(node_crypto.randomBytes(size));
245
250
  }
246
- function encrypt(data, secret) {
251
+ function libsodiumEncryptForPublicKey(data, recipientPublicKey) {
252
+ const ephemeralKeyPair = tweetnacl.box.keyPair();
253
+ const nonce = getRandomBytes(tweetnacl.box.nonceLength);
254
+ const encrypted = tweetnacl.box(data, nonce, recipientPublicKey, ephemeralKeyPair.secretKey);
255
+ const result = new Uint8Array(ephemeralKeyPair.publicKey.length + nonce.length + encrypted.length);
256
+ result.set(ephemeralKeyPair.publicKey, 0);
257
+ result.set(nonce, ephemeralKeyPair.publicKey.length);
258
+ result.set(encrypted, ephemeralKeyPair.publicKey.length + nonce.length);
259
+ return result;
260
+ }
261
+ function encryptLegacy(data, secret) {
247
262
  const nonce = getRandomBytes(tweetnacl.secretbox.nonceLength);
248
263
  const encrypted = tweetnacl.secretbox(new TextEncoder().encode(JSON.stringify(data)), nonce, secret);
249
264
  const result = new Uint8Array(nonce.length + encrypted.length);
@@ -251,7 +266,7 @@ function encrypt(data, secret) {
251
266
  result.set(encrypted, nonce.length);
252
267
  return result;
253
268
  }
254
- function decrypt(data, secret) {
269
+ function decryptLegacy(data, secret) {
255
270
  const nonce = data.slice(0, tweetnacl.secretbox.nonceLength);
256
271
  const encrypted = data.slice(tweetnacl.secretbox.nonceLength);
257
272
  const decrypted = tweetnacl.secretbox.open(encrypted, nonce, secret);
@@ -260,6 +275,61 @@ function decrypt(data, secret) {
260
275
  }
261
276
  return JSON.parse(new TextDecoder().decode(decrypted));
262
277
  }
278
+ function encryptWithDataKey(data, dataKey) {
279
+ const nonce = getRandomBytes(12);
280
+ const cipher = node_crypto.createCipheriv("aes-256-gcm", dataKey, nonce);
281
+ const plaintext = new TextEncoder().encode(JSON.stringify(data));
282
+ const encrypted = Buffer.concat([
283
+ cipher.update(plaintext),
284
+ cipher.final()
285
+ ]);
286
+ const authTag = cipher.getAuthTag();
287
+ const bundle = new Uint8Array(12 + encrypted.length + 16 + 1);
288
+ bundle.set([0], 0);
289
+ bundle.set(nonce, 1);
290
+ bundle.set(new Uint8Array(encrypted), 13);
291
+ bundle.set(new Uint8Array(authTag), 13 + encrypted.length);
292
+ return bundle;
293
+ }
294
+ function decryptWithDataKey(bundle, dataKey) {
295
+ if (bundle.length < 1) {
296
+ return null;
297
+ }
298
+ if (bundle[0] !== 0) {
299
+ return null;
300
+ }
301
+ if (bundle.length < 12 + 16 + 1) {
302
+ return null;
303
+ }
304
+ const nonce = bundle.slice(1, 13);
305
+ const authTag = bundle.slice(bundle.length - 16);
306
+ const ciphertext = bundle.slice(13, bundle.length - 16);
307
+ try {
308
+ const decipher = node_crypto.createDecipheriv("aes-256-gcm", dataKey, nonce);
309
+ decipher.setAuthTag(authTag);
310
+ const decrypted = Buffer.concat([
311
+ decipher.update(ciphertext),
312
+ decipher.final()
313
+ ]);
314
+ return JSON.parse(new TextDecoder().decode(decrypted));
315
+ } catch (error) {
316
+ return null;
317
+ }
318
+ }
319
+ function encrypt(key, variant, data) {
320
+ if (variant === "legacy") {
321
+ return encryptLegacy(data, key);
322
+ } else {
323
+ return encryptWithDataKey(data, key);
324
+ }
325
+ }
326
+ function decrypt(key, variant, data) {
327
+ if (variant === "legacy") {
328
+ return decryptLegacy(data, key);
329
+ } else {
330
+ return decryptWithDataKey(data, key);
331
+ }
332
+ }
263
333
 
264
334
  const defaultSettings = {
265
335
  onboardingCompleted: false
@@ -323,8 +393,13 @@ async function updateSettings(updater) {
323
393
  }
324
394
  }
325
395
  const credentialsSchema = z__namespace.object({
326
- secret: z__namespace.string().base64(),
327
- token: z__namespace.string()
396
+ token: z__namespace.string(),
397
+ secret: z__namespace.string().base64().nullish(),
398
+ // Legacy
399
+ encryption: z__namespace.object({
400
+ publicKey: z__namespace.string().base64(),
401
+ machineKey: z__namespace.string().base64()
402
+ }).nullish()
328
403
  });
329
404
  async function readCredentials() {
330
405
  if (!fs.existsSync(configuration.privateKeyFile)) {
@@ -333,15 +408,30 @@ async function readCredentials() {
333
408
  try {
334
409
  const keyBase64 = await promises.readFile(configuration.privateKeyFile, "utf8");
335
410
  const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
336
- return {
337
- secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
338
- token: credentials.token
339
- };
411
+ if (credentials.secret) {
412
+ return {
413
+ token: credentials.token,
414
+ encryption: {
415
+ type: "legacy",
416
+ secret: new Uint8Array(Buffer.from(credentials.secret, "base64"))
417
+ }
418
+ };
419
+ } else if (credentials.encryption) {
420
+ return {
421
+ token: credentials.token,
422
+ encryption: {
423
+ type: "dataKey",
424
+ publicKey: new Uint8Array(Buffer.from(credentials.encryption.publicKey, "base64")),
425
+ machineKey: new Uint8Array(Buffer.from(credentials.encryption.machineKey, "base64"))
426
+ }
427
+ };
428
+ }
340
429
  } catch {
341
430
  return null;
342
431
  }
432
+ return null;
343
433
  }
344
- async function writeCredentials(credentials) {
434
+ async function writeCredentialsLegacy(credentials) {
345
435
  if (!fs.existsSync(configuration.happyHomeDir)) {
346
436
  await promises.mkdir(configuration.happyHomeDir, { recursive: true });
347
437
  }
@@ -350,6 +440,15 @@ async function writeCredentials(credentials) {
350
440
  token: credentials.token
351
441
  }, null, 2));
352
442
  }
443
+ async function writeCredentialsDataKey(credentials) {
444
+ if (!fs.existsSync(configuration.happyHomeDir)) {
445
+ await promises.mkdir(configuration.happyHomeDir, { recursive: true });
446
+ }
447
+ await promises.writeFile(configuration.privateKeyFile, JSON.stringify({
448
+ encryption: { publicKey: encodeBase64(credentials.publicKey), machineKey: encodeBase64(credentials.machineKey) },
449
+ token: credentials.token
450
+ }, null, 2));
451
+ }
353
452
  async function clearCredentials() {
354
453
  if (fs.existsSync(configuration.privateKeyFile)) {
355
454
  await promises.unlink(configuration.privateKeyFile);
@@ -684,32 +783,6 @@ z.z.object({
684
783
  ]),
685
784
  createdAt: z.z.number()
686
785
  });
687
- z.z.object({
688
- createdAt: z.z.number(),
689
- id: z.z.string(),
690
- seq: z.z.number(),
691
- updatedAt: z.z.number(),
692
- metadata: z.z.any(),
693
- metadataVersion: z.z.number(),
694
- agentState: z.z.any().nullable(),
695
- agentStateVersion: z.z.number(),
696
- // Connectivity tracking (from server)
697
- connectivityStatus: z.z.union([
698
- z.z.enum(["neverConnected", "online", "offline"]),
699
- z.z.string()
700
- // Forward compatibility
701
- ]).optional(),
702
- connectivityStatusSince: z.z.number().optional(),
703
- connectivityStatusReason: z.z.string().optional(),
704
- // State tracking (from server)
705
- state: z.z.union([
706
- z.z.enum(["running", "archiveRequested", "archived"]),
707
- z.z.string()
708
- // Forward compatibility
709
- ]).optional(),
710
- stateSince: z.z.number().optional(),
711
- stateReason: z.z.string().optional()
712
- });
713
786
  z.z.object({
714
787
  host: z.z.string(),
715
788
  platform: z.z.string(),
@@ -734,37 +807,6 @@ z.z.object({
734
807
  // Forward compatibility
735
808
  ]).optional()
736
809
  });
737
- z.z.object({
738
- id: z.z.string(),
739
- metadata: z.z.any(),
740
- // Decrypted MachineMetadata
741
- metadataVersion: z.z.number(),
742
- daemonState: z.z.any().nullable(),
743
- // Decrypted DaemonState
744
- daemonStateVersion: z.z.number(),
745
- // We don't really care about these on the CLI for now
746
- // ApiMachineClient will not sync these
747
- active: z.z.boolean(),
748
- activeAt: z.z.number(),
749
- createdAt: z.z.number(),
750
- updatedAt: z.z.number(),
751
- // Connectivity tracking (from server)
752
- connectivityStatus: z.z.union([
753
- z.z.enum(["neverConnected", "online", "offline"]),
754
- z.z.string()
755
- // Forward compatibility
756
- ]).optional(),
757
- connectivityStatusSince: z.z.number().optional(),
758
- connectivityStatusReason: z.z.string().optional(),
759
- // State tracking (from server)
760
- state: z.z.union([
761
- z.z.enum(["running", "archiveRequested", "archived"]),
762
- z.z.string()
763
- // Forward compatibility
764
- ]).optional(),
765
- stateSince: z.z.number().optional(),
766
- stateReason: z.z.string().optional()
767
- });
768
810
  z.z.object({
769
811
  content: SessionMessageContentSchema,
770
812
  createdAt: z.z.number(),
@@ -888,12 +930,14 @@ class AsyncLock {
888
930
  class RpcHandlerManager {
889
931
  handlers = /* @__PURE__ */ new Map();
890
932
  scopePrefix;
891
- secret;
933
+ encryptionKey;
934
+ encryptionVariant;
892
935
  logger;
893
936
  socket = null;
894
937
  constructor(config) {
895
938
  this.scopePrefix = config.scopePrefix;
896
- this.secret = config.secret;
939
+ this.encryptionKey = config.encryptionKey;
940
+ this.encryptionVariant = config.encryptionVariant;
897
941
  this.logger = config.logger || ((msg, data) => logger.debug(msg, data));
898
942
  }
899
943
  /**
@@ -919,20 +963,19 @@ class RpcHandlerManager {
919
963
  if (!handler) {
920
964
  this.logger("[RPC] [ERROR] Method not found", { method: request.method });
921
965
  const errorResponse = { error: "Method not found" };
922
- const encryptedError = encodeBase64(encrypt(errorResponse, this.secret));
966
+ const encryptedError = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, errorResponse));
923
967
  return encryptedError;
924
968
  }
925
- const decryptedParams = decrypt(decodeBase64(request.params), this.secret);
969
+ const decryptedParams = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(request.params));
926
970
  const result = await handler(decryptedParams);
927
- const encryptedResponse = encodeBase64(encrypt(result, this.secret));
971
+ const encryptedResponse = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, result));
928
972
  return encryptedResponse;
929
973
  } catch (error) {
930
974
  this.logger("[RPC] [ERROR] Error handling request", { error });
931
975
  const errorResponse = {
932
976
  error: error instanceof Error ? error.message : "Unknown error"
933
977
  };
934
- const encryptedError = encodeBase64(encrypt(errorResponse, this.secret));
935
- return encryptedError;
978
+ return encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, errorResponse));
936
979
  }
937
980
  }
938
981
  onSocketConnect(socket) {
@@ -974,7 +1017,7 @@ class RpcHandlerManager {
974
1017
  }
975
1018
  }
976
1019
 
977
- const __dirname$1 = path.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-CsJGQvQ3.cjs', document.baseURI).href))));
1020
+ const __dirname$1 = path.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-CQOz_mPp.cjs', document.baseURI).href))));
978
1021
  function projectPath() {
979
1022
  const path$1 = path.resolve(__dirname$1, "..");
980
1023
  return path$1;
@@ -1277,7 +1320,6 @@ function registerCommonHandlers(rpcHandlerManager) {
1277
1320
 
1278
1321
  class ApiSessionClient extends node_events.EventEmitter {
1279
1322
  token;
1280
- secret;
1281
1323
  sessionId;
1282
1324
  metadata;
1283
1325
  metadataVersion;
@@ -1289,18 +1331,22 @@ class ApiSessionClient extends node_events.EventEmitter {
1289
1331
  rpcHandlerManager;
1290
1332
  agentStateLock = new AsyncLock();
1291
1333
  metadataLock = new AsyncLock();
1292
- constructor(token, secret, session) {
1334
+ encryptionKey;
1335
+ encryptionVariant;
1336
+ constructor(token, session) {
1293
1337
  super();
1294
1338
  this.token = token;
1295
- this.secret = secret;
1296
1339
  this.sessionId = session.id;
1297
1340
  this.metadata = session.metadata;
1298
1341
  this.metadataVersion = session.metadataVersion;
1299
1342
  this.agentState = session.agentState;
1300
1343
  this.agentStateVersion = session.agentStateVersion;
1344
+ this.encryptionKey = session.encryptionKey;
1345
+ this.encryptionVariant = session.encryptionVariant;
1301
1346
  this.rpcHandlerManager = new RpcHandlerManager({
1302
1347
  scopePrefix: this.sessionId,
1303
- secret: this.secret,
1348
+ encryptionKey: this.encryptionKey,
1349
+ encryptionVariant: this.encryptionVariant,
1304
1350
  logger: (msg, data) => logger.debug(msg, data)
1305
1351
  });
1306
1352
  registerCommonHandlers(this.rpcHandlerManager);
@@ -1342,7 +1388,7 @@ class ApiSessionClient extends node_events.EventEmitter {
1342
1388
  return;
1343
1389
  }
1344
1390
  if (data.body.t === "new-message" && data.body.message.content.t === "encrypted") {
1345
- const body = decrypt(decodeBase64(data.body.message.content.c), this.secret);
1391
+ const body = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(data.body.message.content.c));
1346
1392
  logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", body);
1347
1393
  const userResult = UserMessageSchema.safeParse(body);
1348
1394
  if (userResult.success) {
@@ -1356,11 +1402,11 @@ class ApiSessionClient extends node_events.EventEmitter {
1356
1402
  }
1357
1403
  } else if (data.body.t === "update-session") {
1358
1404
  if (data.body.metadata && data.body.metadata.version > this.metadataVersion) {
1359
- this.metadata = decrypt(decodeBase64(data.body.metadata.value), this.secret);
1405
+ this.metadata = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(data.body.metadata.value));
1360
1406
  this.metadataVersion = data.body.metadata.version;
1361
1407
  }
1362
1408
  if (data.body.agentState && data.body.agentState.version > this.agentStateVersion) {
1363
- this.agentState = data.body.agentState.value ? decrypt(decodeBase64(data.body.agentState.value), this.secret) : null;
1409
+ this.agentState = data.body.agentState.value ? decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(data.body.agentState.value)) : null;
1364
1410
  this.agentStateVersion = data.body.agentState.version;
1365
1411
  }
1366
1412
  } else if (data.body.t === "update-machine") {
@@ -1414,7 +1460,7 @@ class ApiSessionClient extends node_events.EventEmitter {
1414
1460
  };
1415
1461
  }
1416
1462
  logger.debugLargeJson("[SOCKET] Sending message through socket:", content);
1417
- const encrypted = encodeBase64(encrypt(content, this.secret));
1463
+ const encrypted = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, content));
1418
1464
  this.socket.emit("message", {
1419
1465
  sid: this.sessionId,
1420
1466
  message: encrypted
@@ -1448,7 +1494,7 @@ class ApiSessionClient extends node_events.EventEmitter {
1448
1494
  sentFrom: "cli"
1449
1495
  }
1450
1496
  };
1451
- const encrypted = encodeBase64(encrypt(content, this.secret));
1497
+ const encrypted = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, content));
1452
1498
  this.socket.emit("message", {
1453
1499
  sid: this.sessionId,
1454
1500
  message: encrypted
@@ -1463,7 +1509,7 @@ class ApiSessionClient extends node_events.EventEmitter {
1463
1509
  data: event
1464
1510
  }
1465
1511
  };
1466
- const encrypted = encodeBase64(encrypt(content, this.secret));
1512
+ const encrypted = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, content));
1467
1513
  this.socket.emit("message", {
1468
1514
  sid: this.sessionId,
1469
1515
  message: encrypted
@@ -1523,14 +1569,14 @@ class ApiSessionClient extends node_events.EventEmitter {
1523
1569
  this.metadataLock.inLock(async () => {
1524
1570
  await backoff(async () => {
1525
1571
  let updated = handler(this.metadata);
1526
- const answer = await this.socket.emitWithAck("update-metadata", { sid: this.sessionId, expectedVersion: this.metadataVersion, metadata: encodeBase64(encrypt(updated, this.secret)) });
1572
+ const answer = await this.socket.emitWithAck("update-metadata", { sid: this.sessionId, expectedVersion: this.metadataVersion, metadata: encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, updated)) });
1527
1573
  if (answer.result === "success") {
1528
- this.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
1574
+ this.metadata = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(answer.metadata));
1529
1575
  this.metadataVersion = answer.version;
1530
1576
  } else if (answer.result === "version-mismatch") {
1531
1577
  if (answer.version > this.metadataVersion) {
1532
1578
  this.metadataVersion = answer.version;
1533
- this.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
1579
+ this.metadata = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(answer.metadata));
1534
1580
  }
1535
1581
  throw new Error("Metadata version mismatch");
1536
1582
  } else if (answer.result === "error") ;
@@ -1546,15 +1592,15 @@ class ApiSessionClient extends node_events.EventEmitter {
1546
1592
  this.agentStateLock.inLock(async () => {
1547
1593
  await backoff(async () => {
1548
1594
  let updated = handler(this.agentState || {});
1549
- const answer = await this.socket.emitWithAck("update-state", { sid: this.sessionId, expectedVersion: this.agentStateVersion, agentState: updated ? encodeBase64(encrypt(updated, this.secret)) : null });
1595
+ const answer = await this.socket.emitWithAck("update-state", { sid: this.sessionId, expectedVersion: this.agentStateVersion, agentState: updated ? encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, updated)) : null });
1550
1596
  if (answer.result === "success") {
1551
- this.agentState = answer.agentState ? decrypt(decodeBase64(answer.agentState), this.secret) : null;
1597
+ this.agentState = answer.agentState ? decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(answer.agentState)) : null;
1552
1598
  this.agentStateVersion = answer.version;
1553
1599
  logger.debug("Agent state updated", this.agentState);
1554
1600
  } else if (answer.result === "version-mismatch") {
1555
1601
  if (answer.version > this.agentStateVersion) {
1556
1602
  this.agentStateVersion = answer.version;
1557
- this.agentState = answer.agentState ? decrypt(decodeBase64(answer.agentState), this.secret) : null;
1603
+ this.agentState = answer.agentState ? decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(answer.agentState)) : null;
1558
1604
  }
1559
1605
  throw new Error("Agent state version mismatch");
1560
1606
  } else if (answer.result === "error") ;
@@ -1584,13 +1630,13 @@ class ApiSessionClient extends node_events.EventEmitter {
1584
1630
  }
1585
1631
 
1586
1632
  class ApiMachineClient {
1587
- constructor(token, secret, machine) {
1633
+ constructor(token, machine) {
1588
1634
  this.token = token;
1589
- this.secret = secret;
1590
1635
  this.machine = machine;
1591
1636
  this.rpcHandlerManager = new RpcHandlerManager({
1592
1637
  scopePrefix: this.machine.id,
1593
- secret: this.secret,
1638
+ encryptionKey: this.machine.encryptionKey,
1639
+ encryptionVariant: this.machine.encryptionVariant,
1594
1640
  logger: (msg, data) => logger.debug(msg, data)
1595
1641
  });
1596
1642
  registerCommonHandlers(this.rpcHandlerManager);
@@ -1604,11 +1650,12 @@ class ApiMachineClient {
1604
1650
  requestShutdown
1605
1651
  }) {
1606
1652
  this.rpcHandlerManager.registerHandler("spawn-happy-session", async (params) => {
1607
- const { directory, sessionId, machineId, approvedNewDirectoryCreation } = params || {};
1653
+ const { directory, sessionId, machineId, approvedNewDirectoryCreation, agent, token } = params || {};
1654
+ logger.debug(`[API MACHINE] Spawning session with params: ${JSON.stringify(params)}`);
1608
1655
  if (!directory) {
1609
1656
  throw new Error("Directory is required");
1610
1657
  }
1611
- const result = await spawnSession({ directory, sessionId, machineId, approvedNewDirectoryCreation });
1658
+ const result = await spawnSession({ directory, sessionId, machineId, approvedNewDirectoryCreation, agent, token });
1612
1659
  switch (result.type) {
1613
1660
  case "success":
1614
1661
  logger.debug(`[API MACHINE] Spawned session ${result.sessionId}`);
@@ -1651,17 +1698,17 @@ class ApiMachineClient {
1651
1698
  const updated = handler(this.machine.metadata);
1652
1699
  const answer = await this.socket.emitWithAck("machine-update-metadata", {
1653
1700
  machineId: this.machine.id,
1654
- metadata: encodeBase64(encrypt(updated, this.secret)),
1701
+ metadata: encodeBase64(encrypt(this.machine.encryptionKey, this.machine.encryptionVariant, updated)),
1655
1702
  expectedVersion: this.machine.metadataVersion
1656
1703
  });
1657
1704
  if (answer.result === "success") {
1658
- this.machine.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
1705
+ this.machine.metadata = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(answer.metadata));
1659
1706
  this.machine.metadataVersion = answer.version;
1660
1707
  logger.debug("[API MACHINE] Metadata updated successfully");
1661
1708
  } else if (answer.result === "version-mismatch") {
1662
1709
  if (answer.version > this.machine.metadataVersion) {
1663
1710
  this.machine.metadataVersion = answer.version;
1664
- this.machine.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
1711
+ this.machine.metadata = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(answer.metadata));
1665
1712
  }
1666
1713
  throw new Error("Metadata version mismatch");
1667
1714
  }
@@ -1676,17 +1723,17 @@ class ApiMachineClient {
1676
1723
  const updated = handler(this.machine.daemonState);
1677
1724
  const answer = await this.socket.emitWithAck("machine-update-state", {
1678
1725
  machineId: this.machine.id,
1679
- daemonState: encodeBase64(encrypt(updated, this.secret)),
1726
+ daemonState: encodeBase64(encrypt(this.machine.encryptionKey, this.machine.encryptionVariant, updated)),
1680
1727
  expectedVersion: this.machine.daemonStateVersion
1681
1728
  });
1682
1729
  if (answer.result === "success") {
1683
- this.machine.daemonState = decrypt(decodeBase64(answer.daemonState), this.secret);
1730
+ this.machine.daemonState = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(answer.daemonState));
1684
1731
  this.machine.daemonStateVersion = answer.version;
1685
1732
  logger.debug("[API MACHINE] Daemon state updated successfully");
1686
1733
  } else if (answer.result === "version-mismatch") {
1687
1734
  if (answer.version > this.machine.daemonStateVersion) {
1688
1735
  this.machine.daemonStateVersion = answer.version;
1689
- this.machine.daemonState = decrypt(decodeBase64(answer.daemonState), this.secret);
1736
+ this.machine.daemonState = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(answer.daemonState));
1690
1737
  }
1691
1738
  throw new Error("Daemon state version mismatch");
1692
1739
  }
@@ -1733,12 +1780,12 @@ class ApiMachineClient {
1733
1780
  const update = data.body;
1734
1781
  if (update.metadata) {
1735
1782
  logger.debug("[API MACHINE] Received external metadata update");
1736
- this.machine.metadata = decrypt(decodeBase64(update.metadata.value), this.secret);
1783
+ this.machine.metadata = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(update.metadata.value));
1737
1784
  this.machine.metadataVersion = update.metadata.version;
1738
1785
  }
1739
1786
  if (update.daemonState) {
1740
1787
  logger.debug("[API MACHINE] Received external daemon state update");
1741
- this.machine.daemonState = decrypt(decodeBase64(update.daemonState.value), this.secret);
1788
+ this.machine.daemonState = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(update.daemonState.value));
1742
1789
  this.machine.daemonStateVersion = update.daemonState.version;
1743
1790
  }
1744
1791
  } else {
@@ -1910,46 +1957,62 @@ class PushNotificationClient {
1910
1957
  }
1911
1958
 
1912
1959
  class ApiClient {
1913
- token;
1914
- secret;
1960
+ static async create(credential) {
1961
+ return new ApiClient(credential);
1962
+ }
1963
+ credential;
1915
1964
  pushClient;
1916
- constructor(token, secret) {
1917
- this.token = token;
1918
- this.secret = secret;
1919
- this.pushClient = new PushNotificationClient(token);
1965
+ constructor(credential) {
1966
+ this.credential = credential;
1967
+ this.pushClient = new PushNotificationClient(credential.token, configuration.serverUrl);
1920
1968
  }
1921
1969
  /**
1922
1970
  * Create a new session or load existing one with the given tag
1923
1971
  */
1924
1972
  async getOrCreateSession(opts) {
1973
+ let dataEncryptionKey = null;
1974
+ let encryptionKey;
1975
+ let encryptionVariant;
1976
+ if (this.credential.encryption.type === "dataKey") {
1977
+ encryptionKey = getRandomBytes(32);
1978
+ encryptionVariant = "dataKey";
1979
+ let encryptedDataKey = libsodiumEncryptForPublicKey(encryptionKey, this.credential.encryption.publicKey);
1980
+ dataEncryptionKey = new Uint8Array(encryptedDataKey.length + 1);
1981
+ dataEncryptionKey.set([0], 0);
1982
+ dataEncryptionKey.set(encryptedDataKey, 1);
1983
+ } else {
1984
+ encryptionKey = this.credential.encryption.secret;
1985
+ encryptionVariant = "legacy";
1986
+ }
1925
1987
  try {
1926
1988
  const response = await axios.post(
1927
1989
  `${configuration.serverUrl}/v1/sessions`,
1928
1990
  {
1929
1991
  tag: opts.tag,
1930
- metadata: encodeBase64(encrypt(opts.metadata, this.secret)),
1931
- agentState: opts.state ? encodeBase64(encrypt(opts.state, this.secret)) : null
1992
+ metadata: encodeBase64(encrypt(encryptionKey, encryptionVariant, opts.metadata)),
1993
+ agentState: opts.state ? encodeBase64(encrypt(encryptionKey, encryptionVariant, opts.state)) : null,
1994
+ dataEncryptionKey: dataEncryptionKey ? encodeBase64(dataEncryptionKey) : null
1932
1995
  },
1933
1996
  {
1934
1997
  headers: {
1935
- "Authorization": `Bearer ${this.token}`,
1998
+ "Authorization": `Bearer ${this.credential.token}`,
1936
1999
  "Content-Type": "application/json"
1937
2000
  },
1938
- timeout: 5e3
1939
- // 5 second timeout
2001
+ timeout: 6e4
2002
+ // 1 minute timeout for very bad network connections
1940
2003
  }
1941
2004
  );
1942
2005
  logger.debug(`Session created/loaded: ${response.data.session.id} (tag: ${opts.tag})`);
1943
2006
  let raw = response.data.session;
1944
2007
  let session = {
1945
2008
  id: raw.id,
1946
- createdAt: raw.createdAt,
1947
- updatedAt: raw.updatedAt,
1948
2009
  seq: raw.seq,
1949
- metadata: decrypt(decodeBase64(raw.metadata), this.secret),
2010
+ metadata: decrypt(encryptionKey, encryptionVariant, decodeBase64(raw.metadata)),
1950
2011
  metadataVersion: raw.metadataVersion,
1951
- agentState: raw.agentState ? decrypt(decodeBase64(raw.agentState), this.secret) : null,
1952
- agentStateVersion: raw.agentStateVersion
2012
+ agentState: raw.agentState ? decrypt(encryptionKey, encryptionVariant, decodeBase64(raw.agentState)) : null,
2013
+ agentStateVersion: raw.agentStateVersion,
2014
+ encryptionKey,
2015
+ encryptionVariant
1953
2016
  };
1954
2017
  return session;
1955
2018
  } catch (error) {
@@ -1957,54 +2020,40 @@ class ApiClient {
1957
2020
  throw new Error(`Failed to get or create session: ${error instanceof Error ? error.message : "Unknown error"}`);
1958
2021
  }
1959
2022
  }
1960
- /**
1961
- * Get machine by ID from the server
1962
- * Returns the current machine state from the server with decrypted metadata and daemonState
1963
- */
1964
- async getMachine(machineId) {
1965
- const response = await axios.get(`${configuration.serverUrl}/v1/machines/${machineId}`, {
1966
- headers: {
1967
- "Authorization": `Bearer ${this.token}`,
1968
- "Content-Type": "application/json"
1969
- },
1970
- timeout: 2e3
1971
- });
1972
- const raw = response.data.machine;
1973
- if (!raw) {
1974
- return null;
1975
- }
1976
- logger.debug(`[API] Machine ${machineId} fetched from server`);
1977
- const machine = {
1978
- id: raw.id,
1979
- metadata: raw.metadata ? decrypt(decodeBase64(raw.metadata), this.secret) : null,
1980
- metadataVersion: raw.metadataVersion || 0,
1981
- daemonState: raw.daemonState ? decrypt(decodeBase64(raw.daemonState), this.secret) : null,
1982
- daemonStateVersion: raw.daemonStateVersion || 0,
1983
- active: raw.active,
1984
- activeAt: raw.activeAt,
1985
- createdAt: raw.createdAt,
1986
- updatedAt: raw.updatedAt
1987
- };
1988
- return machine;
1989
- }
1990
2023
  /**
1991
2024
  * Register or update machine with the server
1992
2025
  * Returns the current machine state from the server with decrypted metadata and daemonState
1993
2026
  */
1994
2027
  async getOrCreateMachine(opts) {
2028
+ let dataEncryptionKey = null;
2029
+ let encryptionKey;
2030
+ let encryptionVariant;
2031
+ if (this.credential.encryption.type === "dataKey") {
2032
+ encryptionVariant = "dataKey";
2033
+ encryptionKey = this.credential.encryption.machineKey;
2034
+ let encryptedDataKey = libsodiumEncryptForPublicKey(this.credential.encryption.machineKey, this.credential.encryption.publicKey);
2035
+ dataEncryptionKey = new Uint8Array(encryptedDataKey.length + 1);
2036
+ dataEncryptionKey.set([0], 0);
2037
+ dataEncryptionKey.set(encryptedDataKey, 1);
2038
+ } else {
2039
+ encryptionKey = this.credential.encryption.secret;
2040
+ encryptionVariant = "legacy";
2041
+ }
1995
2042
  const response = await axios.post(
1996
2043
  `${configuration.serverUrl}/v1/machines`,
1997
2044
  {
1998
2045
  id: opts.machineId,
1999
- metadata: encodeBase64(encrypt(opts.metadata, this.secret)),
2000
- daemonState: opts.daemonState ? encodeBase64(encrypt(opts.daemonState, this.secret)) : void 0
2046
+ metadata: encodeBase64(encrypt(encryptionKey, encryptionVariant, opts.metadata)),
2047
+ daemonState: opts.daemonState ? encodeBase64(encrypt(encryptionKey, encryptionVariant, opts.daemonState)) : void 0,
2048
+ dataEncryptionKey: dataEncryptionKey ? encodeBase64(dataEncryptionKey) : void 0
2001
2049
  },
2002
2050
  {
2003
2051
  headers: {
2004
- "Authorization": `Bearer ${this.token}`,
2052
+ "Authorization": `Bearer ${this.credential.token}`,
2005
2053
  "Content-Type": "application/json"
2006
2054
  },
2007
- timeout: 5e3
2055
+ timeout: 6e4
2056
+ // 1 minute timeout for very bad network connections
2008
2057
  }
2009
2058
  );
2010
2059
  if (response.status !== 200) {
@@ -2016,22 +2065,20 @@ class ApiClient {
2016
2065
  logger.debug(`[API] Machine ${opts.machineId} registered/updated with server`);
2017
2066
  const machine = {
2018
2067
  id: raw.id,
2019
- metadata: raw.metadata ? decrypt(decodeBase64(raw.metadata), this.secret) : null,
2068
+ encryptionKey,
2069
+ encryptionVariant,
2070
+ metadata: raw.metadata ? decrypt(encryptionKey, encryptionVariant, decodeBase64(raw.metadata)) : null,
2020
2071
  metadataVersion: raw.metadataVersion || 0,
2021
- daemonState: raw.daemonState ? decrypt(decodeBase64(raw.daemonState), this.secret) : null,
2022
- daemonStateVersion: raw.daemonStateVersion || 0,
2023
- active: raw.active,
2024
- activeAt: raw.activeAt,
2025
- createdAt: raw.createdAt,
2026
- updatedAt: raw.updatedAt
2072
+ daemonState: raw.daemonState ? decrypt(encryptionKey, encryptionVariant, decodeBase64(raw.daemonState)) : null,
2073
+ daemonStateVersion: raw.daemonStateVersion || 0
2027
2074
  };
2028
2075
  return machine;
2029
2076
  }
2030
2077
  sessionSyncClient(session) {
2031
- return new ApiSessionClient(this.token, this.secret, session);
2078
+ return new ApiSessionClient(this.credential.token, session);
2032
2079
  }
2033
2080
  machineSyncClient(machine) {
2034
- return new ApiMachineClient(this.token, this.secret, machine);
2081
+ return new ApiMachineClient(this.credential.token, machine);
2035
2082
  }
2036
2083
  push() {
2037
2084
  return this.pushClient;
@@ -2049,7 +2096,7 @@ class ApiClient {
2049
2096
  },
2050
2097
  {
2051
2098
  headers: {
2052
- "Authorization": `Bearer ${this.token}`,
2099
+ "Authorization": `Bearer ${this.credential.token}`,
2053
2100
  "Content-Type": "application/json"
2054
2101
  },
2055
2102
  timeout: 5e3
@@ -2137,5 +2184,6 @@ exports.readDaemonState = readDaemonState;
2137
2184
  exports.readSettings = readSettings;
2138
2185
  exports.releaseDaemonLock = releaseDaemonLock;
2139
2186
  exports.updateSettings = updateSettings;
2140
- exports.writeCredentials = writeCredentials;
2187
+ exports.writeCredentialsDataKey = writeCredentialsDataKey;
2188
+ exports.writeCredentialsLegacy = writeCredentialsLegacy;
2141
2189
  exports.writeDaemonState = writeDaemonState;