happy-coder 0.10.0-3 → 0.10.0-4

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-4";
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,6 +114,7 @@ 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",
@@ -187,6 +188,7 @@ var packageJson = {
187
188
 
188
189
  class Configuration {
189
190
  serverUrl;
191
+ webappUrl;
190
192
  isDaemonProcess;
191
193
  // Directories and paths (from persistence)
192
194
  happyHomeDir;
@@ -199,6 +201,7 @@ class Configuration {
199
201
  isExperimentalEnabled;
200
202
  constructor() {
201
203
  this.serverUrl = process.env.HAPPY_SERVER_URL || "https://api.cluster-fluster.com";
204
+ this.webappUrl = process.env.HAPPY_WEBAPP_URL || "https://app.happy.engineering";
202
205
  const args = process.argv.slice(2);
203
206
  this.isDaemonProcess = args.length >= 2 && args[0] === "daemon" && args[1] === "start-sync";
204
207
  if (process.env.HAPPY_HOME_DIR) {
@@ -243,7 +246,17 @@ function decodeBase64(base64, variant = "base64") {
243
246
  function getRandomBytes(size) {
244
247
  return new Uint8Array(node_crypto.randomBytes(size));
245
248
  }
246
- function encrypt(data, secret) {
249
+ function libsodiumEncryptForPublicKey(data, recipientPublicKey) {
250
+ const ephemeralKeyPair = tweetnacl.box.keyPair();
251
+ const nonce = getRandomBytes(tweetnacl.box.nonceLength);
252
+ const encrypted = tweetnacl.box(data, nonce, recipientPublicKey, ephemeralKeyPair.secretKey);
253
+ const result = new Uint8Array(ephemeralKeyPair.publicKey.length + nonce.length + encrypted.length);
254
+ result.set(ephemeralKeyPair.publicKey, 0);
255
+ result.set(nonce, ephemeralKeyPair.publicKey.length);
256
+ result.set(encrypted, ephemeralKeyPair.publicKey.length + nonce.length);
257
+ return result;
258
+ }
259
+ function encryptLegacy(data, secret) {
247
260
  const nonce = getRandomBytes(tweetnacl.secretbox.nonceLength);
248
261
  const encrypted = tweetnacl.secretbox(new TextEncoder().encode(JSON.stringify(data)), nonce, secret);
249
262
  const result = new Uint8Array(nonce.length + encrypted.length);
@@ -251,7 +264,7 @@ function encrypt(data, secret) {
251
264
  result.set(encrypted, nonce.length);
252
265
  return result;
253
266
  }
254
- function decrypt(data, secret) {
267
+ function decryptLegacy(data, secret) {
255
268
  const nonce = data.slice(0, tweetnacl.secretbox.nonceLength);
256
269
  const encrypted = data.slice(tweetnacl.secretbox.nonceLength);
257
270
  const decrypted = tweetnacl.secretbox.open(encrypted, nonce, secret);
@@ -260,6 +273,61 @@ function decrypt(data, secret) {
260
273
  }
261
274
  return JSON.parse(new TextDecoder().decode(decrypted));
262
275
  }
276
+ function encryptWithDataKey(data, dataKey) {
277
+ const nonce = getRandomBytes(12);
278
+ const cipher = node_crypto.createCipheriv("aes-256-gcm", dataKey, nonce);
279
+ const plaintext = new TextEncoder().encode(JSON.stringify(data));
280
+ const encrypted = Buffer.concat([
281
+ cipher.update(plaintext),
282
+ cipher.final()
283
+ ]);
284
+ const authTag = cipher.getAuthTag();
285
+ const bundle = new Uint8Array(12 + encrypted.length + 16 + 1);
286
+ bundle.set([0], 0);
287
+ bundle.set(nonce, 1);
288
+ bundle.set(new Uint8Array(encrypted), 13);
289
+ bundle.set(new Uint8Array(authTag), 13 + encrypted.length);
290
+ return bundle;
291
+ }
292
+ function decryptWithDataKey(bundle, dataKey) {
293
+ if (bundle.length < 1) {
294
+ return null;
295
+ }
296
+ if (bundle[0] !== 0) {
297
+ return null;
298
+ }
299
+ if (bundle.length < 12 + 16 + 1) {
300
+ return null;
301
+ }
302
+ const nonce = bundle.slice(1, 13);
303
+ const authTag = bundle.slice(bundle.length - 16);
304
+ const ciphertext = bundle.slice(13, bundle.length - 16);
305
+ try {
306
+ const decipher = node_crypto.createDecipheriv("aes-256-gcm", dataKey, nonce);
307
+ decipher.setAuthTag(authTag);
308
+ const decrypted = Buffer.concat([
309
+ decipher.update(ciphertext),
310
+ decipher.final()
311
+ ]);
312
+ return JSON.parse(new TextDecoder().decode(decrypted));
313
+ } catch (error) {
314
+ return null;
315
+ }
316
+ }
317
+ function encrypt(key, variant, data) {
318
+ if (variant === "legacy") {
319
+ return encryptLegacy(data, key);
320
+ } else {
321
+ return encryptWithDataKey(data, key);
322
+ }
323
+ }
324
+ function decrypt(key, variant, data) {
325
+ if (variant === "legacy") {
326
+ return decryptLegacy(data, key);
327
+ } else {
328
+ return decryptWithDataKey(data, key);
329
+ }
330
+ }
263
331
 
264
332
  const defaultSettings = {
265
333
  onboardingCompleted: false
@@ -323,8 +391,13 @@ async function updateSettings(updater) {
323
391
  }
324
392
  }
325
393
  const credentialsSchema = z__namespace.object({
326
- secret: z__namespace.string().base64(),
327
- token: z__namespace.string()
394
+ token: z__namespace.string(),
395
+ secret: z__namespace.string().base64().nullish(),
396
+ // Legacy
397
+ encryption: z__namespace.object({
398
+ publicKey: z__namespace.string().base64(),
399
+ machineKey: z__namespace.string().base64()
400
+ }).nullish()
328
401
  });
329
402
  async function readCredentials() {
330
403
  if (!fs.existsSync(configuration.privateKeyFile)) {
@@ -333,15 +406,30 @@ async function readCredentials() {
333
406
  try {
334
407
  const keyBase64 = await promises.readFile(configuration.privateKeyFile, "utf8");
335
408
  const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
336
- return {
337
- secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
338
- token: credentials.token
339
- };
409
+ if (credentials.secret) {
410
+ return {
411
+ token: credentials.token,
412
+ encryption: {
413
+ type: "legacy",
414
+ secret: new Uint8Array(Buffer.from(credentials.secret, "base64"))
415
+ }
416
+ };
417
+ } else if (credentials.encryption) {
418
+ return {
419
+ token: credentials.token,
420
+ encryption: {
421
+ type: "dataKey",
422
+ publicKey: new Uint8Array(Buffer.from(credentials.encryption.publicKey, "base64")),
423
+ machineKey: new Uint8Array(Buffer.from(credentials.encryption.machineKey, "base64"))
424
+ }
425
+ };
426
+ }
340
427
  } catch {
341
428
  return null;
342
429
  }
430
+ return null;
343
431
  }
344
- async function writeCredentials(credentials) {
432
+ async function writeCredentialsLegacy(credentials) {
345
433
  if (!fs.existsSync(configuration.happyHomeDir)) {
346
434
  await promises.mkdir(configuration.happyHomeDir, { recursive: true });
347
435
  }
@@ -350,6 +438,15 @@ async function writeCredentials(credentials) {
350
438
  token: credentials.token
351
439
  }, null, 2));
352
440
  }
441
+ async function writeCredentialsDataKey(credentials) {
442
+ if (!fs.existsSync(configuration.happyHomeDir)) {
443
+ await promises.mkdir(configuration.happyHomeDir, { recursive: true });
444
+ }
445
+ await promises.writeFile(configuration.privateKeyFile, JSON.stringify({
446
+ encryption: { publicKey: encodeBase64(credentials.publicKey), machineKey: encodeBase64(credentials.machineKey) },
447
+ token: credentials.token
448
+ }, null, 2));
449
+ }
353
450
  async function clearCredentials() {
354
451
  if (fs.existsSync(configuration.privateKeyFile)) {
355
452
  await promises.unlink(configuration.privateKeyFile);
@@ -684,32 +781,6 @@ z.z.object({
684
781
  ]),
685
782
  createdAt: z.z.number()
686
783
  });
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
784
  z.z.object({
714
785
  host: z.z.string(),
715
786
  platform: z.z.string(),
@@ -734,37 +805,6 @@ z.z.object({
734
805
  // Forward compatibility
735
806
  ]).optional()
736
807
  });
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
808
  z.z.object({
769
809
  content: SessionMessageContentSchema,
770
810
  createdAt: z.z.number(),
@@ -888,12 +928,14 @@ class AsyncLock {
888
928
  class RpcHandlerManager {
889
929
  handlers = /* @__PURE__ */ new Map();
890
930
  scopePrefix;
891
- secret;
931
+ encryptionKey;
932
+ encryptionVariant;
892
933
  logger;
893
934
  socket = null;
894
935
  constructor(config) {
895
936
  this.scopePrefix = config.scopePrefix;
896
- this.secret = config.secret;
937
+ this.encryptionKey = config.encryptionKey;
938
+ this.encryptionVariant = config.encryptionVariant;
897
939
  this.logger = config.logger || ((msg, data) => logger.debug(msg, data));
898
940
  }
899
941
  /**
@@ -919,20 +961,19 @@ class RpcHandlerManager {
919
961
  if (!handler) {
920
962
  this.logger("[RPC] [ERROR] Method not found", { method: request.method });
921
963
  const errorResponse = { error: "Method not found" };
922
- const encryptedError = encodeBase64(encrypt(errorResponse, this.secret));
964
+ const encryptedError = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, errorResponse));
923
965
  return encryptedError;
924
966
  }
925
- const decryptedParams = decrypt(decodeBase64(request.params), this.secret);
967
+ const decryptedParams = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(request.params));
926
968
  const result = await handler(decryptedParams);
927
- const encryptedResponse = encodeBase64(encrypt(result, this.secret));
969
+ const encryptedResponse = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, result));
928
970
  return encryptedResponse;
929
971
  } catch (error) {
930
972
  this.logger("[RPC] [ERROR] Error handling request", { error });
931
973
  const errorResponse = {
932
974
  error: error instanceof Error ? error.message : "Unknown error"
933
975
  };
934
- const encryptedError = encodeBase64(encrypt(errorResponse, this.secret));
935
- return encryptedError;
976
+ return encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, errorResponse));
936
977
  }
937
978
  }
938
979
  onSocketConnect(socket) {
@@ -974,7 +1015,7 @@ class RpcHandlerManager {
974
1015
  }
975
1016
  }
976
1017
 
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))));
1018
+ 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-BcDnTXMg.cjs', document.baseURI).href))));
978
1019
  function projectPath() {
979
1020
  const path$1 = path.resolve(__dirname$1, "..");
980
1021
  return path$1;
@@ -1277,7 +1318,6 @@ function registerCommonHandlers(rpcHandlerManager) {
1277
1318
 
1278
1319
  class ApiSessionClient extends node_events.EventEmitter {
1279
1320
  token;
1280
- secret;
1281
1321
  sessionId;
1282
1322
  metadata;
1283
1323
  metadataVersion;
@@ -1289,18 +1329,22 @@ class ApiSessionClient extends node_events.EventEmitter {
1289
1329
  rpcHandlerManager;
1290
1330
  agentStateLock = new AsyncLock();
1291
1331
  metadataLock = new AsyncLock();
1292
- constructor(token, secret, session) {
1332
+ encryptionKey;
1333
+ encryptionVariant;
1334
+ constructor(token, session) {
1293
1335
  super();
1294
1336
  this.token = token;
1295
- this.secret = secret;
1296
1337
  this.sessionId = session.id;
1297
1338
  this.metadata = session.metadata;
1298
1339
  this.metadataVersion = session.metadataVersion;
1299
1340
  this.agentState = session.agentState;
1300
1341
  this.agentStateVersion = session.agentStateVersion;
1342
+ this.encryptionKey = session.encryptionKey;
1343
+ this.encryptionVariant = session.encryptionVariant;
1301
1344
  this.rpcHandlerManager = new RpcHandlerManager({
1302
1345
  scopePrefix: this.sessionId,
1303
- secret: this.secret,
1346
+ encryptionKey: this.encryptionKey,
1347
+ encryptionVariant: this.encryptionVariant,
1304
1348
  logger: (msg, data) => logger.debug(msg, data)
1305
1349
  });
1306
1350
  registerCommonHandlers(this.rpcHandlerManager);
@@ -1342,7 +1386,7 @@ class ApiSessionClient extends node_events.EventEmitter {
1342
1386
  return;
1343
1387
  }
1344
1388
  if (data.body.t === "new-message" && data.body.message.content.t === "encrypted") {
1345
- const body = decrypt(decodeBase64(data.body.message.content.c), this.secret);
1389
+ const body = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(data.body.message.content.c));
1346
1390
  logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", body);
1347
1391
  const userResult = UserMessageSchema.safeParse(body);
1348
1392
  if (userResult.success) {
@@ -1356,11 +1400,11 @@ class ApiSessionClient extends node_events.EventEmitter {
1356
1400
  }
1357
1401
  } else if (data.body.t === "update-session") {
1358
1402
  if (data.body.metadata && data.body.metadata.version > this.metadataVersion) {
1359
- this.metadata = decrypt(decodeBase64(data.body.metadata.value), this.secret);
1403
+ this.metadata = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(data.body.metadata.value));
1360
1404
  this.metadataVersion = data.body.metadata.version;
1361
1405
  }
1362
1406
  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;
1407
+ this.agentState = data.body.agentState.value ? decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(data.body.agentState.value)) : null;
1364
1408
  this.agentStateVersion = data.body.agentState.version;
1365
1409
  }
1366
1410
  } else if (data.body.t === "update-machine") {
@@ -1414,7 +1458,7 @@ class ApiSessionClient extends node_events.EventEmitter {
1414
1458
  };
1415
1459
  }
1416
1460
  logger.debugLargeJson("[SOCKET] Sending message through socket:", content);
1417
- const encrypted = encodeBase64(encrypt(content, this.secret));
1461
+ const encrypted = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, content));
1418
1462
  this.socket.emit("message", {
1419
1463
  sid: this.sessionId,
1420
1464
  message: encrypted
@@ -1448,7 +1492,7 @@ class ApiSessionClient extends node_events.EventEmitter {
1448
1492
  sentFrom: "cli"
1449
1493
  }
1450
1494
  };
1451
- const encrypted = encodeBase64(encrypt(content, this.secret));
1495
+ const encrypted = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, content));
1452
1496
  this.socket.emit("message", {
1453
1497
  sid: this.sessionId,
1454
1498
  message: encrypted
@@ -1463,7 +1507,7 @@ class ApiSessionClient extends node_events.EventEmitter {
1463
1507
  data: event
1464
1508
  }
1465
1509
  };
1466
- const encrypted = encodeBase64(encrypt(content, this.secret));
1510
+ const encrypted = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, content));
1467
1511
  this.socket.emit("message", {
1468
1512
  sid: this.sessionId,
1469
1513
  message: encrypted
@@ -1523,14 +1567,14 @@ class ApiSessionClient extends node_events.EventEmitter {
1523
1567
  this.metadataLock.inLock(async () => {
1524
1568
  await backoff(async () => {
1525
1569
  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)) });
1570
+ const answer = await this.socket.emitWithAck("update-metadata", { sid: this.sessionId, expectedVersion: this.metadataVersion, metadata: encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, updated)) });
1527
1571
  if (answer.result === "success") {
1528
- this.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
1572
+ this.metadata = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(answer.metadata));
1529
1573
  this.metadataVersion = answer.version;
1530
1574
  } else if (answer.result === "version-mismatch") {
1531
1575
  if (answer.version > this.metadataVersion) {
1532
1576
  this.metadataVersion = answer.version;
1533
- this.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
1577
+ this.metadata = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(answer.metadata));
1534
1578
  }
1535
1579
  throw new Error("Metadata version mismatch");
1536
1580
  } else if (answer.result === "error") ;
@@ -1546,15 +1590,15 @@ class ApiSessionClient extends node_events.EventEmitter {
1546
1590
  this.agentStateLock.inLock(async () => {
1547
1591
  await backoff(async () => {
1548
1592
  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 });
1593
+ 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
1594
  if (answer.result === "success") {
1551
- this.agentState = answer.agentState ? decrypt(decodeBase64(answer.agentState), this.secret) : null;
1595
+ this.agentState = answer.agentState ? decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(answer.agentState)) : null;
1552
1596
  this.agentStateVersion = answer.version;
1553
1597
  logger.debug("Agent state updated", this.agentState);
1554
1598
  } else if (answer.result === "version-mismatch") {
1555
1599
  if (answer.version > this.agentStateVersion) {
1556
1600
  this.agentStateVersion = answer.version;
1557
- this.agentState = answer.agentState ? decrypt(decodeBase64(answer.agentState), this.secret) : null;
1601
+ this.agentState = answer.agentState ? decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(answer.agentState)) : null;
1558
1602
  }
1559
1603
  throw new Error("Agent state version mismatch");
1560
1604
  } else if (answer.result === "error") ;
@@ -1584,13 +1628,13 @@ class ApiSessionClient extends node_events.EventEmitter {
1584
1628
  }
1585
1629
 
1586
1630
  class ApiMachineClient {
1587
- constructor(token, secret, machine) {
1631
+ constructor(token, machine) {
1588
1632
  this.token = token;
1589
- this.secret = secret;
1590
1633
  this.machine = machine;
1591
1634
  this.rpcHandlerManager = new RpcHandlerManager({
1592
1635
  scopePrefix: this.machine.id,
1593
- secret: this.secret,
1636
+ encryptionKey: this.machine.encryptionKey,
1637
+ encryptionVariant: this.machine.encryptionVariant,
1594
1638
  logger: (msg, data) => logger.debug(msg, data)
1595
1639
  });
1596
1640
  registerCommonHandlers(this.rpcHandlerManager);
@@ -1651,17 +1695,17 @@ class ApiMachineClient {
1651
1695
  const updated = handler(this.machine.metadata);
1652
1696
  const answer = await this.socket.emitWithAck("machine-update-metadata", {
1653
1697
  machineId: this.machine.id,
1654
- metadata: encodeBase64(encrypt(updated, this.secret)),
1698
+ metadata: encodeBase64(encrypt(this.machine.encryptionKey, this.machine.encryptionVariant, updated)),
1655
1699
  expectedVersion: this.machine.metadataVersion
1656
1700
  });
1657
1701
  if (answer.result === "success") {
1658
- this.machine.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
1702
+ this.machine.metadata = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(answer.metadata));
1659
1703
  this.machine.metadataVersion = answer.version;
1660
1704
  logger.debug("[API MACHINE] Metadata updated successfully");
1661
1705
  } else if (answer.result === "version-mismatch") {
1662
1706
  if (answer.version > this.machine.metadataVersion) {
1663
1707
  this.machine.metadataVersion = answer.version;
1664
- this.machine.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
1708
+ this.machine.metadata = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(answer.metadata));
1665
1709
  }
1666
1710
  throw new Error("Metadata version mismatch");
1667
1711
  }
@@ -1676,17 +1720,17 @@ class ApiMachineClient {
1676
1720
  const updated = handler(this.machine.daemonState);
1677
1721
  const answer = await this.socket.emitWithAck("machine-update-state", {
1678
1722
  machineId: this.machine.id,
1679
- daemonState: encodeBase64(encrypt(updated, this.secret)),
1723
+ daemonState: encodeBase64(encrypt(this.machine.encryptionKey, this.machine.encryptionVariant, updated)),
1680
1724
  expectedVersion: this.machine.daemonStateVersion
1681
1725
  });
1682
1726
  if (answer.result === "success") {
1683
- this.machine.daemonState = decrypt(decodeBase64(answer.daemonState), this.secret);
1727
+ this.machine.daemonState = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(answer.daemonState));
1684
1728
  this.machine.daemonStateVersion = answer.version;
1685
1729
  logger.debug("[API MACHINE] Daemon state updated successfully");
1686
1730
  } else if (answer.result === "version-mismatch") {
1687
1731
  if (answer.version > this.machine.daemonStateVersion) {
1688
1732
  this.machine.daemonStateVersion = answer.version;
1689
- this.machine.daemonState = decrypt(decodeBase64(answer.daemonState), this.secret);
1733
+ this.machine.daemonState = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(answer.daemonState));
1690
1734
  }
1691
1735
  throw new Error("Daemon state version mismatch");
1692
1736
  }
@@ -1733,12 +1777,12 @@ class ApiMachineClient {
1733
1777
  const update = data.body;
1734
1778
  if (update.metadata) {
1735
1779
  logger.debug("[API MACHINE] Received external metadata update");
1736
- this.machine.metadata = decrypt(decodeBase64(update.metadata.value), this.secret);
1780
+ this.machine.metadata = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(update.metadata.value));
1737
1781
  this.machine.metadataVersion = update.metadata.version;
1738
1782
  }
1739
1783
  if (update.daemonState) {
1740
1784
  logger.debug("[API MACHINE] Received external daemon state update");
1741
- this.machine.daemonState = decrypt(decodeBase64(update.daemonState.value), this.secret);
1785
+ this.machine.daemonState = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(update.daemonState.value));
1742
1786
  this.machine.daemonStateVersion = update.daemonState.version;
1743
1787
  }
1744
1788
  } else {
@@ -1910,46 +1954,62 @@ class PushNotificationClient {
1910
1954
  }
1911
1955
 
1912
1956
  class ApiClient {
1913
- token;
1914
- secret;
1957
+ static async create(credential) {
1958
+ return new ApiClient(credential);
1959
+ }
1960
+ credential;
1915
1961
  pushClient;
1916
- constructor(token, secret) {
1917
- this.token = token;
1918
- this.secret = secret;
1919
- this.pushClient = new PushNotificationClient(token);
1962
+ constructor(credential) {
1963
+ this.credential = credential;
1964
+ this.pushClient = new PushNotificationClient(credential.token, configuration.serverUrl);
1920
1965
  }
1921
1966
  /**
1922
1967
  * Create a new session or load existing one with the given tag
1923
1968
  */
1924
1969
  async getOrCreateSession(opts) {
1970
+ let dataEncryptionKey = null;
1971
+ let encryptionKey;
1972
+ let encryptionVariant;
1973
+ if (this.credential.encryption.type === "dataKey") {
1974
+ encryptionKey = getRandomBytes(32);
1975
+ encryptionVariant = "dataKey";
1976
+ let encryptedDataKey = libsodiumEncryptForPublicKey(encryptionKey, this.credential.encryption.publicKey);
1977
+ dataEncryptionKey = new Uint8Array(encryptedDataKey.length + 1);
1978
+ dataEncryptionKey.set([0], 0);
1979
+ dataEncryptionKey.set(encryptedDataKey, 1);
1980
+ } else {
1981
+ encryptionKey = this.credential.encryption.secret;
1982
+ encryptionVariant = "legacy";
1983
+ }
1925
1984
  try {
1926
1985
  const response = await axios.post(
1927
1986
  `${configuration.serverUrl}/v1/sessions`,
1928
1987
  {
1929
1988
  tag: opts.tag,
1930
- metadata: encodeBase64(encrypt(opts.metadata, this.secret)),
1931
- agentState: opts.state ? encodeBase64(encrypt(opts.state, this.secret)) : null
1989
+ metadata: encodeBase64(encrypt(encryptionKey, encryptionVariant, opts.metadata)),
1990
+ agentState: opts.state ? encodeBase64(encrypt(encryptionKey, encryptionVariant, opts.state)) : null,
1991
+ dataEncryptionKey: dataEncryptionKey ? encodeBase64(dataEncryptionKey) : null
1932
1992
  },
1933
1993
  {
1934
1994
  headers: {
1935
- "Authorization": `Bearer ${this.token}`,
1995
+ "Authorization": `Bearer ${this.credential.token}`,
1936
1996
  "Content-Type": "application/json"
1937
1997
  },
1938
- timeout: 5e3
1939
- // 5 second timeout
1998
+ timeout: 6e4
1999
+ // 1 minute timeout for very bad network connections
1940
2000
  }
1941
2001
  );
1942
2002
  logger.debug(`Session created/loaded: ${response.data.session.id} (tag: ${opts.tag})`);
1943
2003
  let raw = response.data.session;
1944
2004
  let session = {
1945
2005
  id: raw.id,
1946
- createdAt: raw.createdAt,
1947
- updatedAt: raw.updatedAt,
1948
2006
  seq: raw.seq,
1949
- metadata: decrypt(decodeBase64(raw.metadata), this.secret),
2007
+ metadata: decrypt(encryptionKey, encryptionVariant, decodeBase64(raw.metadata)),
1950
2008
  metadataVersion: raw.metadataVersion,
1951
- agentState: raw.agentState ? decrypt(decodeBase64(raw.agentState), this.secret) : null,
1952
- agentStateVersion: raw.agentStateVersion
2009
+ agentState: raw.agentState ? decrypt(encryptionKey, encryptionVariant, decodeBase64(raw.agentState)) : null,
2010
+ agentStateVersion: raw.agentStateVersion,
2011
+ encryptionKey,
2012
+ encryptionVariant
1953
2013
  };
1954
2014
  return session;
1955
2015
  } catch (error) {
@@ -1957,54 +2017,40 @@ class ApiClient {
1957
2017
  throw new Error(`Failed to get or create session: ${error instanceof Error ? error.message : "Unknown error"}`);
1958
2018
  }
1959
2019
  }
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
2020
  /**
1991
2021
  * Register or update machine with the server
1992
2022
  * Returns the current machine state from the server with decrypted metadata and daemonState
1993
2023
  */
1994
2024
  async getOrCreateMachine(opts) {
2025
+ let dataEncryptionKey = null;
2026
+ let encryptionKey;
2027
+ let encryptionVariant;
2028
+ if (this.credential.encryption.type === "dataKey") {
2029
+ encryptionVariant = "dataKey";
2030
+ encryptionKey = this.credential.encryption.machineKey;
2031
+ let encryptedDataKey = libsodiumEncryptForPublicKey(this.credential.encryption.machineKey, this.credential.encryption.publicKey);
2032
+ dataEncryptionKey = new Uint8Array(encryptedDataKey.length + 1);
2033
+ dataEncryptionKey.set([0], 0);
2034
+ dataEncryptionKey.set(encryptedDataKey, 1);
2035
+ } else {
2036
+ encryptionKey = this.credential.encryption.secret;
2037
+ encryptionVariant = "legacy";
2038
+ }
1995
2039
  const response = await axios.post(
1996
2040
  `${configuration.serverUrl}/v1/machines`,
1997
2041
  {
1998
2042
  id: opts.machineId,
1999
- metadata: encodeBase64(encrypt(opts.metadata, this.secret)),
2000
- daemonState: opts.daemonState ? encodeBase64(encrypt(opts.daemonState, this.secret)) : void 0
2043
+ metadata: encodeBase64(encrypt(encryptionKey, encryptionVariant, opts.metadata)),
2044
+ daemonState: opts.daemonState ? encodeBase64(encrypt(encryptionKey, encryptionVariant, opts.daemonState)) : void 0,
2045
+ dataEncryptionKey: dataEncryptionKey ? encodeBase64(dataEncryptionKey) : void 0
2001
2046
  },
2002
2047
  {
2003
2048
  headers: {
2004
- "Authorization": `Bearer ${this.token}`,
2049
+ "Authorization": `Bearer ${this.credential.token}`,
2005
2050
  "Content-Type": "application/json"
2006
2051
  },
2007
- timeout: 5e3
2052
+ timeout: 6e4
2053
+ // 1 minute timeout for very bad network connections
2008
2054
  }
2009
2055
  );
2010
2056
  if (response.status !== 200) {
@@ -2016,22 +2062,20 @@ class ApiClient {
2016
2062
  logger.debug(`[API] Machine ${opts.machineId} registered/updated with server`);
2017
2063
  const machine = {
2018
2064
  id: raw.id,
2019
- metadata: raw.metadata ? decrypt(decodeBase64(raw.metadata), this.secret) : null,
2065
+ encryptionKey,
2066
+ encryptionVariant,
2067
+ metadata: raw.metadata ? decrypt(encryptionKey, encryptionVariant, decodeBase64(raw.metadata)) : null,
2020
2068
  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
2069
+ daemonState: raw.daemonState ? decrypt(encryptionKey, encryptionVariant, decodeBase64(raw.daemonState)) : null,
2070
+ daemonStateVersion: raw.daemonStateVersion || 0
2027
2071
  };
2028
2072
  return machine;
2029
2073
  }
2030
2074
  sessionSyncClient(session) {
2031
- return new ApiSessionClient(this.token, this.secret, session);
2075
+ return new ApiSessionClient(this.credential.token, session);
2032
2076
  }
2033
2077
  machineSyncClient(machine) {
2034
- return new ApiMachineClient(this.token, this.secret, machine);
2078
+ return new ApiMachineClient(this.credential.token, machine);
2035
2079
  }
2036
2080
  push() {
2037
2081
  return this.pushClient;
@@ -2049,7 +2093,7 @@ class ApiClient {
2049
2093
  },
2050
2094
  {
2051
2095
  headers: {
2052
- "Authorization": `Bearer ${this.token}`,
2096
+ "Authorization": `Bearer ${this.credential.token}`,
2053
2097
  "Content-Type": "application/json"
2054
2098
  },
2055
2099
  timeout: 5e3
@@ -2137,5 +2181,6 @@ exports.readDaemonState = readDaemonState;
2137
2181
  exports.readSettings = readSettings;
2138
2182
  exports.releaseDaemonLock = releaseDaemonLock;
2139
2183
  exports.updateSettings = updateSettings;
2140
- exports.writeCredentials = writeCredentials;
2184
+ exports.writeCredentialsDataKey = writeCredentialsDataKey;
2185
+ exports.writeCredentialsLegacy = writeCredentialsLegacy;
2141
2186
  exports.writeDaemonState = writeDaemonState;