@joe-broadhead/opencode-messaging 0.4.0 → 0.5.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.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
- import { dirname, isAbsolute, join, resolve } from "node:path";
3
+ import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
4
4
  import net, { BlockList, isIP } from "node:net";
5
5
  import { KeyObject, createCipheriv, createDecipheriv, createHash, createPrivateKey, createPublicKey, diffieHellman, generateKeyPairSync, hkdfSync, randomBytes, sign, verify } from "node:crypto";
6
6
  import { homedir, tmpdir } from "node:os";
@@ -274,7 +274,7 @@ function partial(Class, schema, mask, name = "partial") {
274
274
  checks: []
275
275
  }));
276
276
  }
277
- function required(Class, schema, mask) {
277
+ function required$1(Class, schema, mask) {
278
278
  const selected = mask ? new Set(maskedKeys(schema, mask)) : void 0;
279
279
  const newShape = {};
280
280
  mirrorShape(newShape, schema, Reflect.ownKeys(sourceShape(schema)), (value, key) => selected && !selected.has(key) ? value : new Class({
@@ -5450,7 +5450,7 @@ var init_schemas = __esmMin((() => {
5450
5450
  return partial(ZodExactOptional, this, args[0], "exactPartial");
5451
5451
  },
5452
5452
  required(...args) {
5453
- return required(ZodNonOptional, this, args[0]);
5453
+ return required$1(ZodNonOptional, this, args[0]);
5454
5454
  }
5455
5455
  });
5456
5456
  ZodUnion = $constructor("ZodUnion", (inst, def) => {
@@ -5943,7 +5943,8 @@ var init_jelly$3 = __esmMin((() => {
5943
5943
  jti: string$1().regex(/^[\x21-\x7e]{1,128}$/, "printable jti"),
5944
5944
  iss: string$1().max(2048).optional(),
5945
5945
  dev: string$1().max(200).optional(),
5946
- cls: string$1().max(32).optional()
5946
+ cls: string$1().max(32).optional(),
5947
+ mbr: string$1().max(200).optional()
5947
5948
  });
5948
5949
  JellySessionClaims = looseObject({
5949
5950
  aud: string$1().min(1).max(2048),
@@ -6553,7 +6554,8 @@ var init_envelope$1 = __esmMin((() => {
6553
6554
  label: MachineLabel,
6554
6555
  sessions: array(SessionInfo).max(LIMITS.presenceSessions),
6555
6556
  features: array(string$1().max(64)).max(32).optional(),
6556
- direct: array(DirectEndpoint).min(1).max(4).optional()
6557
+ direct: array(DirectEndpoint).min(1).max(4).optional(),
6558
+ away: literal(true).optional()
6557
6559
  });
6558
6560
  PAYLOAD_BY_TYPE = {
6559
6561
  message: MessagePayload,
@@ -6832,7 +6834,7 @@ function hostedRelayAuthPayload(args) {
6832
6834
  ts: args.ts
6833
6835
  };
6834
6836
  }
6835
- var Req, Size, Handle, Email, OrgId, DirBindFrame, HostedHelloFrame, DirAppendFrame, DirKeysFrame, DirSthFrame, DirLookupFrame, DirDeviceFrame, DirResolveFrame, DirSearchFrame, DirWatchFrame, PresenceWatchFrame, DirEraseFrame, HostedWelcomeFrame, DirOkFrame, DirSthIsFrame, DirPersonsFrame, ResolvedPerson, DirResolvedFrame, DirChangedFrame, DirUnboundFrame, DirectoryRelayFrame;
6837
+ var Req, Size, Handle, Email, OrgId, DirBindFrame, HostedHelloFrame, DirAppendFrame, DirKeysFrame, DirSthFrame, DirLookupFrame, DirDeviceFrame, DirResolveFrame, DirSearchFrame, DirWatchFrame, PresenceWatchFrame, DirEraseFrame, WAKE_TICKET_BYTES, WakeUrl, WakeArmFrame, WakeDisarmFrame, HostedWelcomeFrame, DirOkFrame, DirSthIsFrame, DirPersonsFrame, ResolvedPerson, DirResolvedFrame, DirChangedFrame, DirUnboundFrame, DirectoryRelayFrame;
6836
6838
  var init_directory_frames = __esmMin((() => {
6837
6839
  init_zod();
6838
6840
  init_common();
@@ -6916,6 +6918,20 @@ var init_directory_frames = __esmMin((() => {
6916
6918
  entries: array(LogEntry).min(1).max(LIMITS.devicesPerPerson),
6917
6919
  token: Jws
6918
6920
  });
6921
+ WAKE_TICKET_BYTES = 4096;
6922
+ WakeUrl = string$1().max(2048).regex(/^https?:\/\/[^/?#@\s]+\/hosts\/hst_[A-Za-z0-9._:-]{1,123}\/wake$/, "a Jelly ID wake URL");
6923
+ WakeArmFrame = object$1({
6924
+ t: literal("wake.arm"),
6925
+ req: Req,
6926
+ url: WakeUrl,
6927
+ ticket: Jws.refine((ticket) => ticket.length <= WAKE_TICKET_BYTES, { message: `at most ${WAKE_TICKET_BYTES} bytes` }),
6928
+ agents: array(SessionName).max(64),
6929
+ awaiting: array(Ulid).max(500)
6930
+ });
6931
+ WakeDisarmFrame = object$1({
6932
+ t: literal("wake.disarm"),
6933
+ req: Req
6934
+ });
6919
6935
  discriminatedUnion("t", [
6920
6936
  DirBindFrame,
6921
6937
  HostedHelloFrame,
@@ -6928,7 +6944,9 @@ var init_directory_frames = __esmMin((() => {
6928
6944
  DirSearchFrame,
6929
6945
  DirWatchFrame,
6930
6946
  PresenceWatchFrame,
6931
- DirEraseFrame
6947
+ DirEraseFrame,
6948
+ WakeArmFrame,
6949
+ WakeDisarmFrame
6932
6950
  ]);
6933
6951
  HostedWelcomeFrame = object$1({
6934
6952
  t: literal("welcome"),
@@ -7046,7 +7064,7 @@ var init_direct$1 = __esmMin((() => {
7046
7064
  ]);
7047
7065
  }));
7048
7066
  // packages/core/dist/schema/ipc/device.js
7049
- var BindingState, RealmAlarmSummary, DeviceStatus, DeviceListEntry, LinkText, Sas, LinkPreview, KeyFingerprint, LoginUserCode, LoginPurpose, LoginKind, LoginShown, LoginState, DEVICE_METHODS;
7067
+ var BindingState, RealmAlarmSummary, DeviceStatus, DeviceListEntry, LinkText, Sas, LinkPreview, KeyFingerprint, LoginUserCode, LoginPurpose, LoginKind, LoginShown, LoginState, DeviceRemoveParams, DeviceApproveParams, DEVICE_METHODS;
7050
7068
  var init_device = __esmMin((() => {
7051
7069
  init_zod();
7052
7070
  init_common();
@@ -7143,6 +7161,19 @@ var init_device = __esmMin((() => {
7143
7161
  purpose: LoginPurpose
7144
7162
  })
7145
7163
  ]);
7164
+ DeviceRemoveParams = strictObject({
7165
+ device: MachineId,
7166
+ reason: _enum([
7167
+ "self",
7168
+ "removed",
7169
+ "lost"
7170
+ ]).optional(),
7171
+ token: Jws.optional()
7172
+ });
7173
+ DeviceApproveParams = strictObject({
7174
+ link: LinkText,
7175
+ sas: Sas
7176
+ });
7146
7177
  DEVICE_METHODS = {
7147
7178
  "device.status": {
7148
7179
  params: strictObject({}),
@@ -7165,15 +7196,7 @@ var init_device = __esmMin((() => {
7165
7196
  result: object$1({ devices: array(DeviceListEntry) })
7166
7197
  },
7167
7198
  "device.remove": {
7168
- params: strictObject({
7169
- device: MachineId,
7170
- reason: _enum([
7171
- "self",
7172
- "removed",
7173
- "lost"
7174
- ]).optional(),
7175
- token: Jws.optional()
7176
- }),
7199
+ params: DeviceRemoveParams,
7177
7200
  result: object$1({ seq: number$1().int().nonnegative() })
7178
7201
  },
7179
7202
  "device.link": {
@@ -7210,14 +7233,11 @@ var init_device = __esmMin((() => {
7210
7233
  result: object$1({ ok: literal(true) })
7211
7234
  },
7212
7235
  "device.approve.preview": {
7213
- params: strictObject({ link: LinkText }),
7236
+ params: DeviceApproveParams.partial({ sas: true }),
7214
7237
  result: LinkPreview
7215
7238
  },
7216
7239
  "device.approve": {
7217
- params: strictObject({
7218
- link: LinkText,
7219
- sas: Sas
7220
- }),
7240
+ params: DeviceApproveParams,
7221
7241
  result: object$1({
7222
7242
  device: MachineId,
7223
7243
  seq: number$1().int().nonnegative()
@@ -7361,7 +7381,7 @@ var init_shapes = __esmMin((() => {
7361
7381
  });
7362
7382
  }));
7363
7383
  // packages/core/dist/schema/ipc/consent.js
7364
- var GrantTarget, Levels, GrantView, GrantReview, GrantSetParams, AskRequest, ContactRequestView, ContactSuggestion, HeldRequest, RequestAnswer, RequestText, PersonReach, CONSENT_METHODS;
7384
+ var GrantTarget, Levels, GrantView, GrantReview, GrantSetParams, AskRequest, ContactRequestView, ContactSuggestion, HeldRequest, RequestAnswer, RequestText, PersonReach, RequestAnswerParams, ContactRequestParams, CONSENT_METHODS;
7365
7385
  var init_consent$1 = __esmMin((() => {
7366
7386
  init_zod();
7367
7387
  init_common();
@@ -7460,6 +7480,16 @@ var init_consent$1 = __esmMin((() => {
7460
7480
  source: _enum(["card", "default"]),
7461
7481
  at: Millis.optional()
7462
7482
  });
7483
+ RequestAnswerParams = strictObject({
7484
+ id: Ulid,
7485
+ answer: RequestAnswer,
7486
+ all: boolean().optional()
7487
+ });
7488
+ ContactRequestParams = strictObject({
7489
+ email: Email,
7490
+ text: RequestText,
7491
+ suggestion: Ulid.optional()
7492
+ });
7463
7493
  CONSENT_METHODS = {
7464
7494
  "grant.list": {
7465
7495
  params: strictObject({ subject: GrantTarget.optional() }),
@@ -7481,19 +7511,35 @@ var init_consent$1 = __esmMin((() => {
7481
7511
  result: object$1({ requests: array(HeldRequest).max(LIMITS.heldTotal * 3) })
7482
7512
  },
7483
7513
  "request.answer": {
7484
- params: strictObject({
7485
- id: Ulid,
7486
- answer: RequestAnswer,
7487
- all: boolean().optional()
7488
- }),
7514
+ params: RequestAnswerParams,
7489
7515
  result: object$1({ answered: array(Ulid).max(LIMITS.heldTotal) })
7490
7516
  },
7517
+ "request.answer.preview": {
7518
+ params: RequestAnswerParams,
7519
+ result: object$1({
7520
+ kind: _enum([
7521
+ "message",
7522
+ "task",
7523
+ "contact",
7524
+ "suggestion"
7525
+ ]),
7526
+ from: object$1({
7527
+ name: string$1().max(200),
7528
+ email: string$1().max(320).optional(),
7529
+ org: _enum([
7530
+ "same",
7531
+ "other",
7532
+ "none"
7533
+ ])
7534
+ }),
7535
+ agent: SessionName.optional(),
7536
+ to: SessionName.optional(),
7537
+ count: number$1().int().positive(),
7538
+ sentence: string$1().max(500)
7539
+ })
7540
+ },
7491
7541
  "contact.request": {
7492
- params: strictObject({
7493
- email: Email,
7494
- text: RequestText,
7495
- suggestion: Ulid.optional()
7496
- }),
7542
+ params: ContactRequestParams,
7497
7543
  result: object$1({
7498
7544
  id: Ulid,
7499
7545
  conversation: RoomRef,
@@ -7501,7 +7547,7 @@ var init_consent$1 = __esmMin((() => {
7501
7547
  })
7502
7548
  },
7503
7549
  "contact.request.preview": {
7504
- params: strictObject({ email: Email }),
7550
+ params: ContactRequestParams.partial({ text: true }),
7505
7551
  result: object$1({ found: boolean() })
7506
7552
  },
7507
7553
  "card.get": {
@@ -7680,7 +7726,8 @@ var init_surfaces = __esmMin((() => {
7680
7726
  result: PersonSummary.extend({ devices: array(object$1({
7681
7727
  device: MachineId,
7682
7728
  label: MachineLabel,
7683
- self: boolean()
7729
+ self: boolean(),
7730
+ away: literal(true).optional()
7684
7731
  })) })
7685
7732
  }
7686
7733
  };
@@ -7688,7 +7735,10 @@ var init_surfaces = __esmMin((() => {
7688
7735
  var init_jelly$2 = __esmMin((() => {
7689
7736
  init_zod();
7690
7737
  init_common();
7738
+ init_directory_frames();
7739
+ init_jelly$3();
7691
7740
  init_directory$2();
7741
+ init_device();
7692
7742
  init_shapes();
7693
7743
  init_surfaces();
7694
7744
  NoticeId = string$1().regex(/^[mra]_[0-9A-Za-z]{1,64}$/, "notice id");
@@ -7804,8 +7854,45 @@ var init_jelly$2 = __esmMin((() => {
7804
7854
  params: strictObject({ ids: array(NoticeId).min(1).max(200) }),
7805
7855
  result: object$1({ ok: literal(true) })
7806
7856
  },
7857
+ "daemon.woke": {
7858
+ params: strictObject({}),
7859
+ result: object$1({ ok: literal(true) })
7860
+ },
7861
+ "person.attached": {
7862
+ params: strictObject({ attached: boolean() }),
7863
+ result: object$1({ ok: literal(true) })
7864
+ },
7865
+ "wake.arm": {
7866
+ params: strictObject({
7867
+ url: WakeUrl,
7868
+ ticket: Jws.refine((ticket) => ticket.length <= WAKE_TICKET_BYTES, { message: `at most ${WAKE_TICKET_BYTES} bytes` }),
7869
+ agents: array(SessionName).max(64)
7870
+ }),
7871
+ result: object$1({
7872
+ relay: _enum(["armed", "pending"]),
7873
+ awaiting: number$1().int().nonnegative()
7874
+ })
7875
+ },
7876
+ "wake.disarm": {
7877
+ params: strictObject({}),
7878
+ result: object$1({ relay: _enum(["disarmed", "pending"]) })
7879
+ },
7880
+ "daemon.snapshot": {
7881
+ params: strictObject({ done: literal(true).optional() }),
7882
+ result: union$1([object$1({
7883
+ folder: string$1().max(4096),
7884
+ files: array(object$1({
7885
+ name: string$1().max(64),
7886
+ bytes: number$1().int().nonnegative(),
7887
+ sha256: string$1().regex(/^[0-9a-f]{64}$/)
7888
+ })).max(8),
7889
+ storeSchema: number$1().int().positive(),
7890
+ at: Millis,
7891
+ until: Millis
7892
+ }), object$1({ released: boolean() })])
7893
+ },
7807
7894
  "device.remove.preview": {
7808
- params: strictObject({ device: MachineId }),
7895
+ params: DeviceRemoveParams,
7809
7896
  result: object$1({
7810
7897
  label: MachineLabel,
7811
7898
  kind: DeviceKind.optional(),
@@ -7820,7 +7907,7 @@ var init_jelly$2 = __esmMin((() => {
7820
7907
  })
7821
7908
  }
7822
7909
  };
7823
- })), MESSAGING_ERROR_RPC_CODE, SessionHost, Ok, Empty, DaemonStatus, InviteSummary, IPC_METHODS, AGENT_METHODS, HOST_SCOPE_METHODS, IpcEvent;
7910
+ })), MESSAGING_ERROR_RPC_CODE, SessionHost, Ok, Empty, DaemonStatus, InviteSummary, IPC_METHODS, AGENT_METHODS, HUMAN_METHODS, ADMIN_METHODS, HOST_SCOPE_METHODS, IpcEvent;
7824
7911
  var init_ipc = __esmMin((() => {
7825
7912
  init_zod();
7826
7913
  init_device();
@@ -8195,11 +8282,73 @@ var init_ipc = __esmMin((() => {
8195
8282
  "task.update",
8196
8283
  "task.cancel"
8197
8284
  ];
8285
+ HUMAN_METHODS = [
8286
+ "conv.list",
8287
+ "conv.read",
8288
+ "conv.markRead",
8289
+ "conv.subscribe",
8290
+ "person.send",
8291
+ "people.search",
8292
+ "people.get",
8293
+ "grant.list",
8294
+ "grant.set",
8295
+ "grant.set.preview",
8296
+ "request.list",
8297
+ "request.answer",
8298
+ "request.answer.preview",
8299
+ "contact.request",
8300
+ "contact.request.preview",
8301
+ "card.get",
8302
+ "agents.list",
8303
+ "agents.read",
8304
+ "notice.subscribe",
8305
+ "notice.list",
8306
+ "notice.ack",
8307
+ "device.approve.preview",
8308
+ "device.remove.preview",
8309
+ "channel.create",
8310
+ "channel.invite",
8311
+ "channel.leave",
8312
+ "channel.remove"
8313
+ ];
8314
+ ADMIN_METHODS = [
8315
+ "daemon.stop",
8316
+ "daemon.woke",
8317
+ "person.attached",
8318
+ "daemon.snapshot",
8319
+ "wake.arm",
8320
+ "wake.disarm",
8321
+ "device.list",
8322
+ "device.remove",
8323
+ "device.link",
8324
+ "device.login",
8325
+ "device.login.status",
8326
+ "device.login.cancel",
8327
+ "device.approve",
8328
+ "identity.rotateEnc",
8329
+ "team.create",
8330
+ "team.invite",
8331
+ "team.join",
8332
+ "team.members",
8333
+ "team.remove",
8334
+ "team.role",
8335
+ "team.revokeInvite",
8336
+ "team.add",
8337
+ "team.pair",
8338
+ "team.claim",
8339
+ "team.pairCancel",
8340
+ "team.prepare"
8341
+ ];
8198
8342
  HOST_SCOPE_METHODS = [
8199
8343
  "notice.subscribe",
8200
8344
  "notice.list",
8201
8345
  "conv.list",
8202
- "daemon.stop"
8346
+ "daemon.stop",
8347
+ "daemon.woke",
8348
+ "person.attached",
8349
+ "daemon.snapshot",
8350
+ "wake.arm",
8351
+ "wake.disarm"
8203
8352
  ];
8204
8353
  IpcEvent = discriminatedUnion("type", [
8205
8354
  object$1({
@@ -8297,7 +8446,7 @@ var init_b64u = __esmMin((() => {
8297
8446
  // packages/core/dist/version.js
8298
8447
  var VERSION;
8299
8448
  var init_version$1 = __esmMin((() => {
8300
- VERSION = "0.4.0";
8449
+ VERSION = "0.5.0";
8301
8450
  }));
8302
8451
  // packages/core/dist/jcs.js
8303
8452
  function hasLoneSurrogate(text) {
@@ -8808,7 +8957,7 @@ function seal(args, randomness) {
8808
8957
  const payload = parseWith(PAYLOAD_BY_TYPE[type], args.payload, `${type} payload`);
8809
8958
  const plaintext = utf8(canonicalize(payload));
8810
8959
  if (plaintext.length > LIMITS.plaintextBytes) throw new MessagingError("too_large", `payload exceeds ${LIMITS.plaintextBytes} bytes`);
8811
- const klass = type === "message" && payload.conv?.kind === "request" ? "request" : classOf(args.from, args.to);
8960
+ const klass = type === "message" && payload.conv?.kind === "request" ? "request" : classOf$1(args.from, args.to);
8812
8961
  assertMeaning(type, args.from, args.to, klass, payload);
8813
8962
  if (type !== "presence") return sealBytes(args, type, klass, plaintext, randomness);
8814
8963
  if (args.recipientEnc !== void 0) throw new MessagingError("invalid_request", "presence is not sealed");
@@ -8849,7 +8998,7 @@ function base(args, type, klass) {
8849
8998
  ...args.replyTo === void 0 ? {} : { replyTo: args.replyTo }
8850
8999
  };
8851
9000
  }
8852
- function classOf(from, to) {
9001
+ function classOf$1(from, to) {
8853
9002
  return to !== void 0 && "person" in to && to.person === from?.person ? "self" : "contact";
8854
9003
  }
8855
9004
  function sealBytes(args, type, klass, plaintext, randomness) {
@@ -9024,12 +9173,12 @@ function shapeProblem(env) {
9024
9173
  if (env.class === void 0) return "a sealed envelope needs class";
9025
9174
  if (env.class === "request") {
9026
9175
  if (env.type !== "message") return "only a message is of class request";
9027
- if (classOf(env.from, env.to) !== "contact") return "a message request goes to another person";
9176
+ if (classOf$1(env.from, env.to) !== "contact") return "a message request goes to another person";
9028
9177
  if (env.from.agent !== void 0 || "agent" in env.to && env.to.agent !== void 0) return "a message request goes from a person to a person";
9029
9178
  if (env.replyTo !== void 0) return "a message request answers nothing";
9030
9179
  return;
9031
9180
  }
9032
- if (env.class !== classOf(env.from, env.to)) return `class ${env.class} does not match the ends`;
9181
+ if (env.class !== classOf$1(env.from, env.to)) return `class ${env.class} does not match the ends`;
9033
9182
  }
9034
9183
  function isPlainObject(value) {
9035
9184
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -9506,6 +9655,9 @@ var init_roster = __esmMin((() => {
9506
9655
  };
9507
9656
  }));
9508
9657
  // packages/core/dist/jelly-host.js
9658
+ function actionClass(method) {
9659
+ return Object.hasOwn(ACTION_CLASSES, method) ? ACTION_CLASSES[method] : "local";
9660
+ }
9509
9661
  function grantInput(params) {
9510
9662
  return sha256(canonicalize(params ?? {}));
9511
9663
  }
@@ -9567,12 +9719,18 @@ function verifyActGrant(token, check) {
9567
9719
  if (claims.in === void 0 || claims.in !== grantInput(check.params)) throw fail("is for other params");
9568
9720
  checkTimes(claims.iat, claims.exp, 60, check.now, fail);
9569
9721
  }
9570
- if (claims.cls !== void 0 && claims.act !== "host" && claims.cls !== (CHAT_CLASS.has(check.method) ? "chat" : "local")) throw fail("names another action class");
9722
+ if (claims.act === "host") {
9723
+ if (claims.cls !== void 0) throw fail("is host scope, which names no action class");
9724
+ } else {
9725
+ if (claims.cls === void 0) throw fail("names no action class");
9726
+ if (claims.cls !== actionClass(check.method)) throw fail(`names action class ${claims.cls}, not ${actionClass(check.method)}`);
9727
+ }
9571
9728
  return {
9572
9729
  scope: claims.act === "host" ? "host" : "action",
9573
9730
  jti: claims.jti,
9574
9731
  exp: claims.exp,
9575
- dev: claims.dev
9732
+ dev: claims.dev,
9733
+ mbr: claims.mbr
9576
9734
  };
9577
9735
  }
9578
9736
  function verifySessionTicket(token, key, now) {
@@ -9608,7 +9766,7 @@ function parseActKey(text) {
9608
9766
  }
9609
9767
  return parsed.data;
9610
9768
  }
9611
- var SESSION_TICKET_MAX_S, CHAT_CLASS;
9769
+ var SESSION_TICKET_MAX_S, CHAT_METHODS, VIEW_METHODS, classOf, ACTION_CLASSES;
9612
9770
  var init_jelly_host = __esmMin((() => {
9613
9771
  init_b64u();
9614
9772
  init_hash();
@@ -9617,8 +9775,29 @@ var init_jelly_host = __esmMin((() => {
9617
9775
  init_wire();
9618
9776
  init_errors();
9619
9777
  init_jelly$3();
9778
+ init_ipc();
9620
9779
  SESSION_TICKET_MAX_S = 604800;
9621
- CHAT_CLASS = new Set(["person.send", "conv.markRead"]);
9780
+ CHAT_METHODS = [
9781
+ "person.send",
9782
+ "conv.markRead",
9783
+ "notice.ack"
9784
+ ];
9785
+ VIEW_METHODS = [
9786
+ "conv.list",
9787
+ "conv.read",
9788
+ "conv.subscribe",
9789
+ "people.search",
9790
+ "people.get",
9791
+ "grant.list",
9792
+ "request.list",
9793
+ "agents.list",
9794
+ "agents.read",
9795
+ "notice.subscribe",
9796
+ "notice.list",
9797
+ "device.list"
9798
+ ];
9799
+ classOf = (method) => CHAT_METHODS.includes(method) ? "chat" : VIEW_METHODS.includes(method) ? "view" : "local";
9800
+ ACTION_CLASSES = Object.freeze(Object.fromEntries([...HUMAN_METHODS, ...ADMIN_METHODS].map((method) => [method, classOf(method)])));
9622
9801
  }));
9623
9802
  // packages/core/dist/directory/person.js
9624
9803
  function base32Lower(bytes) {
@@ -10672,7 +10851,7 @@ function verifyPersonView(view, context) {
10672
10851
  const { sth, known } = context;
10673
10852
  if (known && known.person !== parsed.person) throw new MessagingError("invalid_request", "the known state is another person's");
10674
10853
  const head = verifyEpochProof(sth, context.head);
10675
- if (head.index !== sth.size - 1) throw fail("the head epoch is not the tree head's last");
10854
+ if (head.index !== sth.size - 1) throw fail$1("the head epoch is not the tree head's last");
10676
10855
  const mapRoot = head.leaf.mapRoot;
10677
10856
  const logContext = {
10678
10857
  ...context,
@@ -10697,7 +10876,7 @@ function verifyPersonView(view, context) {
10697
10876
  let start = held;
10698
10877
  if (parsed.log.length > 0) {
10699
10878
  const first = parsed.log[0].seq;
10700
- if (first > held) throw fail("the log skips entries after those held");
10879
+ if (first > held) throw fail$1("the log skips entries after those held");
10701
10880
  for (const entry of parsed.log.slice(0, Math.max(0, held - first))) {
10702
10881
  const ours = log.entries()[entry.seq];
10703
10882
  if (logEntryHash(ours) !== logEntryHash(entry)) throw new DirectoryInconsistency("the log differs from the one held", {
@@ -10743,7 +10922,7 @@ function verifyPersonView(view, context) {
10743
10922
  ours: ours.seq,
10744
10923
  theirs: statement.seq
10745
10924
  });
10746
- if (ours && statement.seq === ours.seq && keysStatementHash(statement) !== keysStatementHash(ours)) throw fail("two keys statements share a device and seq");
10925
+ if (ours && statement.seq === ours.seq && keysStatementHash(statement) !== keysStatementHash(ours)) throw fail$1("two keys statements share a device and seq");
10747
10926
  keys.set(statement.device, statement);
10748
10927
  }
10749
10928
  for (const [device, ours] of known?.keys ?? []) if (log.device(device)?.current && !keys.has(device)) throw new DirectoryInconsistency("a held keys statement is withheld", {
@@ -10783,11 +10962,11 @@ function verifyPersonView(view, context) {
10783
10962
  function checkWitness(witness, seq, deadline, log, person, sth) {
10784
10963
  const epoch = verifyEpochProof(sth, witness.epoch);
10785
10964
  const committed = witness.value.log;
10786
- if (committed.seq < seq || log.hashAt(committed.seq) !== committed.hash) throw fail("an epoch witness commits to another log");
10787
- if (!verifyMapProof(epoch.leaf.mapRoot, person, mapValue(witness.value), witness.mapProof)) throw fail("an epoch witness's map proof does not verify");
10965
+ if (committed.seq < seq || log.hashAt(committed.seq) !== committed.hash) throw fail$1("an epoch witness commits to another log");
10966
+ if (!verifyMapProof(epoch.leaf.mapRoot, person, mapValue(witness.value), witness.mapProof)) throw fail$1("an epoch witness's map proof does not verify");
10788
10967
  if (epoch.leaf.ts > deadline) throw new MessagingError("binding_invalid", "a Jelly binding first appeared after its token's life");
10789
10968
  }
10790
- var DirectoryInconsistency, fail;
10969
+ var DirectoryInconsistency, fail$1;
10791
10970
  var init_person_view = __esmMin((() => {
10792
10971
  init_check();
10793
10972
  init_directory$2();
@@ -10804,7 +10983,7 @@ var init_person_view = __esmMin((() => {
10804
10983
  this.evidence = evidence;
10805
10984
  }
10806
10985
  };
10807
- fail = (what) => new MessagingError("directory_inconsistent", what);
10986
+ fail$1 = (what) => new MessagingError("directory_inconsistent", what);
10808
10987
  }));
10809
10988
  // packages/core/dist/address.js
10810
10989
  function parseAddress(text) {
@@ -12825,6 +13004,7 @@ var init_dist$3 = __esmMin((() => {
12825
13004
  init_contact_card();
12826
13005
  init_delegation$1();
12827
13006
  init_task();
13007
+ init_a2a();
12828
13008
  init_direct();
12829
13009
  init_peer_text();
12830
13010
  init_agent_tools();
@@ -13271,7 +13451,7 @@ async function createIpcServer(options) {
13271
13451
  method: "hello",
13272
13452
  params: {
13273
13453
  daemon: "messagingd",
13274
- version: options.version ?? "0.4.0",
13454
+ version: options.version ?? "0.5.0",
13275
13455
  protocol: 1,
13276
13456
  pid: process.pid,
13277
13457
  ...options.deployment ? { deployment: options.deployment } : {}
@@ -14288,7 +14468,7 @@ var init_keychain = __esmMin((() => {
14288
14468
  }));
14289
14469
  // packages/daemon/dist/identity/store.js
14290
14470
  var FileStore;
14291
- var init_store$2 = __esmMin((() => {
14471
+ var init_store$3 = __esmMin((() => {
14292
14472
  init_dist$3();
14293
14473
  init_record();
14294
14474
  FileStore = class {
@@ -14365,7 +14545,7 @@ var init_identity = __esmMin((() => {
14365
14545
  init_home();
14366
14546
  init_keychain();
14367
14547
  init_record();
14368
- init_store$2();
14548
+ init_store$3();
14369
14549
  }));
14370
14550
  // packages/daemon/dist/pairing.js
14371
14551
  function pairingPath(home) {
@@ -15509,7 +15689,8 @@ function presencePayload(rows, label, seq, extra, policy) {
15509
15689
  label,
15510
15690
  sessions: [],
15511
15691
  features: [...FEATURES, ...extra?.features ?? []],
15512
- ...extra?.direct ? { direct: extra.direct } : {}
15692
+ ...extra?.direct ? { direct: extra.direct } : {},
15693
+ ...policy.away === true ? { away: true } : {}
15513
15694
  };
15514
15695
  return {
15515
15696
  ...payload,
@@ -17142,6 +17323,11 @@ var init_directory_home = __esmMin((() => {
17142
17323
  this.#options.log("info", "directory.approved", { seq: done.seq });
17143
17324
  return done;
17144
17325
  }
17326
+ async wake(state) {
17327
+ const link = this.#link;
17328
+ if (!link?.wake) return "pending";
17329
+ return link.wake(state);
17330
+ }
17145
17331
  presenceWatch(persons) {
17146
17332
  this.#link?.presenceWatch(persons).catch((error) => this.#failed("directory.presence.watch", error));
17147
17333
  }
@@ -17156,6 +17342,7 @@ var init_directory_home = __esmMin((() => {
17156
17342
  },
17157
17343
  send: (env) => home.#link ? home.#link.send(env) : Promise.reject(noLink()),
17158
17344
  presence: (env) => home.#link?.presence(env),
17345
+ retryNow: () => home.#link?.retryNow?.(),
17159
17346
  appendRoster: () => Promise.reject(new MessagingError("invalid_state", "a directory realm has no roster")),
17160
17347
  syncRoster: () => Promise.reject(new MessagingError("invalid_state", "a directory realm has no roster"))
17161
17348
  };
@@ -17261,7 +17448,8 @@ var init_jelly$1 = __esmMin((() => {
17261
17448
  this.#deps.log("info", "jelly.grant", {
17262
17449
  method: call.method,
17263
17450
  scope: grant.scope,
17264
- ...grant.dev !== void 0 ? { dev: grant.dev.slice(0, 200) } : {}
17451
+ ...grant.dev !== void 0 ? { dev: grant.dev.slice(0, 200) } : {},
17452
+ ...grant.mbr !== void 0 ? { mbr: grant.mbr.slice(0, 200) } : {}
17265
17453
  });
17266
17454
  return {
17267
17455
  scope: grant.scope,
@@ -17294,6 +17482,313 @@ var init_jelly$1 = __esmMin((() => {
17294
17482
  }
17295
17483
  };
17296
17484
  }));
17485
+ // packages/daemon/dist/jelly-env.js
17486
+ function jellyEnvNamed(env) {
17487
+ return JELLY_ENV.filter((name) => (env[name] ?? "") !== "");
17488
+ }
17489
+ function fail(message) {
17490
+ throw new MessagingError("invalid_request", message);
17491
+ }
17492
+ function required(env, name) {
17493
+ const value = env[name] ?? "";
17494
+ if (value === "") fail(`${name} is not set: a Jelly host sets it with JELLY_ACTION_KEY`);
17495
+ return value;
17496
+ }
17497
+ function folder(name, value, separator = delimiter) {
17498
+ if (!isAbsolute(value)) fail(`${name} is not an absolute path`);
17499
+ if (value.includes(separator)) fail(`${name} names more than one folder (joined with "${separator}"); the daemon takes one`);
17500
+ return value;
17501
+ }
17502
+ function relayUrlOf(url) {
17503
+ let parsed;
17504
+ try {
17505
+ parsed = new URL(url);
17506
+ } catch {
17507
+ return fail(`JELLY_RESOURCES' ${JELLY_RESOURCE_NAME} url is not a URL`);
17508
+ }
17509
+ const loopback = /^(?:localhost|127\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[::1\])$/.test(parsed.hostname);
17510
+ const scheme = parsed.protocol === "https:" ? "wss:" : parsed.protocol === "http:" && loopback ? "ws:" : void 0;
17511
+ if (scheme === void 0) fail(`JELLY_RESOURCES' ${JELLY_RESOURCE_NAME} url is not https:// (or http:// to a loopback host)`);
17512
+ if (parsed.username !== "" || parsed.password !== "" || parsed.search !== "" || parsed.hash !== "" || parsed.pathname !== "/" && parsed.pathname !== "") fail(`JELLY_RESOURCES' ${JELLY_RESOURCE_NAME} url is not an origin: the relay is derived from its origin alone`);
17513
+ return `${scheme}//${parsed.host}/v1/connect`;
17514
+ }
17515
+ function jellyEnvRealm(env) {
17516
+ let raw;
17517
+ try {
17518
+ raw = parseWire(required(env, "JELLY_RESOURCES"), { maxBytes: 65536 });
17519
+ } catch (error) {
17520
+ if (error instanceof MessagingError && /JELLY_RESOURCES is not set/.test(error.message)) throw error;
17521
+ return fail("JELLY_RESOURCES is not JSON");
17522
+ }
17523
+ const list = Resources.safeParse(raw);
17524
+ if (!list.success) fail("JELLY_RESOURCES is not a list of discovery entries");
17525
+ const entries = list.data.map((item) => ResourceEntry.safeParse(item)).filter((parsed) => parsed.success && parsed.data.name === "messaging");
17526
+ if (entries.length === 0) fail(`JELLY_RESOURCES has no ${JELLY_RESOURCE_NAME} entry`);
17527
+ if (entries.length > 1) fail(`JELLY_RESOURCES has more than one ${JELLY_RESOURCE_NAME} entry`);
17528
+ const entry = entries[0].data;
17529
+ const realm = DirectoryRealmConfig.strict().safeParse({
17530
+ kind: "directory",
17531
+ url: relayUrlOf(entry.url),
17532
+ issuer: required(env, "JELLY_ID_ISSUER"),
17533
+ resource: entry.resource,
17534
+ key: required(env, "JELLY_SERVICE_ANCHOR")
17535
+ });
17536
+ if (!realm.success) {
17537
+ const at = realm.error.issues[0]?.path[0];
17538
+ fail(at === "issuer" ? "JELLY_ID_ISSUER is not an https:// issuer" : at === "key" ? "JELLY_SERVICE_ANCHOR is not an Ed25519 key in base64url" : `JELLY_RESOURCES' ${JELLY_RESOURCE_NAME} entry is invalid at ${String(at)}`);
17539
+ }
17540
+ try {
17541
+ directoryRealmId(realm.data);
17542
+ } catch {
17543
+ fail("JELLY_SERVICE_ANCHOR is not a usable Ed25519 key");
17544
+ }
17545
+ return realm.data;
17546
+ }
17547
+ function readJellyEnv(env) {
17548
+ const actKeyPath = required(env, "JELLY_ACTION_KEY");
17549
+ if (!isAbsolute(actKeyPath)) fail("JELLY_ACTION_KEY is not an absolute path");
17550
+ const home = folder("STATE_DIRECTORY", required(env, "STATE_DIRECTORY"));
17551
+ const runText = env.RUNTIME_DIRECTORY ?? "";
17552
+ const groupText = env.JELLY_SOCKET_GROUP ?? "";
17553
+ if (groupText !== "" && process.platform === "win32") fail("JELLY_SOCKET_GROUP is not supported on Windows: a named pipe is not shared with a group");
17554
+ if (groupText !== "" && (!/^\d{1,10}$/.test(groupText) || Number(groupText) > 4294967294)) fail("JELLY_SOCKET_GROUP is not a numeric gid");
17555
+ let label;
17556
+ try {
17557
+ label = jellyLabel(required(env, "JELLY_DEVICE_LABEL"));
17558
+ } catch (error) {
17559
+ if (error instanceof MessagingError && /is not set/.test(error.message)) throw error;
17560
+ return fail("JELLY_DEVICE_LABEL has no letter or digit to make a device label from");
17561
+ }
17562
+ return {
17563
+ home,
17564
+ ...runText !== "" ? { run: folder("RUNTIME_DIRECTORY", runText) } : {},
17565
+ actKeyPath,
17566
+ label,
17567
+ ...groupText !== "" ? { socketGroup: Number(groupText) } : {},
17568
+ realm: jellyEnvRealm(env)
17569
+ };
17570
+ }
17571
+ function realmMismatch(pinned, named) {
17572
+ return [
17573
+ "url",
17574
+ "issuer",
17575
+ "resource",
17576
+ "key"
17577
+ ].filter((member) => pinned[member] !== named[member]);
17578
+ }
17579
+ async function pinRealm(realm, get = wellKnownGet) {
17580
+ const origin = new URL(realm.url);
17581
+ const url = `${origin.protocol === "wss:" ? "https:" : "http:"}//${origin.host}/.well-known/messaging.json`;
17582
+ let text;
17583
+ try {
17584
+ text = await get(url);
17585
+ } catch (error) {
17586
+ throw new MessagingError("relay_unavailable", `cannot read the relay's realm document ${url} for the first start (${error instanceof Error ? error.message.slice(0, 200) : "failed"}); the next start tries again`);
17587
+ }
17588
+ let raw;
17589
+ try {
17590
+ raw = parseWire(text, { maxBytes: 16384 });
17591
+ } catch {
17592
+ return fail(`the relay's realm document ${url} is not JSON`);
17593
+ }
17594
+ const { realm: _id, ...published } = typeof raw === "object" && raw !== null ? raw : {};
17595
+ const parsed = DirectoryRealmConfig.safeParse(published);
17596
+ if (!parsed.success) fail(`the relay's realm document ${url} is not a directory realm`);
17597
+ const differs = realmMismatch(parsed.data, realm);
17598
+ if (differs.length > 0) fail(`the relay's realm document ${url} names another ${differs.join(", ")} than the environment: the home is not pinned`);
17599
+ return parsed.data.orgDomains !== void 0 ? {
17600
+ ...realm,
17601
+ orgDomains: parsed.data.orgDomains
17602
+ } : realm;
17603
+ }
17604
+ var JELLY_ENV, JELLY_RESOURCE_NAME, ResourceEntry, Resources, wellKnownGet;
17605
+ var init_jelly_env = __esmMin((() => {
17606
+ init_dist$3();
17607
+ init_zod();
17608
+ init_jelly$1();
17609
+ JELLY_ENV = [
17610
+ "JELLY_ACTION_KEY",
17611
+ "JELLY_ID_ISSUER",
17612
+ "JELLY_RESOURCES",
17613
+ "JELLY_SERVICE_ANCHOR",
17614
+ "JELLY_DEVICE_LABEL",
17615
+ "JELLY_SOCKET_GROUP"
17616
+ ];
17617
+ JELLY_RESOURCE_NAME = "messaging";
17618
+ ResourceEntry = looseObject({
17619
+ name: string$1().max(200),
17620
+ resource: string$1().min(1).max(2048),
17621
+ url: string$1().min(1).max(2048)
17622
+ });
17623
+ Resources = array(unknown()).max(64);
17624
+ wellKnownGet = async (url) => {
17625
+ const response = await fetch(url, {
17626
+ redirect: "error",
17627
+ signal: AbortSignal.timeout(5e3)
17628
+ });
17629
+ if (!response.ok) throw new MessagingError("relay_unavailable", `the relay answered ${response.status}`);
17630
+ const text = await response.text();
17631
+ if (Buffer.byteLength(text) > 16384) throw new MessagingError("too_large", "the relay's realm document is larger than 16 KiB");
17632
+ return text;
17633
+ };
17634
+ }));
17635
+ // packages/daemon/dist/consent/answer-preview.js
17636
+ function who(from) {
17637
+ const name = cleanPeerText(from.name, {
17638
+ singleLine: true,
17639
+ maxChars: 80
17640
+ }) || "someone";
17641
+ const email = from.email === void 0 ? void 0 : cleanPeerText(from.email, {
17642
+ singleLine: true,
17643
+ maxChars: 100
17644
+ });
17645
+ return `${name} (${email !== void 0 && email !== "" ? `${email}, ` : ""}${ORG[from.org]})`;
17646
+ }
17647
+ function answersFor(kind) {
17648
+ return kind === "suggestion" ? ["decline"] : kind === "contact" ? [
17649
+ "accept",
17650
+ "decline",
17651
+ "block"
17652
+ ] : [
17653
+ "once",
17654
+ "always",
17655
+ "decline",
17656
+ "block"
17657
+ ];
17658
+ }
17659
+ function answerSentence(input) {
17660
+ const { kind, answer } = input;
17661
+ if (answer !== void 0 && !answersFor(kind).includes(answer)) throw new MessagingError("invalid_request", `a ${kind === "message" || kind === "task" ? "request to your agent" : kind === "contact" ? "message request" : "suggestion"} takes ${answersFor(kind).join(", ")}`);
17662
+ const sender = who(input.from);
17663
+ const single = input.count <= 1;
17664
+ if (kind === "suggestion") {
17665
+ const email = cleanPeerText(input.email ?? "", {
17666
+ singleLine: true,
17667
+ maxChars: 100
17668
+ });
17669
+ if (answer === void 0) return `Your agent ${input.agent} suggests you send a message request to ${email}.`;
17670
+ return `Dismiss your agent ${input.agent}'s suggestion to contact ${email}. Nothing is sent.`;
17671
+ }
17672
+ if (kind === "contact") {
17673
+ const thing = single ? "their message request" : `their ${input.count} message requests`;
17674
+ if (answer === void 0) return `${sender} asks to message you.`;
17675
+ if (answer === "accept") return `${sender} may message you from now on: ${single ? "their request becomes their first message" : "their requests become their first messages"}, and they get your card.`;
17676
+ if (answer === "decline") return `Decline ${thing} from ${sender}. They are told it was declined.`;
17677
+ return `Block ${sender}: nothing from them or their agents reaches you, and ${thing} ${single ? "is" : "are"} declined.`;
17678
+ }
17679
+ const thing = single ? `this ${kind}` : `these ${input.count} requests`;
17680
+ const src = single ? `${sender}'s agent ${input.agent}` : `${sender}'s agents`;
17681
+ const dst = single ? `your agent ${input.to}` : "your agents";
17682
+ if (answer === void 0) return `${src} asks to send ${kind === "task" ? "a task" : "a message"} to ${dst}.`;
17683
+ if (answer === "once") return `Deliver ${thing} from ${src} to ${dst} this once, guarded: ${single ? "your agent" : "your agents"} may answer only them.`;
17684
+ if (answer === "always") return `Deliver ${thing} from ${src} to ${dst}, and let their agents ask yours from now on without asking you.`;
17685
+ if (answer === "decline") return `Decline ${thing} from ${src}. They are told it was declined.`;
17686
+ return `Block ${sender}: nothing from them or their agents reaches you, and ${thing} ${single ? "is" : "are"} declined.`;
17687
+ }
17688
+ var ORG;
17689
+ var init_answer_preview = __esmMin((() => {
17690
+ init_dist$3();
17691
+ ORG = {
17692
+ same: "your org",
17693
+ other: "another org",
17694
+ none: "no org"
17695
+ };
17696
+ }));
17697
+ // packages/daemon/dist/snapshot.js
17698
+ function sha256File(path) {
17699
+ const data = readFileSync(path);
17700
+ return {
17701
+ bytes: data.length,
17702
+ sha256: createHash("sha256").update(data).digest("hex")
17703
+ };
17704
+ }
17705
+ var SNAPSHOT_HOLD_MS, Snapshots;
17706
+ var init_snapshot = __esmMin((() => {
17707
+ init_dist$3();
17708
+ init_dist$2();
17709
+ SNAPSHOT_HOLD_MS = 36e5;
17710
+ Snapshots = class {
17711
+ #deps;
17712
+ #timer;
17713
+ constructor(deps) {
17714
+ this.#deps = deps;
17715
+ }
17716
+ get folder() {
17717
+ return join(this.#deps.run, "snapshot");
17718
+ }
17719
+ take() {
17720
+ const folder = this.folder;
17721
+ if (existsSync(folder)) throw new MessagingError("invalid_state", "a snapshot is held: release it with daemon.snapshot {done: true} first");
17722
+ const group = this.#deps.group;
17723
+ mkdirSync(folder, { mode: group === void 0 ? 448 : 488 });
17724
+ try {
17725
+ if (!IS_WINDOWS && group !== void 0) {
17726
+ chownSync(folder, process.geteuid?.() ?? -1, group);
17727
+ chmodSync(folder, 488);
17728
+ }
17729
+ const fileMode = group === void 0 ? 384 : 416;
17730
+ const files = [];
17731
+ const db = join(folder, "messaging.db");
17732
+ this.#deps.vacuumInto(db);
17733
+ if (existsSync(this.#deps.config)) writeFileSync(join(folder, "config.json"), readFileSync(this.#deps.config), { mode: fileMode });
17734
+ for (const name of ["messaging.db", "config.json"]) {
17735
+ const path = join(folder, name);
17736
+ if (!existsSync(path)) continue;
17737
+ if (!IS_WINDOWS) {
17738
+ if (group !== void 0) chownSync(path, process.geteuid?.() ?? -1, group);
17739
+ chmodSync(path, fileMode);
17740
+ }
17741
+ files.push({
17742
+ name,
17743
+ ...sha256File(path)
17744
+ });
17745
+ }
17746
+ const at = this.#deps.now();
17747
+ const until = at + SNAPSHOT_HOLD_MS;
17748
+ const manifest = {
17749
+ storeSchema: this.#deps.storeSchema,
17750
+ at,
17751
+ files
17752
+ };
17753
+ writeFileSync(join(folder, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, { mode: fileMode });
17754
+ if (!IS_WINDOWS && group !== void 0) chownSync(join(folder, "manifest.json"), process.geteuid?.() ?? -1, group);
17755
+ clearTimeout(this.#timer);
17756
+ this.#timer = setTimeout(() => this.release(), SNAPSHOT_HOLD_MS);
17757
+ this.#timer.unref();
17758
+ return {
17759
+ folder,
17760
+ files,
17761
+ storeSchema: this.#deps.storeSchema,
17762
+ at,
17763
+ until
17764
+ };
17765
+ } catch (error) {
17766
+ this.release();
17767
+ if (error instanceof MessagingError) throw error;
17768
+ throw new MessagingError("internal", `the snapshot failed: ${error.code ?? "error"}`);
17769
+ }
17770
+ }
17771
+ release() {
17772
+ clearTimeout(this.#timer);
17773
+ this.#timer = void 0;
17774
+ let present;
17775
+ try {
17776
+ lstatSync(this.folder);
17777
+ present = true;
17778
+ } catch {
17779
+ present = false;
17780
+ }
17781
+ if (present) rmSync(this.folder, {
17782
+ recursive: true,
17783
+ force: true
17784
+ });
17785
+ return { released: present };
17786
+ }
17787
+ close() {
17788
+ this.release();
17789
+ }
17790
+ };
17791
+ }));
17297
17792
  // packages/daemon/dist/notices.js
17298
17793
  function firstLine(text) {
17299
17794
  const first = cleanPeerText(text.split(/\r\n|\n|\r/, 1)[0] ?? "", { singleLine: true });
@@ -18298,7 +18793,7 @@ function taskRow(r) {
18298
18793
  return row;
18299
18794
  }
18300
18795
  var DatabaseSync$1, CHAT_COUNTER, MIGRATIONS, BASELINE, kvGet, kvSet, schemaNumber, TASKS, text$1, num, json, opt, Store, UNSPACED;
18301
- var init_store$1 = __esmMin((() => {
18796
+ var init_store$2 = __esmMin((() => {
18302
18797
  init_dist$3();
18303
18798
  ({DatabaseSync: DatabaseSync$1} = process.getBuiltinModule("node:sqlite"));
18304
18799
  CHAT_COUNTER = "chat";
@@ -18601,6 +19096,10 @@ CREATE TABLE IF NOT EXISTS idempotency (
18601
19096
  this.#statements.clear();
18602
19097
  this.#db.close();
18603
19098
  }
19099
+ vacuumInto(path) {
19100
+ if (this.#depth > 0) throw new MessagingError("invalid_state", "a snapshot is not taken inside a transaction");
19101
+ this.#db.exec(`VACUUM INTO '${path.replaceAll("'", "''")}'`);
19102
+ }
18604
19103
  tx(fn) {
18605
19104
  if (this.#depth > 0) {
18606
19105
  const name = `sp${this.#depth}`;
@@ -19296,6 +19795,12 @@ CREATE TABLE IF NOT EXISTS idempotency (
19296
19795
  lastAt: Number(row.at)
19297
19796
  }));
19298
19797
  }
19798
+ awaitedRequests(sessions, terminal, limit) {
19799
+ if (sessions.length === 0) return [];
19800
+ return this.#all(`SELECT request FROM tasks WHERE role = 'requester' AND request IS NOT NULL
19801
+ AND session IN (${sessions.map(() => "?").join(", ")}) AND state NOT IN (${terminal.map(() => "?").join(", ")})
19802
+ ORDER BY updated_at DESC, task_id DESC LIMIT ?`, ...sessions, ...terminal, limit).map((row) => String(row.request));
19803
+ }
19299
19804
  task(taskId, role) {
19300
19805
  const row = this.#get(`SELECT * FROM ${TASKS} WHERE task_id = ? AND role = ?`, taskId, role);
19301
19806
  return row && taskRow(row);
@@ -20687,6 +21192,23 @@ function jellyConfig(path, jelly) {
20687
21192
  if (config.deployment === "jelly") throw new MessagingError("invalid_request", "config.json says deployment jelly: start messagingd with --deployment jelly and the host's --act-key");
20688
21193
  return;
20689
21194
  }
21195
+ if (jelly.realm !== void 0) {
21196
+ if (config.realm === void 0) {
21197
+ if (config.relay !== void 0 || config.person !== void 0) throw new MessagingError("invalid_request", `${path} is a roster home (relay, person): a Jelly host's home is pinned to a directory realm; give the service a new STATE_DIRECTORY`);
21198
+ if (config.ipc?.agentGroup !== void 0) throw new MessagingError("invalid_request", "deployment jelly shares its sockets with --socket-group, not ipc.agentGroup");
21199
+ writeConfig(path, {
21200
+ ...config,
21201
+ realm: jelly.realm,
21202
+ deployment: "jelly"
21203
+ });
21204
+ return;
21205
+ }
21206
+ const differs = realmMismatch(config.realm, jelly.realm);
21207
+ if (differs.length > 0) {
21208
+ const which = differs.map((member) => `${member} ${JSON.stringify(config.realm[member])} (the environment: ${JSON.stringify(jelly.realm[member])})`).join(", ");
21209
+ throw new MessagingError("invalid_request", `${path} pins another realm: ${which}. A home never changes realm, and the daemon does not rewrite it; restore the variables the home was made with, or give the service a new STATE_DIRECTORY`);
21210
+ }
21211
+ }
20690
21212
  if (config.realm === void 0) throw new MessagingError("invalid_request", "deployment jelly needs a directory realm: config.json has no realm (a Jelly host supervises no roster realm)");
20691
21213
  if (config.ipc?.agentGroup !== void 0) throw new MessagingError("invalid_request", "deployment jelly shares its sockets with --socket-group, not ipc.agentGroup");
20692
21214
  if (config.deployment !== "jelly") writeConfig(path, {
@@ -20715,13 +21237,16 @@ var init_daemon = __esmMin((() => {
20715
21237
  init_roster_identity();
20716
21238
  init_directory_home();
20717
21239
  init_jelly$1();
21240
+ init_jelly_env();
21241
+ init_answer_preview();
21242
+ init_snapshot();
20718
21243
  init_notices();
20719
21244
  init_counterpart();
20720
21245
  init_resolve();
20721
21246
  init_secrets();
20722
21247
  init_roster_sync();
20723
21248
  init_storage_errors();
20724
- init_store$1();
21249
+ init_store$2();
20725
21250
  init_tasks();
20726
21251
  init_version();
20727
21252
  init_wake_bridge();
@@ -20776,6 +21301,8 @@ var init_daemon = __esmMin((() => {
20776
21301
  #closed = false;
20777
21302
  #online = new Set();
20778
21303
  #holders = new Map();
21304
+ #ticketEnds = new Map();
21305
+ #lapsed = new Map();
20779
21306
  #policy = {
20780
21307
  rate: inboundRateLimiter(),
20781
21308
  presence: responseRateLimiter(),
@@ -20795,6 +21322,9 @@ var init_daemon = __esmMin((() => {
20795
21322
  #requests;
20796
21323
  #notices;
20797
21324
  #jellyHost;
21325
+ #attached = false;
21326
+ #wakeArmed = false;
21327
+ #snapshots;
20798
21328
  #jellyAuth;
20799
21329
  #state = "starting";
20800
21330
  #sync;
@@ -20908,6 +21438,14 @@ var init_daemon = __esmMin((() => {
20908
21438
  address: (sender, session) => peerRef(sender, session).address,
20909
21439
  held: (row) => this.#notices.held(row)
20910
21440
  });
21441
+ this.#snapshots = new Snapshots({
21442
+ run: paths.run,
21443
+ config: paths.config,
21444
+ vacuumInto: (path) => this.#store.vacuumInto(path),
21445
+ storeSchema: 9,
21446
+ group: options.jellyHost?.socketGroup,
21447
+ now: this.#now
21448
+ });
20911
21449
  this.#notices = new Notices({
20912
21450
  store,
20913
21451
  now: this.#now,
@@ -20947,6 +21485,14 @@ var init_daemon = __esmMin((() => {
20947
21485
  static async start(options) {
20948
21486
  const paths = homePaths(options.home, options.jellyHost?.run);
20949
21487
  ensureHome(paths);
21488
+ const jelly = options.jellyHost;
21489
+ if (jelly?.realm !== void 0 && readConfig(paths.config).realm === void 0) options = {
21490
+ ...options,
21491
+ jellyHost: {
21492
+ ...jelly,
21493
+ realm: await pinRealm(jelly.realm, options.wellKnown)
21494
+ }
21495
+ };
20950
21496
  jellyConfig(paths.config, options.jellyHost);
20951
21497
  const lock = acquireLock(paths.lock);
20952
21498
  let store;
@@ -20980,6 +21526,7 @@ var init_daemon = __esmMin((() => {
20980
21526
  } else if (entries.length > 0) this.#store.clearRoster();
20981
21527
  this.#sync.load();
20982
21528
  this.#store.keepAllLive(this.#now(), "cli");
21529
+ this.#snapshots.release();
20983
21530
  const handlers = this.#handlers();
20984
21531
  const jelly = this.#jellyHost;
20985
21532
  if (jelly) this.#jellyAuth = new JellyAuth({
@@ -21117,6 +21664,7 @@ var init_daemon = __esmMin((() => {
21117
21664
  for (const server of this.#ipc) await server.close().catch(() => {});
21118
21665
  await Promise.race([this.#flushing, new Promise((resolve) => setTimeout(resolve, 2e3).unref())]);
21119
21666
  this.#store.close();
21667
+ this.#snapshots.close();
21120
21668
  } finally {
21121
21669
  this.#lock.release();
21122
21670
  this.#log("info", "daemon.stopped", { pid: process.pid });
@@ -21289,6 +21837,7 @@ var init_daemon = __esmMin((() => {
21289
21837
  },
21290
21838
  "request.list": async () => ({ requests: [...this.#holds.list().requests, ...await this.#requests.list()] }),
21291
21839
  "request.answer": (p) => this.#requests.holds(p.id) ? this.#requests.answer(p) : this.#holds.answer(p),
21840
+ "request.answer.preview": (p) => this.#answerPreview(p),
21292
21841
  "contact.request": (p) => this.#requests.request(p),
21293
21842
  "contact.request.preview": (p) => this.#requests.preview(p),
21294
21843
  "card.get": async (p) => {
@@ -21297,6 +21846,11 @@ var init_daemon = __esmMin((() => {
21297
21846
  return { sent: await this.#cards.get(p.person) };
21298
21847
  },
21299
21848
  "daemon.health": () => this.#health(),
21849
+ "daemon.woke": () => this.#woke(),
21850
+ "wake.arm": (p) => this.#wakeArm(p),
21851
+ "wake.disarm": () => this.#wakeDisarm(),
21852
+ "daemon.snapshot": (p) => p.done ? this.#snapshots.release() : this.#snapshots.take(),
21853
+ "person.attached": (p) => this.#personAttached(p.attached),
21300
21854
  "agents.list": () => this.#agentsList(),
21301
21855
  "agents.read": (p) => this.#agentsRead(p),
21302
21856
  "notice.subscribe": (p, ctx, authority) => this.#notices.subscribe(ctx, p, authority?.scope === "host" ? authority.expiresAt : void 0),
@@ -21498,6 +22052,11 @@ var init_daemon = __esmMin((() => {
21498
22052
  let holders = this.#holders.get(p.key);
21499
22053
  if (!holders) this.#holders.set(p.key, holders = new Set());
21500
22054
  holders.add(ctx.id);
22055
+ if (ticket) {
22056
+ this.#ticketEnds.set(p.key, Math.max(this.#ticketEnds.get(p.key) ?? 0, ticket.exp * 1e3));
22057
+ for (const id of this.#lapsed.get(p.key) ?? []) holders.add(id);
22058
+ this.#lapsed.delete(p.key);
22059
+ }
21501
22060
  this.#sessionsChanged();
21502
22061
  return {
21503
22062
  session: name,
@@ -21505,9 +22064,23 @@ var init_daemon = __esmMin((() => {
21505
22064
  };
21506
22065
  }
21507
22066
  #bound(p, ctx) {
22067
+ const ends = this.#ticketEnds.get(p.key);
22068
+ const held = this.#holders.get(p.key)?.has(ctx.id) === true || this.#lapsed.get(p.key)?.has(ctx.id) === true;
22069
+ if (ends !== void 0 && ends <= this.#now() && held) throw new MessagingError("ticket_invalid", "the session ticket this session registered with has expired: register again with a fresh ticket from the host");
21508
22070
  if (!this.#holders.get(p.key)?.has(ctx.id)) throw new MessagingError("not_found", "this session key is not registered on this connection");
21509
22071
  return p;
21510
22072
  }
22073
+ #expireTickets(now) {
22074
+ let changed = false;
22075
+ for (const [key, ends] of this.#ticketEnds) {
22076
+ if (ends > now || !this.#holders.has(key)) continue;
22077
+ this.#lapsed.set(key, this.#holders.get(key));
22078
+ this.#holders.delete(key);
22079
+ this.#log("info", "session.ticket.expired", {});
22080
+ if (this.#store.keepSession(key, now)) changed = true;
22081
+ }
22082
+ if (changed) this.#sessionsChanged();
22083
+ }
21511
22084
  #sessionOf(key) {
21512
22085
  const session = this.#store.sessionByKey(key);
21513
22086
  if (!session) throw new MessagingError("not_found", "this session key is not registered");
@@ -21528,6 +22101,8 @@ var init_daemon = __esmMin((() => {
21528
22101
  }
21529
22102
  #unregister(p) {
21530
22103
  this.#holders.delete(p.key);
22104
+ this.#ticketEnds.delete(p.key);
22105
+ this.#lapsed.delete(p.key);
21531
22106
  if (this.#store.keepSession(p.key, this.#now())) this.#sessionsChanged();
21532
22107
  return { ok: true };
21533
22108
  }
@@ -21536,9 +22111,11 @@ var init_daemon = __esmMin((() => {
21536
22111
  this.#chats.disconnected(ctx);
21537
22112
  this.#notices.disconnected(ctx.id);
21538
22113
  let changed = false;
22114
+ for (const [key, lapsed] of [...this.#lapsed]) if (lapsed.delete(ctx.id) && lapsed.size === 0) this.#lapsed.delete(key);
21539
22115
  for (const [key, holders] of [...this.#holders]) {
21540
22116
  if (!holders.delete(ctx.id) || holders.size > 0) continue;
21541
22117
  this.#holders.delete(key);
22118
+ this.#ticketEnds.delete(key);
21542
22119
  if (this.#store.sessionByKey(key)?.agent === "cli") continue;
21543
22120
  if (this.#store.keepSession(key, this.#now())) changed = true;
21544
22121
  }
@@ -21586,7 +22163,8 @@ var init_daemon = __esmMin((() => {
21586
22163
  unregisteredAt: s.unregisteredAt ?? this.#now()
21587
22164
  })) : this.#store.sessions(), me.label, this.#store.next("presence"), this.#transport?.advertise?.(), {
21588
22165
  wake: (s) => this.#wakes(s),
21589
- share: new Set(this.#currentConfig().presence?.share ?? [])
22166
+ share: new Set(this.#currentConfig().presence?.share ?? []),
22167
+ away: this.#away()
21590
22168
  }),
21591
22169
  now: this.#now()
21592
22170
  });
@@ -21947,10 +22525,17 @@ var init_daemon = __esmMin((() => {
21947
22525
  async #peopleGet(p) {
21948
22526
  const { realm, identity } = this.#joined();
21949
22527
  const profile = await realm.profile(p.person);
22528
+ const fresh = this.#now() - (realm.kind === "directory" ? this.#tuning.presenceOnlineMs : this.#tuning.presenceMaxMs * 2);
22529
+ const away = (machine) => {
22530
+ if (machine === identity.machine) return this.#away();
22531
+ const row = this.#store.directoryRow(machine);
22532
+ return row !== void 0 && row.receivedAt >= fresh && row.presence.away === true;
22533
+ };
21950
22534
  const devices = realm.known(p.person).devices.map((device) => ({
21951
22535
  device: device.machine,
21952
22536
  label: device.label,
21953
- self: device.machine === identity.machine
22537
+ self: device.machine === identity.machine,
22538
+ ...away(device.machine) ? { away: true } : {}
21954
22539
  }));
21955
22540
  return {
21956
22541
  person: p.person,
@@ -22879,6 +23464,7 @@ var init_daemon = __esmMin((() => {
22879
23464
  if (this.#closed) return;
22880
23465
  this.#directory?.sweep();
22881
23466
  const now = this.#now();
23467
+ this.#expireTickets(now);
22882
23468
  let sessions = 0;
22883
23469
  try {
22884
23470
  this.#tx(() => {
@@ -23149,6 +23735,135 @@ var init_daemon = __esmMin((() => {
23149
23735
  self: d.machine === identity.machine
23150
23736
  })) };
23151
23737
  }
23738
+ #away() {
23739
+ return this.#jellyHost !== void 0 && !this.#attached;
23740
+ }
23741
+ #personAttached(attached) {
23742
+ if (attached !== this.#attached) {
23743
+ this.#attached = attached;
23744
+ this.#log("info", "person.attached", { attached });
23745
+ this.#presenceSoon();
23746
+ }
23747
+ return { ok: true };
23748
+ }
23749
+ #woke() {
23750
+ this.#log("info", "daemon.woke", {});
23751
+ this.#transport?.retryNow?.();
23752
+ this.sweep();
23753
+ this.#presenceSoon();
23754
+ this.#wokeHook();
23755
+ return { ok: true };
23756
+ }
23757
+ #wokeHook() {
23758
+ if (!this.#wakeArmed) return;
23759
+ this.#wakeDisarm().catch((error) => this.#log("warn", "wake.disarm.failed", { code: codeOf$2(error) }));
23760
+ }
23761
+ async #wakeArm(p) {
23762
+ const home = this.#directory;
23763
+ if (!home) throw new MessagingError("invalid_state", "wake tickets are for a directory realm");
23764
+ if (home.person() === void 0) throw new MessagingError("invalid_state", "this device is not bound yet");
23765
+ const issuer = this.#config.realm?.issuer;
23766
+ if (issuer === void 0 || new URL(p.url).origin !== new URL(issuer).origin) throw new MessagingError("invalid_request", "the wake URL is not on the realm's Jelly ID");
23767
+ const agents = [...new Set(p.agents)];
23768
+ const awaiting = this.#store.awaitedRequests(agents, TERMINAL_STATES, 500);
23769
+ const sent = await home.wake({
23770
+ url: p.url,
23771
+ ticket: p.ticket,
23772
+ agents,
23773
+ awaiting
23774
+ });
23775
+ this.#wakeArmed = true;
23776
+ this.#log("info", "wake.arm", {
23777
+ relay: sent === "sent" ? "armed" : "pending",
23778
+ agents: agents.length,
23779
+ awaiting: awaiting.length
23780
+ });
23781
+ return {
23782
+ relay: sent === "sent" ? "armed" : "pending",
23783
+ awaiting: awaiting.length
23784
+ };
23785
+ }
23786
+ async #wakeDisarm() {
23787
+ const home = this.#directory;
23788
+ if (!home) throw new MessagingError("invalid_state", "wake tickets are for a directory realm");
23789
+ const sent = await home.wake("disarm");
23790
+ this.#wakeArmed = false;
23791
+ this.#log("info", "wake.disarm", { relay: sent === "sent" ? "disarmed" : "pending" });
23792
+ return { relay: sent === "sent" ? "disarmed" : "pending" };
23793
+ }
23794
+ async #answerPreview(p) {
23795
+ const { realm, me } = this.#joined();
23796
+ const now = this.#now();
23797
+ const profile = async (person) => {
23798
+ const found = await realm.profile(person).catch(() => void 0);
23799
+ return found ? {
23800
+ name: found.name.slice(0, 200),
23801
+ ...found.email !== void 0 ? { email: found.email.slice(0, 320) } : {},
23802
+ org: found.org
23803
+ } : {
23804
+ name: String(person).slice(0, 200),
23805
+ org: "none"
23806
+ };
23807
+ };
23808
+ const answer = p.answer;
23809
+ const ask = this.#store.held(p.id);
23810
+ if (ask && ask.exp > now) {
23811
+ const kind = ask.payload.kind;
23812
+ const count = p.all ? this.#store.helds(ask.person).filter((row) => row.exp > now).length : 1;
23813
+ const from = await profile(ask.person);
23814
+ const agent = ask.envelope.from.agent ?? "";
23815
+ return {
23816
+ kind,
23817
+ from,
23818
+ ...agent !== "" ? { agent } : {},
23819
+ to: ask.session,
23820
+ count,
23821
+ sentence: answerSentence({
23822
+ kind,
23823
+ from,
23824
+ agent,
23825
+ to: ask.session,
23826
+ answer,
23827
+ count
23828
+ })
23829
+ };
23830
+ }
23831
+ const request = this.#store.contactRequest(p.id);
23832
+ if (request && request.exp > now) {
23833
+ const count = p.all ? this.#store.contactRequests(request.person).filter((row) => row.exp > now).length : 1;
23834
+ const from = await profile(request.person);
23835
+ return {
23836
+ kind: "contact",
23837
+ from,
23838
+ count,
23839
+ sentence: answerSentence({
23840
+ kind: "contact",
23841
+ from,
23842
+ answer,
23843
+ count
23844
+ })
23845
+ };
23846
+ }
23847
+ const suggestion = this.#store.contactSuggestion(p.id);
23848
+ if (suggestion) {
23849
+ const from = await profile(me.person);
23850
+ return {
23851
+ kind: "suggestion",
23852
+ from,
23853
+ agent: suggestion.session,
23854
+ count: 1,
23855
+ sentence: answerSentence({
23856
+ kind: "suggestion",
23857
+ from,
23858
+ agent: suggestion.session,
23859
+ email: suggestion.email,
23860
+ answer,
23861
+ count: 1
23862
+ })
23863
+ };
23864
+ }
23865
+ throw new MessagingError("not_found", "no request with that id is held here");
23866
+ }
23152
23867
  #me() {
23153
23868
  return (this.#realm ?? this.#directory?.realm)?.self()?.person;
23154
23869
  }
@@ -26709,6 +27424,9 @@ var init_direct_transport = __esmMin((() => {
26709
27424
  presence(env) {
26710
27425
  this.#relay.presence(env);
26711
27426
  }
27427
+ retryNow() {
27428
+ this.#relay.retryNow?.();
27429
+ }
26712
27430
  appendRoster(entry) {
26713
27431
  return this.#relay.appendRoster(entry);
26714
27432
  }
@@ -26767,7 +27485,7 @@ var init_pages = __esmMin((() => {
26767
27485
  }));
26768
27486
  // packages/relay/dist/store.js
26769
27487
  var DatabaseSync;
26770
- var init_store = __esmMin((() => {
27488
+ var init_store$1 = __esmMin((() => {
26771
27489
  init_dist$3();
26772
27490
  ({DatabaseSync} = process.getBuiltinModule("node:sqlite"));
26773
27491
  }));
@@ -26789,6 +27507,10 @@ var init_feed = __esmMin((() => {
26789
27507
  var init_pacing = __esmMin((() => {
26790
27508
  init_dist$3();
26791
27509
  }));
27510
+ // packages/relay/dist/directory/wakes.js
27511
+ var init_wakes = __esmMin((() => {
27512
+ init_dist$3();
27513
+ }));
26792
27514
  // packages/relay/dist/directory/module.js
26793
27515
  var init_module = __esmMin((() => {
26794
27516
  init_dist$3();
@@ -26796,6 +27518,11 @@ var init_module = __esmMin((() => {
26796
27518
  init_feed();
26797
27519
  init_jelly();
26798
27520
  init_pacing();
27521
+ init_wakes();
27522
+ }));
27523
+ // packages/relay/dist/directory/store.js
27524
+ var init_store = __esmMin((() => {
27525
+ init_dist$3();
26799
27526
  }));
26800
27527
  // packages/relay/dist/server.js
26801
27528
  var init_server = __esmMin((() => {
@@ -26804,8 +27531,9 @@ var init_server = __esmMin((() => {
26804
27531
  init_frames();
26805
27532
  init_limits();
26806
27533
  init_pages();
26807
- init_store();
27534
+ init_store$1();
26808
27535
  init_module();
27536
+ init_store();
26809
27537
  }));
26810
27538
  // packages/relay/dist/backoff.js
26811
27539
  function backoffDelay(attempt, minMs, maxMs, random = Math.random) {
@@ -27170,6 +27898,13 @@ var init_client = __esmMin((() => {
27170
27898
  this.#attempt = 0;
27171
27899
  if (!this.#ws && !this.#retryTimer) this.#open();
27172
27900
  }
27901
+ retryNow() {
27902
+ if (!this.#running || this.#ws || !this.#retryTimer) return;
27903
+ clearTimeout(this.#retryTimer);
27904
+ this.#retryTimer = void 0;
27905
+ this.#attempt = 0;
27906
+ this.#open();
27907
+ }
27173
27908
  async stop() {
27174
27909
  this.#running = false;
27175
27910
  clearTimeout(this.#retryTimer);
@@ -27276,6 +28011,18 @@ var init_client = __esmMin((() => {
27276
28011
  persons
27277
28012
  });
27278
28013
  }
28014
+ wakeArm(arm) {
28015
+ return this.#dir("dir.ok", {
28016
+ t: "wake.arm",
28017
+ url: arm.url,
28018
+ ticket: arm.ticket,
28019
+ agents: arm.agents,
28020
+ awaiting: arm.awaiting
28021
+ });
28022
+ }
28023
+ wakeDisarm() {
28024
+ return this.#dir("dir.ok", { t: "wake.disarm" });
28025
+ }
27279
28026
  dirErase(entries, token) {
27280
28027
  return this.#dir("dir.ok", {
27281
28028
  t: "dir.erase",
@@ -27613,6 +28360,7 @@ var init_relay_transport = __esmMin((() => {
27613
28360
  #running = false;
27614
28361
  #watched = [];
27615
28362
  #presence = [];
28363
+ #wake;
27616
28364
  #opening = new Set();
27617
28365
  constructor(options) {
27618
28366
  this.#options = options;
@@ -27649,6 +28397,7 @@ var init_relay_transport = __esmMin((() => {
27649
28397
  for (const done of [...this.#opening]) done();
27650
28398
  if (this.#watched.length > 0) client.dirWatch(this.#watched).catch(() => {});
27651
28399
  if (this.#presence.length > 0) client.presenceWatch(this.#presence).catch(() => {});
28400
+ if (this.#wake !== void 0) this.#sendWake(client, this.#wake).catch(() => {});
27652
28401
  const sth = "sth" in state ? STH.safeParse(state.sth) : void 0;
27653
28402
  pushes.state(true, sth?.success ? sth.data : void 0);
27654
28403
  },
@@ -27760,6 +28509,30 @@ var init_relay_transport = __esmMin((() => {
27760
28509
  presence(env) {
27761
28510
  this.#client?.presence(env);
27762
28511
  }
28512
+ retryNow() {
28513
+ this.#client?.retryNow();
28514
+ }
28515
+ async wake(state) {
28516
+ this.#wake = state;
28517
+ const client = this.#connect();
28518
+ if (!client?.connected) return "pending";
28519
+ try {
28520
+ await this.#sendWake(client, state);
28521
+ return "sent";
28522
+ } catch (error) {
28523
+ if (isMessagingError(error) && error.code === "relay_unavailable") return "pending";
28524
+ throw error;
28525
+ }
28526
+ }
28527
+ async #sendWake(client, state) {
28528
+ try {
28529
+ parse(DirOkFrame, await (state === "disarm" ? client.wakeDisarm() : client.wakeArm(state)));
28530
+ } catch (error) {
28531
+ if (!(isMessagingError(error) && error.code === "relay_unavailable") && this.#wake === state) this.#wake = void 0;
28532
+ throw error;
28533
+ }
28534
+ if (this.#wake === state) this.#wake = void 0;
28535
+ }
27763
28536
  async presenceWatch(persons) {
27764
28537
  this.#presence = [...persons];
27765
28538
  const client = this.#connect();
@@ -27777,6 +28550,7 @@ var dist_exports = __exportAll({
27777
28550
  Config: () => Config,
27778
28551
  DirectConfig: () => DirectConfig,
27779
28552
  DirectTransport: () => DirectTransport,
28553
+ JELLY_ENV: () => JELLY_ENV,
27780
28554
  LockHeldError: () => LockHeldError,
27781
28555
  PROTOCOL: () => PROTOCOL,
27782
28556
  RelayDirectoryLink: () => RelayDirectoryLink,
@@ -27785,12 +28559,15 @@ var dist_exports = __exportAll({
27785
28559
  acquireLock: () => acquireLock,
27786
28560
  createLogger: () => createLogger,
27787
28561
  homePaths: () => homePaths,
28562
+ jellyEnvNamed: () => jellyEnvNamed,
27788
28563
  jellyLabel: () => jellyLabel,
27789
28564
  readActKey: () => readActKey,
27790
28565
  readConfig: () => readConfig,
28566
+ readJellyEnv: () => readJellyEnv,
27791
28567
  relayDirectoryLink: () => relayDirectoryLink,
27792
28568
  relayOps: () => relayOps,
27793
28569
  relayTransport: () => relayTransport,
28570
+ relayUrlOf: () => relayUrlOf,
27794
28571
  startDaemon: () => startDaemon,
27795
28572
  withDirect: () => withDirect
27796
28573
  });
@@ -27812,7 +28589,8 @@ var init_dist = __esmMin((() => {
27812
28589
  init_log();
27813
28590
  init_version();
27814
28591
  init_jelly$1();
27815
- init_store$1();
28592
+ init_jelly_env();
28593
+ init_store$2();
27816
28594
  init_dist$3();
27817
28595
  }));
27818
28596
  // packages/daemon/dist/bin/daemon-main.js
@@ -27827,7 +28605,13 @@ Runs the Messaging daemon in the foreground for one MESSAGING_HOME (default ~/.m
27827
28605
  --version print the version, the store schema and the protocol (--json: as JSON); needs no home
27828
28606
  --help print this help
27829
28607
 
27830
- Under a Jelly host (spec §8.11):
28608
+ Under a Jelly host (spec §8.11), with no arguments: the environment start contract. JELLY_ACTION_KEY (the path of
28609
+ the host's public action key) starts it; STATE_DIRECTORY is the home, RUNTIME_DIRECTORY the socket folder,
28610
+ JELLY_DEVICE_LABEL the label, JELLY_SOCKET_GROUP the group, and JELLY_ID_ISSUER, JELLY_RESOURCES (the messaging entry)
28611
+ and JELLY_SERVICE_ANCHOR the realm, which the first start writes to config.json. Flags win: a start with
28612
+ --deployment jelly reads none of these; any other start with JELLY_* variables set is refused.
28613
+
28614
+ Or by flags:
27831
28615
  --deployment jelly every human and admin call needs the host's action grant
27832
28616
  --act-key <path> the host's public action key (an Ed25519 JWK); required
27833
28617
  --label <label> this device's label, from the host; required
@@ -27884,7 +28668,16 @@ function versionText(json, info) {
27884
28668
  if (json) return `${JSON.stringify(info)}\n`;
27885
28669
  return `messagingd ${info.version} (store schema ${info.storeSchema}, protocol ${info.protocol})\n`;
27886
28670
  }
27887
- async function daemonMain(argv) {
28671
+ function startSource(values, env, named) {
28672
+ if (Object.keys(values).length > 0) {
28673
+ if (values["--deployment"] === "jelly" || named.length === 0) return "flags";
28674
+ return `${named[0]} is set, so the environment starts a Jelly host, but the flags start a self-host daemon; use one or the other`;
28675
+ }
28676
+ if ((env.JELLY_ACTION_KEY ?? "") !== "") return "environment";
28677
+ if (named.length > 0) return `${named[0]} is set without JELLY_ACTION_KEY: a Jelly start needs the host's action key`;
28678
+ return "self";
28679
+ }
28680
+ async function daemonMain(argv, env = process.env) {
27888
28681
  const args = parseArgs(argv);
27889
28682
  if (typeof args === "string") {
27890
28683
  process.stderr.write(`messagingd: ${args}\n\n${USAGE}`);
@@ -27894,7 +28687,7 @@ async function daemonMain(argv) {
27894
28687
  process.stdout.write(USAGE);
27895
28688
  return 0;
27896
28689
  }
27897
- const { startDaemon, homePaths, createLogger, readActKey, jellyLabel, VERSION, STORE_SCHEMA, PROTOCOL } = await Promise.resolve().then(() => (init_dist(), dist_exports));
28690
+ const { startDaemon, homePaths, createLogger, readActKey, jellyLabel, readJellyEnv, jellyEnvNamed, VERSION, STORE_SCHEMA, PROTOCOL } = await Promise.resolve().then(() => (init_dist(), dist_exports));
27898
28691
  if (args.version) {
27899
28692
  process.stdout.write(versionText(args.json === true, {
27900
28693
  version: VERSION,
@@ -27904,7 +28697,19 @@ async function daemonMain(argv) {
27904
28697
  return 0;
27905
28698
  }
27906
28699
  const { values } = args;
27907
- const home = values["--home"] ? resolve(values["--home"]) : messagingHome();
28700
+ const source = startSource(values, env, jellyEnvNamed(env));
28701
+ if (source !== "flags" && source !== "environment" && source !== "self") {
28702
+ process.stderr.write(`messagingd: ${source}\n`);
28703
+ return 2;
28704
+ }
28705
+ let fromEnv;
28706
+ if (source === "environment") try {
28707
+ fromEnv = readJellyEnv(env);
28708
+ } catch (error) {
28709
+ process.stderr.write(`messagingd: ${error instanceof Error ? error.message : "the Jelly environment is invalid"}\n`);
28710
+ return 1;
28711
+ }
28712
+ const home = fromEnv ? resolve(fromEnv.home) : values["--home"] ? resolve(values["--home"]) : messagingHome();
27908
28713
  const level = process.env.MESSAGING_LOG_LEVEL;
27909
28714
  const log = createLogger({
27910
28715
  file: homePaths(home).log,
@@ -27913,7 +28718,14 @@ async function daemonMain(argv) {
27913
28718
  });
27914
28719
  let daemon;
27915
28720
  try {
27916
- const jellyHost = values["--deployment"] === "jelly" ? {
28721
+ const jellyHost = fromEnv ? {
28722
+ actKey: readActKey(fromEnv.actKeyPath),
28723
+ label: fromEnv.label,
28724
+ kind: "jelly-host",
28725
+ realm: fromEnv.realm,
28726
+ ...fromEnv.socketGroup !== void 0 ? { socketGroup: fromEnv.socketGroup } : {},
28727
+ ...fromEnv.run !== void 0 ? { run: resolve(fromEnv.run) } : {}
28728
+ } : values["--deployment"] === "jelly" ? {
27917
28729
  actKey: readActKey(resolve(values["--act-key"])),
27918
28730
  label: jellyLabel(values["--label"]),
27919
28731
  kind: "jelly-host",
@@ -27925,6 +28737,10 @@ async function daemonMain(argv) {
27925
28737
  log,
27926
28738
  ...jellyHost ? { jellyHost } : {}
27927
28739
  });
28740
+ if (jellyHost) log("info", "daemon.start", {
28741
+ deployment: "jelly",
28742
+ source
28743
+ });
27928
28744
  } catch (error) {
27929
28745
  process.stderr.write(`messagingd: ${error instanceof Error ? error.message : "failed to start"}\n`);
27930
28746
  return 1;