@joe-broadhead/messaging-bin-win32-x64 0.4.0 → 0.6.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.
@@ -6391,7 +6391,8 @@ var init_jelly = __esm(() => {
6391
6391
  jti: string2().regex(/^[\x21-\x7e]{1,128}$/, "printable jti"),
6392
6392
  iss: string2().max(2048).optional(),
6393
6393
  dev: string2().max(200).optional(),
6394
- cls: string2().max(32).optional()
6394
+ cls: string2().max(32).optional(),
6395
+ mbr: string2().max(200).optional()
6395
6396
  });
6396
6397
  JellySessionClaims = looseObject({
6397
6398
  aud: string2().min(1).max(2048),
@@ -6900,7 +6901,8 @@ var init_envelope = __esm(() => {
6900
6901
  label: MachineLabel,
6901
6902
  sessions: array(SessionInfo).max(LIMITS.presenceSessions),
6902
6903
  features: array(string2().max(64)).max(32).optional(),
6903
- direct: array(DirectEndpoint).min(1).max(4).optional()
6904
+ direct: array(DirectEndpoint).min(1).max(4).optional(),
6905
+ away: literal(true).optional()
6904
6906
  });
6905
6907
  PAYLOAD_BY_TYPE = {
6906
6908
  message: MessagePayload,
@@ -7102,7 +7104,7 @@ var init_relay = __esm(() => {
7102
7104
  function hostedRelayAuthPayload(args) {
7103
7105
  return { proto: RELAY_PROTOCOL, relay: args.relay, realm: args.realm, person: args.person, device: args.device, nonce: args.nonce, ts: args.ts };
7104
7106
  }
7105
- var Req2, Size, Handle, Email, OrgId, DirBindFrame, HostedHelloFrame, DirAppendFrame, DirKeysFrame, DirSthFrame, DirLookupFrame, DirDeviceFrame, DirResolveFrame, DirSearchFrame, DirWatchFrame, PresenceWatchFrame, DirEraseFrame, DirectoryClientFrame, HostedWelcomeFrame, DirOkFrame, DirSthIsFrame, DirPersonsFrame, ResolvedPerson, DirResolvedFrame, DirChangedFrame, DirUnboundFrame, DirectoryRelayFrame;
7107
+ var Req2, Size, Handle, Email, OrgId, DirBindFrame, HostedHelloFrame, DirAppendFrame, DirKeysFrame, DirSthFrame, DirLookupFrame, DirDeviceFrame, DirResolveFrame, DirSearchFrame, DirWatchFrame, PresenceWatchFrame, DirEraseFrame, WAKE_AGENTS = 64, WAKE_AWAITING = 500, WAKE_TICKET_BYTES = 4096, WAKE_TICKET_MAX_S, WakeUrl, WakeArmFrame, WakeDisarmFrame, DirectoryClientFrame, HostedWelcomeFrame, DirOkFrame, DirSthIsFrame, DirPersonsFrame, ResolvedPerson, DirResolvedFrame, DirChangedFrame, DirUnboundFrame, DirectoryRelayFrame;
7106
7108
  var init_directory_frames = __esm(() => {
7107
7109
  init_zod();
7108
7110
  init_common();
@@ -7167,6 +7169,17 @@ var init_directory_frames = __esm(() => {
7167
7169
  entries: array(LogEntry).min(1).max(LIMITS.devicesPerPerson),
7168
7170
  token: Jws
7169
7171
  });
7172
+ WAKE_TICKET_MAX_S = 7 * 24 * 60 * 60;
7173
+ WakeUrl = string2().max(2048).regex(/^https?:\/\/[^/?#@\s]+\/hosts\/hst_[A-Za-z0-9._:-]{1,123}\/wake$/, "a Jelly ID wake URL");
7174
+ WakeArmFrame = object({
7175
+ t: literal("wake.arm"),
7176
+ req: Req2,
7177
+ url: WakeUrl,
7178
+ ticket: Jws.refine((ticket) => ticket.length <= WAKE_TICKET_BYTES, { message: `at most ${WAKE_TICKET_BYTES} bytes` }),
7179
+ agents: array(SessionName).max(WAKE_AGENTS),
7180
+ awaiting: array(Ulid).max(WAKE_AWAITING)
7181
+ });
7182
+ WakeDisarmFrame = object({ t: literal("wake.disarm"), req: Req2 });
7170
7183
  DirectoryClientFrame = discriminatedUnion("t", [
7171
7184
  DirBindFrame,
7172
7185
  HostedHelloFrame,
@@ -7179,7 +7192,9 @@ var init_directory_frames = __esm(() => {
7179
7192
  DirSearchFrame,
7180
7193
  DirWatchFrame,
7181
7194
  PresenceWatchFrame,
7182
- DirEraseFrame
7195
+ DirEraseFrame,
7196
+ WakeArmFrame,
7197
+ WakeDisarmFrame
7183
7198
  ]);
7184
7199
  HostedWelcomeFrame = object({ t: literal("welcome"), sth: STH });
7185
7200
  DirOkFrame = object({
@@ -7261,14 +7276,14 @@ var init_direct = __esm(() => {
7261
7276
  });
7262
7277
 
7263
7278
  // packages/core/dist/schema/ipc/device.js
7264
- var BindingState, RealmAlarmSummary, DeviceStatus, DeviceListEntry, LinkText, Sas, LinkPreview, KeyFingerprint, LoginUserCode, LoginPurpose, LoginKind, LoginShown, LoginState, DEVICE_METHODS;
7279
+ var BindingState, RealmAlarmSummary, DeviceStatus, DeviceChangeView, DeviceListEntry, LinkText, Sas, LinkPreview, KeyFingerprint, LoginUserCode, LoginPurpose, LoginKind, LoginShown, LoginState, DeviceRemoveParams, DeviceApproveParams, IdentityReplaceParams, SignInStep, DirectoryEraseParams, DEVICE_METHODS;
7265
7280
  var init_device = __esm(() => {
7266
7281
  init_zod();
7267
7282
  init_common();
7268
7283
  init_directory();
7269
7284
  init_errors3();
7270
7285
  init_jelly();
7271
- BindingState = _enum(["none", "bound", "attest-due", "removed"]);
7286
+ BindingState = _enum(["none", "bound", "attest-due", "removed", "replace"]);
7272
7287
  RealmAlarmSummary = object({
7273
7288
  kind: _enum(["directory", "restored"]),
7274
7289
  reason: string2().max(500),
@@ -7283,6 +7298,16 @@ var init_device = __esm(() => {
7283
7298
  sth: object({ size: number2().int().positive(), root: Hash, ts: Millis }).optional(),
7284
7299
  alarm: RealmAlarmSummary.optional()
7285
7300
  });
7301
+ DeviceChangeView = object({
7302
+ change: _enum(["added", "vouched", "removed", "replaced"]),
7303
+ device: MachineId,
7304
+ label: MachineLabel,
7305
+ kind: DeviceKind.optional(),
7306
+ vouch: _enum(["jelly", "device", "admin"]),
7307
+ via: _enum(["host", "browser"]).optional(),
7308
+ approver: MachineLabel.optional(),
7309
+ at: Millis
7310
+ });
7286
7311
  DeviceListEntry = object({
7287
7312
  device: MachineId,
7288
7313
  label: MachineLabel,
@@ -7299,7 +7324,7 @@ var init_device = __esm(() => {
7299
7324
  LinkPreview = object({ device: MachineId, label: MachineLabel, kind: DeviceKind, sas: Sas });
7300
7325
  KeyFingerprint = string2().regex(/^[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}$/, "key fingerprint");
7301
7326
  LoginUserCode = string2().regex(/^[A-HJ-NP-Z2-9]{5}-[A-HJ-NP-Z2-9]{5}$/, "user code");
7302
- LoginPurpose = _enum(["bind", "remove"]);
7327
+ LoginPurpose = _enum(["bind", "remove", "replace", "erase"]);
7303
7328
  LoginKind = _enum(["laptop", "server", "other"]);
7304
7329
  LoginShown = object({
7305
7330
  verificationUriComplete: string2().url().max(2048),
@@ -7315,6 +7340,16 @@ var init_device = __esm(() => {
7315
7340
  object({ state: literal("failed"), purpose: LoginPurpose, code: ErrorCode, message: string2().max(500) }),
7316
7341
  object({ state: literal("cancelled"), purpose: LoginPurpose })
7317
7342
  ]);
7343
+ DeviceRemoveParams = strictObject({ device: MachineId, reason: _enum(["self", "removed", "lost"]).optional(), token: Jws.optional() });
7344
+ DeviceApproveParams = strictObject({ link: LinkText, sas: Sas });
7345
+ IdentityReplaceParams = strictObject({
7346
+ token: Jws.optional(),
7347
+ label: MachineLabel.optional()
7348
+ });
7349
+ SignInStep = LoginShown.extend({ state: literal("sign-in"), purpose: LoginPurpose, interval: number2().int().positive() });
7350
+ DirectoryEraseParams = strictObject({
7351
+ token: Jws.optional()
7352
+ });
7318
7353
  DEVICE_METHODS = {
7319
7354
  "device.status": { params: strictObject({}), result: DeviceStatus },
7320
7355
  "device.bind": {
@@ -7323,7 +7358,7 @@ var init_device = __esm(() => {
7323
7358
  },
7324
7359
  "device.list": { params: strictObject({}), result: object({ devices: array(DeviceListEntry) }) },
7325
7360
  "device.remove": {
7326
- params: strictObject({ device: MachineId, reason: _enum(["self", "removed", "lost"]).optional(), token: Jws.optional() }),
7361
+ params: DeviceRemoveParams,
7327
7362
  result: object({ seq: number2().int().nonnegative() })
7328
7363
  },
7329
7364
  "device.link": {
@@ -7340,10 +7375,51 @@ var init_device = __esm(() => {
7340
7375
  },
7341
7376
  "device.login.status": { params: strictObject({ waitMs: number2().int().min(0).max(120000).optional() }), result: LoginState },
7342
7377
  "device.login.cancel": { params: strictObject({}), result: object({ ok: literal(true) }) },
7343
- "device.approve.preview": { params: strictObject({ link: LinkText }), result: LinkPreview },
7378
+ "device.approve.preview": { params: DeviceApproveParams.partial({ sas: true }), result: LinkPreview },
7344
7379
  "device.approve": {
7345
- params: strictObject({ link: LinkText, sas: Sas }),
7380
+ params: DeviceApproveParams,
7346
7381
  result: object({ device: MachineId, seq: number2().int().nonnegative() })
7382
+ },
7383
+ "identity.replace": {
7384
+ params: IdentityReplaceParams,
7385
+ result: discriminatedUnion("state", [
7386
+ object({ state: literal("done"), person: PersonId, device: MachineId, predecessor: MachineId, seq: number2().int().nonnegative() }),
7387
+ object({ state: literal("token"), device: MachineId }),
7388
+ SignInStep.extend({ device: MachineId })
7389
+ ])
7390
+ },
7391
+ "identity.replace.preview": {
7392
+ params: IdentityReplaceParams,
7393
+ result: object({
7394
+ predecessor: object({
7395
+ device: MachineId,
7396
+ label: MachineLabel,
7397
+ kind: DeviceKind.optional(),
7398
+ vouch: _enum(["jelly", "device"]),
7399
+ via: _enum(["host", "browser"]).optional(),
7400
+ since: Millis.optional(),
7401
+ current: boolean2()
7402
+ }),
7403
+ alarm: _enum(["keys_missing", "restored"]).nullable(),
7404
+ device: MachineId.optional(),
7405
+ signIns: number2().int().min(0).max(2),
7406
+ sentence: string2().max(600)
7407
+ })
7408
+ },
7409
+ "directory.erase": {
7410
+ params: DirectoryEraseParams,
7411
+ result: discriminatedUnion("state", [object({ state: literal("done"), person: PersonId, devices: number2().int().nonnegative() }), SignInStep])
7412
+ },
7413
+ "directory.erase.preview": {
7414
+ params: DirectoryEraseParams,
7415
+ result: object({
7416
+ person: PersonId,
7417
+ devices: array(object({ device: MachineId, label: MachineLabel, kind: DeviceKind.optional(), self: boolean2() })),
7418
+ signIns: number2().int().min(0).max(1),
7419
+ perDay: number2().int().positive(),
7420
+ lost: array(string2().max(300)).max(10),
7421
+ kept: array(string2().max(300)).max(10)
7422
+ })
7347
7423
  }
7348
7424
  };
7349
7425
  });
@@ -7458,7 +7534,7 @@ var init_shapes = __esm(() => {
7458
7534
  });
7459
7535
 
7460
7536
  // packages/core/dist/schema/ipc/consent.js
7461
- var GrantTarget, Levels, GrantView, GrantReview, GrantSetParams, AskRequest, ContactRequestView, ContactSuggestion, HeldRequest, RequestAnswer, RequestText, PersonReach, CONSENT_METHODS;
7537
+ var GrantTarget, Levels, GrantView, GrantReview, GrantSetParams, AskRequest, ContactRequestView, ContactSuggestion, HeldRequest, RequestAnswer, RequestText, PersonReach, RequestAnswerParams, ContactRequestParams, CONSENT_METHODS;
7462
7538
  var init_consent2 = __esm(() => {
7463
7539
  init_zod();
7464
7540
  init_common();
@@ -7524,7 +7600,9 @@ var init_consent2 = __esm(() => {
7524
7600
  HeldRequest = union([AskRequest, ContactRequestView, ContactSuggestion]);
7525
7601
  RequestAnswer = _enum(["once", "always", "accept", "decline", "block"]);
7526
7602
  RequestText = string2().min(1).refine((text) => text.length <= LIMITS.requestChars, { message: `at most ${LIMITS.requestChars} UTF-16 code units` });
7527
- PersonReach = object({ person: PersonId, can: CardCan, source: _enum(["card", "default"]), at: Millis.optional() });
7603
+ PersonReach = object({ person: PersonId, can: CardCan, source: _enum(["card", "default"]), at: Millis.optional(), devicesChangedAt: Millis.optional() });
7604
+ RequestAnswerParams = strictObject({ id: Ulid, answer: RequestAnswer, all: boolean2().optional() });
7605
+ ContactRequestParams = strictObject({ email: Email, text: RequestText, suggestion: Ulid.optional() });
7528
7606
  CONSENT_METHODS = {
7529
7607
  "grant.list": {
7530
7608
  params: strictObject({ subject: GrantTarget.optional() }),
@@ -7534,14 +7612,25 @@ var init_consent2 = __esm(() => {
7534
7612
  "grant.set.preview": { params: GrantSetParams, result: object({ sentence: string2().max(500) }) },
7535
7613
  "request.list": { params: strictObject({}), result: object({ requests: array(HeldRequest).max(LIMITS.heldTotal * 3) }) },
7536
7614
  "request.answer": {
7537
- params: strictObject({ id: Ulid, answer: RequestAnswer, all: boolean2().optional() }),
7615
+ params: RequestAnswerParams,
7538
7616
  result: object({ answered: array(Ulid).max(LIMITS.heldTotal) })
7539
7617
  },
7618
+ "request.answer.preview": {
7619
+ params: RequestAnswerParams,
7620
+ result: object({
7621
+ kind: _enum(["message", "task", "contact", "suggestion"]),
7622
+ from: object({ name: string2().max(200), email: string2().max(320).optional(), org: _enum(["same", "other", "none"]) }),
7623
+ agent: SessionName.optional(),
7624
+ to: SessionName.optional(),
7625
+ count: number2().int().positive(),
7626
+ sentence: string2().max(500)
7627
+ })
7628
+ },
7540
7629
  "contact.request": {
7541
- params: strictObject({ email: Email, text: RequestText, suggestion: Ulid.optional() }),
7630
+ params: ContactRequestParams,
7542
7631
  result: object({ id: Ulid, conversation: RoomRef, person: PersonId })
7543
7632
  },
7544
- "contact.request.preview": { params: strictObject({ email: Email }), result: object({ found: boolean2() }) },
7633
+ "contact.request.preview": { params: ContactRequestParams.partial({ text: true }), result: object({ found: boolean2() }) },
7545
7634
  "card.get": { params: strictObject({ person: PersonId }), result: object({ sent: boolean2() }) }
7546
7635
  };
7547
7636
  });
@@ -7556,6 +7645,7 @@ var init_surfaces = __esm(() => {
7556
7645
  init_directory_frames();
7557
7646
  init_errors3();
7558
7647
  init_consent2();
7648
+ init_device();
7559
7649
  init_shapes();
7560
7650
  ConversationRef = string2().regex(/^(?:dm:(?:[a-z][a-z0-9-]{0,31}|p_[a-z2-7]{26})|channel:[a-z][a-z0-9-]{0,31}|agent:(?:[a-z][a-z0-9-]{0,31}|p_[a-z2-7]{26})\/[a-z0-9][a-z0-9._-]{0,63})$/, "dm:<person>, channel:<name> or agent:<person>/<agent>");
7561
7651
  ConvPlace = union([ChatSeq, ConvCursor]);
@@ -7660,26 +7750,38 @@ var init_surfaces = __esm(() => {
7660
7750
  },
7661
7751
  "people.get": {
7662
7752
  params: strictObject({ person: PersonId }),
7663
- result: PersonSummary.extend({ devices: array(object({ device: MachineId, label: MachineLabel, self: boolean2() })) })
7753
+ result: PersonSummary.extend({
7754
+ devices: array(object({ device: MachineId, label: MachineLabel, self: boolean2(), away: literal(true).optional() })),
7755
+ deviceChanges: array(DeviceChangeView).max(10).optional()
7756
+ })
7664
7757
  }
7665
7758
  };
7666
7759
  });
7667
7760
 
7668
7761
  // packages/core/dist/schema/ipc/jelly.js
7669
- var NoticeId, NOTICE_LINE_CHARS = 120, Notice, NoticeCounts, DaemonHealth, AgentSummary, AgentConversation, JELLY_METHODS;
7762
+ var NoticeId, NOTICE_LINE_CHARS = 120, NoticeDevice, Notice, NoticeCounts, DaemonHealth, AgentSummary, AgentConversation, JELLY_METHODS;
7670
7763
  var init_jelly2 = __esm(() => {
7671
7764
  init_zod();
7672
7765
  init_common();
7766
+ init_directory_frames();
7767
+ init_jelly();
7673
7768
  init_directory();
7769
+ init_device();
7674
7770
  init_shapes();
7675
7771
  init_surfaces();
7676
7772
  NoticeId = string2().regex(/^[mra]_[0-9A-Za-z]{1,64}$/, "notice id");
7773
+ NoticeDevice = DeviceChangeView.extend({
7774
+ replaces: object({ device: MachineId, label: MachineLabel }).optional(),
7775
+ current: boolean2(),
7776
+ remove: _enum(["token", "sign-in", "team"]).optional()
7777
+ });
7677
7778
  Notice = object({
7678
7779
  id: NoticeId,
7679
7780
  category: _enum(["message", "request", "alert"]),
7680
7781
  conversation: ConversationRef.optional(),
7681
7782
  from: object({ name: string2().max(200), email: string2().max(320).optional(), org: _enum(["same", "other", "none"]) }).optional(),
7682
7783
  line: string2().max(NOTICE_LINE_CHARS).optional(),
7784
+ device: NoticeDevice.optional(),
7683
7785
  at: Millis
7684
7786
  });
7685
7787
  NoticeCounts = object({ unread: number2().int().nonnegative(), requests: number2().int().nonnegative() });
@@ -7724,8 +7826,32 @@ var init_jelly2 = __esm(() => {
7724
7826
  result: object({ notices: array(Notice), counts: NoticeCounts })
7725
7827
  },
7726
7828
  "notice.ack": { params: strictObject({ ids: array(NoticeId).min(1).max(200) }), result: object({ ok: literal(true) }) },
7829
+ "daemon.woke": { params: strictObject({}), result: object({ ok: literal(true) }) },
7830
+ "person.attached": { params: strictObject({ attached: boolean2() }), result: object({ ok: literal(true) }) },
7831
+ "wake.arm": {
7832
+ params: strictObject({
7833
+ url: WakeUrl,
7834
+ ticket: Jws.refine((ticket) => ticket.length <= WAKE_TICKET_BYTES, { message: `at most ${WAKE_TICKET_BYTES} bytes` }),
7835
+ agents: array(SessionName).max(WAKE_AGENTS)
7836
+ }),
7837
+ result: object({ relay: _enum(["armed", "pending"]), awaiting: number2().int().nonnegative() })
7838
+ },
7839
+ "wake.disarm": { params: strictObject({}), result: object({ relay: _enum(["disarmed", "pending"]) }) },
7840
+ "daemon.snapshot": {
7841
+ params: strictObject({ done: literal(true).optional() }),
7842
+ result: union([
7843
+ object({
7844
+ folder: string2().max(4096),
7845
+ files: array(object({ name: string2().max(64), bytes: number2().int().nonnegative(), sha256: string2().regex(/^[0-9a-f]{64}$/) })).max(8),
7846
+ storeSchema: number2().int().positive(),
7847
+ at: Millis,
7848
+ until: Millis
7849
+ }),
7850
+ object({ released: boolean2() })
7851
+ ])
7852
+ },
7727
7853
  "device.remove.preview": {
7728
- params: strictObject({ device: MachineId }),
7854
+ params: DeviceRemoveParams,
7729
7855
  result: object({
7730
7856
  label: MachineLabel,
7731
7857
  kind: DeviceKind.optional(),
@@ -7739,7 +7865,7 @@ var init_jelly2 = __esm(() => {
7739
7865
  });
7740
7866
 
7741
7867
  // packages/core/dist/schema/ipc.js
7742
- var MESSAGING_ERROR_RPC_CODE = -32000, SessionHost, IPC_PROTOCOL = 1, IpcHello, Ok, Empty, DaemonStatus, InviteSummary, IPC_METHODS, AGENT_METHODS, HOST_SCOPE_METHODS, IpcEvent;
7868
+ var MESSAGING_ERROR_RPC_CODE = -32000, SessionHost, IPC_PROTOCOL = 1, IpcHello, Ok, Empty, DaemonStatus, InviteSummary, IPC_METHODS, AGENT_METHODS, HUMAN_METHODS, ADMIN_METHODS, HOST_SCOPE_METHODS, IpcEvent;
7743
7869
  var init_ipc = __esm(() => {
7744
7870
  init_zod();
7745
7871
  init_device();
@@ -7980,7 +8106,68 @@ var init_ipc = __esm(() => {
7980
8106
  "task.update",
7981
8107
  "task.cancel"
7982
8108
  ];
7983
- HOST_SCOPE_METHODS = ["notice.subscribe", "notice.list", "conv.list", "daemon.stop"];
8109
+ HUMAN_METHODS = [
8110
+ "conv.list",
8111
+ "conv.read",
8112
+ "conv.markRead",
8113
+ "conv.subscribe",
8114
+ "person.send",
8115
+ "people.search",
8116
+ "people.get",
8117
+ "grant.list",
8118
+ "grant.set",
8119
+ "grant.set.preview",
8120
+ "request.list",
8121
+ "request.answer",
8122
+ "request.answer.preview",
8123
+ "contact.request",
8124
+ "contact.request.preview",
8125
+ "card.get",
8126
+ "agents.list",
8127
+ "agents.read",
8128
+ "notice.subscribe",
8129
+ "notice.list",
8130
+ "notice.ack",
8131
+ "device.approve.preview",
8132
+ "device.remove.preview",
8133
+ "identity.replace.preview",
8134
+ "directory.erase.preview",
8135
+ "channel.create",
8136
+ "channel.invite",
8137
+ "channel.leave",
8138
+ "channel.remove"
8139
+ ];
8140
+ ADMIN_METHODS = [
8141
+ "daemon.stop",
8142
+ "daemon.woke",
8143
+ "person.attached",
8144
+ "daemon.snapshot",
8145
+ "wake.arm",
8146
+ "wake.disarm",
8147
+ "device.list",
8148
+ "device.remove",
8149
+ "device.link",
8150
+ "device.login",
8151
+ "device.login.status",
8152
+ "device.login.cancel",
8153
+ "device.approve",
8154
+ "identity.rotateEnc",
8155
+ "identity.replace",
8156
+ "directory.erase",
8157
+ "team.create",
8158
+ "team.invite",
8159
+ "team.join",
8160
+ "team.members",
8161
+ "team.remove",
8162
+ "team.role",
8163
+ "team.revokeInvite",
8164
+ "team.add",
8165
+ "team.pair",
8166
+ "team.claim",
8167
+ "team.pairCancel",
8168
+ "team.prepare"
8169
+ ];
8170
+ HOST_SCOPE_METHODS = ["notice.subscribe", "notice.list", "conv.list", "daemon.stop", "daemon.woke", "person.attached", "daemon.snapshot", "wake.arm", "wake.disarm"];
7984
8171
  IpcEvent = discriminatedUnion("type", [
7985
8172
  object({ type: literal("inbox"), key: SessionKey, item: InboxItem }),
7986
8173
  object({ type: literal("receipt"), key: SessionKey, envelopeId: Ulid, to: SessionAddress, stage: DeliveryStage, code: ErrorCode.optional() }),
@@ -8047,7 +8234,7 @@ var init_b64u = __esm(() => {
8047
8234
  });
8048
8235
 
8049
8236
  // packages/core/dist/version.js
8050
- var VERSION = "0.4.0";
8237
+ var VERSION = "0.6.0";
8051
8238
 
8052
8239
  // packages/core/dist/jcs.js
8053
8240
  function hasLoneSurrogate(text) {
@@ -9273,6 +9460,9 @@ var init_roster2 = __esm(() => {
9273
9460
 
9274
9461
  // packages/core/dist/jelly-host.js
9275
9462
  import { verify as ed25519Verify2 } from "crypto";
9463
+ function actionClass(method) {
9464
+ return Object.hasOwn(ACTION_CLASSES, method) ? ACTION_CLASSES[method] : "local";
9465
+ }
9276
9466
  function grantInput(params) {
9277
9467
  return sha256(canonicalize(params ?? {}));
9278
9468
  }
@@ -9342,9 +9532,16 @@ function verifyActGrant(token, check) {
9342
9532
  throw fail("is for other params");
9343
9533
  checkTimes(claims.iat, claims.exp, ACT_GRANT_MAX_S, check.now, fail);
9344
9534
  }
9345
- if (claims.cls !== undefined && claims.act !== "host" && claims.cls !== (CHAT_CLASS.has(check.method) ? "chat" : "local"))
9346
- throw fail("names another action class");
9347
- return { scope: claims.act === "host" ? "host" : "action", jti: claims.jti, exp: claims.exp, dev: claims.dev };
9535
+ if (claims.act === "host") {
9536
+ if (claims.cls !== undefined)
9537
+ throw fail("is host scope, which names no action class");
9538
+ } else {
9539
+ if (claims.cls === undefined)
9540
+ throw fail("names no action class");
9541
+ if (claims.cls !== actionClass(check.method))
9542
+ throw fail(`names action class ${claims.cls}, not ${actionClass(check.method)}`);
9543
+ }
9544
+ return { scope: claims.act === "host" ? "host" : "action", jti: claims.jti, exp: claims.exp, dev: claims.dev, mbr: claims.mbr };
9348
9545
  }
9349
9546
  function verifySessionTicket(token, key, now) {
9350
9547
  const fail = (why) => new MessagingError("ticket_invalid", `the session ticket ${why}`);
@@ -9379,7 +9576,7 @@ function parseActKey(text) {
9379
9576
  }
9380
9577
  return parsed.data;
9381
9578
  }
9382
- var ACT_GRANT_MAX_S = 60, HOST_GRANT_MAX_S = 600, SESSION_TICKET_MAX_S, JELLY_HOST_SKEW_S = 5, CHAT_CLASS;
9579
+ var ACT_GRANT_MAX_S = 60, HOST_GRANT_MAX_S = 600, SESSION_TICKET_MAX_S, JELLY_HOST_SKEW_S = 5, CHAT_METHODS, VIEW_METHODS, classOf2 = (method) => CHAT_METHODS.includes(method) ? "chat" : VIEW_METHODS.includes(method) ? "view" : "local", ACTION_CLASSES;
9383
9580
  var init_jelly_host = __esm(() => {
9384
9581
  init_b64u();
9385
9582
  init_hash();
@@ -9388,8 +9585,24 @@ var init_jelly_host = __esm(() => {
9388
9585
  init_wire();
9389
9586
  init_errors3();
9390
9587
  init_jelly();
9588
+ init_ipc();
9391
9589
  SESSION_TICKET_MAX_S = 7 * 24 * 60 * 60;
9392
- CHAT_CLASS = new Set(["person.send", "conv.markRead"]);
9590
+ CHAT_METHODS = ["person.send", "conv.markRead", "notice.ack"];
9591
+ VIEW_METHODS = [
9592
+ "conv.list",
9593
+ "conv.read",
9594
+ "conv.subscribe",
9595
+ "people.search",
9596
+ "people.get",
9597
+ "grant.list",
9598
+ "request.list",
9599
+ "agents.list",
9600
+ "agents.read",
9601
+ "notice.subscribe",
9602
+ "notice.list",
9603
+ "device.list"
9604
+ ];
9605
+ ACTION_CLASSES = Object.freeze(Object.fromEntries([...HUMAN_METHODS, ...ADMIN_METHODS].map((method) => [method, classOf2(method)])));
9393
9606
  });
9394
9607
 
9395
9608
  // packages/core/dist/directory/person.js
@@ -9830,6 +10043,13 @@ class DeviceLog {
9830
10043
  token(seq) {
9831
10044
  return this.#state.tokens.get(seq);
9832
10045
  }
10046
+ spentSignIn(jti) {
10047
+ for (const [seq, token] of this.#state.tokens) {
10048
+ if (token.claims.src === JELLY_DEVICE_SOURCE && token.claims.jti === jti && this.#state.entries[seq].op !== "unbind")
10049
+ return true;
10050
+ }
10051
+ return false;
10052
+ }
9833
10053
  bindingDeadline(seq) {
9834
10054
  const token = this.#state.tokens.get(seq);
9835
10055
  return token === undefined ? undefined : token.claims.exp * 1000 + LIMITS.clockSkewMs;
@@ -9882,6 +10102,14 @@ class DeviceLog {
9882
10102
  const { claims } = token;
9883
10103
  if (personId(claims.iss, claims.sub) !== this.person)
9884
10104
  throw refuse("Jelly token names another person");
10105
+ if (claims.src === JELLY_DEVICE_SOURCE) {
10106
+ for (const [seq, used] of state.tokens) {
10107
+ if (used.claims.src !== JELLY_DEVICE_SOURCE || used.claims.jti !== claims.jti)
10108
+ continue;
10109
+ if (parsed.op !== "unbind" || state.entries[seq].op !== "unbind")
10110
+ throw refuse("a sign-in's token vouches for one act: this one was used");
10111
+ }
10112
+ }
9885
10113
  if (parsed.ts < claims.iat * 1000 - LIMITS.clockSkewMs || parsed.ts > claims.exp * 1000 + LIMITS.clockSkewMs)
9886
10114
  throw refuse("log entry is dated outside its Jelly token's life");
9887
10115
  tokens.set(parsed.seq, token);
@@ -9966,6 +10194,7 @@ var init_device_log = __esm(() => {
9966
10194
  init_keys();
9967
10195
  init_sign();
9968
10196
  init_common();
10197
+ init_jelly();
9969
10198
  init_directory();
9970
10199
  init_errors3();
9971
10200
  init_jelly_token();
@@ -12634,6 +12863,7 @@ var init_dist = __esm(() => {
12634
12863
  init_contact_card();
12635
12864
  init_delegation2();
12636
12865
  init_task();
12866
+ init_a2a();
12637
12867
  init_direct2();
12638
12868
  init_peer_text();
12639
12869
  init_agent_tools();
@@ -15583,7 +15813,8 @@ function presencePayload(rows, label, seq, extra, policy) {
15583
15813
  label,
15584
15814
  sessions: [],
15585
15815
  features: [...FEATURES, ...extra?.features ?? []],
15586
- ...extra?.direct ? { direct: extra.direct } : {}
15816
+ ...extra?.direct ? { direct: extra.direct } : {},
15817
+ ...policy.away === true ? { away: true } : {}
15587
15818
  };
15588
15819
  return { ...payload, sessions: fitPresence(sessions, jcsBytes(payload), LIMITS.presenceBytes) };
15589
15820
  }
@@ -15810,14 +16041,16 @@ class RosterIdentity {
15810
16041
  #machine;
15811
16042
  #catchUp;
15812
16043
  #now;
15813
- #listeners = { "devices-changed": [], "self-changed": [], alarm: [] };
16044
+ #listeners = { "devices-changed": [], "self-changed": [], "device-log": [], alarm: [] };
15814
16045
  #me;
16046
+ #length;
15815
16047
  constructor(options) {
15816
16048
  this.roster = options.roster;
15817
16049
  this.#machine = options.machine;
15818
16050
  this.#catchUp = options.catchUp;
15819
16051
  this.#now = options.now;
15820
16052
  this.#me = this.#selfRecord();
16053
+ this.#length = options.roster.entries().length;
15821
16054
  }
15822
16055
  get realm() {
15823
16056
  return this.roster.realm;
@@ -15848,6 +16081,31 @@ class RosterIdentity {
15848
16081
  successor(_device) {
15849
16082
  return;
15850
16083
  }
16084
+ deviceChanges(person) {
16085
+ const labels = new Map;
16086
+ const out = [];
16087
+ for (const entry of this.roster.entries()) {
16088
+ const member = entry.kind === "member.add" ? entry.body : entry.kind === "invite.redeem" ? entry.body.member : entry.kind === "genesis" ? entry.body.founder : undefined;
16089
+ if (member) {
16090
+ labels.set(member.machine, member.label);
16091
+ if (member.person === person && entry.kind !== "genesis")
16092
+ out.push({ seq: entry.seq, change: "added", device: member.machine, label: member.label, vouch: "admin", by: entry.by, at: entry.ts });
16093
+ } else if (entry.kind === "member.remove") {
16094
+ const owner = this.#ownerAt(entry.body.machine);
16095
+ if (owner === person)
16096
+ out.push({ seq: entry.seq, change: "removed", device: entry.body.machine, label: labels.get(entry.body.machine) ?? "machine", vouch: "admin", by: entry.by, at: entry.ts });
16097
+ }
16098
+ }
16099
+ return out;
16100
+ }
16101
+ #ownerAt(machine) {
16102
+ for (const entry of this.roster.entries()) {
16103
+ const member = entry.kind === "member.add" ? entry.body : entry.kind === "invite.redeem" ? entry.body.member : entry.kind === "genesis" ? entry.body.founder : undefined;
16104
+ if (member?.machine === machine)
16105
+ return member.person;
16106
+ }
16107
+ return;
16108
+ }
15851
16109
  async devices(person, opts = {}) {
15852
16110
  if (opts.atLeast !== undefined && this.relate(opts.atLeast).relation === "ahead") {
15853
16111
  throw new MessagingError("roster_conflict", "the roster has not reached that anchor");
@@ -15891,6 +16149,20 @@ class RosterIdentity {
15891
16149
  }
15892
16150
  for (const listener of [...this.#listeners["devices-changed"]])
15893
16151
  listener();
16152
+ const entries = this.roster.entries();
16153
+ if (entries.length !== this.#length) {
16154
+ const persons = new Set;
16155
+ for (const entry of entries.slice(this.#length)) {
16156
+ const member = entry.kind === "member.add" ? entry.body : entry.kind === "invite.redeem" ? entry.body.member : undefined;
16157
+ const person = member?.person ?? (entry.kind === "member.remove" ? this.#ownerAt(entry.body.machine) : undefined);
16158
+ if (person !== undefined)
16159
+ persons.add(person);
16160
+ }
16161
+ this.#length = entries.length;
16162
+ if (persons.size > 0)
16163
+ for (const listener of [...this.#listeners["device-log"]])
16164
+ listener([...persons].sort());
16165
+ }
15894
16166
  }
15895
16167
  #selfRecord() {
15896
16168
  const me = this.roster.member(this.#machine);
@@ -16106,16 +16378,18 @@ class DirectoryIdentity {
16106
16378
  #options;
16107
16379
  #cache;
16108
16380
  #keyring;
16109
- #listeners = { "devices-changed": [], "self-changed": [], alarm: [] };
16381
+ #listeners = { "devices-changed": [], "self-changed": [], "device-log": [], alarm: [] };
16110
16382
  #head;
16111
16383
  #persons = new Map;
16112
16384
  #index = new Map;
16113
16385
  #own;
16114
16386
  #relations = new Map;
16115
16387
  #watching = new Set;
16388
+ #watchRead = Promise.resolve();
16116
16389
  #alarm;
16117
16390
  #selfRecord;
16118
16391
  #monitoredAt = Number.NEGATIVE_INFINITY;
16392
+ #bindAccepted = false;
16119
16393
  #pendingLink;
16120
16394
  constructor(options) {
16121
16395
  this.#options = options;
@@ -16175,7 +16449,10 @@ class DirectoryIdentity {
16175
16449
  const person = this.#own.person;
16176
16450
  if (!person)
16177
16451
  return false;
16178
- const record = this.#persons.get(person)?.state.log.device(this.#machine());
16452
+ const held = this.#persons.get(person)?.state;
16453
+ if (held?.erased)
16454
+ return true;
16455
+ const record = held?.log.device(this.#machine());
16179
16456
  return record !== undefined && !record.current;
16180
16457
  }
16181
16458
  persons() {
@@ -16196,6 +16473,12 @@ class DirectoryIdentity {
16196
16473
  }
16197
16474
  return;
16198
16475
  }
16476
+ deviceChanges(person) {
16477
+ const held = isHosted(person) ? this.#persons.get(person) : undefined;
16478
+ if (!held || held.state.erased)
16479
+ return [];
16480
+ return deviceChangesOf(held.state.log);
16481
+ }
16199
16482
  async devices(person, opts = {}) {
16200
16483
  if (!isHosted(person))
16201
16484
  return { person, devices: [], anchor: this.anchor(), fetchedAt: this.#options.now() };
@@ -16290,15 +16573,23 @@ class DirectoryIdentity {
16290
16573
  }
16291
16574
  }
16292
16575
  watch(persons) {
16293
- let grew = false;
16576
+ const added = [];
16294
16577
  for (const person of persons) {
16295
16578
  if (!isHosted(person) || this.#watching.has(person) || this.#watching.size >= LIMITS.watchPersons)
16296
16579
  continue;
16297
16580
  this.#watching.add(person);
16298
- grew = true;
16581
+ added.push(person);
16299
16582
  }
16300
- if (grew)
16301
- this.#call((link) => link.watch([...this.#watching])).catch(() => {});
16583
+ if (added.length === 0)
16584
+ return this.#watchRead;
16585
+ this.#watchRead = this.#call((link) => link.watch([...this.#watching])).then(async () => {
16586
+ const held = added.filter((person) => this.#persons.has(person));
16587
+ if (held.length === 0)
16588
+ return;
16589
+ await this.#latest();
16590
+ await this.#fetch(held);
16591
+ }).catch(() => {});
16592
+ return this.#watchRead;
16302
16593
  }
16303
16594
  on(event, listener) {
16304
16595
  const listeners = this.#listeners[event];
@@ -16309,6 +16600,10 @@ class DirectoryIdentity {
16309
16600
  listeners.splice(at, 1);
16310
16601
  };
16311
16602
  }
16603
+ linkPerson() {
16604
+ const binding = this.binding();
16605
+ return binding.state !== "none" || this.#bindAccepted ? binding.person : undefined;
16606
+ }
16312
16607
  alarm() {
16313
16608
  return this.#alarm;
16314
16609
  }
@@ -16318,6 +16613,8 @@ class DirectoryIdentity {
16318
16613
  binding() {
16319
16614
  const person = this.#own.person;
16320
16615
  const record = person ? this.#persons.get(person)?.state.log.device(this.#machine()) : undefined;
16616
+ if (person && this.#persons.get(person)?.state.erased)
16617
+ return { state: "removed", person };
16321
16618
  if (!person || !record)
16322
16619
  return { state: "none", ...person ? { person } : {} };
16323
16620
  if (!record.current)
@@ -16358,13 +16655,29 @@ class DirectoryIdentity {
16358
16655
  authority: { kind: "jelly", token }
16359
16656
  });
16360
16657
  this.#signed(entry, person);
16361
- return current ? await this.#call((link) => link.append(entry)) : await this.#call((link) => link.bind(entry));
16658
+ if (current)
16659
+ return await this.#call((link) => link.append(entry));
16660
+ const bound = await this.#call((link) => link.bind(entry));
16661
+ this.#bindAccepted = true;
16662
+ return bound;
16362
16663
  });
16363
16664
  await this.#adoptAnnounced(done.sth);
16364
16665
  await this.#fetch([person]);
16365
16666
  await this.publishKeys();
16366
16667
  return { person, seq: done.seq };
16367
16668
  }
16669
+ async awaitSelf() {
16670
+ const person = this.#own.person;
16671
+ if (!person)
16672
+ throw new MessagingError("no_identity", "this device is not bound to an account");
16673
+ for (let tries = 0;this.self() === undefined || !this.#index.has(this.#machine()); tries++) {
16674
+ if (tries >= LOG_RETRIES)
16675
+ throw unavailable("the directory has not committed this device's binding and keys yet");
16676
+ await new Promise((resolve) => setTimeout(resolve, LOG_RETRY_MS));
16677
+ await this.#latest();
16678
+ await this.#fetch([person]);
16679
+ }
16680
+ }
16368
16681
  async publishKeys() {
16369
16682
  if (this.#alarm?.kind === "restored")
16370
16683
  throw new MessagingError("invalid_state", "this device runs from an older copy of its home; it publishes no keys");
@@ -16415,6 +16728,45 @@ class DirectoryIdentity {
16415
16728
  await this.#fetch([person]);
16416
16729
  return { seq: done.seq };
16417
16730
  }
16731
+ async erase(token) {
16732
+ const identity = this.#options.identity();
16733
+ const person = this.#own.person;
16734
+ if (!person || this.binding().state === "none" || this.removed())
16735
+ throw new MessagingError("no_identity", "this device is not bound to an account");
16736
+ if (this.binding().record?.vouch !== "jelly")
16737
+ throw new MessagingError("forbidden", "a device approved by link erases nothing; erase from a device Jelly ID vouched for");
16738
+ const entries = await this.#extendLog(async () => {
16739
+ await this.#latest();
16740
+ await this.#fetch([person]);
16741
+ const log = this.#persons.get(person).state.log;
16742
+ let head = log.head();
16743
+ const out = [];
16744
+ const devices = log.devices().sort((a, b) => Number(a.device === identity.machine) - Number(b.device === identity.machine));
16745
+ for (const device of devices) {
16746
+ const self = device.device === identity.machine;
16747
+ const entry = signLogEntry(identity, {
16748
+ v: PROTOCOL,
16749
+ realm: this.realm,
16750
+ person,
16751
+ seq: head.seq + 1,
16752
+ prev: head.hash,
16753
+ ts: this.#options.now(),
16754
+ op: "unbind",
16755
+ device: device.device,
16756
+ reason: self ? "self" : "removed",
16757
+ authority: self ? { kind: "self" } : { kind: "jelly", token }
16758
+ });
16759
+ out.push(entry);
16760
+ head = { seq: entry.seq, hash: logEntryHash(entry) };
16761
+ }
16762
+ for (const entry of out)
16763
+ this.#signed(entry, person);
16764
+ await this.#call((link) => link.erase(out, token));
16765
+ return out;
16766
+ });
16767
+ await this.#latest().then(() => this.#fetch([person])).catch(() => {});
16768
+ return { person, devices: entries.length, seq: entries.at(-1).seq };
16769
+ }
16418
16770
  createLink(args) {
16419
16771
  if (this.#own.person !== undefined) {
16420
16772
  throw new MessagingError("invalid_state", this.removed() ? "this device was removed from its account; it cannot be bound again" : "this device is already bound");
@@ -16633,15 +16985,25 @@ class DirectoryIdentity {
16633
16985
  throw new MessagingError("directory_inconsistent", "the directory answered at another tree head");
16634
16986
  const now = this.#options.now();
16635
16987
  const views = new Map(answer.persons.map((view) => [view.person, view]));
16988
+ const grew = [];
16636
16989
  for (const person of persons) {
16637
16990
  const view = views.get(person);
16638
16991
  if (!view)
16639
16992
  continue;
16993
+ const before = this.#persons.get(person)?.state;
16640
16994
  const state = await this.#verify(view, head);
16641
16995
  this.#persons.set(person, { state, size: head.sth.size, fetchedAt: now });
16642
16996
  this.#cache.putPerson({ person, entries: [...state.log.entries()], keys: [...state.keys.values()], erased: state.erased, size: head.sth.size, fetchedAt: now });
16997
+ if (state.log.length !== (before?.log.length ?? 0) || state.erased !== (before?.erased ?? false))
16998
+ grew.push(person);
16643
16999
  }
16644
17000
  this.#changed();
17001
+ if (grew.length > 0)
17002
+ for (const listener of [...this.#listeners["device-log"]])
17003
+ listener(grew);
17004
+ const unwatched = persons.filter((person) => views.has(person) && !this.#watching.has(person));
17005
+ if (unwatched.length > 0)
17006
+ this.watch(unwatched);
16645
17007
  }
16646
17008
  async#verify(view, head) {
16647
17009
  const known = this.#persons.get(view.person)?.state;
@@ -16763,6 +17125,56 @@ ${this.#index.get(me.machine)?.enc ?? ""}`;
16763
17125
  return mine === theirs ? "same" : "other";
16764
17126
  }
16765
17127
  }
17128
+ function deviceChangesOf(log) {
17129
+ const vouch = new Map;
17130
+ const record = new Map;
17131
+ const out = [];
17132
+ for (const entry of log.entries()) {
17133
+ const token = log.token(entry.seq);
17134
+ const via = token === undefined ? undefined : token.claims.hst !== undefined ? "host" : "browser";
17135
+ if (entry.op === "bind") {
17136
+ const v = entry.authority.kind === "jelly" ? "jelly" : "device";
17137
+ vouch.set(entry.device, v);
17138
+ record.set(entry.device, { label: entry.label, kind: entry.kind, host: via === "host" });
17139
+ out.push({
17140
+ seq: entry.seq,
17141
+ change: "added",
17142
+ device: entry.device,
17143
+ label: entry.label,
17144
+ kind: entry.kind,
17145
+ vouch: v,
17146
+ ...via ? { via } : {},
17147
+ ...entry.authority.kind === "device" ? { approver: entry.authority.approver } : {},
17148
+ by: entry.by,
17149
+ at: entry.ts
17150
+ });
17151
+ } else if (entry.op === "attest") {
17152
+ const known = record.get(entry.device);
17153
+ const next = { label: entry.label ?? known?.label ?? "device", kind: entry.kind ?? known?.kind ?? "other", host: via === "host" };
17154
+ record.set(entry.device, next);
17155
+ const raised = vouch.get(entry.device) !== "jelly";
17156
+ vouch.set(entry.device, "jelly");
17157
+ if (raised)
17158
+ out.push({ seq: entry.seq, change: "vouched", device: entry.device, label: next.label, kind: next.kind, vouch: "jelly", ...via ? { via } : {}, by: entry.by, at: entry.ts });
17159
+ } else {
17160
+ const known = record.get(entry.device);
17161
+ const v = vouch.get(entry.device) ?? "device";
17162
+ out.push({
17163
+ seq: entry.seq,
17164
+ change: entry.reason === "replaced" ? "replaced" : "removed",
17165
+ device: entry.device,
17166
+ label: known?.label ?? "device",
17167
+ ...known ? { kind: known.kind } : {},
17168
+ vouch: v,
17169
+ ...v === "jelly" && known ? { via: known.host ? "host" : "browser" } : {},
17170
+ by: entry.by,
17171
+ reason: entry.reason,
17172
+ at: entry.ts
17173
+ });
17174
+ }
17175
+ }
17176
+ return out;
17177
+ }
16766
17178
  function orgOf(email, domains) {
16767
17179
  if (email === undefined)
16768
17180
  return;
@@ -16847,7 +17259,7 @@ class DirectoryHome {
16847
17259
  url: this.#options.config.url,
16848
17260
  realm: realm.realm,
16849
17261
  identity: this.#options.identity,
16850
- person: () => realm.binding().person,
17262
+ person: () => realm.linkPerson(),
16851
17263
  pushes: {
16852
17264
  changed: (persons, sth) => void realm.changed(persons, sth).catch((error) => this.#failed("directory.changed", error)),
16853
17265
  unbound: (entry, sth) => void realm.changed([entry.person], sth).catch((error) => this.#failed("directory.unbound", error)),
@@ -16999,7 +17411,27 @@ class DirectoryHome {
16999
17411
  this.#login.cancel();
17000
17412
  return { ok: true };
17001
17413
  }
17414
+ async signIn(purpose, args) {
17415
+ this.#removing = undefined;
17416
+ this.#binding = undefined;
17417
+ const started = await this.#login.start({ purpose, label: args.label, kind: args.kind });
17418
+ return {
17419
+ purpose,
17420
+ verificationUriComplete: started.verificationUriComplete,
17421
+ verificationUri: started.verificationUri,
17422
+ userCode: started.userCode,
17423
+ fingerprint: started.fingerprint,
17424
+ expires: started.expires,
17425
+ interval: started.interval
17426
+ };
17427
+ }
17002
17428
  async#completeLogin(token, purpose) {
17429
+ if (purpose === "replace" || purpose === "erase") {
17430
+ const step = purpose === "replace" ? this.#options.replaceStep : this.#options.eraseStep;
17431
+ if (!step)
17432
+ throw new MessagingError("invalid_state", `this daemon does not ${purpose} through a sign-in`);
17433
+ return step(token);
17434
+ }
17003
17435
  if (purpose === "remove") {
17004
17436
  const removing = this.#removing;
17005
17437
  if (!removing)
@@ -17019,6 +17451,12 @@ class DirectoryHome {
17019
17451
  this.#options.log("info", "directory.approved", { seq: done.seq });
17020
17452
  return done;
17021
17453
  }
17454
+ async wake(state) {
17455
+ const link = this.#link;
17456
+ if (!link?.wake)
17457
+ return "pending";
17458
+ return link.wake(state);
17459
+ }
17022
17460
  presenceWatch(persons) {
17023
17461
  this.#link?.presenceWatch(persons).catch((error) => this.#failed("directory.presence.watch", error));
17024
17462
  }
@@ -17033,6 +17471,7 @@ class DirectoryHome {
17033
17471
  },
17034
17472
  send: (env) => home.#link ? home.#link.send(env) : Promise.reject(noLink()),
17035
17473
  presence: (env) => home.#link?.presence(env),
17474
+ retryNow: () => home.#link?.retryNow?.(),
17036
17475
  appendRoster: () => Promise.reject(new MessagingError("invalid_state", "a directory realm has no roster")),
17037
17476
  syncRoster: () => Promise.reject(new MessagingError("invalid_state", "a directory realm has no roster"))
17038
17477
  };
@@ -17058,6 +17497,28 @@ var init_directory_home = __esm(() => {
17058
17497
  init_directory_identity();
17059
17498
  });
17060
17499
 
17500
+ // packages/daemon/dist/realm/directory-cache.js
17501
+ function memoryDirectoryCache(from) {
17502
+ let head = from?.head();
17503
+ const persons = new Map((from?.persons() ?? []).map((p) => [p.person, p]));
17504
+ let archive = from?.archive() ?? [];
17505
+ let own = from?.own() ?? { keysSeq: -1, signed: [] };
17506
+ let alarm = from?.alarm();
17507
+ const copy = (value) => structuredClone(value);
17508
+ return {
17509
+ head: () => copy(head),
17510
+ setHead: (value) => void (head = copy(value)),
17511
+ persons: () => [...persons.values()].map(copy),
17512
+ putPerson: (value) => void persons.set(value.person, copy(value)),
17513
+ archive: () => copy(archive),
17514
+ setArchive: (keys) => void (archive = copy(keys)),
17515
+ own: () => copy(own),
17516
+ setOwn: (value) => void (own = copy(value)),
17517
+ alarm: () => copy(alarm),
17518
+ setAlarm: (value) => void (alarm = copy(value))
17519
+ };
17520
+ }
17521
+
17061
17522
  // packages/daemon/dist/jelly.js
17062
17523
  import { closeSync as closeSync6, constants as constants6, fstatSync as fstatSync3, readSync } from "fs";
17063
17524
  function jellyLabel(text) {
@@ -17147,7 +17608,12 @@ class JellyAuth {
17147
17608
  return refuse(new MessagingError("grant_invalid", "the host-scope grant is bound to another connection"));
17148
17609
  }
17149
17610
  }
17150
- this.#deps.log("info", "jelly.grant", { method: call.method, scope: grant.scope, ...grant.dev !== undefined ? { dev: grant.dev.slice(0, 200) } : {} });
17611
+ this.#deps.log("info", "jelly.grant", {
17612
+ method: call.method,
17613
+ scope: grant.scope,
17614
+ ...grant.dev !== undefined ? { dev: grant.dev.slice(0, 200) } : {},
17615
+ ...grant.mbr !== undefined ? { mbr: grant.mbr.slice(0, 200) } : {}
17616
+ });
17151
17617
  return { scope: grant.scope, expiresAt: (grant.exp + JELLY_HOST_SKEW_S) * 1000 };
17152
17618
  }
17153
17619
  ticket(p) {
@@ -17185,6 +17651,292 @@ var init_jelly3 = __esm(() => {
17185
17651
  HOST_SCOPE = new Set(HOST_SCOPE_METHODS);
17186
17652
  });
17187
17653
 
17654
+ // packages/daemon/dist/jelly-env.js
17655
+ import { delimiter, isAbsolute } from "path";
17656
+ function jellyEnvNamed(env) {
17657
+ return JELLY_ENV.filter((name) => (env[name] ?? "") !== "");
17658
+ }
17659
+ function fail2(message) {
17660
+ throw new MessagingError("invalid_request", message);
17661
+ }
17662
+ function required3(env, name) {
17663
+ const value = env[name] ?? "";
17664
+ if (value === "")
17665
+ fail2(`${name} is not set: a Jelly host sets it with JELLY_ACTION_KEY`);
17666
+ return value;
17667
+ }
17668
+ function folder(name, value, separator = delimiter) {
17669
+ if (!isAbsolute(value))
17670
+ fail2(`${name} is not an absolute path`);
17671
+ if (value.includes(separator))
17672
+ fail2(`${name} names more than one folder (joined with "${separator}"); the daemon takes one`);
17673
+ return value;
17674
+ }
17675
+ function relayUrlOf(url) {
17676
+ let parsed;
17677
+ try {
17678
+ parsed = new URL(url);
17679
+ } catch {
17680
+ return fail2(`JELLY_RESOURCES' ${JELLY_RESOURCE_NAME} url is not a URL`);
17681
+ }
17682
+ const loopback = /^(?:localhost|127\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[::1\])$/.test(parsed.hostname);
17683
+ const scheme = parsed.protocol === "https:" ? "wss:" : parsed.protocol === "http:" && loopback ? "ws:" : undefined;
17684
+ if (scheme === undefined)
17685
+ fail2(`JELLY_RESOURCES' ${JELLY_RESOURCE_NAME} url is not https:// (or http:// to a loopback host)`);
17686
+ if (parsed.username !== "" || parsed.password !== "" || parsed.search !== "" || parsed.hash !== "" || parsed.pathname !== "/" && parsed.pathname !== "") {
17687
+ fail2(`JELLY_RESOURCES' ${JELLY_RESOURCE_NAME} url is not an origin: the relay is derived from its origin alone`);
17688
+ }
17689
+ return `${scheme}//${parsed.host}/v1/connect`;
17690
+ }
17691
+ function jellyEnvRealm(env) {
17692
+ let raw;
17693
+ try {
17694
+ raw = parseWire(required3(env, "JELLY_RESOURCES"), { maxBytes: 64 * 1024 });
17695
+ } catch (error) {
17696
+ if (error instanceof MessagingError && /JELLY_RESOURCES is not set/.test(error.message))
17697
+ throw error;
17698
+ return fail2("JELLY_RESOURCES is not JSON");
17699
+ }
17700
+ const list = Resources.safeParse(raw);
17701
+ if (!list.success)
17702
+ fail2("JELLY_RESOURCES is not a list of discovery entries");
17703
+ const entries = list.data.map((item) => ResourceEntry.safeParse(item)).filter((parsed) => parsed.success && parsed.data.name === JELLY_RESOURCE_NAME);
17704
+ if (entries.length === 0)
17705
+ fail2(`JELLY_RESOURCES has no ${JELLY_RESOURCE_NAME} entry`);
17706
+ if (entries.length > 1)
17707
+ fail2(`JELLY_RESOURCES has more than one ${JELLY_RESOURCE_NAME} entry`);
17708
+ const entry = entries[0].data;
17709
+ const realm = DirectoryRealmConfig.strict().safeParse({
17710
+ kind: "directory",
17711
+ url: relayUrlOf(entry.url),
17712
+ issuer: required3(env, "JELLY_ID_ISSUER"),
17713
+ resource: entry.resource,
17714
+ key: required3(env, "JELLY_SERVICE_ANCHOR")
17715
+ });
17716
+ if (!realm.success) {
17717
+ const at = realm.error.issues[0]?.path[0];
17718
+ fail2(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)}`);
17719
+ }
17720
+ try {
17721
+ directoryRealmId(realm.data);
17722
+ } catch {
17723
+ fail2("JELLY_SERVICE_ANCHOR is not a usable Ed25519 key");
17724
+ }
17725
+ return realm.data;
17726
+ }
17727
+ function readJellyEnv(env) {
17728
+ const actKeyPath = required3(env, "JELLY_ACTION_KEY");
17729
+ if (!isAbsolute(actKeyPath))
17730
+ fail2("JELLY_ACTION_KEY is not an absolute path");
17731
+ const home = folder("STATE_DIRECTORY", required3(env, "STATE_DIRECTORY"));
17732
+ const runText = env.RUNTIME_DIRECTORY ?? "";
17733
+ const groupText = env.JELLY_SOCKET_GROUP ?? "";
17734
+ if (groupText !== "" && process.platform === "win32")
17735
+ fail2("JELLY_SOCKET_GROUP is not supported on Windows: a named pipe is not shared with a group");
17736
+ if (groupText !== "" && (!/^\d{1,10}$/.test(groupText) || Number(groupText) > 4294967294))
17737
+ fail2("JELLY_SOCKET_GROUP is not a numeric gid");
17738
+ let label;
17739
+ try {
17740
+ label = jellyLabel(required3(env, "JELLY_DEVICE_LABEL"));
17741
+ } catch (error) {
17742
+ if (error instanceof MessagingError && /is not set/.test(error.message))
17743
+ throw error;
17744
+ return fail2("JELLY_DEVICE_LABEL has no letter or digit to make a device label from");
17745
+ }
17746
+ return {
17747
+ home,
17748
+ ...runText !== "" ? { run: folder("RUNTIME_DIRECTORY", runText) } : {},
17749
+ actKeyPath,
17750
+ label,
17751
+ ...groupText !== "" ? { socketGroup: Number(groupText) } : {},
17752
+ realm: jellyEnvRealm(env)
17753
+ };
17754
+ }
17755
+ function realmMismatch(pinned, named) {
17756
+ return ["url", "issuer", "resource", "key"].filter((member) => pinned[member] !== named[member]);
17757
+ }
17758
+ async function pinRealm(realm, get = wellKnownGet) {
17759
+ const origin = new URL(realm.url);
17760
+ const url = `${origin.protocol === "wss:" ? "https:" : "http:"}//${origin.host}/.well-known/messaging.json`;
17761
+ let text;
17762
+ try {
17763
+ text = await get(url);
17764
+ } catch (error) {
17765
+ 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`);
17766
+ }
17767
+ let raw;
17768
+ try {
17769
+ raw = parseWire(text, { maxBytes: 16 * 1024 });
17770
+ } catch {
17771
+ return fail2(`the relay's realm document ${url} is not JSON`);
17772
+ }
17773
+ const { realm: _id, ...published } = typeof raw === "object" && raw !== null ? raw : {};
17774
+ const parsed = DirectoryRealmConfig.safeParse(published);
17775
+ if (!parsed.success)
17776
+ fail2(`the relay's realm document ${url} is not a directory realm`);
17777
+ const differs = realmMismatch(parsed.data, realm);
17778
+ if (differs.length > 0)
17779
+ fail2(`the relay's realm document ${url} names another ${differs.join(", ")} than the environment: the home is not pinned`);
17780
+ return parsed.data.orgDomains !== undefined ? { ...realm, orgDomains: parsed.data.orgDomains } : realm;
17781
+ }
17782
+ var JELLY_ENV, JELLY_RESOURCE_NAME = "messaging", ResourceEntry, Resources, wellKnownGet = async (url) => {
17783
+ const response = await fetch(url, { redirect: "error", signal: AbortSignal.timeout(5000) });
17784
+ if (!response.ok)
17785
+ throw new MessagingError("relay_unavailable", `the relay answered ${response.status}`);
17786
+ const text = await response.text();
17787
+ if (Buffer.byteLength(text) > 16 * 1024)
17788
+ throw new MessagingError("too_large", "the relay's realm document is larger than 16 KiB");
17789
+ return text;
17790
+ };
17791
+ var init_jelly_env = __esm(() => {
17792
+ init_dist();
17793
+ init_zod();
17794
+ init_jelly3();
17795
+ JELLY_ENV = ["JELLY_ACTION_KEY", "JELLY_ID_ISSUER", "JELLY_RESOURCES", "JELLY_SERVICE_ANCHOR", "JELLY_DEVICE_LABEL", "JELLY_SOCKET_GROUP"];
17796
+ ResourceEntry = looseObject({ name: string2().max(200), resource: string2().min(1).max(2048), url: string2().min(1).max(2048) });
17797
+ Resources = array(unknown()).max(64);
17798
+ });
17799
+
17800
+ // packages/daemon/dist/consent/answer-preview.js
17801
+ function who3(from) {
17802
+ const name = cleanPeerText(from.name, { singleLine: true, maxChars: 80 }) || "someone";
17803
+ const email = from.email === undefined ? undefined : cleanPeerText(from.email, { singleLine: true, maxChars: 100 });
17804
+ return `${name} (${email !== undefined && email !== "" ? `${email}, ` : ""}${ORG[from.org]})`;
17805
+ }
17806
+ function answersFor(kind) {
17807
+ return kind === "suggestion" ? ["decline"] : kind === "contact" ? ["accept", "decline", "block"] : ["once", "always", "decline", "block"];
17808
+ }
17809
+ function answerSentence(input) {
17810
+ const { kind, answer } = input;
17811
+ if (answer !== undefined && !answersFor(kind).includes(answer)) {
17812
+ throw new MessagingError("invalid_request", `a ${kind === "message" || kind === "task" ? "request to your agent" : kind === "contact" ? "message request" : "suggestion"} takes ${answersFor(kind).join(", ")}`);
17813
+ }
17814
+ const sender = who3(input.from);
17815
+ const single = input.count <= 1;
17816
+ if (kind === "suggestion") {
17817
+ const email = cleanPeerText(input.email ?? "", { singleLine: true, maxChars: 100 });
17818
+ if (answer === undefined)
17819
+ return `Your agent ${input.agent} suggests you send a message request to ${email}.`;
17820
+ return `Dismiss your agent ${input.agent}'s suggestion to contact ${email}. Nothing is sent.`;
17821
+ }
17822
+ if (kind === "contact") {
17823
+ const thing = single ? "their message request" : `their ${input.count} message requests`;
17824
+ if (answer === undefined)
17825
+ return `${sender} asks to message you.`;
17826
+ if (answer === "accept")
17827
+ 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.`;
17828
+ if (answer === "decline")
17829
+ return `Decline ${thing} from ${sender}. They are told it was declined.`;
17830
+ return `Block ${sender}: nothing from them or their agents reaches you, and ${thing} ${single ? "is" : "are"} declined.`;
17831
+ }
17832
+ const thing = single ? `this ${kind}` : `these ${input.count} requests`;
17833
+ const src = single ? `${sender}'s agent ${input.agent}` : `${sender}'s agents`;
17834
+ const dst = single ? `your agent ${input.to}` : "your agents";
17835
+ if (answer === undefined)
17836
+ return `${src} asks to send ${kind === "task" ? "a task" : "a message"} to ${dst}.`;
17837
+ if (answer === "once")
17838
+ return `Deliver ${thing} from ${src} to ${dst} this once, guarded: ${single ? "your agent" : "your agents"} may answer only them.`;
17839
+ if (answer === "always")
17840
+ return `Deliver ${thing} from ${src} to ${dst}, and let their agents ask yours from now on without asking you.`;
17841
+ if (answer === "decline")
17842
+ return `Decline ${thing} from ${src}. They are told it was declined.`;
17843
+ return `Block ${sender}: nothing from them or their agents reaches you, and ${thing} ${single ? "is" : "are"} declined.`;
17844
+ }
17845
+ var ORG;
17846
+ var init_answer_preview = __esm(() => {
17847
+ init_dist();
17848
+ ORG = { same: "your org", other: "another org", none: "no org" };
17849
+ });
17850
+
17851
+ // packages/daemon/dist/snapshot.js
17852
+ import { createHash as createHash10 } from "crypto";
17853
+ import { chmodSync as chmodSync4, chownSync as chownSync2, existsSync as existsSync6, lstatSync as lstatSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync8, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "fs";
17854
+ import { join as join7 } from "path";
17855
+ function sha256File(path) {
17856
+ const data = readFileSync8(path);
17857
+ return { bytes: data.length, sha256: createHash10("sha256").update(data).digest("hex") };
17858
+ }
17859
+
17860
+ class Snapshots {
17861
+ #deps;
17862
+ #timer;
17863
+ constructor(deps) {
17864
+ this.#deps = deps;
17865
+ }
17866
+ get folder() {
17867
+ return join7(this.#deps.run, "snapshot");
17868
+ }
17869
+ take() {
17870
+ const folder = this.folder;
17871
+ if (existsSync6(folder))
17872
+ throw new MessagingError("invalid_state", "a snapshot is held: release it with daemon.snapshot {done: true} first");
17873
+ const group = this.#deps.group;
17874
+ mkdirSync5(folder, { mode: group === undefined ? 448 : 488 });
17875
+ try {
17876
+ if (!IS_WINDOWS && group !== undefined) {
17877
+ chownSync2(folder, process.geteuid?.() ?? -1, group);
17878
+ chmodSync4(folder, 488);
17879
+ }
17880
+ const fileMode = group === undefined ? 384 : 416;
17881
+ const files = [];
17882
+ const db = join7(folder, "messaging.db");
17883
+ this.#deps.vacuumInto(db);
17884
+ if (existsSync6(this.#deps.config))
17885
+ writeFileSync5(join7(folder, "config.json"), readFileSync8(this.#deps.config), { mode: fileMode });
17886
+ for (const name of ["messaging.db", "config.json"]) {
17887
+ const path = join7(folder, name);
17888
+ if (!existsSync6(path))
17889
+ continue;
17890
+ if (!IS_WINDOWS) {
17891
+ if (group !== undefined)
17892
+ chownSync2(path, process.geteuid?.() ?? -1, group);
17893
+ chmodSync4(path, fileMode);
17894
+ }
17895
+ files.push({ name, ...sha256File(path) });
17896
+ }
17897
+ const at = this.#deps.now();
17898
+ const until = at + SNAPSHOT_HOLD_MS;
17899
+ const manifest = { storeSchema: this.#deps.storeSchema, at, files };
17900
+ writeFileSync5(join7(folder, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
17901
+ `, { mode: fileMode });
17902
+ if (!IS_WINDOWS && group !== undefined)
17903
+ chownSync2(join7(folder, "manifest.json"), process.geteuid?.() ?? -1, group);
17904
+ clearTimeout(this.#timer);
17905
+ this.#timer = setTimeout(() => this.release(), SNAPSHOT_HOLD_MS);
17906
+ this.#timer.unref();
17907
+ return { folder, files, storeSchema: this.#deps.storeSchema, at, until };
17908
+ } catch (error) {
17909
+ this.release();
17910
+ if (error instanceof MessagingError)
17911
+ throw error;
17912
+ throw new MessagingError("internal", `the snapshot failed: ${error.code ?? "error"}`);
17913
+ }
17914
+ }
17915
+ release() {
17916
+ clearTimeout(this.#timer);
17917
+ this.#timer = undefined;
17918
+ let present;
17919
+ try {
17920
+ lstatSync4(this.folder);
17921
+ present = true;
17922
+ } catch {
17923
+ present = false;
17924
+ }
17925
+ if (present)
17926
+ rmSync4(this.folder, { recursive: true, force: true });
17927
+ return { released: present };
17928
+ }
17929
+ close() {
17930
+ this.release();
17931
+ }
17932
+ }
17933
+ var SNAPSHOT_HOLD_MS;
17934
+ var init_snapshot = __esm(() => {
17935
+ init_dist();
17936
+ init_dist2();
17937
+ SNAPSHOT_HOLD_MS = 60 * 60000;
17938
+ });
17939
+
17188
17940
  // packages/daemon/dist/notices.js
17189
17941
  class Notices {
17190
17942
  #host;
@@ -17206,6 +17958,12 @@ class Notices {
17206
17958
  alert(kind, at) {
17207
17959
  this.#file({ id: `a_${kind.replace(/_/g, "")}${at}`, category: "alert", source: kind, at });
17208
17960
  }
17961
+ device(seq, at) {
17962
+ this.#file({ id: `a_dev${seq}`, category: "alert", source: `device:${seq}`, at });
17963
+ }
17964
+ trust(grantId, person, until, at) {
17965
+ this.#file({ id: `a_trust${grantId}`, category: "alert", person, source: `trust:${until}`, at });
17966
+ }
17209
17967
  #file(row) {
17210
17968
  if (!this.#host.store.putNotice(row))
17211
17969
  return;
@@ -17271,7 +18029,15 @@ class Notices {
17271
18029
  const own = row.person === this.#host.me();
17272
18030
  notice.from = profile ? { name: profile.name.slice(0, 200), ...profile.email !== undefined ? { email: profile.email.slice(0, 320) } : {}, org: profile.org } : { name: row.person, org: own ? "same" : "none" };
17273
18031
  }
17274
- if (row.category === "alert")
18032
+ if (row.category === "alert" && row.source.startsWith("device:")) {
18033
+ const device = this.#host.device?.(Number(row.source.slice("device:".length)));
18034
+ if (device)
18035
+ notice.device = device;
18036
+ notice.line = device ? deviceLine(device) : "A device of your account changed.";
18037
+ } else if (row.category === "alert" && row.source.startsWith("trust:")) {
18038
+ const until = Number(row.source.slice("trust:".length));
18039
+ notice.line = `Trust you gave this person ends on ${Number.isSafeInteger(until) ? new Date(until).toISOString().slice(0, 10) : "a date soon"}; renew it or let it lapse.`;
18040
+ } else if (row.category === "alert")
17275
18041
  notice.line = ALERT_TEXT[row.source] ?? "An alarm was raised on this device.";
17276
18042
  else if (row.category === "message" && lines) {
17277
18043
  const line = this.#host.store.line(row.source);
@@ -17281,6 +18047,18 @@ class Notices {
17281
18047
  return notice;
17282
18048
  }
17283
18049
  }
18050
+ function vouchWords2(d) {
18051
+ if (d.vouch === "admin")
18052
+ return "admitted by the team's admin";
18053
+ if (d.vouch === "device")
18054
+ return d.approver !== undefined ? `approved on '${d.approver}'` : "approved by link";
18055
+ return d.via === "browser" ? "signed in through the browser" : "vouched by Jelly ID on a Jelly host";
18056
+ }
18057
+ function deviceLine(d) {
18058
+ const what = `'${d.label}'${d.kind !== undefined ? ` (${d.kind})` : ""}`;
18059
+ const line = d.change === "added" ? `New device on your account: ${what}, ${vouchWords2(d)}.` : d.change === "vouched" ? `${what} on your account is now vouched by Jelly ID: ${vouchWords2(d)}.` : d.change === "replaced" ? `'${d.replaces?.label ?? "a device"}' was replaced by ${what}, ${vouchWords2(d)}.` : `${what} was removed from your account.`;
18060
+ return line.length <= NOTICE_LINE_CHARS ? line : `${line.slice(0, NOTICE_LINE_CHARS - 1)}\u2026`;
18061
+ }
17284
18062
  function firstLine(text) {
17285
18063
  const first = cleanPeerText(text.split(/\r\n|\n|\r/, 1)[0] ?? "", { singleLine: true });
17286
18064
  if (first.length <= NOTICE_LINE_CHARS)
@@ -17861,7 +18639,7 @@ var init_storage_errors = __esm(() => {
17861
18639
  });
17862
18640
 
17863
18641
  // packages/daemon/dist/store.js
17864
- import { chmodSync as chmodSync4, closeSync as closeSync7, openSync as openSync3 } from "fs";
18642
+ import { chmodSync as chmodSync5, closeSync as closeSync7, openSync as openSync3 } from "fs";
17865
18643
  import { DatabaseSync } from "node:sqlite";
17866
18644
  function additiveColumn(decl) {
17867
18645
  if (/\b(PRIMARY|UNIQUE|CHECK|REFERENCES|COLLATE|GENERATED|AS)\b/i.test(decl))
@@ -17950,7 +18728,7 @@ class Store {
17950
18728
  constructor(path) {
17951
18729
  if (path !== ":memory:") {
17952
18730
  closeSync7(openSync3(path, "a", 384));
17953
- chmodSync4(path, 384);
18731
+ chmodSync5(path, 384);
17954
18732
  }
17955
18733
  this.#db = new DatabaseSync(path, { timeout: 5000 });
17956
18734
  this.#db.exec("PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL;");
@@ -17965,6 +18743,11 @@ class Store {
17965
18743
  this.#statements.clear();
17966
18744
  this.#db.close();
17967
18745
  }
18746
+ vacuumInto(path) {
18747
+ if (this.#depth > 0)
18748
+ throw new MessagingError("invalid_state", "a snapshot is not taken inside a transaction");
18749
+ this.#db.exec(`VACUUM INTO '${path.replaceAll("'", "''")}'`);
18750
+ }
17968
18751
  tx(fn) {
17969
18752
  if (this.#depth > 0) {
17970
18753
  const name = `sp${this.#depth}`;
@@ -18602,6 +19385,14 @@ class Store {
18602
19385
  SELECT substr(peer_address, 1, instr(peer_address || '@', '@') - 1), 0, created_at FROM tasks WHERE key = ?
18603
19386
  ) GROUP BY peer ORDER BY MAX(at) DESC, peer LIMIT ?`, key, key, key, limit).map((row) => ({ peer: String(row.peer), unread: Number(row.unread), lastAt: Number(row.at) }));
18604
19387
  }
19388
+ awaitedRequests(sessions, terminal, limit) {
19389
+ if (sessions.length === 0)
19390
+ return [];
19391
+ const rows = this.#all(`SELECT request FROM tasks WHERE role = 'requester' AND request IS NOT NULL
19392
+ AND session IN (${sessions.map(() => "?").join(", ")}) AND state NOT IN (${terminal.map(() => "?").join(", ")})
19393
+ ORDER BY updated_at DESC, task_id DESC LIMIT ?`, ...sessions, ...terminal, limit);
19394
+ return rows.map((row) => String(row.request));
19395
+ }
18605
19396
  task(taskId, role) {
18606
19397
  const row = this.#get(`SELECT * FROM ${TASKS} WHERE task_id = ? AND role = ?`, taskId, role);
18607
19398
  return row && taskRow2(row);
@@ -19173,14 +19964,14 @@ var init_version = __esm(() => {
19173
19964
  });
19174
19965
 
19175
19966
  // packages/daemon/dist/wake-bridge.js
19176
- import { closeSync as closeSync8, constants as constants7, fstatSync as fstatSync4, lstatSync as lstatSync4, openSync as openSync4, readSync as readSync2 } from "fs";
19967
+ import { closeSync as closeSync8, constants as constants7, fstatSync as fstatSync4, lstatSync as lstatSync5, openSync as openSync4, readSync as readSync2 } from "fs";
19177
19968
  import { request } from "http";
19178
19969
  import { homedir as homedir2 } from "os";
19179
- import { isAbsolute, join as join7 } from "path";
19970
+ import { isAbsolute as isAbsolute2, join as join8 } from "path";
19180
19971
  function serviceFile(env = process.env, windows = IS_WINDOWS) {
19181
19972
  const home = (windows ? env.USERPROFILE : env.HOME) || homedir2();
19182
- const state = env.XDG_STATE_HOME && isAbsolute(env.XDG_STATE_HOME) ? env.XDG_STATE_HOME : join7(home, ".local", "state");
19183
- return join7(state, "opencode", "service.json");
19973
+ const state = env.XDG_STATE_HOME && isAbsolute2(env.XDG_STATE_HOME) ? env.XDG_STATE_HOME : join8(home, ".local", "state");
19974
+ return join8(state, "opencode", "service.json");
19184
19975
  }
19185
19976
  function loopbackOrigin(url) {
19186
19977
  let parsed;
@@ -19199,7 +19990,7 @@ function loopbackOrigin(url) {
19199
19990
  function readService(file) {
19200
19991
  if (IS_WINDOWS) {
19201
19992
  try {
19202
- const stats = lstatSync4(file);
19993
+ const stats = lstatSync5(file);
19203
19994
  if (stats.isSymbolicLink() || !stats.isFile() || aclProblem(file, { pinnedOwner: false }) !== undefined)
19204
19995
  return "service_unsafe";
19205
19996
  } catch (error) {
@@ -19492,6 +20283,16 @@ class Grants {
19492
20283
  hasGrant(person) {
19493
20284
  return this.#current(grantSubjectKey({ person })) !== undefined;
19494
20285
  }
20286
+ expiringTrust(withinMs) {
20287
+ const now = this.#host.now();
20288
+ const current = this.#currentDevice();
20289
+ return this.#all().flatMap(({ grant }) => {
20290
+ const until = grant.trustedUntil;
20291
+ if (!("person" in grant.subject) || until === undefined || until <= now || until > now + withinMs || !current(grant.by))
20292
+ return [];
20293
+ return [{ id: grant.id, person: grant.subject.person, until }];
20294
+ });
20295
+ }
19495
20296
  list(p) {
19496
20297
  const only = p.subject === undefined ? undefined : this.#subjectKey(p.subject, false);
19497
20298
  const grants = this.#all().filter(({ subject }) => only === undefined || subject === only).map(({ grant }) => this.#view(grant)).sort((a, b) => b.ts - a.ts || (a.id < b.id ? 1 : -1));
@@ -19566,6 +20367,25 @@ class Grants {
19566
20367
  this.#host.log("grant.sync", { id: env.id, what: conv.what, merged });
19567
20368
  this.sweep();
19568
20369
  }
20370
+ resignFrom(predecessor) {
20371
+ const { identity } = this.#host.joined();
20372
+ const now = this.#host.now();
20373
+ const resigned = [];
20374
+ for (const { subject, grant } of this.#all()) {
20375
+ if (grant.by !== predecessor)
20376
+ continue;
20377
+ const copy = resignGrant(identity, grant, { now, keep: "all" });
20378
+ this.#store.putGrant(subject, copy);
20379
+ resigned.push(copy);
20380
+ if ("person" in copy.subject)
20381
+ this.#host.changed?.(copy.subject.person);
20382
+ }
20383
+ if (resigned.length > 0) {
20384
+ this.#sync("grant", resigned);
20385
+ this.#host.log("grant.resigned", { grants: resigned.length });
20386
+ }
20387
+ return resigned.length;
20388
+ }
19569
20389
  sweep() {
19570
20390
  const { identity, realm } = this.#host.joined();
19571
20391
  if (realm.removed())
@@ -20211,6 +21031,8 @@ class Daemon {
20211
21031
  #closed = false;
20212
21032
  #online = new Set;
20213
21033
  #holders = new Map;
21034
+ #ticketEnds = new Map;
21035
+ #lapsed = new Map;
20214
21036
  #policy = {
20215
21037
  rate: inboundRateLimiter(),
20216
21038
  presence: responseRateLimiter(),
@@ -20230,6 +21052,9 @@ class Daemon {
20230
21052
  #requests;
20231
21053
  #notices;
20232
21054
  #jellyHost;
21055
+ #attached = false;
21056
+ #wakeArmed = false;
21057
+ #snapshots;
20233
21058
  #jellyAuth;
20234
21059
  #state = "starting";
20235
21060
  #sync;
@@ -20340,6 +21165,14 @@ class Daemon {
20340
21165
  address: (sender, session) => peerRef(sender, session).address,
20341
21166
  held: (row) => this.#notices.held(row)
20342
21167
  });
21168
+ this.#snapshots = new Snapshots({
21169
+ run: paths.run,
21170
+ config: paths.config,
21171
+ vacuumInto: (path) => this.#store.vacuumInto(path),
21172
+ storeSchema: STORE_SCHEMA,
21173
+ group: options.jellyHost?.socketGroup,
21174
+ now: this.#now
21175
+ });
20343
21176
  this.#notices = new Notices({
20344
21177
  store,
20345
21178
  now: this.#now,
@@ -20348,6 +21181,7 @@ class Daemon {
20348
21181
  const realm = this.#realm ?? this.#directory?.realm;
20349
21182
  return realm ? await realm.profile(person) : undefined;
20350
21183
  },
21184
+ device: (seq) => this.#noticeDevice(seq),
20351
21185
  emit: (event) => this.#emit(event)
20352
21186
  });
20353
21187
  this.#keyring = new Keyring({
@@ -20379,6 +21213,10 @@ class Daemon {
20379
21213
  static async start(options) {
20380
21214
  const paths = homePaths(options.home, options.jellyHost?.run);
20381
21215
  ensureHome(paths);
21216
+ const jelly = options.jellyHost;
21217
+ if (jelly?.realm !== undefined && readConfig(paths.config).realm === undefined) {
21218
+ options = { ...options, jellyHost: { ...jelly, realm: await pinRealm(jelly.realm, options.wellKnown) } };
21219
+ }
20382
21220
  jellyConfig(paths.config, options.jellyHost);
20383
21221
  const lock = acquireLock(paths.lock);
20384
21222
  let store;
@@ -20417,6 +21255,7 @@ class Daemon {
20417
21255
  }
20418
21256
  this.#sync.load();
20419
21257
  this.#store.keepAllLive(this.#now(), CLI_AGENT);
21258
+ this.#snapshots.release();
20420
21259
  const handlers = this.#handlers();
20421
21260
  const jelly = this.#jellyHost;
20422
21261
  if (jelly)
@@ -20448,6 +21287,7 @@ class Daemon {
20448
21287
  }
20449
21288
  this.#log("info", "daemon.started", { pid: process.pid, version: VERSION, joined: identity !== undefined, store: identityStore?.kind, ...jelly ? { deployment: "jelly" } : {} });
20450
21289
  this.#state = "running";
21290
+ this.#deviceNotices(true);
20451
21291
  this.#startTransport();
20452
21292
  this.#sweepTimer = setInterval(() => this.sweep(), this.#tuning.sweepMs);
20453
21293
  this.#sweepTimer.unref();
@@ -20487,7 +21327,12 @@ class Daemon {
20487
21327
  this.#alerts();
20488
21328
  },
20489
21329
  onDeliver: (raw) => this.#onDeliver(raw),
20490
- state: (connected) => this.#onHostedState(connected)
21330
+ state: (connected) => this.#onHostedState(connected),
21331
+ replaceStep: (token) => this.#replaceWith(token),
21332
+ eraseStep: async (token) => {
21333
+ const done = await this.#eraseWith(token);
21334
+ return { person: done.person, device: this.#identity.machine, seq: done.seq };
21335
+ }
20491
21336
  });
20492
21337
  this.#directory = home;
20493
21338
  const realm = home.realm;
@@ -20505,6 +21350,7 @@ class Daemon {
20505
21350
  else
20506
21351
  this.#presenceSoon();
20507
21352
  });
21353
+ realm.on("device-log", (persons) => this.#deviceLogGrew(persons));
20508
21354
  this.#removed = realm.removed();
20509
21355
  this.#transport = home.transport();
20510
21356
  home.start();
@@ -20541,6 +21387,7 @@ class Daemon {
20541
21387
  await server.close().catch(() => {});
20542
21388
  await Promise.race([this.#flushing, new Promise((resolve) => setTimeout(resolve, 2000).unref())]);
20543
21389
  this.#store.close();
21390
+ this.#snapshots.close();
20544
21391
  } finally {
20545
21392
  this.#lock.release();
20546
21393
  this.#log("info", "daemon.stopped", { pid: process.pid });
@@ -20604,7 +21451,11 @@ class Daemon {
20604
21451
  return {
20605
21452
  "daemon.status": () => this.status(),
20606
21453
  "device.status": () => this.#deviceStatus(),
20607
- "device.bind": (p) => this.#homeDirectory("device.bind").bind(this.#jellyHost ? { ...p, label: this.#jellyHost.label, kind: this.#jellyHost.kind } : p),
21454
+ "device.bind": async (p) => {
21455
+ if (this.#store.getKv(PREDECESSOR_KEY) !== undefined && this.#directory)
21456
+ return this.#replaceWith(p.token, p.label);
21457
+ return this.#homeDirectory("device.bind").bind(this.#jellyHost ? { ...p, label: this.#jellyHost.label, kind: this.#jellyHost.kind } : p);
21458
+ },
20608
21459
  "device.list": () => this.#directory?.list() ?? this.#rosterDevices(),
20609
21460
  "device.remove": (p) => this.#homeDirectory("device.remove").remove(p),
20610
21461
  "device.link": (p) => this.#homeDirectory("device.link").link(p),
@@ -20617,6 +21468,10 @@ class Daemon {
20617
21468
  "device.login.cancel": () => this.#homeDirectory("device.login.cancel").loginCancel(),
20618
21469
  "device.approve.preview": (p) => this.#homeDirectory("device.approve.preview").approvePreview(p),
20619
21470
  "device.approve": (p) => this.#homeDirectory("device.approve").approve(p),
21471
+ "identity.replace": (p) => this.#replace(p),
21472
+ "identity.replace.preview": (p) => this.#replacePreview(p),
21473
+ "directory.erase": (p) => this.#erase(p),
21474
+ "directory.erase.preview": (p) => this.#erasePreview(p),
20620
21475
  "daemon.stop": () => this.#stop(),
20621
21476
  "session.register": (p, ctx) => this.#register(p, ctx),
20622
21477
  "session.update": (p, ctx) => this.#updateSession(this.#bound(p, ctx)),
@@ -20689,6 +21544,7 @@ class Daemon {
20689
21544
  },
20690
21545
  "request.list": async () => ({ requests: [...this.#holds.list().requests, ...await this.#requests.list()] }),
20691
21546
  "request.answer": (p) => this.#requests.holds(p.id) ? this.#requests.answer(p) : this.#holds.answer(p),
21547
+ "request.answer.preview": (p) => this.#answerPreview(p),
20692
21548
  "contact.request": (p) => this.#requests.request(p),
20693
21549
  "contact.request.preview": (p) => this.#requests.preview(p),
20694
21550
  "card.get": async (p) => {
@@ -20697,6 +21553,11 @@ class Daemon {
20697
21553
  return { sent: await this.#cards.get(p.person) };
20698
21554
  },
20699
21555
  "daemon.health": () => this.#health(),
21556
+ "daemon.woke": () => this.#woke(),
21557
+ "wake.arm": (p) => this.#wakeArm(p),
21558
+ "wake.disarm": () => this.#wakeDisarm(),
21559
+ "daemon.snapshot": (p) => p.done ? this.#snapshots.release() : this.#snapshots.take(),
21560
+ "person.attached": (p) => this.#personAttached(p.attached),
20700
21561
  "agents.list": () => this.#agentsList(),
20701
21562
  "agents.read": (p) => this.#agentsRead(p),
20702
21563
  "notice.subscribe": (p, ctx, authority) => this.#notices.subscribe(ctx, p, authority?.scope === "host" ? authority.expiresAt : undefined),
@@ -20711,10 +21572,30 @@ class Daemon {
20711
21572
  const out = [];
20712
21573
  for (const person of all) {
20713
21574
  const org = realm.kind === "roster" ? "same" : await realm.profile(person).then((profile) => profile.org, () => "none");
20714
- out.push(this.#cards.reach(person, org === "same"));
21575
+ const reach = this.#cards.reach(person, org === "same");
21576
+ const changedAt = this.#recentDeviceChanges(realm, person).at(0)?.at;
21577
+ out.push(changedAt === undefined ? reach : { ...reach, devicesChangedAt: changedAt });
20715
21578
  }
20716
21579
  return out;
20717
21580
  }
21581
+ #recentDeviceChanges(realm, person) {
21582
+ const since = this.#now() - DEVICES_CHANGED_MS;
21583
+ const changes = realm.deviceChanges(person);
21584
+ const labelOf = (device) => changes.filter((c) => c.device === device).at(-1)?.label;
21585
+ return changes.filter((change) => change.change !== "vouched" && change.at > since).reverse().slice(0, 10).map((change) => {
21586
+ const approver = change.approver !== undefined ? labelOf(change.approver) : undefined;
21587
+ return {
21588
+ change: change.change,
21589
+ device: change.device,
21590
+ label: MachineLabel.safeParse(change.label).success ? change.label : "device",
21591
+ ...change.kind !== undefined ? { kind: change.kind } : {},
21592
+ vouch: change.vouch === "service" ? "device" : change.vouch,
21593
+ ...change.via !== undefined ? { via: change.via } : {},
21594
+ ...approver !== undefined && MachineLabel.safeParse(approver).success ? { approver } : {},
21595
+ at: change.at
21596
+ };
21597
+ });
21598
+ }
20718
21599
  async#grantSubject(p) {
20719
21600
  const { realm } = this.#joined();
20720
21601
  if ("person" in p.subject)
@@ -20815,6 +21696,7 @@ class Daemon {
20815
21696
  now: this.#now
20816
21697
  });
20817
21698
  realm.on("devices-changed", () => this.#realmChanged());
21699
+ realm.on("device-log", (persons) => this.#deviceLogGrew(persons));
20818
21700
  return realm;
20819
21701
  }
20820
21702
  #admin() {
@@ -20893,14 +21775,39 @@ class Daemon {
20893
21775
  if (!holders)
20894
21776
  this.#holders.set(p.key, holders = new Set);
20895
21777
  holders.add(ctx.id);
21778
+ if (ticket) {
21779
+ this.#ticketEnds.set(p.key, Math.max(this.#ticketEnds.get(p.key) ?? 0, ticket.exp * 1000));
21780
+ for (const id of this.#lapsed.get(p.key) ?? [])
21781
+ holders.add(id);
21782
+ this.#lapsed.delete(p.key);
21783
+ }
20896
21784
  this.#sessionsChanged();
20897
21785
  return { session: name, address: formatAddress(me.person, name, me.label) };
20898
21786
  }
20899
21787
  #bound(p, ctx) {
21788
+ const ends = this.#ticketEnds.get(p.key);
21789
+ const held = this.#holders.get(p.key)?.has(ctx.id) === true || this.#lapsed.get(p.key)?.has(ctx.id) === true;
21790
+ if (ends !== undefined && ends <= this.#now() && held) {
21791
+ throw new MessagingError("ticket_invalid", "the session ticket this session registered with has expired: register again with a fresh ticket from the host");
21792
+ }
20900
21793
  if (!this.#holders.get(p.key)?.has(ctx.id))
20901
21794
  throw new MessagingError("not_found", "this session key is not registered on this connection");
20902
21795
  return p;
20903
21796
  }
21797
+ #expireTickets(now) {
21798
+ let changed = false;
21799
+ for (const [key, ends] of this.#ticketEnds) {
21800
+ if (ends > now || !this.#holders.has(key))
21801
+ continue;
21802
+ this.#lapsed.set(key, this.#holders.get(key));
21803
+ this.#holders.delete(key);
21804
+ this.#log("info", "session.ticket.expired", {});
21805
+ if (this.#store.keepSession(key, now))
21806
+ changed = true;
21807
+ }
21808
+ if (changed)
21809
+ this.#sessionsChanged();
21810
+ }
20904
21811
  #sessionOf(key) {
20905
21812
  const session = this.#store.sessionByKey(key);
20906
21813
  if (!session)
@@ -20923,6 +21830,8 @@ class Daemon {
20923
21830
  }
20924
21831
  #unregister(p) {
20925
21832
  this.#holders.delete(p.key);
21833
+ this.#ticketEnds.delete(p.key);
21834
+ this.#lapsed.delete(p.key);
20926
21835
  if (this.#store.keepSession(p.key, this.#now()))
20927
21836
  this.#sessionsChanged();
20928
21837
  return { ok: true };
@@ -20933,10 +21842,14 @@ class Daemon {
20933
21842
  this.#chats.disconnected(ctx);
20934
21843
  this.#notices.disconnected(ctx.id);
20935
21844
  let changed = false;
21845
+ for (const [key, lapsed] of [...this.#lapsed])
21846
+ if (lapsed.delete(ctx.id) && lapsed.size === 0)
21847
+ this.#lapsed.delete(key);
20936
21848
  for (const [key, holders] of [...this.#holders]) {
20937
21849
  if (!holders.delete(ctx.id) || holders.size > 0)
20938
21850
  continue;
20939
21851
  this.#holders.delete(key);
21852
+ this.#ticketEnds.delete(key);
20940
21853
  if (this.#store.sessionByKey(key)?.agent === CLI_AGENT)
20941
21854
  continue;
20942
21855
  if (this.#store.keepSession(key, this.#now()))
@@ -20984,7 +21897,8 @@ class Daemon {
20984
21897
  to: { team: true },
20985
21898
  payload: presencePayload(final ? this.#store.sessions().map((s) => ({ ...s, unregisteredAt: s.unregisteredAt ?? this.#now() })) : this.#store.sessions(), me.label, this.#store.next("presence"), this.#transport?.advertise?.(), {
20986
21899
  wake: (s) => this.#wakes(s),
20987
- share: new Set(this.#currentConfig().presence?.share ?? [])
21900
+ share: new Set(this.#currentConfig().presence?.share ?? []),
21901
+ away: this.#away()
20988
21902
  }),
20989
21903
  now: this.#now()
20990
21904
  });
@@ -21267,8 +22181,22 @@ class Daemon {
21267
22181
  async#peopleGet(p) {
21268
22182
  const { realm, identity } = this.#joined();
21269
22183
  const profile = await realm.profile(p.person);
21270
- const devices = realm.known(p.person).devices.map((device) => ({ device: device.machine, label: device.label, self: device.machine === identity.machine }));
21271
- return { person: p.person, ...profile, devices };
22184
+ const fresh = this.#now() - (realm.kind === "directory" ? this.#tuning.presenceOnlineMs : this.#tuning.presenceMaxMs * 2);
22185
+ const away = (machine) => {
22186
+ if (machine === identity.machine)
22187
+ return this.#away();
22188
+ const row = this.#store.directoryRow(machine);
22189
+ return row !== undefined && row.receivedAt >= fresh && row.presence.away === true;
22190
+ };
22191
+ const devices = realm.known(p.person).devices.map((device) => ({
22192
+ device: device.machine,
22193
+ label: device.label,
22194
+ self: device.machine === identity.machine,
22195
+ ...away(device.machine) ? { away: true } : {}
22196
+ }));
22197
+ const own = p.person === this.#ownPerson();
22198
+ const deviceChanges = own ? [] : this.#recentDeviceChanges(realm, p.person);
22199
+ return { person: p.person, ...profile, devices, ...deviceChanges.length > 0 ? { deviceChanges } : {} };
21272
22200
  }
21273
22201
  #seal(type, toMachine, session, payload, extra = {}) {
21274
22202
  const { identity, realm, me } = this.#joined();
@@ -22143,6 +23071,7 @@ ${row.receipt ? "receipt" : "envelope"}`;
22143
23071
  return;
22144
23072
  this.#directory?.sweep();
22145
23073
  const now = this.#now();
23074
+ this.#expireTickets(now);
22146
23075
  let sessions = 0;
22147
23076
  try {
22148
23077
  this.#tx(() => {
@@ -22404,8 +23333,14 @@ ${row.receipt ? "receipt" : "envelope"}`;
22404
23333
  await transport?.stop().catch(() => {});
22405
23334
  }
22406
23335
  #deviceStatus() {
22407
- if (this.#directory)
22408
- return this.#directory.status();
23336
+ if (this.#directory) {
23337
+ const status = this.#directory.status();
23338
+ return this.#store.getKv(PREDECESSOR_KEY) !== undefined && status.binding !== "removed" ? { ...status, binding: "replace" } : status;
23339
+ }
23340
+ if (this.#config.realm) {
23341
+ const person = this.#store.directoryCache().own().person;
23342
+ return { realm: "directory", binding: "none", ...person ? { person } : {} };
23343
+ }
22409
23344
  const identity = this.#identity;
22410
23345
  const me = identity ? this.#realm?.device(identity.machine) : undefined;
22411
23346
  const binding = this.#removed ? "removed" : me ? "bound" : "none";
@@ -22416,6 +23351,86 @@ ${row.receipt ? "receipt" : "envelope"}`;
22416
23351
  const devices = realm.known(me.person).devices.map((d) => ({ device: d.machine, label: d.label, vouched: "admin", current: true, self: d.machine === identity.machine }));
22417
23352
  return { devices };
22418
23353
  }
23354
+ #away() {
23355
+ return this.#jellyHost !== undefined && !this.#attached;
23356
+ }
23357
+ #personAttached(attached) {
23358
+ if (attached !== this.#attached) {
23359
+ this.#attached = attached;
23360
+ this.#log("info", "person.attached", { attached });
23361
+ this.#presenceSoon();
23362
+ }
23363
+ return { ok: true };
23364
+ }
23365
+ #woke() {
23366
+ this.#log("info", "daemon.woke", {});
23367
+ this.#transport?.retryNow?.();
23368
+ this.sweep();
23369
+ this.#presenceSoon();
23370
+ this.#wokeHook();
23371
+ return { ok: true };
23372
+ }
23373
+ #wokeHook() {
23374
+ if (!this.#wakeArmed)
23375
+ return;
23376
+ this.#wakeDisarm().catch((error) => this.#log("warn", "wake.disarm.failed", { code: codeOf(error) }));
23377
+ }
23378
+ async#wakeArm(p) {
23379
+ const home = this.#directory;
23380
+ if (!home)
23381
+ throw new MessagingError("invalid_state", "wake tickets are for a directory realm");
23382
+ if (home.person() === undefined)
23383
+ throw new MessagingError("invalid_state", "this device is not bound yet");
23384
+ const issuer = this.#config.realm?.issuer;
23385
+ if (issuer === undefined || new URL(p.url).origin !== new URL(issuer).origin)
23386
+ throw new MessagingError("invalid_request", "the wake URL is not on the realm's Jelly ID");
23387
+ const agents = [...new Set(p.agents)];
23388
+ const awaiting = this.#store.awaitedRequests(agents, TERMINAL_STATES, WAKE_AWAITING);
23389
+ const sent = await home.wake({ url: p.url, ticket: p.ticket, agents, awaiting });
23390
+ this.#wakeArmed = true;
23391
+ this.#log("info", "wake.arm", { relay: sent === "sent" ? "armed" : "pending", agents: agents.length, awaiting: awaiting.length });
23392
+ return { relay: sent === "sent" ? "armed" : "pending", awaiting: awaiting.length };
23393
+ }
23394
+ async#wakeDisarm() {
23395
+ const home = this.#directory;
23396
+ if (!home)
23397
+ throw new MessagingError("invalid_state", "wake tickets are for a directory realm");
23398
+ const sent = await home.wake("disarm");
23399
+ this.#wakeArmed = false;
23400
+ this.#log("info", "wake.disarm", { relay: sent === "sent" ? "disarmed" : "pending" });
23401
+ return { relay: sent === "sent" ? "disarmed" : "pending" };
23402
+ }
23403
+ async#answerPreview(p) {
23404
+ const { realm, me } = this.#joined();
23405
+ const now = this.#now();
23406
+ const profile = async (person) => {
23407
+ const found = await realm.profile(person).catch(() => {
23408
+ return;
23409
+ });
23410
+ return found ? { name: found.name.slice(0, 200), ...found.email !== undefined ? { email: found.email.slice(0, 320) } : {}, org: found.org } : { name: String(person).slice(0, 200), org: "none" };
23411
+ };
23412
+ const answer = p.answer;
23413
+ const ask = this.#store.held(p.id);
23414
+ if (ask && ask.exp > now) {
23415
+ const kind = ask.payload.kind;
23416
+ const count = p.all ? this.#store.helds(ask.person).filter((row) => row.exp > now).length : 1;
23417
+ const from = await profile(ask.person);
23418
+ const agent = ask.envelope.from.agent ?? "";
23419
+ return { kind, from, ...agent !== "" ? { agent } : {}, to: ask.session, count, sentence: answerSentence({ kind, from, agent, to: ask.session, answer, count }) };
23420
+ }
23421
+ const request = this.#store.contactRequest(p.id);
23422
+ if (request && request.exp > now) {
23423
+ const count = p.all ? this.#store.contactRequests(request.person).filter((row) => row.exp > now).length : 1;
23424
+ const from = await profile(request.person);
23425
+ return { kind: "contact", from, count, sentence: answerSentence({ kind: "contact", from, answer, count }) };
23426
+ }
23427
+ const suggestion = this.#store.contactSuggestion(p.id);
23428
+ if (suggestion) {
23429
+ const from = await profile(me.person);
23430
+ return { kind: "suggestion", from, agent: suggestion.session, count: 1, sentence: answerSentence({ kind: "suggestion", from, agent: suggestion.session, email: suggestion.email, answer, count: 1 }) };
23431
+ }
23432
+ throw new MessagingError("not_found", "no request with that id is held here");
23433
+ }
22419
23434
  #me() {
22420
23435
  return (this.#realm ?? this.#directory?.realm)?.self()?.person;
22421
23436
  }
@@ -22462,6 +23477,232 @@ ${row.receipt ? "receipt" : "envelope"}`;
22462
23477
  const { rows, more } = this.#store.conversation(session.key, p.conversation.slice("agent:".length), { before: p.before }, p.limit ?? 30);
22463
23478
  return { kind: "entries", conversation: p.conversation, entries: rows.map(convEntry), more };
22464
23479
  }
23480
+ #heldRealm(config) {
23481
+ if (this.#directory)
23482
+ return this.#directory.realm;
23483
+ const throwaway = generateIdentity();
23484
+ return new DirectoryIdentity({ config, identity: () => throwaway, link: () => {
23485
+ return;
23486
+ }, cache: memoryDirectoryCache(this.#store.directoryCache()), jelly: this.#jelly ?? jellyFromIssuer(config.issuer), now: this.#now });
23487
+ }
23488
+ #predecessor() {
23489
+ return this.#store.getKv(PREDECESSOR_KEY) ?? this.#store.getKv(MACHINE_KEY) ?? this.#identity?.machine;
23490
+ }
23491
+ async#replacePreview(p) {
23492
+ const config = this.#config.realm;
23493
+ if (!config)
23494
+ throw new MessagingError("invalid_state", "identity.replace is for a directory realm; a roster realm replaces a machine with team.remove and a new invite");
23495
+ const person = this.#store.directoryCache().own().person;
23496
+ if (!person)
23497
+ throw new MessagingError("no_identity", "this home was never bound to an account: bind it instead (device.bind or messaging login)");
23498
+ const realm = this.#heldRealm(config);
23499
+ const pred = this.#predecessor();
23500
+ const record = realm.ownDevices().find((d) => d.device === pred);
23501
+ if (!record)
23502
+ throw new MessagingError("not_found", "this home's device is not in its account's log");
23503
+ const pending = this.#store.getKv(PREDECESSOR_KEY) !== undefined && this.#identity !== undefined && this.#identity.machine !== pred;
23504
+ const bound = pending && realm.binding().state !== "none";
23505
+ const signIns = !pending ? 2 : !bound ? 2 : record.current ? 1 : 0;
23506
+ const alarm = this.#keysMissing ? "keys_missing" : realm.alarm()?.kind === "restored" ? "restored" : null;
23507
+ const label = record.label;
23508
+ const why = alarm === "keys_missing" ? " (its key files are missing)" : alarm === "restored" ? " (it runs from an older copy of its home)" : "";
23509
+ const sentence = `This device gets a new key and replaces '${label}'${why} on your account. Your conversations, grants and history stay here, and tasks continue; ` + `mail still queued at the relay for '${label}' is lost.` + (signIns === 2 ? " Through the browser you approve two sign-ins at Jelly ID: one binds the new key, one removes the old." : signIns === 1 ? ` You approve one sign-in at Jelly ID, to remove '${label}'.` : "");
23510
+ return {
23511
+ predecessor: {
23512
+ device: pred,
23513
+ label,
23514
+ kind: record.kind,
23515
+ vouch: record.vouch,
23516
+ ...record.vouch === "jelly" ? { via: record.host !== undefined ? "host" : "browser" } : {},
23517
+ since: record.since,
23518
+ current: record.current
23519
+ },
23520
+ alarm,
23521
+ ...pending ? { device: this.#identity.machine } : {},
23522
+ signIns,
23523
+ sentence: sentence.slice(0, 600)
23524
+ };
23525
+ }
23526
+ async#replace(p) {
23527
+ const config = this.#config.realm;
23528
+ if (!config)
23529
+ throw new MessagingError("invalid_state", "identity.replace is for a directory realm; a roster realm replaces a machine with team.remove and a new invite");
23530
+ if (this.#removed || this.#directory?.realm.removed())
23531
+ throw new MessagingError("invalid_state", "this device was removed from its account; it cannot replace anything");
23532
+ const person = this.#store.directoryCache().own().person;
23533
+ if (!person)
23534
+ throw new MessagingError("no_identity", "this home was never bound to an account: bind it instead (device.bind or messaging login)");
23535
+ const pred = this.#predecessor();
23536
+ if (!pred)
23537
+ throw new MessagingError("no_identity", "this home names no device to replace");
23538
+ const record = this.#heldRealm(config).ownDevices().find((d) => d.device === pred);
23539
+ if (!record)
23540
+ throw new MessagingError("not_found", "this home's device is not in its account's log");
23541
+ if (this.#store.getKv(PREDECESSOR_KEY) === undefined || this.#identity === undefined || this.#identity.machine === pred) {
23542
+ if (!record.current)
23543
+ throw new MessagingError("invalid_state", "this home's device was removed from its account; it cannot be replaced");
23544
+ await this.#newKeys(pred, config);
23545
+ }
23546
+ return this.#replaceNext(p);
23547
+ }
23548
+ async#replaceNext(p) {
23549
+ const home = this.#directory;
23550
+ const me = this.#identity.machine;
23551
+ if (p.token !== undefined) {
23552
+ const done = await this.#replaceWith(p.token, p.label);
23553
+ if (this.#store.getKv(PREDECESSOR_KEY) === undefined)
23554
+ return { state: "done", person: done.person, device: me, predecessor: done.predecessor, seq: done.seq };
23555
+ } else if (this.#replaceFinished()) {
23556
+ return this.#replaceDone();
23557
+ }
23558
+ if (this.#store.getKv(PREDECESSOR_KEY) === undefined)
23559
+ return this.#replaceDone();
23560
+ if (this.#jellyHost || p.token !== undefined)
23561
+ return { state: "token", device: me };
23562
+ const pred = this.#store.getKv(PREDECESSOR_KEY);
23563
+ const record = home.realm.ownDevices().find((d) => d.device === pred);
23564
+ const label = p.label ?? this.#store.getKv(REPLACE_LABEL_KEY) ?? record?.label ?? "device";
23565
+ const kind = record && record.kind !== "jelly-host" ? record.kind : "laptop";
23566
+ const started = await home.signIn("replace", { label, kind });
23567
+ return { state: "sign-in", device: me, ...started };
23568
+ }
23569
+ #replaceFinished() {
23570
+ const pred = this.#store.getKv(PREDECESSOR_KEY);
23571
+ if (pred === undefined)
23572
+ return true;
23573
+ const realm = this.#directory?.realm;
23574
+ if (!realm || realm.binding().state === "none")
23575
+ return false;
23576
+ return realm.ownDevices().find((d) => d.device === pred)?.current !== true;
23577
+ }
23578
+ #replaceDone() {
23579
+ const realm = this.#directory.realm;
23580
+ const pred = this.#store.getKv(PREDECESSOR_KEY) ?? this.#store.getKv(REPLACED_KEY);
23581
+ this.#store.deleteKv(PREDECESSOR_KEY);
23582
+ const me = this.#identity.machine;
23583
+ const binding = realm.binding();
23584
+ const record = pred !== undefined ? realm.ownDevices().find((d) => d.device === pred) : undefined;
23585
+ return { state: "done", person: binding.person, device: me, predecessor: pred ?? me, seq: record?.unbound ? realm.deviceChanges(binding.person).find((c) => c.device === pred && c.change === "replaced")?.seq ?? 0 : 0 };
23586
+ }
23587
+ async#replaceWith(token, label) {
23588
+ const home = this.#directory;
23589
+ const pred = this.#store.getKv(PREDECESSOR_KEY);
23590
+ if (!home || pred === undefined)
23591
+ throw new MessagingError("invalid_state", "no replacement is pending: start one with identity.replace");
23592
+ const realm = home.realm;
23593
+ const me = this.#identity.machine;
23594
+ const record = realm.ownDevices().find((d) => d.device === pred);
23595
+ let seq = 0;
23596
+ let spent = false;
23597
+ if (realm.binding().state === "none") {
23598
+ const kind = this.#jellyHost?.kind ?? (record && record.kind !== "jelly-host" ? record.kind : "laptop");
23599
+ const done = await home.bind({ token, label: this.#jellyHost?.label ?? label ?? this.#store.getKv(REPLACE_LABEL_KEY) ?? record?.label ?? "device", kind });
23600
+ seq = done.seq;
23601
+ spent = signInToken(token);
23602
+ this.#log("info", "identity.replace.bound", { seq });
23603
+ }
23604
+ await realm.awaitSelf();
23605
+ this.#tx(() => this.#grants.resignFrom(pred));
23606
+ let unbound = false;
23607
+ if (!spent && realm.ownDevices().find((d) => d.device === pred)?.current) {
23608
+ const done = await realm.unbind(pred, { reason: "replaced", token });
23609
+ seq = done.seq;
23610
+ unbound = true;
23611
+ this.#log("info", "identity.replace.done", { seq });
23612
+ }
23613
+ if (unbound || this.#replaceFinished()) {
23614
+ this.#store.setKv(REPLACED_KEY, pred);
23615
+ this.#store.deleteKv(PREDECESSOR_KEY);
23616
+ this.#store.deleteKv(REPLACE_LABEL_KEY);
23617
+ this.#emit({ type: "contacts" });
23618
+ }
23619
+ return { person: realm.binding().person, device: me, predecessor: pred, seq };
23620
+ }
23621
+ async#newKeys(pred, config) {
23622
+ const label = this.#heldRealm(config).ownDevices().find((d) => d.device === pred)?.label;
23623
+ this.#store.setKv(PREDECESSOR_KEY, pred);
23624
+ if (label !== undefined)
23625
+ this.#store.setKv(REPLACE_LABEL_KEY, label);
23626
+ const keys = { identity: generateIdentity(), retained: [] };
23627
+ let identityStore = this.#keyring.store;
23628
+ if (identityStore)
23629
+ identityStore.save(keys);
23630
+ else
23631
+ identityStore = saveNewIdentity(this.paths.identity, keys, identitySettings(this.#config), this.#storeDeps());
23632
+ this.#store.setKv(MACHINE_KEY, keys.identity.machine);
23633
+ this.#log("info", "identity.replace.keys", { store: identityStore.kind });
23634
+ const old = this.#directory;
23635
+ this.#directory = undefined;
23636
+ this.#realm = undefined;
23637
+ this.#transport = undefined;
23638
+ if (old) {
23639
+ await old.stop();
23640
+ this.#onHostedState(false);
23641
+ }
23642
+ const cache = this.#store.directoryCache();
23643
+ if (cache.alarm()?.kind === "restored")
23644
+ cache.setAlarm(undefined);
23645
+ for (const row of this.#store.outbox())
23646
+ if (row.env.from.device === pred)
23647
+ this.#store.deleteOutbox(row.id);
23648
+ this.#keysMissing = false;
23649
+ this.#keyring.adopt(keys, identityStore);
23650
+ this.#openDirectory(config, keys, identityStore);
23651
+ }
23652
+ #erasePreview(_p) {
23653
+ const home = this.#homeDirectory("directory.erase.preview");
23654
+ const binding = home.realm.binding();
23655
+ if (!binding.person || binding.state === "none" || binding.state === "removed")
23656
+ throw new MessagingError("no_identity", "this device is not bound to an account");
23657
+ const devices = home.realm.ownDevices().filter((d) => d.current);
23658
+ const me = this.#identity.machine;
23659
+ return {
23660
+ person: binding.person,
23661
+ devices: devices.map((d) => ({ device: d.device, label: d.label, kind: d.kind, self: d.device === me })),
23662
+ signIns: this.#jellyHost || binding.record?.host !== undefined ? 0 : 1,
23663
+ perDay: ERASURES_PER_DAY,
23664
+ lost: [
23665
+ `Every device of your account (${devices.length}, this one included) is removed from Messaging, and none can be bound again.`,
23666
+ "Your Jelly account can never be bound to Messaging again: the erasure is for good, and a new sign-in is refused.",
23667
+ "The directory deletes your name, email and handle, and every device record and key statement it held for you.",
23668
+ "Messages still queued at the relay for your devices are deleted.",
23669
+ "Your contacts see a deleted account, and drop the grants and cards they hold for you."
23670
+ ],
23671
+ kept: ["Each device keeps its own history (conversations, transcripts, grants) on its disk until you delete its home.", "Your Jelly account itself is not deleted: only its Messaging account is."]
23672
+ };
23673
+ }
23674
+ async#erase(p) {
23675
+ const home = this.#homeDirectory("directory.erase");
23676
+ const binding = home.realm.binding();
23677
+ if (!binding.person || binding.state === "none" || binding.state === "removed")
23678
+ throw new MessagingError("no_identity", "this device is not bound to an account");
23679
+ if (binding.record?.vouch !== "jelly")
23680
+ throw new MessagingError("forbidden", "a device approved by link erases nothing; erase from a device Jelly ID vouched for");
23681
+ if (p.token !== undefined) {
23682
+ const done = await this.#eraseWith(p.token);
23683
+ return { state: "done", person: done.person, devices: done.devices };
23684
+ }
23685
+ if (this.#jellyHost)
23686
+ throw new MessagingError("invalid_request", "under a Jelly host, directory.erase needs a fresh token for this device from its host (token)");
23687
+ const record = binding.record;
23688
+ const started = await home.signIn("erase", { label: record.label, kind: record.kind !== "jelly-host" ? record.kind : "laptop" });
23689
+ return { state: "sign-in", ...started };
23690
+ }
23691
+ async#eraseWith(token) {
23692
+ const home = this.#homeDirectory("directory.erase");
23693
+ let done;
23694
+ try {
23695
+ done = await home.realm.erase(token);
23696
+ } catch (error) {
23697
+ if (!(error instanceof MessagingError) || error.code !== "rate_limited")
23698
+ throw error;
23699
+ const wait = error.retryAfterMs;
23700
+ const when = typeof wait === "number" && wait > 0 ? `; try again in about ${Math.max(1, Math.ceil(wait / 60000))} min` : "; try again tomorrow";
23701
+ throw new MessagingError("rate_limited", `the directory allows ${ERASURES_PER_DAY} erasures per person a day${when}`);
23702
+ }
23703
+ this.#log("warn", "directory.erased", { devices: done.devices });
23704
+ return done;
23705
+ }
22465
23706
  #removePreview(p) {
22466
23707
  const entry = this.#homeDirectory("device.remove.preview").list().devices.find((d) => d.device === p.device);
22467
23708
  if (!entry)
@@ -22475,6 +23716,93 @@ ${row.receipt ? "receipt" : "envelope"}`;
22475
23716
  ...entry.since !== undefined ? { since: entry.since } : {}
22476
23717
  };
22477
23718
  }
23719
+ #deviceLogGrew(persons) {
23720
+ try {
23721
+ this.#deviceNotices();
23722
+ } catch (error) {
23723
+ this.#log("error", "notice.device.failed", { code: codeOf(error) });
23724
+ }
23725
+ if (persons.some((person) => person !== this.#me()))
23726
+ this.#emit({ type: "contacts" });
23727
+ }
23728
+ #ownPerson() {
23729
+ return this.#directory?.person() ?? this.#me();
23730
+ }
23731
+ #deviceNotices(start = false) {
23732
+ const realm = this.#realm ?? this.#directory?.realm;
23733
+ const person = this.#ownPerson();
23734
+ const me = this.#identity?.machine;
23735
+ if (!realm || !person || !me)
23736
+ return;
23737
+ const changes = realm.deviceChanges(person);
23738
+ const head = changes.at(-1)?.seq ?? -1;
23739
+ let last = this.#store.getKv(DEVICE_NOTICES_KEY);
23740
+ if (last === undefined) {
23741
+ const admitted = changes.find((change) => change.device === me && change.change === "added");
23742
+ if (start || admitted === undefined) {
23743
+ this.#store.setKv(DEVICE_NOTICES_KEY, String(head));
23744
+ return;
23745
+ }
23746
+ last = String(admitted.seq);
23747
+ }
23748
+ const fresh = changes.filter((change) => change.seq > Number(last));
23749
+ if (fresh.length === 0)
23750
+ return;
23751
+ const replacers = new Set(fresh.filter((change) => change.change === "replaced").map((change) => change.by));
23752
+ for (const change of fresh) {
23753
+ if (change.device === me || change.by === me)
23754
+ continue;
23755
+ if (change.change === "added" && replacers.has(change.device))
23756
+ continue;
23757
+ this.#notices.device(change.seq, change.at);
23758
+ }
23759
+ this.#store.setKv(DEVICE_NOTICES_KEY, String(Math.max(head, Number(last))));
23760
+ }
23761
+ #noticeDevice(seq) {
23762
+ const realm = this.#realm ?? this.#directory?.realm;
23763
+ const person = this.#ownPerson();
23764
+ if (!realm || !person)
23765
+ return;
23766
+ const changes = realm.deviceChanges(person);
23767
+ const change = changes.find((c) => c.seq === seq);
23768
+ if (!change)
23769
+ return;
23770
+ const labelOf = (device) => changes.filter((c) => c.device === device).at(-1)?.label;
23771
+ const gone = (device) => changes.some((c) => c.device === device && (c.change === "removed" || c.change === "replaced"));
23772
+ const subject = change.change === "replaced" ? change.by : change.device;
23773
+ const latest = change.change === "replaced" ? changes.filter((c) => c.device === subject && c.change !== "removed" && c.change !== "replaced").at(-1) : change;
23774
+ const label = latest?.label ?? labelOf(subject) ?? "device";
23775
+ const current = !gone(subject);
23776
+ const view = {
23777
+ change: change.change,
23778
+ device: subject,
23779
+ label: MachineLabel.safeParse(label).success ? label : "device",
23780
+ ...latest?.kind !== undefined ? { kind: latest.kind } : {},
23781
+ vouch: ((v) => v === "service" ? "device" : v)(latest?.vouch ?? change.vouch),
23782
+ ...(latest?.via ?? change.via) !== undefined ? { via: latest?.via ?? change.via } : {},
23783
+ ...change.approver !== undefined && labelOf(change.approver) !== undefined ? { approver: labelOf(change.approver) } : {},
23784
+ ...change.change === "replaced" ? { replaces: { device: change.device, label: change.label } } : {},
23785
+ current,
23786
+ at: change.at
23787
+ };
23788
+ const remove = current ? this.#removeWith() : undefined;
23789
+ if (remove)
23790
+ view.remove = remove;
23791
+ return view;
23792
+ }
23793
+ #removeWith() {
23794
+ const home = this.#directory;
23795
+ if (!home)
23796
+ return this.#roster?.isAdmin(this.#identity?.machine ?? "") ? "team" : undefined;
23797
+ const self = home.list().devices.find((device) => device.self && device.current);
23798
+ if (!self || self.vouched !== "jelly")
23799
+ return;
23800
+ return self.via === "browser" ? "sign-in" : "token";
23801
+ }
23802
+ #trustNotices() {
23803
+ for (const grant of this.#grants.expiringTrust(TRUST_NOTICE_MS))
23804
+ this.#notices.trust(grant.id, grant.person, grant.until, this.#now());
23805
+ }
22478
23806
  #alerts() {
22479
23807
  const realmAlarm = this.#directory?.status().alarm;
22480
23808
  const roster = this.#keysMissing ? "keys_missing" : this.#keyring.restored() ? "restored" : undefined;
@@ -22492,6 +23820,16 @@ ${row.receipt ? "receipt" : "envelope"}`;
22492
23820
  }
22493
23821
  if (realmAlarm)
22494
23822
  this.#notices.alert(realmAlarm.kind, realmAlarm.at);
23823
+ this.#deviceNotices();
23824
+ if (this.#joinedOrUndefined())
23825
+ this.#trustNotices();
23826
+ }
23827
+ #joinedOrUndefined() {
23828
+ try {
23829
+ return this.#joined();
23830
+ } catch {
23831
+ return;
23832
+ }
22495
23833
  }
22496
23834
  #homeDirectory(method) {
22497
23835
  if (this.#directory)
@@ -22834,6 +24172,22 @@ function jellyConfig(path, jelly) {
22834
24172
  }
22835
24173
  return;
22836
24174
  }
24175
+ if (jelly.realm !== undefined) {
24176
+ if (config.realm === undefined) {
24177
+ if (config.relay !== undefined || config.person !== undefined) {
24178
+ 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`);
24179
+ }
24180
+ if (config.ipc?.agentGroup !== undefined)
24181
+ throw new MessagingError("invalid_request", "deployment jelly shares its sockets with --socket-group, not ipc.agentGroup");
24182
+ writeConfig(path, { ...config, realm: jelly.realm, deployment: "jelly" });
24183
+ return;
24184
+ }
24185
+ const differs = realmMismatch(config.realm, jelly.realm);
24186
+ if (differs.length > 0) {
24187
+ const which = differs.map((member) => `${member} ${JSON.stringify(config.realm[member])} (the environment: ${JSON.stringify(jelly.realm[member])})`).join(", ");
24188
+ 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`);
24189
+ }
24190
+ }
22837
24191
  if (config.realm === undefined)
22838
24192
  throw new MessagingError("invalid_request", "deployment jelly needs a directory realm: config.json has no realm (a Jelly host supervises no roster realm)");
22839
24193
  if (config.ipc?.agentGroup !== undefined)
@@ -22844,7 +24198,15 @@ function jellyConfig(path, jelly) {
22844
24198
  function isLineTarget(to) {
22845
24199
  return to.startsWith("#") || !to.includes("/");
22846
24200
  }
22847
- var DAY3 = 86400000, KEYCHAIN_LEFTOVERS_KEY = "keychain.stale", DEFAULT_TUNING, KEPT_MS, INPUT_TIMEOUT_MS, TASK_BYTES = 1048576, MACHINE_KEY = "identity.machine", DEFAULT_INVITE_TTL_MS, LIST_BYTES = 1572864;
24201
+ function signInToken(token) {
24202
+ try {
24203
+ const claims = parseWire(Buffer.from(token.split(".")[1] ?? "", "base64url").toString("utf8"));
24204
+ return typeof claims === "object" && claims !== null && claims.src === "device";
24205
+ } catch {
24206
+ return false;
24207
+ }
24208
+ }
24209
+ var DAY3 = 86400000, KEYCHAIN_LEFTOVERS_KEY = "keychain.stale", DEFAULT_TUNING, KEPT_MS, INPUT_TIMEOUT_MS, TASK_BYTES = 1048576, PREDECESSOR_KEY = "identity.predecessor", REPLACE_LABEL_KEY = "identity.replace.label", REPLACED_KEY = "identity.replaced", ERASURES_PER_DAY = 10, DEVICE_NOTICES_KEY = "notice.devices.seq", TRUST_NOTICE_MS, DEVICES_CHANGED_MS, MACHINE_KEY = "identity.machine", DEFAULT_INVITE_TTL_MS, LIST_BYTES = 1572864;
22848
24210
  var init_daemon = __esm(() => {
22849
24211
  init_dist();
22850
24212
  init_dist2();
@@ -22861,7 +24223,11 @@ var init_daemon = __esm(() => {
22861
24223
  init_policy();
22862
24224
  init_roster_identity();
22863
24225
  init_directory_home();
24226
+ init_directory_identity();
22864
24227
  init_jelly3();
24228
+ init_jelly_env();
24229
+ init_answer_preview();
24230
+ init_snapshot();
22865
24231
  init_notices();
22866
24232
  init_resolve();
22867
24233
  init_secrets();
@@ -22887,6 +24253,8 @@ var init_daemon = __esm(() => {
22887
24253
  };
22888
24254
  KEPT_MS = DAY3;
22889
24255
  INPUT_TIMEOUT_MS = DAY3;
24256
+ TRUST_NOTICE_MS = 7 * DAY3;
24257
+ DEVICES_CHANGED_MS = 7 * DAY3;
22890
24258
  DEFAULT_INVITE_TTL_MS = DAY3;
22891
24259
  });
22892
24260
 
@@ -23483,6 +24851,9 @@ class DirectTransport {
23483
24851
  presence(env) {
23484
24852
  this.#relay.presence(env);
23485
24853
  }
24854
+ retryNow() {
24855
+ this.#relay.retryNow?.();
24856
+ }
23486
24857
  appendRoster(entry) {
23487
24858
  return this.#relay.appendRoster(entry);
23488
24859
  }
@@ -23584,6 +24955,11 @@ var init_pacing = __esm(() => {
23584
24955
  PAIR_WINDOW_MS = 30 * DAY_MS5;
23585
24956
  });
23586
24957
 
24958
+ // packages/relay/dist/directory/wakes.js
24959
+ var init_wakes = __esm(() => {
24960
+ init_dist();
24961
+ });
24962
+
23587
24963
  // packages/relay/dist/directory/module.js
23588
24964
  var DAY4, DIRECTORY_FRAMES;
23589
24965
  var init_module = __esm(() => {
@@ -23592,9 +24968,30 @@ var init_module = __esm(() => {
23592
24968
  init_feed();
23593
24969
  init_jelly4();
23594
24970
  init_pacing();
24971
+ init_wakes();
23595
24972
  DAY4 = 24 * 60;
23596
- DIRECTORY_FRAMES = new Set(["dir.bind", "dir.append", "dir.keys", "dir.sth", "dir.lookup", "dir.device", "dir.resolve", "dir.search", "dir.watch", "presence.watch", "dir.erase"]);
24973
+ DIRECTORY_FRAMES = new Set([
24974
+ "dir.bind",
24975
+ "dir.append",
24976
+ "dir.keys",
24977
+ "dir.sth",
24978
+ "dir.lookup",
24979
+ "dir.device",
24980
+ "dir.resolve",
24981
+ "dir.search",
24982
+ "dir.watch",
24983
+ "presence.watch",
24984
+ "dir.erase",
24985
+ "wake.arm",
24986
+ "wake.disarm"
24987
+ ]);
23597
24988
  });
24989
+
24990
+ // packages/relay/dist/directory/store.js
24991
+ var init_store4 = __esm(() => {
24992
+ init_dist();
24993
+ });
24994
+
23598
24995
  // packages/relay/dist/server.js
23599
24996
  var HIGH_WATER, MAX_BUFFERED;
23600
24997
  var init_server2 = __esm(() => {
@@ -23605,6 +25002,7 @@ var init_server2 = __esm(() => {
23605
25002
  init_pages();
23606
25003
  init_store3();
23607
25004
  init_module();
25005
+ init_store4();
23608
25006
  HIGH_WATER = 1024 * 1024;
23609
25007
  MAX_BUFFERED = 16 * 1024 * 1024;
23610
25008
  });
@@ -23657,6 +25055,14 @@ class RelayClient {
23657
25055
  if (!this.#ws && !this.#retryTimer)
23658
25056
  this.#open();
23659
25057
  }
25058
+ retryNow() {
25059
+ if (!this.#running || this.#ws || !this.#retryTimer)
25060
+ return;
25061
+ clearTimeout(this.#retryTimer);
25062
+ this.#retryTimer = undefined;
25063
+ this.#attempt = 0;
25064
+ this.#open();
25065
+ }
23660
25066
  async stop() {
23661
25067
  this.#running = false;
23662
25068
  clearTimeout(this.#retryTimer);
@@ -23733,6 +25139,12 @@ class RelayClient {
23733
25139
  presenceWatch(persons) {
23734
25140
  return this.#dir("dir.ok", { t: "presence.watch", persons });
23735
25141
  }
25142
+ wakeArm(arm) {
25143
+ return this.#dir("dir.ok", { t: "wake.arm", url: arm.url, ticket: arm.ticket, agents: arm.agents, awaiting: arm.awaiting });
25144
+ }
25145
+ wakeDisarm() {
25146
+ return this.#dir("dir.ok", { t: "wake.disarm" });
25147
+ }
23736
25148
  dirErase(entries, token) {
23737
25149
  return this.#dir("dir.ok", { t: "dir.erase", entries, token });
23738
25150
  }
@@ -24310,6 +25722,7 @@ class RelayDirectoryLink {
24310
25722
  #running = false;
24311
25723
  #watched = [];
24312
25724
  #presence = [];
25725
+ #wake;
24313
25726
  #opening = new Set;
24314
25727
  constructor(options) {
24315
25728
  this.#options = options;
@@ -24351,6 +25764,8 @@ class RelayDirectoryLink {
24351
25764
  client.dirWatch(this.#watched).catch(() => {});
24352
25765
  if (this.#presence.length > 0)
24353
25766
  client.presenceWatch(this.#presence).catch(() => {});
25767
+ if (this.#wake !== undefined)
25768
+ this.#sendWake(client, this.#wake).catch(() => {});
24354
25769
  const sth = "sth" in state ? STH.safeParse(state.sth) : undefined;
24355
25770
  pushes.state(true, sth?.success ? sth.data : undefined);
24356
25771
  },
@@ -24403,6 +25818,9 @@ class RelayDirectoryLink {
24403
25818
  throw shape();
24404
25819
  return { seq: reply.seq, sth: reply.sth };
24405
25820
  }
25821
+ async erase(entries, token) {
25822
+ parse5(DirOkFrame, await link((await this.#authed()).dirErase(entries, token)));
25823
+ }
24406
25824
  async keys(statement) {
24407
25825
  const reply = parse5(DirOkFrame, await link((await this.#authed()).dirKeys(statement)));
24408
25826
  if (reply.sth === undefined)
@@ -24458,6 +25876,34 @@ class RelayDirectoryLink {
24458
25876
  presence(env) {
24459
25877
  this.#client?.presence(env);
24460
25878
  }
25879
+ retryNow() {
25880
+ this.#client?.retryNow();
25881
+ }
25882
+ async wake(state) {
25883
+ this.#wake = state;
25884
+ const client = this.#connect();
25885
+ if (!client?.connected)
25886
+ return "pending";
25887
+ try {
25888
+ await this.#sendWake(client, state);
25889
+ return "sent";
25890
+ } catch (error) {
25891
+ if (isMessagingError(error) && error.code === "relay_unavailable")
25892
+ return "pending";
25893
+ throw error;
25894
+ }
25895
+ }
25896
+ async#sendWake(client, state) {
25897
+ try {
25898
+ parse5(DirOkFrame, await (state === "disarm" ? client.wakeDisarm() : client.wakeArm(state)));
25899
+ } catch (error) {
25900
+ if (!(isMessagingError(error) && error.code === "relay_unavailable") && this.#wake === state)
25901
+ this.#wake = undefined;
25902
+ throw error;
25903
+ }
25904
+ if (this.#wake === state)
25905
+ this.#wake = undefined;
25906
+ }
24461
25907
  async presenceWatch(persons) {
24462
25908
  this.#presence = [...persons];
24463
25909
  const client = this.#connect();
@@ -24516,6 +25962,7 @@ var init_dist4 = __esm(() => {
24516
25962
  init_log();
24517
25963
  init_version();
24518
25964
  init_jelly3();
25965
+ init_jelly_env();
24519
25966
  init_store2();
24520
25967
  init_dist();
24521
25968
  });
@@ -25756,6 +27203,12 @@ var people = {
25756
27203
  }
25757
27204
  ctx.out.line(`${ctx.out.style.bold(inline(person.name, 64))} handle ${inline(person.handle, 64)} org ${person.org} vouched ${person.vouched}`);
25758
27205
  ctx.out.lines(table(["DEVICE", "LABEL", ""], person.devices.map((d) => [d.device, inline(d.label, 32), d.self ? "this device" : ""]), ctx.out.style));
27206
+ if (person.deviceChanges !== undefined) {
27207
+ const now = ctx.now();
27208
+ ctx.out.line(`${ctx.out.style.yellow(`${inline(person.name, 64)}'s devices changed:`)}`);
27209
+ for (const change of person.deviceChanges)
27210
+ ctx.out.line(` ${change.change} '${inline(change.label, 32)}' (${vouchWords(change)}) ${ago(change.at, now)}`);
27211
+ }
25759
27212
  return;
25760
27213
  }
25761
27214
  if (ctx.json) {
@@ -25872,8 +27325,9 @@ var notices = {
25872
27325
  usage: "[--lines] [--ack]",
25873
27326
  details: [
25874
27327
  "Each line is a message for you (a DM or channel line from someone else, or your own agent's line to you),",
25875
- "a request held for you to answer (`messaging requests`), or an alert about this device. --lines adds a",
25876
- "message's first line (peer text). --ack marks every notice shown as seen."
27328
+ "a request held for you to answer (`messaging requests`), or an alert: about this device, a device added to or",
27329
+ "removed from your account (with how to remove one that is not yours), or trust you gave that ends within 7 days.",
27330
+ "--lines adds a message's first line (peer text). --ack marks every notice shown as seen."
25877
27331
  ].join(`
25878
27332
  `),
25879
27333
  options: {
@@ -25900,13 +27354,25 @@ var notices = {
25900
27354
  ago(n.at, now),
25901
27355
  n.category,
25902
27356
  n.from ? inline(n.from.name, 40) : "",
25903
- n.conversation !== undefined ? inline(n.conversation, 60) : "",
27357
+ n.conversation !== undefined ? inline(n.conversation, 60) : n.device !== undefined ? `device ${n.device.device}` : "",
25904
27358
  n.line !== undefined ? inline(n.line, 120) : ""
25905
27359
  ]), ctx.out.style));
27360
+ const removable = list.flatMap((n) => n.device?.remove !== undefined && n.device.current ? [n.device] : []);
27361
+ for (const d of removable) {
27362
+ const how = d.remove === "sign-in" ? `messaging login --remove ${d.device}` : d.remove === "team" ? `messaging team remove ${d.device}` : "Remove it from the Jelly app on this device";
27363
+ ctx.out.note(`Not yours? '${inline(d.label, 32)}': ${how}`);
27364
+ }
25906
27365
  if (values.ack === true)
25907
27366
  ctx.out.line(ctx.out.style.dim(`${list.length} marked seen`));
25908
27367
  }
25909
27368
  };
27369
+ function vouchWords(d) {
27370
+ if (d.vouch === "admin")
27371
+ return "admitted by the team's admin";
27372
+ if (d.vouch === "device")
27373
+ return d.approver !== undefined ? `approved on '${inline(d.approver, 32)}'` : "approved by link";
27374
+ return d.via === "browser" ? "signed in through the browser" : "vouched by Jelly ID";
27375
+ }
25910
27376
  var DIRECTORY_COMMANDS = [status, who2, people, devices, notices];
25911
27377
 
25912
27378
  // packages/daemon/dist/cli/commands/identity.js
@@ -26176,6 +27642,55 @@ function aborted2(signal) {
26176
27642
  signal.addEventListener("abort", () => resolve(), { once: true });
26177
27643
  });
26178
27644
  }
27645
+ async function showSignIn(ctx, started, open) {
27646
+ if (!ctx.json) {
27647
+ const { bold } = ctx.out.style;
27648
+ const lead = started.purpose === "remove" ? "To remove that device, sign in at Jelly ID:" : started.purpose === "replace" ? "To replace your old device, sign in at Jelly ID with this one:" : started.purpose === "erase" ? "To erase your account, sign in at Jelly ID:" : "To sign this device in, open:";
27649
+ ctx.out.line(lead);
27650
+ ctx.out.line(` ${started.verificationUriComplete}`);
27651
+ ctx.out.line(`and check that the page shows the code ${bold(started.userCode)} and the key ${bold(started.fingerprint)}.`);
27652
+ ctx.out.line(`(Or open ${started.verificationUri} and type the code.)`);
27653
+ }
27654
+ if (open) {
27655
+ const opened = await (ctx.io.openUrl ?? openInBrowser)(started.verificationUriComplete);
27656
+ if (!opened)
27657
+ ctx.out.note("Could not open a browser here; open the link on any device.");
27658
+ } else if (!ctx.json && (ctx.io.env.SSH_CONNECTION !== undefined || ctx.io.env.SSH_TTY !== undefined)) {
27659
+ ctx.out.note("Over SSH: open the link on your laptop or phone; the code ties it to this machine.");
27660
+ }
27661
+ }
27662
+ async function waitSignIn(ctx, client, started) {
27663
+ const minutes = Math.max(1, Math.round((started.expires - ctx.now()) / 60000));
27664
+ if (!ctx.json)
27665
+ ctx.out.note(`Waiting for your approval (the code expires in ${minutes} min; Ctrl-C cancels)...`);
27666
+ const interrupted = aborted2(ctx.io.signal);
27667
+ let state = { ...started, state: "pending" };
27668
+ while (state.state === "pending") {
27669
+ const asked = client.call("device.login.status", { waitMs: STATUS_WAIT_MS }, { timeoutMs: STATUS_WAIT_MS + 15000 });
27670
+ const next = await Promise.race([asked, interrupted.then(() => {
27671
+ return;
27672
+ })]);
27673
+ if (next === undefined) {
27674
+ asked.catch(() => {});
27675
+ await client.call("device.login.cancel", {}).catch(() => {});
27676
+ ctx.out.note("Cancelled.");
27677
+ return;
27678
+ }
27679
+ state = next;
27680
+ }
27681
+ if (state.state === "failed")
27682
+ throw new MessagingError(state.code, state.message);
27683
+ return state;
27684
+ }
27685
+ async function confirmed(ctx, yes, question, word = "y") {
27686
+ if (yes)
27687
+ return true;
27688
+ if (ctx.json)
27689
+ throw new UsageError("with --json there is no prompt: confirm with --yes");
27690
+ ctx.out.prompt(`${question} Type ${word} to confirm: `);
27691
+ const answer = (await readLine(ctx.io.stdin) ?? "").trim().toLowerCase();
27692
+ return word === "y" ? /^y(es)?$/.test(answer) : answer === word;
27693
+ }
26179
27694
  var login = {
26180
27695
  name: "login",
26181
27696
  summary: "Sign this device in to your account through the browser (hosted realm)",
@@ -26219,45 +27734,15 @@ var login = {
26219
27734
  ctx.out.json(started);
26220
27735
  return;
26221
27736
  }
26222
- if (!ctx.json) {
26223
- const { bold } = ctx.out.style;
26224
- ctx.out.line(started.purpose === "remove" ? "To remove that device, sign in at Jelly ID:" : "To sign this device in, open:");
26225
- ctx.out.line(` ${started.verificationUriComplete}`);
26226
- ctx.out.line(`and check that the page shows the code ${bold(started.userCode)} and the key ${bold(started.fingerprint)}.`);
26227
- ctx.out.line(`(Or open ${started.verificationUri} and type the code.)`);
26228
- }
26229
- if (values.open === true) {
26230
- const opened = await (ctx.io.openUrl ?? openInBrowser)(started.verificationUriComplete);
26231
- if (!opened)
26232
- ctx.out.note("Could not open a browser here; open the link on any device.");
26233
- } else if (!ctx.json && (ctx.io.env.SSH_CONNECTION !== undefined || ctx.io.env.SSH_TTY !== undefined)) {
26234
- ctx.out.note("Over SSH: open the link on your laptop or phone; the code ties it to this machine.");
26235
- }
27737
+ await showSignIn(ctx, started, values.open === true);
26236
27738
  const minutes = Math.max(1, Math.round((started.expires - ctx.now()) / 60000));
26237
27739
  if (!wait) {
26238
27740
  ctx.out.note(`The daemon waits ${minutes} min for your approval. See it with: messaging devices`);
26239
27741
  return;
26240
27742
  }
26241
- if (!ctx.json)
26242
- ctx.out.note(`Waiting for your approval (the code expires in ${minutes} min; Ctrl-C cancels)...`);
26243
- const signal = ctx.io.signal;
26244
- const interrupted = aborted2(signal);
26245
- let state = { ...started, state: "pending" };
26246
- while (state.state === "pending") {
26247
- const asked = client.call("device.login.status", { waitMs: STATUS_WAIT_MS }, { timeoutMs: STATUS_WAIT_MS + 15000 });
26248
- const next = await Promise.race([asked, interrupted.then(() => {
26249
- return;
26250
- })]);
26251
- if (next === undefined) {
26252
- asked.catch(() => {});
26253
- await client.call("device.login.cancel", {}).catch(() => {});
26254
- ctx.out.note("Cancelled.");
26255
- return 130;
26256
- }
26257
- state = next;
26258
- }
26259
- if (state.state === "failed")
26260
- throw new MessagingError(state.code, state.message);
27743
+ const state = await waitSignIn(ctx, client, started);
27744
+ if (state === undefined)
27745
+ return 130;
26261
27746
  if (ctx.json) {
26262
27747
  ctx.out.json(state);
26263
27748
  return state.state === "done" ? undefined : 1;
@@ -26274,7 +27759,128 @@ var login = {
26274
27759
  }
26275
27760
  }
26276
27761
  };
26277
- var LOGIN_COMMANDS = [login];
27762
+ var replace = {
27763
+ name: "identity replace",
27764
+ summary: "Replace this home's device with new keys after a restore (hosted realm)",
27765
+ usage: "[--label <label>] [--open] [--yes]",
27766
+ details: [
27767
+ "For a home restored from a backup or a snapshot (alarm keys_missing or restored), or for a fresh key on purpose.",
27768
+ "This device gets a new key, binds it to your account and removes the old device (reason replaced): your",
27769
+ "conversations, grants and history stay, and tasks continue. Mail still queued at the relay for the old device is",
27770
+ "lost. Through the browser you approve two sign-ins at Jelly ID: one binds the new key, one removes the old."
27771
+ ].join(`
27772
+ `),
27773
+ options: {
27774
+ label: { type: "string", value: "<label>", description: "The new device's label (default: the old one's)" },
27775
+ open: { type: "boolean", description: "Open each link in this machine's browser" },
27776
+ yes: { type: "boolean", short: "y", description: "Do not ask for confirmation" }
27777
+ },
27778
+ args: { min: 0, max: 0 },
27779
+ async run(ctx, _positionals, values) {
27780
+ const label = stringValue(values, "label");
27781
+ const client = await ctx.client();
27782
+ const preview = await client.call("identity.replace.preview", {});
27783
+ if (!ctx.json)
27784
+ ctx.out.line(inline(preview.sentence, 600));
27785
+ if (!await confirmed(ctx, values.yes === true, `Replace '${inline(preview.predecessor.label, 32)}' (device ${preview.predecessor.device}) with this device?`)) {
27786
+ ctx.out.note("Not replaced.");
27787
+ return 1;
27788
+ }
27789
+ const params = label !== undefined ? { label: parseWith2(MachineLabel, label, "--label must be a lowercase label, e.g. desk") } : {};
27790
+ for (let step = 0;step < 3; step++) {
27791
+ const result = await client.call("identity.replace", params);
27792
+ if (result.state === "done") {
27793
+ if (ctx.json)
27794
+ ctx.out.json(result);
27795
+ else
27796
+ ctx.out.line(`Replaced: this device (${result.device}) replaces ${result.predecessor} on ${inline(result.person, 64)}.`);
27797
+ return;
27798
+ }
27799
+ if (result.state === "token") {
27800
+ if (ctx.json)
27801
+ ctx.out.json(result);
27802
+ else
27803
+ ctx.out.note(`This device (${result.device}) waits for its Jelly host to bind the new key; the host's app finishes the replacement.`);
27804
+ return;
27805
+ }
27806
+ await showSignIn(ctx, result, values.open === true);
27807
+ const state = await waitSignIn(ctx, client, result);
27808
+ if (state === undefined)
27809
+ return 130;
27810
+ if (state.state !== "done") {
27811
+ ctx.out.note("The sign-in was cancelled (another sign-in started, or the daemon stopped).");
27812
+ return 1;
27813
+ }
27814
+ if ((await client.call("device.status", {})).binding !== "replace") {
27815
+ const done = { state: "done", person: state.person, device: result.device, predecessor: preview.predecessor.device, seq: state.seq };
27816
+ if (ctx.json)
27817
+ ctx.out.json(done);
27818
+ else
27819
+ ctx.out.line(`Replaced: this device (${result.device}) replaces ${preview.predecessor.device} on ${inline(state.person, 64)}.`);
27820
+ return;
27821
+ }
27822
+ if (!ctx.json)
27823
+ ctx.out.note("The new key is bound. One more sign-in removes the old device.");
27824
+ }
27825
+ throw new MessagingError("internal", "the replacement did not finish; run messaging identity replace again");
27826
+ }
27827
+ };
27828
+ var erase = {
27829
+ name: "erase",
27830
+ summary: "Erase your account from Messaging, for good (hosted realm)",
27831
+ usage: "[--open] [--yes]",
27832
+ details: [
27833
+ "Removes every device of your account and deletes what the directory holds about you. Your Jelly account can",
27834
+ "never be bound to Messaging again. The directory allows 10 erasures per person a day. Shows what is lost and",
27835
+ "asks you to type erase, unless --yes. A device approved by link cannot erase; one signed in through the browser",
27836
+ "approves one more sign-in at Jelly ID."
27837
+ ].join(`
27838
+ `),
27839
+ options: {
27840
+ open: { type: "boolean", description: "Open the link in this machine's browser" },
27841
+ yes: { type: "boolean", short: "y", description: "Do not ask for confirmation" }
27842
+ },
27843
+ args: { min: 0, max: 0 },
27844
+ async run(ctx, _positionals, values) {
27845
+ const client = await ctx.client();
27846
+ const preview = await client.call("directory.erase.preview", {});
27847
+ if (!ctx.json) {
27848
+ ctx.out.line(`Erasing ${inline(preview.person, 64)} from Messaging. What is lost:`);
27849
+ for (const line of preview.lost)
27850
+ ctx.out.line(` - ${inline(line, 300)}`);
27851
+ ctx.out.line(`Devices removed: ${preview.devices.map((d) => `'${inline(d.label, 32)}'${d.self ? " (this one)" : ""}`).join(", ")}`);
27852
+ ctx.out.line("What stays:");
27853
+ for (const line of preview.kept)
27854
+ ctx.out.line(` - ${inline(line, 300)}`);
27855
+ ctx.out.note(`The directory allows ${preview.perDay} erasures per person a day.`);
27856
+ }
27857
+ if (!await confirmed(ctx, values.yes === true, "This cannot be undone.", "erase")) {
27858
+ ctx.out.note("Not erased.");
27859
+ return 1;
27860
+ }
27861
+ const result = await client.call("directory.erase", {});
27862
+ if (result.state === "done") {
27863
+ if (ctx.json)
27864
+ ctx.out.json(result);
27865
+ else
27866
+ ctx.out.line(`Erased: ${result.devices} device(s) removed; this account is gone from Messaging.`);
27867
+ return;
27868
+ }
27869
+ await showSignIn(ctx, result, values.open === true);
27870
+ const state = await waitSignIn(ctx, client, result);
27871
+ if (state === undefined)
27872
+ return 130;
27873
+ if (state.state !== "done") {
27874
+ ctx.out.note("The sign-in was cancelled (another sign-in started, or the daemon stopped).");
27875
+ return 1;
27876
+ }
27877
+ if (ctx.json)
27878
+ ctx.out.json({ state: "done", person: state.person, devices: preview.devices.length });
27879
+ else
27880
+ ctx.out.line(`Erased: ${preview.devices.length} device(s) removed; this account is gone from Messaging.`);
27881
+ }
27882
+ };
27883
+ var LOGIN_COMMANDS = [login, replace, erase];
26278
27884
 
26279
27885
  // packages/daemon/dist/cli/commands/messages.js
26280
27886
  init_dist();
@@ -27026,13 +28632,13 @@ var HANDLES_INTERRUPT = new Set(["listen", "login"]);
27026
28632
  var GROUPS = {
27027
28633
  daemon: "Run the local daemon, messagingd",
27028
28634
  team: "Manage the team: create, invite, join, members, roles (only the CLI may do this)",
27029
- identity: "Show, move or rotate this machine's keys (only the CLI may do this)",
28635
+ identity: "Show, move, rotate or replace this machine's keys (only the CLI may do this)",
27030
28636
  chat: "Your DMs and channels: list, read, post, search"
27031
28637
  };
27032
28638
  var SECTIONS = [
27033
28639
  ["Daemon", ["daemon start", "daemon stop", "status"]],
27034
28640
  ["Team", ["team prepare", "team create", "team card", "team invite", "team join", "team pair", "team add", "team claim", "team pair-cancel", "team members", "team remove", "team role", "team revoke-invite"]],
27035
- ["Identity", ["identity show", "identity migrate", "identity rotate-enc"]],
28641
+ ["Identity", ["identity show", "identity migrate", "identity rotate-enc", "identity replace", "erase"]],
27036
28642
  ["People", ["login", "people", "devices", "grants", "grant", "requests", "contact", "card", "notices"]],
27037
28643
  ["Messages", ["who", "send", "inbox", "read", "message-status"]],
27038
28644
  ["Chats", ["chat list", "chat read", "chat post", "chat search"]],
@@ -27276,7 +28882,13 @@ Runs the Messaging daemon in the foreground for one MESSAGING_HOME (default ~/.m
27276
28882
  --version print the version, the store schema and the protocol (--json: as JSON); needs no home
27277
28883
  --help print this help
27278
28884
 
27279
- Under a Jelly host (spec \xA78.11):
28885
+ Under a Jelly host (spec \xA78.11), with no arguments: the environment start contract. JELLY_ACTION_KEY (the path of
28886
+ the host's public action key) starts it; STATE_DIRECTORY is the home, RUNTIME_DIRECTORY the socket folder,
28887
+ JELLY_DEVICE_LABEL the label, JELLY_SOCKET_GROUP the group, and JELLY_ID_ISSUER, JELLY_RESOURCES (the messaging entry)
28888
+ and JELLY_SERVICE_ANCHOR the realm, which the first start writes to config.json. Flags win: a start with
28889
+ --deployment jelly reads none of these; any other start with JELLY_* variables set is refused.
28890
+
28891
+ Or by flags:
27280
28892
  --deployment jelly every human and admin call needs the host's action grant
27281
28893
  --act-key <path> the host's public action key (an Ed25519 JWK); required
27282
28894
  --label <label> this device's label, from the host; required
@@ -27336,7 +28948,20 @@ function versionText(json, info) {
27336
28948
  return `messagingd ${info.version} (store schema ${info.storeSchema}, protocol ${info.protocol})
27337
28949
  `;
27338
28950
  }
27339
- async function daemonMain(argv) {
28951
+ function startSource(values, env, named) {
28952
+ const flags = Object.keys(values).length > 0;
28953
+ if (flags) {
28954
+ if (values["--deployment"] === "jelly" || named.length === 0)
28955
+ return "flags";
28956
+ 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`;
28957
+ }
28958
+ if ((env.JELLY_ACTION_KEY ?? "") !== "")
28959
+ return "environment";
28960
+ if (named.length > 0)
28961
+ return `${named[0]} is set without JELLY_ACTION_KEY: a Jelly start needs the host's action key`;
28962
+ return "self";
28963
+ }
28964
+ async function daemonMain(argv, env = process.env) {
27340
28965
  const args = parseArgs2(argv);
27341
28966
  if (typeof args === "string") {
27342
28967
  process.stderr.write(`messagingd: ${args}
@@ -27354,7 +28979,23 @@ ${USAGE}`);
27354
28979
  return 0;
27355
28980
  }
27356
28981
  const { values } = args;
27357
- const home = values["--home"] ? resolve4(values["--home"]) : messagingHome();
28982
+ const source = startSource(values, env, jellyEnvNamed(env));
28983
+ if (source !== "flags" && source !== "environment" && source !== "self") {
28984
+ process.stderr.write(`messagingd: ${source}
28985
+ `);
28986
+ return 2;
28987
+ }
28988
+ let fromEnv;
28989
+ if (source === "environment") {
28990
+ try {
28991
+ fromEnv = readJellyEnv(env);
28992
+ } catch (error) {
28993
+ process.stderr.write(`messagingd: ${error instanceof Error ? error.message : "the Jelly environment is invalid"}
28994
+ `);
28995
+ return 1;
28996
+ }
28997
+ }
28998
+ const home = fromEnv ? resolve4(fromEnv.home) : values["--home"] ? resolve4(values["--home"]) : messagingHome();
27358
28999
  const level = process.env.MESSAGING_LOG_LEVEL;
27359
29000
  const log = createLogger({
27360
29001
  file: homePaths(home).log,
@@ -27363,7 +29004,14 @@ ${USAGE}`);
27363
29004
  });
27364
29005
  let daemon;
27365
29006
  try {
27366
- const jellyHost = values["--deployment"] === "jelly" ? {
29007
+ const jellyHost = fromEnv ? {
29008
+ actKey: readActKey(fromEnv.actKeyPath),
29009
+ label: fromEnv.label,
29010
+ kind: "jelly-host",
29011
+ realm: fromEnv.realm,
29012
+ ...fromEnv.socketGroup !== undefined ? { socketGroup: fromEnv.socketGroup } : {},
29013
+ ...fromEnv.run !== undefined ? { run: resolve4(fromEnv.run) } : {}
29014
+ } : values["--deployment"] === "jelly" ? {
27367
29015
  actKey: readActKey(resolve4(values["--act-key"])),
27368
29016
  label: jellyLabel(values["--label"]),
27369
29017
  kind: "jelly-host",
@@ -27371,6 +29019,8 @@ ${USAGE}`);
27371
29019
  ...values["--run"] !== undefined ? { run: resolve4(values["--run"]) } : {}
27372
29020
  } : undefined;
27373
29021
  daemon = await startDaemon2({ home, log, ...jellyHost ? { jellyHost } : {} });
29022
+ if (jellyHost)
29023
+ log("info", "daemon.start", { deployment: "jelly", source });
27374
29024
  } catch (error) {
27375
29025
  process.stderr.write(`messagingd: ${error instanceof Error ? error.message : "failed to start"}
27376
29026
  `);