@joe-broadhead/messaging-bin-win32-x64 0.5.0 → 0.7.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.
@@ -5129,12 +5129,6 @@ function _ensureDefaultMemoizer() {
5129
5129
  function string2(params) {
5130
5130
  return _string(ZodString, params);
5131
5131
  }
5132
- function ipv42(params) {
5133
- return _ipv4(ZodIPv4, params);
5134
- }
5135
- function ipv62(params) {
5136
- return _ipv6(ZodIPv6, params);
5137
- }
5138
5132
  function number2(params) {
5139
5133
  return _number(ZodNumber, params);
5140
5134
  }
@@ -6122,12 +6116,11 @@ var init_common = __esm(() => {
6122
6116
  card: `messaging/1/card
6123
6117
  `,
6124
6118
  seal: "messaging/1/seal",
6125
- directAuth: `messaging/1/direct-auth
6126
- `,
6127
- directLink: "messaging/1/direct-link",
6128
6119
  person: `messaging/1/person
6129
6120
  `,
6130
6121
  tombstone: "messaging/1/tombstone",
6122
+ dirRead: `messaging/1/dir-read
6123
+ `,
6131
6124
  deviceLog: `messaging/1/device-log
6132
6125
  `,
6133
6126
  deviceKeys: `messaging/1/device-keys
@@ -6163,7 +6156,7 @@ var init_common = __esm(() => {
6163
6156
  Millis = number2().int().nonnegative();
6164
6157
  Address = string2().min(1).max(200);
6165
6158
  SessionAddress = string2().regex(/^(?:[a-z][a-z0-9-]{0,31}|p_[a-z2-7]{26})\/[a-z0-9][a-z0-9._-]{0,63}@[a-z0-9][a-z0-9-]{0,31}$/, "person/session@label");
6166
- MemberRole = _enum(["admin", "member", "service"]);
6159
+ MemberRole = _enum(["admin", "member"]);
6167
6160
  SessionState = _enum(["idle", "busy"]);
6168
6161
  DeliveryTier = _enum(["push", "pull"]);
6169
6162
  DeliveryStage = _enum([
@@ -6404,7 +6397,7 @@ var init_jelly = __esm(() => {
6404
6397
  });
6405
6398
 
6406
6399
  // packages/core/dist/schema/directory.js
6407
- var MapProof, MapValue, DeviceId, DeviceKind, UnbindReason, Signature, LinkRequest, Authority, LogEntry, KeysStatement, EpochLeaf, EpochProof, STH, KeyRotation, EntryEpoch, PersonView;
6400
+ var MapProof, MapValue, DeviceId, DeviceKind, UnbindReason, Signature, LinkRequest, Authority, LogEntry, KeysStatement, EpochLeaf, EpochProof, STH, KeyRotation, EntryEpoch, PersonHead, PersonView;
6408
6401
  var init_directory = __esm(() => {
6409
6402
  init_zod();
6410
6403
  init_common();
@@ -6499,6 +6492,12 @@ var init_directory = __esm(() => {
6499
6492
  value: MapValue,
6500
6493
  mapProof: MapProof
6501
6494
  });
6495
+ PersonHead = strictObject({
6496
+ person: HostedPersonId,
6497
+ mapProof: MapProof,
6498
+ value: MapValue.optional(),
6499
+ tombstone: literal(true).optional()
6500
+ });
6502
6501
  PersonView = strictObject({
6503
6502
  person: HostedPersonId,
6504
6503
  log: array(LogEntry).max(1024),
@@ -6510,7 +6509,7 @@ var init_directory = __esm(() => {
6510
6509
  });
6511
6510
 
6512
6511
  // packages/core/dist/schema/a2a.js
6513
- var Struct, TaskState, TERMINAL_STATES, Role, Part, Message, TaskStatus, Artifact, Task, TaskStatusUpdateEvent, TaskArtifactUpdateEvent, AgentInterface, AgentExtension, AgentCapabilities, AgentSkill, AgentCard, SendMessageConfiguration, SendMessageRequest, SendMessageResponse, StreamResponse, GetTaskRequest, CancelTaskRequest, SubscribeToTaskRequest, ListTasksRequest, ListTasksResponse;
6512
+ var Struct, TaskState, TERMINAL_STATES, Role, Part, Message, TaskStatus, Artifact, Task, TaskStatusUpdateEvent, TaskArtifactUpdateEvent;
6514
6513
  var init_a2a = __esm(() => {
6515
6514
  init_zod();
6516
6515
  Struct = record(string2(), unknown());
@@ -6587,92 +6586,6 @@ var init_a2a = __esm(() => {
6587
6586
  lastChunk: boolean2().optional(),
6588
6587
  metadata: Struct.optional()
6589
6588
  });
6590
- AgentInterface = object({
6591
- url: string2().min(1),
6592
- protocolBinding: string2().min(1).describe("JSONRPC | GRPC | HTTP+JSON | a binding URI"),
6593
- tenant: string2().optional(),
6594
- protocolVersion: string2().min(1)
6595
- });
6596
- AgentExtension = object({
6597
- uri: string2(),
6598
- description: string2().optional(),
6599
- required: boolean2().optional(),
6600
- params: Struct.optional()
6601
- });
6602
- AgentCapabilities = object({
6603
- streaming: boolean2().optional(),
6604
- pushNotifications: boolean2().optional(),
6605
- extensions: array(AgentExtension).optional(),
6606
- extendedAgentCard: boolean2().optional()
6607
- });
6608
- AgentSkill = object({
6609
- id: string2().min(1),
6610
- name: string2().min(1),
6611
- description: string2(),
6612
- tags: array(string2()),
6613
- examples: array(string2()).optional(),
6614
- inputModes: array(string2()).optional(),
6615
- outputModes: array(string2()).optional(),
6616
- securityRequirements: array(unknown()).optional()
6617
- });
6618
- AgentCard = object({
6619
- name: string2().min(1),
6620
- description: string2(),
6621
- supportedInterfaces: array(AgentInterface).min(1),
6622
- provider: object({ url: string2(), organization: string2() }).optional(),
6623
- version: string2(),
6624
- documentationUrl: string2().optional(),
6625
- capabilities: AgentCapabilities,
6626
- securitySchemes: record(string2(), unknown()).optional(),
6627
- securityRequirements: array(unknown()).optional(),
6628
- defaultInputModes: array(string2()),
6629
- defaultOutputModes: array(string2()),
6630
- skills: array(AgentSkill),
6631
- signatures: array(object({ protected: string2(), signature: string2(), header: Struct.optional() })).optional(),
6632
- iconUrl: string2().optional()
6633
- });
6634
- SendMessageConfiguration = object({
6635
- acceptedOutputModes: array(string2()).optional(),
6636
- taskPushNotificationConfig: unknown().optional(),
6637
- historyLength: number2().int().nonnegative().optional(),
6638
- returnImmediately: boolean2().optional()
6639
- });
6640
- SendMessageRequest = object({
6641
- tenant: string2().optional(),
6642
- message: Message,
6643
- configuration: SendMessageConfiguration.optional(),
6644
- metadata: Struct.optional()
6645
- });
6646
- SendMessageResponse = union([object({ task: Task }), object({ message: Message })]);
6647
- StreamResponse = union([
6648
- object({ task: Task }),
6649
- object({ message: Message }),
6650
- object({ statusUpdate: TaskStatusUpdateEvent }),
6651
- object({ artifactUpdate: TaskArtifactUpdateEvent })
6652
- ]);
6653
- GetTaskRequest = object({
6654
- tenant: string2().optional(),
6655
- id: string2().min(1),
6656
- historyLength: number2().int().nonnegative().optional()
6657
- });
6658
- CancelTaskRequest = object({ tenant: string2().optional(), id: string2().min(1), metadata: Struct.optional() });
6659
- SubscribeToTaskRequest = object({ tenant: string2().optional(), id: string2().min(1) });
6660
- ListTasksRequest = object({
6661
- tenant: string2().optional(),
6662
- contextId: string2().optional(),
6663
- status: TaskState.or(literal("TASK_STATE_UNSPECIFIED")).optional(),
6664
- pageSize: number2().int().min(1).max(100).optional(),
6665
- pageToken: string2().optional(),
6666
- historyLength: number2().int().nonnegative().optional(),
6667
- statusTimestampAfter: string2().optional(),
6668
- includeArtifacts: boolean2().optional()
6669
- });
6670
- ListTasksResponse = object({
6671
- tasks: array(Task),
6672
- nextPageToken: string2(),
6673
- pageSize: number2().int(),
6674
- totalSize: number2().int()
6675
- });
6676
6589
  });
6677
6590
 
6678
6591
  // packages/core/dist/schema/consent.js
@@ -6760,13 +6673,12 @@ var init_consent = __esm(() => {
6760
6673
  });
6761
6674
 
6762
6675
  // packages/core/dist/schema/delegation.js
6763
- var DelegationScope, ExternalPrincipal, DelegationGrant, DelegationChain, DelegationSummary;
6676
+ var DelegationScope, DelegationGrant, DelegationChain, DelegationSummary;
6764
6677
  var init_delegation = __esm(() => {
6765
6678
  init_zod();
6766
6679
  init_common();
6767
6680
  init_realm();
6768
6681
  DelegationScope = string2().regex(/^[a-z0-9][a-z0-9._:/-]{0,63}$/, "scope label");
6769
- ExternalPrincipal = string2().min(1).max(64);
6770
6682
  DelegationGrant = looseObject({
6771
6683
  realm: RealmId,
6772
6684
  task: Ulid,
@@ -6778,7 +6690,6 @@ var init_delegation = __esm(() => {
6778
6690
  maxDepth: number2().int().min(1).max(LIMITS.delegationDepth),
6779
6691
  scope: array(DelegationScope).max(LIMITS.delegationScopes).optional(),
6780
6692
  parent: Hash.optional(),
6781
- external: looseObject({ principal: ExternalPrincipal }).optional(),
6782
6693
  sig: Base64Url
6783
6694
  });
6784
6695
  DelegationChain = array(DelegationGrant).min(1).max(LIMITS.delegationDepth);
@@ -6787,7 +6698,6 @@ var init_delegation = __esm(() => {
6787
6698
  depth: number2().int().min(1).max(LIMITS.delegationDepth),
6788
6699
  maxDepth: number2().int().min(1).max(LIMITS.delegationDepth),
6789
6700
  scope: array(DelegationScope).max(LIMITS.delegationScopes).optional(),
6790
- external: object({ principal: ExternalPrincipal, gateway: MachineId }).optional(),
6791
6701
  exp: Millis
6792
6702
  });
6793
6703
  });
@@ -6800,7 +6710,7 @@ function requestMessage(message) {
6800
6710
  function cardMessage(message, what) {
6801
6711
  return bare(message, "data") && (what === "card" ? CardData : CardGetData).safeParse(message.parts[0].data).success;
6802
6712
  }
6803
- var EnvelopeType, SEAL_ALG = "x25519-hkdf-sha256-a256gcm", EnvelopeClass, Party, Sender, Recipient, Envelope, ChannelPersons, ChannelConv, SelfConv, RequestConv, CardConv, Conv, MessagePayload, BARE_MESSAGE, bare = (message, part) => Object.keys(message).every((key) => BARE_MESSAGE.has(key)) && message.parts.length === 1 && Object.keys(message.parts[0]).join() === part, TaskStatusPayload, TaskArtifactPayload, TaskCancelPayload, ReceiptPayload, SessionInfo, DirectEndpoint, PresencePayload, PAYLOAD_BY_TYPE, Provenance;
6713
+ var EnvelopeType, SEAL_ALG = "x25519-hkdf-sha256-a256gcm", EnvelopeClass, Party, Sender, Recipient, Envelope, ChannelPersons, ChannelConv, SelfConv, RequestConv, CardConv, Conv, MessagePayload, BARE_MESSAGE, bare = (message, part) => Object.keys(message).every((key) => BARE_MESSAGE.has(key)) && message.parts.length === 1 && Object.keys(message.parts[0]).join() === part, TaskStatusPayload, TaskArtifactPayload, TaskCancelPayload, ReceiptPayload, SessionInfo, PresencePayload, PAYLOAD_BY_TYPE, Provenance;
6804
6714
  var init_envelope = __esm(() => {
6805
6715
  init_zod();
6806
6716
  init_a2a();
@@ -6855,7 +6765,6 @@ var init_envelope = __esm(() => {
6855
6765
  kind: _enum(["message", "task"]),
6856
6766
  message: Message,
6857
6767
  wake: boolean2().optional(),
6858
- external: object({ principal: string2().min(1).max(64) }).optional(),
6859
6768
  delegation: DelegationChain.optional(),
6860
6769
  conv: Conv.optional()
6861
6770
  }).refine((p) => p.kind === "task" ? p.message.taskId !== undefined : true, { message: "a task message needs message.taskId" }).refine((p) => p.kind === "task" === (p.delegation !== undefined), { message: "a task request carries a delegation chain, and only a task request" }).refine((p) => p.conv?.kind !== "self" || p.message.parts.length === 1 && GrantSync.safeParse(p.message.parts[0].data).success, {
@@ -6892,16 +6801,11 @@ var init_envelope = __esm(() => {
6892
6801
  kept: literal(true).optional(),
6893
6802
  wake: literal(true).optional()
6894
6803
  });
6895
- DirectEndpoint = object({
6896
- host: union([ipv42(), ipv62()]),
6897
- port: number2().int().min(1).max(65535)
6898
- });
6899
6804
  PresencePayload = object({
6900
6805
  seq: number2().int().nonnegative(),
6901
6806
  label: MachineLabel,
6902
6807
  sessions: array(SessionInfo).max(LIMITS.presenceSessions),
6903
6808
  features: array(string2().max(64)).max(32).optional(),
6904
- direct: array(DirectEndpoint).min(1).max(4).optional(),
6905
6809
  away: literal(true).optional()
6906
6810
  });
6907
6811
  PAYLOAD_BY_TYPE = {
@@ -6920,8 +6824,7 @@ var init_envelope = __esm(() => {
6920
6824
  device: MachineId,
6921
6825
  label: MachineLabel,
6922
6826
  verified: literal("signature"),
6923
- receivedAt: Millis,
6924
- external: object({ principal: string2().max(64), gateway: MachineId }).optional()
6827
+ receivedAt: Millis
6925
6828
  });
6926
6829
  });
6927
6830
 
@@ -6974,7 +6877,6 @@ var init_roster = __esm(() => {
6974
6877
  inviteId: Ulid,
6975
6878
  invitePub: Key32,
6976
6879
  person: PersonName,
6977
- role: _enum(["member", "service"]),
6978
6880
  expires: Millis
6979
6881
  })
6980
6882
  });
@@ -7011,7 +6913,6 @@ var init_roster = __esm(() => {
7011
6913
  inviteId: Ulid,
7012
6914
  secret: Key32,
7013
6915
  person: PersonName,
7014
- role: _enum(["member", "service"]),
7015
6916
  expires: Millis
7016
6917
  });
7017
6918
  });
@@ -7096,6 +6997,7 @@ var init_relay = __esm(() => {
7096
6997
  protocolError: 4002,
7097
6998
  removed: 4003,
7098
6999
  replaced: 4004,
7000
+ full: 1013,
7099
7001
  tooLarge: 1009
7100
7002
  };
7101
7003
  });
@@ -7104,7 +7006,10 @@ var init_relay = __esm(() => {
7104
7006
  function hostedRelayAuthPayload(args) {
7105
7007
  return { proto: RELAY_PROTOCOL, relay: args.relay, realm: args.realm, person: args.person, device: args.device, nonce: args.nonce, ts: args.ts };
7106
7008
  }
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;
7009
+ function dirReadPayload(args) {
7010
+ return { proto: RELAY_PROTOCOL, relay: args.relay, realm: args.realm, device: args.device, nonce: args.nonce };
7011
+ }
7012
+ var Req2, Size, Handle, Email, OrgId, DirReadProof, DirBindFrame, HostedHelloFrame, DirAppendFrame, DirKeysFrame, DirSthFrame, DirLookupFrame, DirHeadsFrame, 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, DirHeadsAreFrame, ResolvedPerson, DirResolvedFrame, DirChangedFrame, DirUnboundFrame, DirectoryRelayFrame;
7108
7013
  var init_directory_frames = __esm(() => {
7109
7014
  init_zod();
7110
7015
  init_common();
@@ -7117,6 +7022,7 @@ var init_directory_frames = __esm(() => {
7117
7022
  Handle = string2().regex(/^[a-z][a-z0-9-]{0,31}$/, "lowercase handle");
7118
7023
  Email = string2().max(254).regex(/^[^\s@A-Z]{1,64}@(?:[a-z0-9-]+\.)+[a-z0-9-]+$/, "lower-case email address");
7119
7024
  OrgId = string2().min(1).max(200);
7025
+ DirReadProof = strictObject({ device: DeviceId, sig: Base64Url });
7120
7026
  DirBindFrame = object({ t: literal("dir.bind"), req: Req2, entry: LogEntry });
7121
7027
  HostedHelloFrame = object({
7122
7028
  t: literal("hello"),
@@ -7139,6 +7045,13 @@ var init_directory_frames = __esm(() => {
7139
7045
  t: literal("dir.lookup"),
7140
7046
  req: Req2,
7141
7047
  persons: array(object({ person: HostedPersonId, knownSeq: number2().int().nonnegative().optional() })).min(1).max(LIMITS.dirLookupPersons),
7048
+ size: Size.optional(),
7049
+ proof: DirReadProof.optional()
7050
+ });
7051
+ DirHeadsFrame = object({
7052
+ t: literal("dir.heads"),
7053
+ req: Req2,
7054
+ persons: array(HostedPersonId).min(1).max(LIMITS.dirLookupPersons),
7142
7055
  size: Size.optional()
7143
7056
  });
7144
7057
  DirDeviceFrame = object({
@@ -7188,6 +7101,7 @@ var init_directory_frames = __esm(() => {
7188
7101
  DirSthFrame,
7189
7102
  DirLookupFrame,
7190
7103
  DirDeviceFrame,
7104
+ DirHeadsFrame,
7191
7105
  DirResolveFrame,
7192
7106
  DirSearchFrame,
7193
7107
  DirWatchFrame,
@@ -7220,6 +7134,13 @@ var init_directory_frames = __esm(() => {
7220
7134
  head: EpochProof,
7221
7135
  persons: array(PersonView).max(LIMITS.dirLookupPersons)
7222
7136
  });
7137
+ DirHeadsAreFrame = object({
7138
+ t: literal("dir.headsAre"),
7139
+ req: Req2,
7140
+ sth: STH,
7141
+ head: EpochProof,
7142
+ persons: array(PersonHead).max(LIMITS.dirLookupPersons)
7143
+ });
7223
7144
  ResolvedPerson = object({
7224
7145
  person: HostedPersonId,
7225
7146
  handle: Handle,
@@ -7243,47 +7164,22 @@ var init_directory_frames = __esm(() => {
7243
7164
  DirOkFrame,
7244
7165
  DirSthIsFrame,
7245
7166
  DirPersonsFrame,
7167
+ DirHeadsAreFrame,
7246
7168
  DirResolvedFrame,
7247
7169
  DirChangedFrame,
7248
7170
  DirUnboundFrame
7249
7171
  ]);
7250
7172
  });
7251
7173
 
7252
- // packages/core/dist/schema/direct.js
7253
- var DIRECT_PROTOCOL = "messaging-direct/1", DIRECT_HANDSHAKE_BYTES = 4096, DirectHalf, DirectHelloFrame, DirectAcceptFrame, DirectAuthFrame, DirectIdentity, DirectSendRecord, DirectAckRecord, DirectNackRecord, DirectRecord;
7254
- var init_direct = __esm(() => {
7255
- init_zod();
7256
- init_common();
7257
- init_errors3();
7258
- DirectHalf = object({ epk: Key32, nonce: Key32 });
7259
- DirectHelloFrame = object({ t: literal("hello"), proto: literal(DIRECT_PROTOCOL), epk: Key32, nonce: Key32 });
7260
- DirectAcceptFrame = object({ t: literal("accept"), epk: Key32, nonce: Key32, box: Base64Url.max(1024) });
7261
- DirectAuthFrame = object({ t: literal("auth"), box: Base64Url.max(1024) });
7262
- DirectIdentity = object({ team: TeamId, machine: MachineId, sig: Base64Url });
7263
- DirectSendRecord = object({
7264
- t: literal("send"),
7265
- env: looseObject({
7266
- v: string2(),
7267
- id: Ulid,
7268
- type: string2(),
7269
- from: looseObject({ device: MachineId }),
7270
- to: looseObject({ device: MachineId })
7271
- })
7272
- });
7273
- DirectAckRecord = object({ t: literal("ack"), id: Ulid });
7274
- DirectNackRecord = object({ t: literal("nack"), id: Ulid, code: ErrorCode });
7275
- DirectRecord = discriminatedUnion("t", [DirectSendRecord, DirectAckRecord, DirectNackRecord]);
7276
- });
7277
-
7278
7174
  // packages/core/dist/schema/ipc/device.js
7279
- var BindingState, RealmAlarmSummary, DeviceStatus, DeviceListEntry, LinkText, Sas, LinkPreview, KeyFingerprint, LoginUserCode, LoginPurpose, LoginKind, LoginShown, LoginState, DeviceRemoveParams, DeviceApproveParams, DEVICE_METHODS;
7175
+ var BindingState, RealmAlarmSummary, DeviceStatus, DeviceChangeView, DeviceListEntry, LinkText, Sas, LinkPreview, KeyFingerprint, LoginUserCode, LoginPurpose, LoginKind, LoginShown, LoginState, DeviceRemoveParams, DeviceApproveParams, IdentityReplaceParams, SignInStep, DirectoryEraseParams, DEVICE_METHODS;
7280
7176
  var init_device = __esm(() => {
7281
7177
  init_zod();
7282
7178
  init_common();
7283
7179
  init_directory();
7284
7180
  init_errors3();
7285
7181
  init_jelly();
7286
- BindingState = _enum(["none", "bound", "attest-due", "removed"]);
7182
+ BindingState = _enum(["none", "bound", "attest-due", "removed", "replace"]);
7287
7183
  RealmAlarmSummary = object({
7288
7184
  kind: _enum(["directory", "restored"]),
7289
7185
  reason: string2().max(500),
@@ -7298,6 +7194,16 @@ var init_device = __esm(() => {
7298
7194
  sth: object({ size: number2().int().positive(), root: Hash, ts: Millis }).optional(),
7299
7195
  alarm: RealmAlarmSummary.optional()
7300
7196
  });
7197
+ DeviceChangeView = object({
7198
+ change: _enum(["added", "vouched", "removed", "replaced"]),
7199
+ device: MachineId,
7200
+ label: MachineLabel,
7201
+ kind: DeviceKind.optional(),
7202
+ vouch: _enum(["jelly", "device", "admin"]),
7203
+ via: _enum(["host", "browser"]).optional(),
7204
+ approver: MachineLabel.optional(),
7205
+ at: Millis
7206
+ });
7301
7207
  DeviceListEntry = object({
7302
7208
  device: MachineId,
7303
7209
  label: MachineLabel,
@@ -7314,10 +7220,9 @@ var init_device = __esm(() => {
7314
7220
  LinkPreview = object({ device: MachineId, label: MachineLabel, kind: DeviceKind, sas: Sas });
7315
7221
  KeyFingerprint = string2().regex(/^[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}$/, "key fingerprint");
7316
7222
  LoginUserCode = string2().regex(/^[A-HJ-NP-Z2-9]{5}-[A-HJ-NP-Z2-9]{5}$/, "user code");
7317
- LoginPurpose = _enum(["bind", "remove"]);
7223
+ LoginPurpose = _enum(["bind", "remove", "replace", "erase"]);
7318
7224
  LoginKind = _enum(["laptop", "server", "other"]);
7319
7225
  LoginShown = object({
7320
- verificationUriComplete: string2().url().max(2048),
7321
7226
  verificationUri: string2().url().max(2048),
7322
7227
  userCode: LoginUserCode,
7323
7228
  fingerprint: KeyFingerprint,
@@ -7332,6 +7237,14 @@ var init_device = __esm(() => {
7332
7237
  ]);
7333
7238
  DeviceRemoveParams = strictObject({ device: MachineId, reason: _enum(["self", "removed", "lost"]).optional(), token: Jws.optional() });
7334
7239
  DeviceApproveParams = strictObject({ link: LinkText, sas: Sas });
7240
+ IdentityReplaceParams = strictObject({
7241
+ token: Jws.optional(),
7242
+ label: MachineLabel.optional()
7243
+ });
7244
+ SignInStep = LoginShown.extend({ state: literal("sign-in"), purpose: LoginPurpose, interval: number2().int().positive() });
7245
+ DirectoryEraseParams = strictObject({
7246
+ token: Jws.optional()
7247
+ });
7335
7248
  DEVICE_METHODS = {
7336
7249
  "device.status": { params: strictObject({}), result: DeviceStatus },
7337
7250
  "device.bind": {
@@ -7361,6 +7274,47 @@ var init_device = __esm(() => {
7361
7274
  "device.approve": {
7362
7275
  params: DeviceApproveParams,
7363
7276
  result: object({ device: MachineId, seq: number2().int().nonnegative() })
7277
+ },
7278
+ "identity.replace": {
7279
+ params: IdentityReplaceParams,
7280
+ result: discriminatedUnion("state", [
7281
+ object({ state: literal("done"), person: PersonId, device: MachineId, predecessor: MachineId, seq: number2().int().nonnegative() }),
7282
+ object({ state: literal("token"), device: MachineId }),
7283
+ SignInStep.extend({ device: MachineId })
7284
+ ])
7285
+ },
7286
+ "identity.replace.preview": {
7287
+ params: IdentityReplaceParams,
7288
+ result: object({
7289
+ predecessor: object({
7290
+ device: MachineId,
7291
+ label: MachineLabel,
7292
+ kind: DeviceKind.optional(),
7293
+ vouch: _enum(["jelly", "device"]),
7294
+ via: _enum(["host", "browser"]).optional(),
7295
+ since: Millis.optional(),
7296
+ current: boolean2()
7297
+ }),
7298
+ alarm: _enum(["keys_missing", "restored"]).nullable(),
7299
+ device: MachineId.optional(),
7300
+ signIns: number2().int().min(0).max(2),
7301
+ sentence: string2().max(600)
7302
+ })
7303
+ },
7304
+ "directory.erase": {
7305
+ params: DirectoryEraseParams,
7306
+ result: discriminatedUnion("state", [object({ state: literal("done"), person: PersonId, devices: number2().int().nonnegative() }), SignInStep])
7307
+ },
7308
+ "directory.erase.preview": {
7309
+ params: DirectoryEraseParams,
7310
+ result: object({
7311
+ person: PersonId,
7312
+ devices: array(object({ device: MachineId, label: MachineLabel, kind: DeviceKind.optional(), self: boolean2() })),
7313
+ signIns: number2().int().min(0).max(1),
7314
+ perDay: number2().int().positive(),
7315
+ lost: array(string2().max(300)).max(10),
7316
+ kept: array(string2().max(300)).max(10)
7317
+ })
7364
7318
  }
7365
7319
  };
7366
7320
  });
@@ -7541,7 +7495,7 @@ var init_consent2 = __esm(() => {
7541
7495
  HeldRequest = union([AskRequest, ContactRequestView, ContactSuggestion]);
7542
7496
  RequestAnswer = _enum(["once", "always", "accept", "decline", "block"]);
7543
7497
  RequestText = string2().min(1).refine((text) => text.length <= LIMITS.requestChars, { message: `at most ${LIMITS.requestChars} UTF-16 code units` });
7544
- PersonReach = object({ person: PersonId, can: CardCan, source: _enum(["card", "default"]), at: Millis.optional() });
7498
+ PersonReach = object({ person: PersonId, can: CardCan, source: _enum(["card", "default"]), at: Millis.optional(), devicesChangedAt: Millis.optional() });
7545
7499
  RequestAnswerParams = strictObject({ id: Ulid, answer: RequestAnswer, all: boolean2().optional() });
7546
7500
  ContactRequestParams = strictObject({ email: Email, text: RequestText, suggestion: Ulid.optional() });
7547
7501
  CONSENT_METHODS = {
@@ -7586,6 +7540,7 @@ var init_surfaces = __esm(() => {
7586
7540
  init_directory_frames();
7587
7541
  init_errors3();
7588
7542
  init_consent2();
7543
+ init_device();
7589
7544
  init_shapes();
7590
7545
  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>");
7591
7546
  ConvPlace = union([ChatSeq, ConvCursor]);
@@ -7626,7 +7581,6 @@ var init_surfaces = __esm(() => {
7626
7581
  referenceTaskIds: array(Ulid).max(16).optional(),
7627
7582
  wake: boolean2().optional().describe("hint that the recipient may wake an idle session; default true"),
7628
7583
  request: strictObject({ contact: Email }).optional(),
7629
- external: strictObject({ principal: string2().min(1).max(64) }).optional(),
7630
7584
  onBehalfOf: strictObject({
7631
7585
  taskId: Ulid.describe("a task this session executes"),
7632
7586
  scope: array(DelegationScope).max(LIMITS.delegationScopes).optional().describe("narrow the scope; never wider than the task's"),
@@ -7690,13 +7644,16 @@ var init_surfaces = __esm(() => {
7690
7644
  },
7691
7645
  "people.get": {
7692
7646
  params: strictObject({ person: PersonId }),
7693
- result: PersonSummary.extend({ devices: array(object({ device: MachineId, label: MachineLabel, self: boolean2(), away: literal(true).optional() })) })
7647
+ result: PersonSummary.extend({
7648
+ devices: array(object({ device: MachineId, label: MachineLabel, self: boolean2(), away: literal(true).optional() })),
7649
+ deviceChanges: array(DeviceChangeView).max(10).optional()
7650
+ })
7694
7651
  }
7695
7652
  };
7696
7653
  });
7697
7654
 
7698
7655
  // packages/core/dist/schema/ipc/jelly.js
7699
- var NoticeId, NOTICE_LINE_CHARS = 120, Notice, NoticeCounts, DaemonHealth, AgentSummary, AgentConversation, JELLY_METHODS;
7656
+ var NoticeId, NOTICE_LINE_CHARS = 120, NoticeDevice, Notice, NoticeCounts, DaemonHealth, AgentSummary, AgentConversation, JELLY_METHODS;
7700
7657
  var init_jelly2 = __esm(() => {
7701
7658
  init_zod();
7702
7659
  init_common();
@@ -7707,12 +7664,18 @@ var init_jelly2 = __esm(() => {
7707
7664
  init_shapes();
7708
7665
  init_surfaces();
7709
7666
  NoticeId = string2().regex(/^[mra]_[0-9A-Za-z]{1,64}$/, "notice id");
7667
+ NoticeDevice = DeviceChangeView.extend({
7668
+ replaces: object({ device: MachineId, label: MachineLabel }).optional(),
7669
+ current: boolean2(),
7670
+ remove: _enum(["token", "sign-in", "team"]).optional()
7671
+ });
7710
7672
  Notice = object({
7711
7673
  id: NoticeId,
7712
7674
  category: _enum(["message", "request", "alert"]),
7713
7675
  conversation: ConversationRef.optional(),
7714
7676
  from: object({ name: string2().max(200), email: string2().max(320).optional(), org: _enum(["same", "other", "none"]) }).optional(),
7715
7677
  line: string2().max(NOTICE_LINE_CHARS).optional(),
7678
+ device: NoticeDevice.optional(),
7716
7679
  at: Millis
7717
7680
  });
7718
7681
  NoticeCounts = object({ unread: number2().int().nonnegative(), requests: number2().int().nonnegative() });
@@ -7852,7 +7815,6 @@ var init_ipc = __esm(() => {
7852
7815
  InviteSummary = object({
7853
7816
  inviteId: Ulid,
7854
7817
  person: PersonName,
7855
- role: _enum(["member", "service"]),
7856
7818
  expires: Millis,
7857
7819
  state: _enum(["open", "redeemed", "revoked", "expired"])
7858
7820
  });
@@ -7863,7 +7825,7 @@ var init_ipc = __esm(() => {
7863
7825
  "session.register": {
7864
7826
  params: strictObject({
7865
7827
  key: SessionKey,
7866
- agent: AgentKind.describe('client kind, e.g. "opencode", "claude-code", "a2a-gateway"'),
7828
+ agent: AgentKind.describe('client kind, e.g. "opencode", "claude-code"'),
7867
7829
  name: SessionName.optional().describe("requested name; the daemon may suffix it to keep it unique"),
7868
7830
  cwd: string2().max(1024).optional(),
7869
7831
  repo: string2().max(200).optional(),
@@ -7943,7 +7905,7 @@ var init_ipc = __esm(() => {
7943
7905
  result: object({ team: TeamId, name: TeamName, machine: MachineId })
7944
7906
  },
7945
7907
  "team.invite": {
7946
- params: strictObject({ person: PersonName, role: _enum(["member", "service"]).optional(), ttlMs: number2().int().min(60000).max(LIMITS.ttlMs).optional() }),
7908
+ params: strictObject({ person: PersonName, ttlMs: number2().int().min(60000).max(LIMITS.ttlMs).optional() }),
7947
7909
  result: object({ ticket: string2(), inviteId: Ulid, expires: Millis })
7948
7910
  },
7949
7911
  "team.join": {
@@ -7967,8 +7929,7 @@ var init_ipc = __esm(() => {
7967
7929
  machine: MachineId,
7968
7930
  enc: EncKey,
7969
7931
  person: PersonName,
7970
- label: MachineLabel,
7971
- role: _enum(["member", "service"]).optional()
7932
+ label: MachineLabel
7972
7933
  }),
7973
7934
  result: Ok
7974
7935
  },
@@ -8061,6 +8022,8 @@ var init_ipc = __esm(() => {
8061
8022
  "notice.ack",
8062
8023
  "device.approve.preview",
8063
8024
  "device.remove.preview",
8025
+ "identity.replace.preview",
8026
+ "directory.erase.preview",
8064
8027
  "channel.create",
8065
8028
  "channel.invite",
8066
8029
  "channel.leave",
@@ -8081,6 +8044,8 @@ var init_ipc = __esm(() => {
8081
8044
  "device.login.cancel",
8082
8045
  "device.approve",
8083
8046
  "identity.rotateEnc",
8047
+ "identity.replace",
8048
+ "directory.erase",
8084
8049
  "team.create",
8085
8050
  "team.invite",
8086
8051
  "team.join",
@@ -8130,7 +8095,6 @@ var init_schema = __esm(() => {
8130
8095
  init_roster();
8131
8096
  init_relay();
8132
8097
  init_directory_frames();
8133
- init_direct();
8134
8098
  init_ipc();
8135
8099
  init_device();
8136
8100
  init_consent2();
@@ -8161,7 +8125,7 @@ var init_b64u = __esm(() => {
8161
8125
  });
8162
8126
 
8163
8127
  // packages/core/dist/version.js
8164
- var VERSION = "0.5.0";
8128
+ var VERSION = "0.7.0";
8165
8129
 
8166
8130
  // packages/core/dist/jcs.js
8167
8131
  function hasLoneSurrogate(text) {
@@ -8880,7 +8844,7 @@ function assertMeaning(type, from, to, klass, payload) {
8880
8844
  throw new MessagingError("invalid_request", "a self sync goes between devices of one person");
8881
8845
  if (p.conv?.kind === "card" && (from?.person === to.person || from?.agent !== undefined))
8882
8846
  throw new MessagingError("invalid_request", "a card goes from a person to another person");
8883
- const line = p.kind === "message" && p.external === undefined && p.message.taskId === undefined && Ulid.safeParse(p.message.messageId).success && (p.message.contextId === undefined || Ulid.safeParse(p.message.contextId).success);
8847
+ const line = p.kind === "message" && p.message.taskId === undefined && Ulid.safeParse(p.message.messageId).success && (p.message.contextId === undefined || Ulid.safeParse(p.message.contextId).success);
8884
8848
  if (!line)
8885
8849
  throw new MessagingError("invalid_request", "a chat line is kind message, outside any task, with ULID messageId and contextId");
8886
8850
  }
@@ -9192,7 +9156,7 @@ class Roster {
9192
9156
  if (!verify(invite.invitePub, DOMAIN.invite, redeemClaim(this.team, inviteId, member), proof)) {
9193
9157
  throw new MessagingError("invite_invalid", "invite proof does not verify for this member");
9194
9158
  }
9195
- if (member.person !== invite.person || member.role !== invite.role) {
9159
+ if (member.person !== invite.person || member.role !== "member") {
9196
9160
  throw new MessagingError("invite_invalid", "member does not match the invite");
9197
9161
  }
9198
9162
  this.#requireNew(member);
@@ -9328,14 +9292,12 @@ function createInvite(roster, identity, args) {
9328
9292
  inviteId,
9329
9293
  secret,
9330
9294
  person: args.person,
9331
- role: args.role,
9332
9295
  expires
9333
9296
  });
9334
9297
  const entry = createEntry(roster, identity, "invite.create", {
9335
9298
  inviteId,
9336
9299
  invitePub: rawPublicKey(seedKey(secret)),
9337
9300
  person: args.person,
9338
- role: args.role,
9339
9301
  expires
9340
9302
  }, now);
9341
9303
  return { entry, ticket, secret };
@@ -9357,7 +9319,7 @@ function decodeTicket(text) {
9357
9319
  function createRedeem(roster, ticket, identity, args) {
9358
9320
  if (ticket.team !== roster.team)
9359
9321
  throw new MessagingError("invite_invalid", "ticket is for another team");
9360
- const member = { machine: identity.machine, enc: identity.enc, person: ticket.person, role: ticket.role, label: args.label };
9322
+ const member = { machine: identity.machine, enc: identity.enc, person: ticket.person, role: "member", label: args.label };
9361
9323
  const proof = signWith(seedKey(ticket.secret), DOMAIN.invite, redeemClaim(roster.team, ticket.inviteId, member));
9362
9324
  return createEntry(roster, identity, "invite.redeem", { inviteId: ticket.inviteId, proof, member }, args.now);
9363
9325
  }
@@ -9970,6 +9932,13 @@ class DeviceLog {
9970
9932
  token(seq) {
9971
9933
  return this.#state.tokens.get(seq);
9972
9934
  }
9935
+ spentSignIn(jti) {
9936
+ for (const [seq, token] of this.#state.tokens) {
9937
+ if (token.claims.src === JELLY_DEVICE_SOURCE && token.claims.jti === jti && this.#state.entries[seq].op !== "unbind")
9938
+ return true;
9939
+ }
9940
+ return false;
9941
+ }
9973
9942
  bindingDeadline(seq) {
9974
9943
  const token = this.#state.tokens.get(seq);
9975
9944
  return token === undefined ? undefined : token.claims.exp * 1000 + LIMITS.clockSkewMs;
@@ -10022,6 +9991,14 @@ class DeviceLog {
10022
9991
  const { claims } = token;
10023
9992
  if (personId(claims.iss, claims.sub) !== this.person)
10024
9993
  throw refuse("Jelly token names another person");
9994
+ if (claims.src === JELLY_DEVICE_SOURCE) {
9995
+ for (const [seq, used] of state.tokens) {
9996
+ if (used.claims.src !== JELLY_DEVICE_SOURCE || used.claims.jti !== claims.jti)
9997
+ continue;
9998
+ if (parsed.op !== "unbind" || state.entries[seq].op !== "unbind")
9999
+ throw refuse("a sign-in's token vouches for one act: this one was used");
10000
+ }
10001
+ }
10025
10002
  if (parsed.ts < claims.iat * 1000 - LIMITS.clockSkewMs || parsed.ts > claims.exp * 1000 + LIMITS.clockSkewMs)
10026
10003
  throw refuse("log entry is dated outside its Jelly token's life");
10027
10004
  tokens.set(parsed.seq, token);
@@ -10106,6 +10083,7 @@ var init_device_log = __esm(() => {
10106
10083
  init_keys();
10107
10084
  init_sign();
10108
10085
  init_common();
10086
+ init_jelly();
10109
10087
  init_directory();
10110
10088
  init_errors3();
10111
10089
  init_jelly_token();
@@ -10167,6 +10145,8 @@ function page(url, issuer) {
10167
10145
  }
10168
10146
  if (at.origin !== new URL(issuer).origin)
10169
10147
  throw new MessagingError("jelly_unavailable", "Jelly ID's sign-in page is not on its own origin");
10148
+ if (at.search !== "" || at.hash !== "" || at.username !== "" || at.password !== "")
10149
+ throw new MessagingError("jelly_unavailable", "Jelly ID's sign-in page carries a query or credentials");
10170
10150
  return at.href;
10171
10151
  }
10172
10152
  function readDeviceCodeAnswer(status, text, issuer) {
@@ -10175,9 +10155,7 @@ function readDeviceCodeAnswer(status, text, issuer) {
10175
10155
  if (!parsed.success)
10176
10156
  throw new MessagingError("jelly_unavailable", "Jelly ID's sign-in code could not be read");
10177
10157
  const a = parsed.data;
10178
- const complete = page(a.verification_uri_complete, issuer);
10179
- const plain = page(a.verification_uri, issuer);
10180
- return { deviceCode: a.device_code, userCode: a.user_code, verificationUri: plain, verificationUriComplete: complete, expiresInS: a.expires_in, intervalS: a.interval ?? LOGIN_INTERVAL_DEFAULT_S };
10158
+ return { deviceCode: a.device_code, userCode: a.user_code, verificationUri: page(a.verification_uri, issuer), expiresInS: a.expires_in, intervalS: a.interval };
10181
10159
  }
10182
10160
  const answer = errorOf(text);
10183
10161
  switch (answer?.error) {
@@ -10217,8 +10195,6 @@ function readDevicePoll(status, text) {
10217
10195
  case "invalid_client":
10218
10196
  case "unsupported_grant_type":
10219
10197
  throw unsupported("it refused this device client");
10220
- case "invalid_request":
10221
- throw new MessagingError("invalid_request", "Jelly ID refused the poll as malformed");
10222
10198
  default:
10223
10199
  return { kind: "retry" };
10224
10200
  }
@@ -10236,7 +10212,7 @@ function deviceCodeForm(args) {
10236
10212
  function devicePollForm(args) {
10237
10213
  return new URLSearchParams({ grant_type: DEVICE_GRANT_TYPE, device_code: args.deviceCode, client_id: args.clientId });
10238
10214
  }
10239
- var DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code", LOGIN_SLOW_DOWN_S = 5, LOGIN_INTERVAL_DEFAULT_S = 5, LOGIN_INTERVAL_MAX_S = 60, JELLY_ANSWER_BYTES = 65536, ResourceName, Metadata, unsupported = (why) => new MessagingError("login_unsupported", `Jelly ID does not offer device sign-in yet: ${why}`), CodeAnswer, ErrorAnswer, TokenAnswer;
10215
+ var DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code", LOGIN_SLOW_DOWN_S = 5, LOGIN_INTERVAL_MAX_S = 60, JELLY_ANSWER_BYTES = 65536, ResourceName, Metadata, unsupported = (why) => new MessagingError("login_unsupported", `Jelly ID does not offer device sign-in yet: ${why}`), CodeAnswer, ErrorAnswer, TokenAnswer;
10240
10216
  var init_jelly_device = __esm(() => {
10241
10217
  init_b64u();
10242
10218
  init_keys();
@@ -10251,12 +10227,11 @@ var init_jelly_device = __esm(() => {
10251
10227
  jelly_resource_device_token_endpoint: string2().max(2048).optional()
10252
10228
  });
10253
10229
  CodeAnswer = looseObject({
10254
- device_code: string2().min(1).max(512),
10230
+ device_code: string2().min(22).max(256),
10255
10231
  user_code: string2().regex(/^[A-HJ-NP-Z2-9]{5}-[A-HJ-NP-Z2-9]{5}$/),
10256
10232
  verification_uri: string2().max(2048),
10257
- verification_uri_complete: string2().max(2048),
10258
10233
  expires_in: number2().int().positive().max(3600),
10259
- interval: number2().int().positive().max(LOGIN_INTERVAL_MAX_S).optional()
10234
+ interval: number2().int().positive().max(LOGIN_INTERVAL_MAX_S)
10260
10235
  });
10261
10236
  ErrorAnswer = looseObject({ error: string2().max(64), detail: string2().max(64).optional() });
10262
10237
  TokenAnswer = looseObject({ access_token: string2().min(1).max(8192), token_type: string2().max(32) });
@@ -10655,6 +10630,31 @@ function verifyPersonView(view, context) {
10655
10630
  throw asMessagingError(error, "person view");
10656
10631
  }
10657
10632
  }
10633
+ function checkPersonSuccession(earlier, later) {
10634
+ if (earlier.person !== later.person)
10635
+ throw new MessagingError("invalid_request", "the two states are of two persons");
10636
+ if (earlier.erased || later.erased)
10637
+ return;
10638
+ const ours = earlier.log.entries();
10639
+ const theirs = later.log.entries();
10640
+ if (theirs.length < ours.length)
10641
+ throw new DirectoryInconsistency("a later tree head serves a shorter log", { kind: "rollback", what: "log", ours: ours.length, theirs: theirs.length });
10642
+ for (let seq = 0;seq < ours.length; seq++) {
10643
+ if (logEntryHash(ours[seq]) !== logEntryHash(theirs[seq]))
10644
+ throw new DirectoryInconsistency("the log differs from the one held", { kind: "fork", seq, ours: ours[seq], theirs: theirs[seq] });
10645
+ }
10646
+ for (const [device, statement] of earlier.keys) {
10647
+ if (!later.log.device(device)?.current)
10648
+ continue;
10649
+ const next = later.keys.get(device);
10650
+ if (!next)
10651
+ throw new DirectoryInconsistency("a held keys statement is withheld", { kind: "rollback", what: "keys", ours: statement.seq, theirs: -1 });
10652
+ if (next.seq < statement.seq)
10653
+ throw new DirectoryInconsistency("a keys statement is older than the one held", { kind: "rollback", what: "keys", ours: statement.seq, theirs: next.seq });
10654
+ if (next.seq === statement.seq && keysStatementHash(next) !== keysStatementHash(statement))
10655
+ throw fail("two keys statements share a device and seq");
10656
+ }
10657
+ }
10658
10658
  function checkWitness(witness, seq, deadline, log, person, sth) {
10659
10659
  const epoch = verifyEpochProof(sth, witness.epoch);
10660
10660
  const committed = witness.value.log;
@@ -10665,6 +10665,33 @@ function checkWitness(witness, seq, deadline, log, person, sth) {
10665
10665
  if (epoch.leaf.ts > deadline)
10666
10666
  throw new MessagingError("binding_invalid", "a Jelly binding first appeared after its token's life");
10667
10667
  }
10668
+ function verifyPersonHead(view, context) {
10669
+ try {
10670
+ const parsed = parseWith(PersonHead, view, "person head");
10671
+ const { sth } = context;
10672
+ const head = verifyEpochProof(sth, context.head);
10673
+ if (head.index !== sth.size - 1)
10674
+ throw fail("the head epoch is not the tree head's last");
10675
+ const mapRoot = head.leaf.mapRoot;
10676
+ if (parsed.tombstone) {
10677
+ if (parsed.value)
10678
+ throw new MessagingError("invalid_request", "an erased person has no head");
10679
+ if (!verifyMapProof(mapRoot, parsed.person, TOMBSTONE, parsed.mapProof))
10680
+ throw new DirectoryInconsistency("the map does not hold the tombstone", { kind: "map", sth, head });
10681
+ return { person: parsed.person, state: "erased" };
10682
+ }
10683
+ if (!parsed.value) {
10684
+ if (!verifyMapProof(mapRoot, parsed.person, undefined, parsed.mapProof))
10685
+ throw new DirectoryInconsistency("the map holds a person served as absent", { kind: "map", sth, head });
10686
+ return { person: parsed.person, state: "absent" };
10687
+ }
10688
+ if (!verifyMapProof(mapRoot, parsed.person, mapValue(parsed.value), parsed.mapProof))
10689
+ throw new DirectoryInconsistency("the map does not commit to the head served", { kind: "map", sth, head });
10690
+ return { person: parsed.person, state: "live", log: parsed.value.log, keys: parsed.value.keys };
10691
+ } catch (error) {
10692
+ throw asMessagingError(error, "person head");
10693
+ }
10694
+ }
10668
10695
  var DirectoryInconsistency, fail = (what) => new MessagingError("directory_inconsistent", what);
10669
10696
  var init_person_view = __esm(() => {
10670
10697
  init_check();
@@ -11016,8 +11043,6 @@ function grant(identity, args) {
11016
11043
  const raw = args.parent;
11017
11044
  const parent = raw === undefined ? undefined : parseWith(DelegationGrant, raw, "parent grant");
11018
11045
  if (parent !== undefined) {
11019
- if (args.external !== undefined)
11020
- throw new MessagingError("invalid_request", "only a root grant names an external principal");
11021
11046
  if (parent.to !== args.from)
11022
11047
  throw new MessagingError("invalid_request", "a grant extends the grant to its delegating session");
11023
11048
  }
@@ -11042,8 +11067,7 @@ function grant(identity, args) {
11042
11067
  exp,
11043
11068
  maxDepth,
11044
11069
  ...scope !== undefined ? { scope: [...scope] } : {},
11045
- ...raw !== undefined ? { parent: grantHash(raw) } : {},
11046
- ...args.external !== undefined ? { external: { principal: args.external.principal } } : {}
11070
+ ...raw !== undefined ? { parent: grantHash(raw) } : {}
11047
11071
  };
11048
11072
  parseWith(DelegationGrant.omit({ sig: true }), unsigned, "delegation grant");
11049
11073
  return { ...unsigned, sig: sign(identity, DOMAIN.delegation, unsigned) };
@@ -11065,7 +11089,7 @@ function check2(raw, ctx) {
11065
11089
  throw new MessagingError("too_large", `a delegation chain is limited to ${LIMITS.delegationBytes} bytes`);
11066
11090
  let maxDepth = LIMITS.delegationDepth;
11067
11091
  let previous;
11068
- for (const [index, grant] of grants.entries()) {
11092
+ for (const grant of grants) {
11069
11093
  const { sig, ...unsigned } = grant;
11070
11094
  const input = signingInput(DOMAIN.delegation, unsigned);
11071
11095
  if (grant.realm !== realm.realm)
@@ -11087,9 +11111,6 @@ function check2(raw, ctx) {
11087
11111
  }
11088
11112
  if (!verifyInput(grant.by, input, sig))
11089
11113
  throw new MessagingError("bad_signature", "a grant signature is invalid");
11090
- if (grant.external !== undefined && (index > 0 || signer.role !== "service")) {
11091
- throw new MessagingError("forbidden", "only a service member's root grant names an external principal");
11092
- }
11093
11114
  if (previous === undefined) {
11094
11115
  if (grant.parent !== undefined)
11095
11116
  throw new MessagingError("invalid_request", "a root grant has no parent");
@@ -11118,9 +11139,6 @@ function check2(raw, ctx) {
11118
11139
  if (last.by !== hop.by || last.from !== hop.from || last.to !== hop.to || last.task !== hop.task) {
11119
11140
  throw new MessagingError("forbidden", "the chain does not authorize this request");
11120
11141
  }
11121
- const external = grants.length === 1 ? root.external?.principal : undefined;
11122
- if (external !== hop.external)
11123
- throw new MessagingError("forbidden", "the chain's external principal does not match the request");
11124
11142
  return {
11125
11143
  grants: structuredClone(grants),
11126
11144
  summary: {
@@ -11128,7 +11146,6 @@ function check2(raw, ctx) {
11128
11146
  depth: grants.length,
11129
11147
  maxDepth,
11130
11148
  ...last.scope !== undefined ? { scope: [...last.scope] } : {},
11131
- ...root.external !== undefined ? { external: { principal: root.external.principal, gateway: root.by } } : {},
11132
11149
  exp: Math.min(...grants.map((grant) => grant.exp))
11133
11150
  }
11134
11151
  };
@@ -11190,103 +11207,8 @@ var init_task = __esm(() => {
11190
11207
  };
11191
11208
  });
11192
11209
 
11193
- // packages/core/dist/direct.js
11194
- import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, createHash as createHash7, diffieHellman as diffieHellman2, generateKeyPairSync as generateKeyPairSync3, hkdfSync as hkdfSync2, randomBytes as randomBytes4 } from "crypto";
11195
- function directEphemeral() {
11196
- const key = generateKeyPairSync3("x25519").privateKey;
11197
- return { key, half: { epk: rawPublicKey(key), nonce: b64u.encode(randomBytes4(32)) } };
11198
- }
11199
- function directTranscript(initiator, responder) {
11200
- return { proto: DIRECT_PROTOCOL, initiator: { epk: initiator.epk, nonce: initiator.nonce }, responder: { epk: responder.epk, nonce: responder.nonce } };
11201
- }
11202
- function directAuthPayload(args) {
11203
- const { transcript, team, role, machine, peer } = args;
11204
- if (role === "initiator" !== (peer !== undefined))
11205
- throw new MessagingError("invalid_request", "only the initiator names its peer");
11206
- return { ...transcript, team, role, machine, ...peer === undefined ? {} : { peer } };
11207
- }
11208
- function directKeys(own, peerEpk, transcript) {
11209
- let shared;
11210
- try {
11211
- shared = diffieHellman2({ privateKey: own, publicKey: publicKeyFromRaw("x25519", peerEpk) });
11212
- } catch {
11213
- throw new MessagingError("bad_signature", "the peer's handshake key is not usable");
11214
- }
11215
- if (shared.every((byte) => byte === 0))
11216
- throw new MessagingError("bad_signature", "the peer's handshake key is not usable");
11217
- const salt = createHash7("sha256").update(canonicalize(transcript)).digest();
11218
- const okm = new Uint8Array(hkdfSync2("sha256", shared, salt, DOMAIN.directLink, 128));
11219
- return {
11220
- box: { responder: okm.slice(0, 32), initiator: okm.slice(32, 64) },
11221
- record: { initiator: okm.slice(64, 96), responder: okm.slice(96, 128) }
11222
- };
11223
- }
11224
- function sealDirectBox(key, transcript, identity) {
11225
- const cipher = createCipheriv2("aes-256-gcm", key, ZERO_NONCE, { authTagLength: TAG_BYTES2 });
11226
- cipher.setAAD(utf8(canonicalize(transcript)));
11227
- return b64u.encode(Buffer.concat([cipher.update(utf8(canonicalize(identity))), cipher.final(), cipher.getAuthTag()]));
11228
- }
11229
- function openDirectBox(key, transcript, box) {
11230
- let plaintext;
11231
- try {
11232
- const bytes = b64u.decode(box);
11233
- if (bytes.length <= TAG_BYTES2)
11234
- throw new Error("short");
11235
- const decipher = createDecipheriv2("aes-256-gcm", key, ZERO_NONCE, { authTagLength: TAG_BYTES2 });
11236
- decipher.setAAD(utf8(canonicalize(transcript)));
11237
- decipher.setAuthTag(bytes.subarray(bytes.length - TAG_BYTES2));
11238
- plaintext = Buffer.concat([decipher.update(bytes.subarray(0, bytes.length - TAG_BYTES2)), decipher.final()]);
11239
- } catch {
11240
- throw new MessagingError("bad_signature", "the handshake box does not open");
11241
- }
11242
- return parseWire(plaintext.toString("utf8"), { maxBytes: DIRECT_HANDSHAKE_BYTES });
11243
- }
11244
-
11245
- class DirectCipher {
11246
- #key;
11247
- #counter = 0;
11248
- constructor(key) {
11249
- if (key.length !== 32)
11250
- throw new MessagingError("invalid_request", "a record key is 32 bytes");
11251
- this.#key = key;
11252
- }
11253
- seal(plaintext) {
11254
- const cipher = createCipheriv2("aes-256-gcm", this.#key, this.#nonce(), { authTagLength: TAG_BYTES2 });
11255
- return Buffer.concat([cipher.update(plaintext), cipher.final(), cipher.getAuthTag()]);
11256
- }
11257
- open(record) {
11258
- try {
11259
- if (record.length < TAG_BYTES2)
11260
- throw new Error("short");
11261
- const decipher = createDecipheriv2("aes-256-gcm", this.#key, this.#nonce(), { authTagLength: TAG_BYTES2 });
11262
- decipher.setAuthTag(record.subarray(record.length - TAG_BYTES2));
11263
- return Buffer.concat([decipher.update(record.subarray(0, record.length - TAG_BYTES2)), decipher.final()]);
11264
- } catch {
11265
- throw new MessagingError("bad_signature", "a direct record does not open");
11266
- }
11267
- }
11268
- #nonce() {
11269
- if (this.#counter >= Number.MAX_SAFE_INTEGER)
11270
- throw new MessagingError("internal", "record counter exhausted");
11271
- const nonce = Buffer.alloc(NONCE_BYTES2);
11272
- nonce.writeBigUInt64BE(BigInt(this.#counter++), 4);
11273
- return nonce;
11274
- }
11275
- }
11276
- var NONCE_BYTES2 = 12, TAG_BYTES2 = 16, ZERO_NONCE;
11277
- var init_direct2 = __esm(() => {
11278
- init_b64u();
11279
- init_jcs();
11280
- init_keys();
11281
- init_common();
11282
- init_direct();
11283
- init_errors3();
11284
- init_wire();
11285
- ZERO_NONCE = new Uint8Array(NONCE_BYTES2);
11286
- });
11287
-
11288
11210
  // packages/core/dist/peer-text.js
11289
- import { randomBytes as randomBytes5 } from "crypto";
11211
+ import { randomBytes as randomBytes4 } from "crypto";
11290
11212
  function addReading(code, letters) {
11291
11213
  const c = String.fromCodePoint(code);
11292
11214
  READINGS.set(c, (READINGS.get(c) ?? "") + letters);
@@ -11523,7 +11445,7 @@ function createFence(options) {
11523
11445
  if (!TAG.test(tag))
11524
11446
  throw new Error("fence tag must be lowercase words joined by hyphens");
11525
11447
  const stem = tag.split("-")[0];
11526
- const random = options.random ?? randomBytes5;
11448
+ const random = options.random ?? randomBytes4;
11527
11449
  const opened = new RegExp(`<${tag}-[${NONCE_ALPHABET}]{${NONCE_LENGTH}}[ >]`);
11528
11450
  const neutralize = (text, clean = {}) => render(text, { singleLine: clean.singleLine === true, maxChars: clean.maxChars, terminal: false, attribute: false, stem });
11529
11451
  function fence(body, attributes = {}, clean = {}, block) {
@@ -12645,7 +12567,6 @@ var init_agent_tools = __esm(() => {
12645
12567
  at: iso,
12646
12568
  parts: array(Part2),
12647
12569
  verified: _enum(["signature", "none"]).optional().describe("signature: the daemon checked the sender's signature"),
12648
- external: fenced("An outside A2A principal it came from, through the gateway").optional(),
12649
12570
  replyTo: string2().optional(),
12650
12571
  chain: Chain.optional(),
12651
12572
  task: Task2.optional(),
@@ -12775,7 +12696,6 @@ var init_dist = __esm(() => {
12775
12696
  init_delegation2();
12776
12697
  init_task();
12777
12698
  init_a2a();
12778
- init_direct2();
12779
12699
  init_peer_text();
12780
12700
  init_agent_tools();
12781
12701
  init_operations();
@@ -12903,13 +12823,13 @@ var init_paths = __esm(() => {
12903
12823
  });
12904
12824
 
12905
12825
  // packages/ipc/dist/pipes.js
12906
- import { randomBytes as randomBytes6 } from "crypto";
12826
+ import { randomBytes as randomBytes5 } from "crypto";
12907
12827
  import { readFileSync, renameSync, rmSync, writeFileSync } from "fs";
12908
12828
  function newPipeName(tier) {
12909
- return `\\\\.\\pipe\\messaging-${randomBytes6(16).toString("hex")}-${tier}`;
12829
+ return `\\\\.\\pipe\\messaging-${randomBytes5(16).toString("hex")}-${tier}`;
12910
12830
  }
12911
12831
  function writePointer(path, pointer) {
12912
- const temp = `${path}.${process.pid}.${randomBytes6(4).toString("hex")}.tmp`;
12832
+ const temp = `${path}.${process.pid}.${randomBytes5(4).toString("hex")}.tmp`;
12913
12833
  try {
12914
12834
  writeFileSync(temp, `${JSON.stringify(pointer)}
12915
12835
  `, { flag: "wx" });
@@ -12965,7 +12885,7 @@ var init_rpc = __esm(() => {
12965
12885
 
12966
12886
  // packages/ipc/dist/windows-acl.js
12967
12887
  import { spawnSync } from "child_process";
12968
- import { randomBytes as randomBytes7 } from "crypto";
12888
+ import { randomBytes as randomBytes6 } from "crypto";
12969
12889
  import { existsSync, readFileSync as readFileSync2, rmSync as rmSync2 } from "fs";
12970
12890
  import { tmpdir } from "os";
12971
12891
  import { join as join2 } from "path";
@@ -13080,7 +13000,7 @@ function currentUserSid() {
13080
13000
  return sid;
13081
13001
  }
13082
13002
  function readDacl(path) {
13083
- const out = join2(tmpdir(), `messaging-acl-${process.pid}-${randomBytes7(8).toString("hex")}.txt`);
13003
+ const out = join2(tmpdir(), `messaging-acl-${process.pid}-${randomBytes6(8).toString("hex")}.txt`);
13084
13004
  try {
13085
13005
  run("icacls.exe", [path, "/save", out, "/q"]);
13086
13006
  return parseSddlDacl(sddlLine(readFileSync2(out)));
@@ -13859,85 +13779,6 @@ var init_dist2 = __esm(() => {
13859
13779
  init_windows_acl();
13860
13780
  });
13861
13781
 
13862
- // packages/daemon/dist/direct-config.js
13863
- import { BlockList, isIP } from "net";
13864
- function parseHostPort(text) {
13865
- const match = /^\[([0-9A-Fa-f:.]+)\](?::(\d{1,5}))?$/.exec(text) ?? /^([0-9.]+)(?::(\d{1,5}))?$/.exec(text);
13866
- if (!match)
13867
- return;
13868
- const host = match[1];
13869
- const family = isIP(host);
13870
- if (family === 0 || family === 6 !== text.startsWith("["))
13871
- return;
13872
- const port = match[2] === undefined ? undefined : Number(match[2]);
13873
- if (port !== undefined && port > 65535)
13874
- return;
13875
- return { host, port };
13876
- }
13877
- function family(host) {
13878
- const f = isIP(host);
13879
- return f === 4 ? "ipv4" : f === 6 ? "ipv6" : undefined;
13880
- }
13881
- function isLoopback(host) {
13882
- const f = family(host);
13883
- return f !== undefined && LOOPBACK.check(host, f);
13884
- }
13885
- function isWildcard(host) {
13886
- return host === "0.0.0.0" || host === "::";
13887
- }
13888
- function isDirectHost(host) {
13889
- const f = family(host);
13890
- return f !== undefined && (LOOPBACK.check(host, f) || LAN.check(host, f));
13891
- }
13892
- function mayDial(endpoint, ownListenerOnLoopback) {
13893
- if (!isDirectHost(endpoint.host))
13894
- return false;
13895
- return !isLoopback(endpoint.host) || ownListenerOnLoopback;
13896
- }
13897
- function advertisedEndpoints(config, bound) {
13898
- if (config.advertise === undefined)
13899
- return isWildcard(bound.host) ? [] : [{ host: bound.host, port: bound.port }];
13900
- return config.advertise.map((text) => {
13901
- const parsed = parseHostPort(text);
13902
- return { host: parsed.host, port: parsed.port ?? bound.port };
13903
- });
13904
- }
13905
- var DEFAULT_DIRECT_LISTEN = "127.0.0.1:0", LOOPBACK, LAN, ListenAddress, AdvertiseAddress, DirectConfig;
13906
- var init_direct_config = __esm(() => {
13907
- init_zod();
13908
- LOOPBACK = new BlockList;
13909
- LOOPBACK.addSubnet("127.0.0.0", 8, "ipv4");
13910
- LOOPBACK.addAddress("::1", "ipv6");
13911
- LAN = new BlockList;
13912
- for (const [net, prefix] of [
13913
- ["10.0.0.0", 8],
13914
- ["172.16.0.0", 12],
13915
- ["192.168.0.0", 16],
13916
- ["100.64.0.0", 10],
13917
- ["169.254.0.0", 16]
13918
- ]) {
13919
- LAN.addSubnet(net, prefix, "ipv4");
13920
- }
13921
- LAN.addSubnet("fe80::", 10, "ipv6");
13922
- LAN.addSubnet("fc00::", 7, "ipv6");
13923
- ListenAddress = string2().refine((text) => {
13924
- const parsed = parseHostPort(text);
13925
- return parsed?.port !== undefined && (isWildcard(parsed.host) || isDirectHost(parsed.host));
13926
- }, "listen must be ip:port (IPv6 in brackets) on loopback, a private or link-local address, or all interfaces");
13927
- AdvertiseAddress = string2().refine((text) => {
13928
- const parsed = parseHostPort(text);
13929
- return parsed !== undefined && parsed.port !== 0 && isDirectHost(parsed.host);
13930
- }, "advertise entries must be ip or ip:port (IPv6 in brackets) on loopback or a private or link-local address");
13931
- DirectConfig = object({
13932
- enabled: boolean2().optional(),
13933
- listen: ListenAddress.optional(),
13934
- advertise: array(AdvertiseAddress).min(1).max(4).optional()
13935
- }).strict().refine((c) => c.enabled !== true || c.advertise !== undefined || !isWildcard(parseHostPort(c.listen ?? DEFAULT_DIRECT_LISTEN).host), {
13936
- message: "advertise is required when listening on all interfaces",
13937
- path: ["advertise"]
13938
- });
13939
- });
13940
-
13941
13782
  // packages/daemon/dist/identity/record.js
13942
13783
  import { createPrivateKey as createPrivateKey2 } from "crypto";
13943
13784
  import { closeSync as closeSync2, constants as constants3, existsSync as existsSync3, fstatSync, fsyncSync, linkSync, readFileSync as readFileSync4, renameSync as renameSync2, rmSync as rmSync3, statSync, unlinkSync, writeSync } from "fs";
@@ -14214,7 +14055,6 @@ var init_home = __esm(() => {
14214
14055
  init_zod();
14215
14056
  init_dist();
14216
14057
  init_dist2();
14217
- init_direct_config();
14218
14058
  init_record();
14219
14059
  PRESENCE_FIELDS = ["repo", "branch", "doing"];
14220
14060
  PresenceField = _enum(PRESENCE_FIELDS);
@@ -14225,8 +14065,6 @@ var init_home = __esm(() => {
14225
14065
  label: MachineLabel.optional(),
14226
14066
  realm: DirectoryRealmConfig.strict().optional(),
14227
14067
  inbound: object({
14228
- refuse: array(PersonName).max(0, { error: 'inbound.refuse was replaced by block grants (spec \xA76.9): remove it, and block each person with grant.set {subject: {person}, chat: "blocked"}' }).optional(),
14229
- wake: literal("team", { error: "inbound.wake was removed: a push-tier session may be woken for what consent delivers to it; register sessions with tier pull to never wake them (spec \xA78.3)" }).optional(),
14230
14068
  quota: object({ items: number2().int().min(1), bytes: number2().int().min(1) }).partial().strict().optional()
14231
14069
  }).strict().optional(),
14232
14070
  presence: object({ share: array(PresenceField).max(PRESENCE_FIELDS.length) }).strict().optional(),
@@ -14240,7 +14078,6 @@ var init_home = __esm(() => {
14240
14078
  transcriptDays: number2().int().min(1).max(36500)
14241
14079
  }).partial().strict().optional(),
14242
14080
  ipc: object({ agentGroup: number2().int().min(0).max(4294967294) }).partial().strict().optional(),
14243
- transport: object({ direct: DirectConfig.optional() }).strict().optional(),
14244
14081
  identity: object({
14245
14082
  store: _enum(["auto", "file", "keychain"]),
14246
14083
  prompt: boolean2(),
@@ -14828,8 +14665,6 @@ function machineLoopGuard() {
14828
14665
  function inboundPolicy(facts, state, now) {
14829
14666
  if (!facts.recipient)
14830
14667
  return "unknown_session";
14831
- if (facts.recipient.agent === GATEWAY_AGENT)
14832
- return "refused";
14833
14668
  if (facts.task && facts.recipient.agent === CLI_AGENT)
14834
14669
  return "refused";
14835
14670
  const agents = facts.sender.agent !== CLI_AGENT && facts.recipient.agent !== CLI_AGENT;
@@ -14840,8 +14675,6 @@ ${facts.recipient.session}`, now))
14840
14675
  if (agents && !state.machineLoop.take(`${facts.sender.machine}
14841
14676
  ${facts.recipient.session}`, now))
14842
14677
  return "loop_guard";
14843
- if (facts.external && facts.sender.role !== "service")
14844
- return "forbidden";
14845
14678
  return;
14846
14679
  }
14847
14680
  function isFinalStage(stage) {
@@ -14857,7 +14690,7 @@ function canAdvance(from, to) {
14857
14690
  function inboxAdvances(from, to) {
14858
14691
  return INBOX_RANK[to] > INBOX_RANK[from];
14859
14692
  }
14860
- var RESPONSE_RATE_FACTOR = 4, CHAT_SEND_PER_MACHINE_PER_MINUTE, DEFER_MARGIN_MS = 60000, RESPONSE_TYPES, LOOP_MACHINE_FACTOR = 2, GATEWAY_AGENT = "a2a-gateway", STAGE_RANK, INBOX_RANK;
14693
+ var RESPONSE_RATE_FACTOR = 4, CHAT_SEND_PER_MACHINE_PER_MINUTE, DEFER_MARGIN_MS = 60000, RESPONSE_TYPES, LOOP_MACHINE_FACTOR = 2, STAGE_RANK, INBOX_RANK;
14861
14694
  var init_policy = __esm(() => {
14862
14695
  init_dist();
14863
14696
  CHAT_SEND_PER_MACHINE_PER_MINUTE = LIMITS.inboundPerMachinePerMinute - 10;
@@ -14877,7 +14710,7 @@ var init_policy = __esm(() => {
14877
14710
  });
14878
14711
 
14879
14712
  // packages/daemon/dist/chat.js
14880
- import { createHash as createHash8 } from "crypto";
14713
+ import { createHash as createHash7 } from "crypto";
14881
14714
 
14882
14715
  class Chats {
14883
14716
  #host;
@@ -14952,7 +14785,7 @@ class Chats {
14952
14785
  }
14953
14786
  send(p, session, scope = "") {
14954
14787
  const room = p.conversation;
14955
- const digest = p.clientId === undefined ? undefined : createHash8("sha256").update(JSON.stringify([room, p.thread ?? "", p.text])).digest("base64url");
14788
+ const digest = p.clientId === undefined ? undefined : createHash7("sha256").update(JSON.stringify([room, p.thread ?? "", p.text])).digest("base64url");
14956
14789
  if (p.clientId !== undefined) {
14957
14790
  const earlier = this.#store.chatSend(scope, p.clientId);
14958
14791
  if (earlier) {
@@ -15349,7 +15182,7 @@ var init_chat = __esm(() => {
15349
15182
  });
15350
15183
 
15351
15184
  // packages/daemon/dist/log.js
15352
- import { closeSync as closeSync5, constants as constants5, lstatSync as lstatSync3, renameSync as renameSync6, writeSync as writeSync2 } from "fs";
15185
+ import { closeSync as closeSync5, constants as constants5, lstatSync as lstatSync3, renameSync as renameSync6, writeSync as writeSync3 } from "fs";
15353
15186
  function formatLine(level, event, fields = {}) {
15354
15187
  const record = { t: new Date().toISOString(), level, event };
15355
15188
  for (const [name, value] of Object.entries(fields))
@@ -15378,7 +15211,7 @@ function createLogger(options) {
15378
15211
  }
15379
15212
  const fd = openNoFollow(file, constants5.O_WRONLY | constants5.O_APPEND | constants5.O_CREAT, 384);
15380
15213
  try {
15381
- writeSync2(fd, line);
15214
+ writeSync3(fd, line);
15382
15215
  } finally {
15383
15216
  closeSync5(fd);
15384
15217
  }
@@ -15445,9 +15278,9 @@ var init_log = __esm(() => {
15445
15278
  });
15446
15279
 
15447
15280
  // packages/daemon/dist/keyring.js
15448
- import { createHash as createHash9, generateKeyPairSync as generateKeyPairSync4 } from "crypto";
15281
+ import { createHash as createHash8, generateKeyPairSync as generateKeyPairSync3 } from "crypto";
15449
15282
  function rotationOffset(machine) {
15450
- return createHash9("sha256").update(machine).digest().readUInt32BE(0) % ROTATION_SPREAD_MS;
15283
+ return createHash8("sha256").update(machine).digest().readUInt32BE(0) % ROTATION_SPREAD_MS;
15451
15284
  }
15452
15285
  function rotationDue(now, rotatedAt, days, machine) {
15453
15286
  return days > 0 && now >= rotatedAt + days * DAY_MS + rotationOffset(machine);
@@ -15666,7 +15499,7 @@ class Keyring {
15666
15499
  if (kept.length >= MAX_RETAINED) {
15667
15500
  throw new MessagingError("invalid_state", "too many retained encryption keys; wait until the oldest expires, then rotate again");
15668
15501
  }
15669
- const next = withEncKey(identity, generateKeyPairSync4("x25519").privateKey);
15502
+ const next = withEncKey(identity, generateKeyPairSync3("x25519").privateKey);
15670
15503
  const pending = { identity: next, retained: [{ key: identity.encKey }, ...kept] };
15671
15504
  store.save(pending);
15672
15505
  this.#keys = pending;
@@ -15701,7 +15534,7 @@ var init_keyring = __esm(() => {
15701
15534
  });
15702
15535
 
15703
15536
  // packages/daemon/dist/directory.js
15704
- function presencePayload(rows, label, seq, extra, policy) {
15537
+ function presencePayload(rows, label, seq, policy) {
15705
15538
  const { share } = policy;
15706
15539
  const sessions = rows.slice(0, LIMITS.presenceSessions).map((s) => {
15707
15540
  const live = s.unregisteredAt === undefined;
@@ -15723,8 +15556,7 @@ function presencePayload(rows, label, seq, extra, policy) {
15723
15556
  seq,
15724
15557
  label,
15725
15558
  sessions: [],
15726
- features: [...FEATURES, ...extra?.features ?? []],
15727
- ...extra?.direct ? { direct: extra.direct } : {},
15559
+ features: [...FEATURES],
15728
15560
  ...policy.away === true ? { away: true } : {}
15729
15561
  };
15730
15562
  return { ...payload, sessions: fitPresence(sessions, jcsBytes(payload), LIMITS.presenceBytes) };
@@ -15898,10 +15730,9 @@ function signHop(args) {
15898
15730
  now,
15899
15731
  ...last !== undefined ? { parent: last } : {},
15900
15732
  ...scope !== undefined ? { scope } : {},
15901
- ...maxDepth !== undefined ? { maxDepth } : {},
15902
- ...request.external !== undefined ? { external: { principal: request.external.principal } } : {}
15733
+ ...maxDepth !== undefined ? { maxDepth } : {}
15903
15734
  });
15904
- const hop = { by: identity.machine, from, to: args.to, task: args.task, ...request.external !== undefined ? { external: request.external.principal } : {} };
15735
+ const hop = { by: identity.machine, from, to: args.to, task: args.task };
15905
15736
  return verifyChain([...args.parent ?? [], grant], { realm, now, hop });
15906
15737
  }
15907
15738
  function inboundChain(raw, ctx) {
@@ -15909,8 +15740,7 @@ function inboundChain(raw, ctx) {
15909
15740
  by: ctx.sender.machine,
15910
15741
  from: formatAddress(ctx.sender.person, ctx.fromSession, ctx.sender.label),
15911
15742
  to: formatAddress(ctx.me.person, ctx.toSession, ctx.me.label),
15912
- task: ctx.task,
15913
- ...ctx.external !== undefined ? { external: ctx.external.principal } : {}
15743
+ task: ctx.task
15914
15744
  };
15915
15745
  return verifyChain(raw, { realm: ctx.realm, now: ctx.now, hop });
15916
15746
  }
@@ -15952,14 +15782,16 @@ class RosterIdentity {
15952
15782
  #machine;
15953
15783
  #catchUp;
15954
15784
  #now;
15955
- #listeners = { "devices-changed": [], "self-changed": [], alarm: [] };
15785
+ #listeners = { "devices-changed": [], "self-changed": [], "device-log": [], alarm: [] };
15956
15786
  #me;
15787
+ #length;
15957
15788
  constructor(options) {
15958
15789
  this.roster = options.roster;
15959
15790
  this.#machine = options.machine;
15960
15791
  this.#catchUp = options.catchUp;
15961
15792
  this.#now = options.now;
15962
15793
  this.#me = this.#selfRecord();
15794
+ this.#length = options.roster.entries().length;
15963
15795
  }
15964
15796
  get realm() {
15965
15797
  return this.roster.realm;
@@ -15990,6 +15822,31 @@ class RosterIdentity {
15990
15822
  successor(_device) {
15991
15823
  return;
15992
15824
  }
15825
+ deviceChanges(person) {
15826
+ const labels = new Map;
15827
+ const out = [];
15828
+ for (const entry of this.roster.entries()) {
15829
+ const member = entry.kind === "member.add" ? entry.body : entry.kind === "invite.redeem" ? entry.body.member : entry.kind === "genesis" ? entry.body.founder : undefined;
15830
+ if (member) {
15831
+ labels.set(member.machine, member.label);
15832
+ if (member.person === person && entry.kind !== "genesis")
15833
+ out.push({ seq: entry.seq, change: "added", device: member.machine, label: member.label, vouch: "admin", by: entry.by, at: entry.ts });
15834
+ } else if (entry.kind === "member.remove") {
15835
+ const owner = this.#ownerAt(entry.body.machine);
15836
+ if (owner === person)
15837
+ 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 });
15838
+ }
15839
+ }
15840
+ return out;
15841
+ }
15842
+ #ownerAt(machine) {
15843
+ for (const entry of this.roster.entries()) {
15844
+ const member = entry.kind === "member.add" ? entry.body : entry.kind === "invite.redeem" ? entry.body.member : entry.kind === "genesis" ? entry.body.founder : undefined;
15845
+ if (member?.machine === machine)
15846
+ return member.person;
15847
+ }
15848
+ return;
15849
+ }
15993
15850
  async devices(person, opts = {}) {
15994
15851
  if (opts.atLeast !== undefined && this.relate(opts.atLeast).relation === "ahead") {
15995
15852
  throw new MessagingError("roster_conflict", "the roster has not reached that anchor");
@@ -16033,6 +15890,20 @@ class RosterIdentity {
16033
15890
  }
16034
15891
  for (const listener of [...this.#listeners["devices-changed"]])
16035
15892
  listener();
15893
+ const entries = this.roster.entries();
15894
+ if (entries.length !== this.#length) {
15895
+ const persons = new Set;
15896
+ for (const entry of entries.slice(this.#length)) {
15897
+ const member = entry.kind === "member.add" ? entry.body : entry.kind === "invite.redeem" ? entry.body.member : undefined;
15898
+ const person = member?.person ?? (entry.kind === "member.remove" ? this.#ownerAt(entry.body.machine) : undefined);
15899
+ if (person !== undefined)
15900
+ persons.add(person);
15901
+ }
15902
+ this.#length = entries.length;
15903
+ if (persons.size > 0)
15904
+ for (const listener of [...this.#listeners["device-log"]])
15905
+ listener([...persons].sort());
15906
+ }
16036
15907
  }
16037
15908
  #selfRecord() {
16038
15909
  const me = this.roster.member(this.#machine);
@@ -16134,7 +16005,6 @@ class DeviceLogin {
16134
16005
  const pending = {
16135
16006
  state: "pending",
16136
16007
  purpose: args.purpose,
16137
- verificationUriComplete: code.verificationUriComplete,
16138
16008
  verificationUri: code.verificationUri,
16139
16009
  userCode: code.userCode,
16140
16010
  fingerprint: jellyKeyFingerprint(device),
@@ -16239,7 +16109,7 @@ var init_device_login = __esm(() => {
16239
16109
  });
16240
16110
 
16241
16111
  // packages/daemon/dist/realm/directory-identity.js
16242
- import { randomBytes as randomBytes8 } from "crypto";
16112
+ import { randomBytes as randomBytes7 } from "crypto";
16243
16113
 
16244
16114
  class DirectoryIdentity {
16245
16115
  kind = "directory";
@@ -16248,16 +16118,18 @@ class DirectoryIdentity {
16248
16118
  #options;
16249
16119
  #cache;
16250
16120
  #keyring;
16251
- #listeners = { "devices-changed": [], "self-changed": [], alarm: [] };
16121
+ #listeners = { "devices-changed": [], "self-changed": [], "device-log": [], alarm: [] };
16252
16122
  #head;
16253
16123
  #persons = new Map;
16254
16124
  #index = new Map;
16255
16125
  #own;
16256
16126
  #relations = new Map;
16257
16127
  #watching = new Set;
16128
+ #watchRead = Promise.resolve();
16258
16129
  #alarm;
16259
16130
  #selfRecord;
16260
16131
  #monitoredAt = Number.NEGATIVE_INFINITY;
16132
+ #bindAccepted = false;
16261
16133
  #pendingLink;
16262
16134
  constructor(options) {
16263
16135
  this.#options = options;
@@ -16317,7 +16189,10 @@ class DirectoryIdentity {
16317
16189
  const person = this.#own.person;
16318
16190
  if (!person)
16319
16191
  return false;
16320
- const record = this.#persons.get(person)?.state.log.device(this.#machine());
16192
+ const held = this.#persons.get(person)?.state;
16193
+ if (held?.erased)
16194
+ return true;
16195
+ const record = held?.log.device(this.#machine());
16321
16196
  return record !== undefined && !record.current;
16322
16197
  }
16323
16198
  persons() {
@@ -16338,6 +16213,12 @@ class DirectoryIdentity {
16338
16213
  }
16339
16214
  return;
16340
16215
  }
16216
+ deviceChanges(person) {
16217
+ const held = isHosted(person) ? this.#persons.get(person) : undefined;
16218
+ if (!held || held.state.erased)
16219
+ return [];
16220
+ return deviceChangesOf(held.state.log);
16221
+ }
16341
16222
  async devices(person, opts = {}) {
16342
16223
  if (!isHosted(person))
16343
16224
  return { person, devices: [], anchor: this.anchor(), fetchedAt: this.#options.now() };
@@ -16432,15 +16313,23 @@ class DirectoryIdentity {
16432
16313
  }
16433
16314
  }
16434
16315
  watch(persons) {
16435
- let grew = false;
16316
+ const added = [];
16436
16317
  for (const person of persons) {
16437
16318
  if (!isHosted(person) || this.#watching.has(person) || this.#watching.size >= LIMITS.watchPersons)
16438
16319
  continue;
16439
16320
  this.#watching.add(person);
16440
- grew = true;
16321
+ added.push(person);
16441
16322
  }
16442
- if (grew)
16443
- this.#call((link) => link.watch([...this.#watching])).catch(() => {});
16323
+ if (added.length === 0)
16324
+ return this.#watchRead;
16325
+ this.#watchRead = this.#call((link) => link.watch([...this.#watching])).then(async () => {
16326
+ const held = added.filter((person) => this.#persons.has(person));
16327
+ if (held.length === 0)
16328
+ return;
16329
+ await this.#latest();
16330
+ await this.#fetch(held);
16331
+ }).catch(() => {});
16332
+ return this.#watchRead;
16444
16333
  }
16445
16334
  on(event, listener) {
16446
16335
  const listeners = this.#listeners[event];
@@ -16451,6 +16340,10 @@ class DirectoryIdentity {
16451
16340
  listeners.splice(at, 1);
16452
16341
  };
16453
16342
  }
16343
+ linkPerson() {
16344
+ const binding = this.binding();
16345
+ return binding.state !== "none" || this.#bindAccepted ? binding.person : undefined;
16346
+ }
16454
16347
  alarm() {
16455
16348
  return this.#alarm;
16456
16349
  }
@@ -16460,6 +16353,8 @@ class DirectoryIdentity {
16460
16353
  binding() {
16461
16354
  const person = this.#own.person;
16462
16355
  const record = person ? this.#persons.get(person)?.state.log.device(this.#machine()) : undefined;
16356
+ if (person && this.#persons.get(person)?.state.erased)
16357
+ return { state: "removed", person };
16463
16358
  if (!person || !record)
16464
16359
  return { state: "none", ...person ? { person } : {} };
16465
16360
  if (!record.current)
@@ -16481,12 +16376,21 @@ class DirectoryIdentity {
16481
16376
  throw new MessagingError("binding_invalid", "this device was removed from its account; it cannot be bound again");
16482
16377
  const done = await this.#extendLog(async () => {
16483
16378
  await this.#latest();
16484
- await this.#fetch([person]);
16485
- const log = this.#persons.get(person)?.state.log;
16486
- const current = log?.device(identity.machine);
16487
- if (current && !current.current)
16488
- throw new MessagingError("binding_invalid", "this device was removed from its account; it cannot be bound again");
16489
- const head = log?.head();
16379
+ let current;
16380
+ let head;
16381
+ if (this.#own.person === person && this.binding().state !== "none") {
16382
+ await this.#fetch([person]);
16383
+ const log = this.#persons.get(person)?.state.log;
16384
+ current = log?.device(identity.machine);
16385
+ if (current && !current.current)
16386
+ throw new MessagingError("binding_invalid", "this device was removed from its account; it cannot be bound again");
16387
+ head = log?.head();
16388
+ } else {
16389
+ const verified = await this.#headOf(person);
16390
+ if (verified.state === "erased")
16391
+ throw new MessagingError("binding_invalid", "the account was erased");
16392
+ head = verified.state === "live" ? verified.log : undefined;
16393
+ }
16490
16394
  const entry = signLogEntry(identity, {
16491
16395
  v: PROTOCOL,
16492
16396
  realm: this.realm,
@@ -16500,13 +16404,29 @@ class DirectoryIdentity {
16500
16404
  authority: { kind: "jelly", token }
16501
16405
  });
16502
16406
  this.#signed(entry, person);
16503
- return current ? await this.#call((link) => link.append(entry)) : await this.#call((link) => link.bind(entry));
16407
+ if (current)
16408
+ return await this.#call((link) => link.append(entry));
16409
+ const bound = await this.#call((link) => link.bind(entry));
16410
+ this.#bindAccepted = true;
16411
+ return bound;
16504
16412
  });
16505
16413
  await this.#adoptAnnounced(done.sth);
16506
16414
  await this.#fetch([person]);
16507
16415
  await this.publishKeys();
16508
16416
  return { person, seq: done.seq };
16509
16417
  }
16418
+ async awaitSelf() {
16419
+ const person = this.#own.person;
16420
+ if (!person)
16421
+ throw new MessagingError("no_identity", "this device is not bound to an account");
16422
+ for (let tries = 0;this.self() === undefined || !this.#index.has(this.#machine()); tries++) {
16423
+ if (tries >= LOG_RETRIES)
16424
+ throw unavailable("the directory has not committed this device's binding and keys yet");
16425
+ await new Promise((resolve) => setTimeout(resolve, LOG_RETRY_MS));
16426
+ await this.#latest();
16427
+ await this.#fetch([person]);
16428
+ }
16429
+ }
16510
16430
  async publishKeys() {
16511
16431
  if (this.#alarm?.kind === "restored")
16512
16432
  throw new MessagingError("invalid_state", "this device runs from an older copy of its home; it publishes no keys");
@@ -16557,11 +16477,50 @@ class DirectoryIdentity {
16557
16477
  await this.#fetch([person]);
16558
16478
  return { seq: done.seq };
16559
16479
  }
16480
+ async erase(token) {
16481
+ const identity = this.#options.identity();
16482
+ const person = this.#own.person;
16483
+ if (!person || this.binding().state === "none" || this.removed())
16484
+ throw new MessagingError("no_identity", "this device is not bound to an account");
16485
+ if (this.binding().record?.vouch !== "jelly")
16486
+ throw new MessagingError("forbidden", "a device approved by link erases nothing; erase from a device Jelly ID vouched for");
16487
+ const entries = await this.#extendLog(async () => {
16488
+ await this.#latest();
16489
+ await this.#fetch([person]);
16490
+ const log = this.#persons.get(person).state.log;
16491
+ let head = log.head();
16492
+ const out = [];
16493
+ const devices = log.devices().sort((a, b) => Number(a.device === identity.machine) - Number(b.device === identity.machine));
16494
+ for (const device of devices) {
16495
+ const self = device.device === identity.machine;
16496
+ const entry = signLogEntry(identity, {
16497
+ v: PROTOCOL,
16498
+ realm: this.realm,
16499
+ person,
16500
+ seq: head.seq + 1,
16501
+ prev: head.hash,
16502
+ ts: this.#options.now(),
16503
+ op: "unbind",
16504
+ device: device.device,
16505
+ reason: self ? "self" : "removed",
16506
+ authority: self ? { kind: "self" } : { kind: "jelly", token }
16507
+ });
16508
+ out.push(entry);
16509
+ head = { seq: entry.seq, hash: logEntryHash(entry) };
16510
+ }
16511
+ for (const entry of out)
16512
+ this.#signed(entry, person);
16513
+ await this.#call((link) => link.erase(out, token));
16514
+ return out;
16515
+ });
16516
+ await this.#latest().then(() => this.#fetch([person])).catch(() => {});
16517
+ return { person, devices: entries.length, seq: entries.at(-1).seq };
16518
+ }
16560
16519
  createLink(args) {
16561
16520
  if (this.#own.person !== undefined) {
16562
16521
  throw new MessagingError("invalid_state", this.removed() ? "this device was removed from its account; it cannot be bound again" : "this device is already bound");
16563
16522
  }
16564
- const link = createLinkRequest(this.#options.identity(), { realm: this.realm, label: args.label, kind: args.kind, nonce: randomBytes8(16), ts: this.#options.now() });
16523
+ const link = createLinkRequest(this.#options.identity(), { realm: this.realm, label: args.label, kind: args.kind, nonce: randomBytes7(16), ts: this.#options.now() });
16565
16524
  this.#pendingLink = link;
16566
16525
  return { link: encodeLink(link), sas: linkSas(link), expires: link.ts + LIMITS.linkTtlMs };
16567
16526
  }
@@ -16755,8 +16714,9 @@ class DirectoryIdentity {
16755
16714
  async#fetch(persons) {
16756
16715
  if (!this.#head)
16757
16716
  await this.#latest();
16717
+ const basis = new Map(persons.map((person) => [person, this.#persons.get(person)]));
16758
16718
  const ask = persons.map((person) => {
16759
- const held = this.#persons.get(person);
16719
+ const held = basis.get(person);
16760
16720
  const length = held?.state.erased ? 0 : held?.state.log.length ?? 0;
16761
16721
  return length > 0 ? { person, knownSeq: length - 1 } : { person };
16762
16722
  });
@@ -16775,32 +16735,84 @@ class DirectoryIdentity {
16775
16735
  throw new MessagingError("directory_inconsistent", "the directory answered at another tree head");
16776
16736
  const now = this.#options.now();
16777
16737
  const views = new Map(answer.persons.map((view) => [view.person, view]));
16738
+ const grew = [];
16778
16739
  for (const person of persons) {
16779
16740
  const view = views.get(person);
16780
16741
  if (!view)
16781
16742
  continue;
16782
- const state = await this.#verify(view, head);
16743
+ const state = await this.#verify(view, head, basis.get(person)?.state);
16744
+ const current = this.#persons.get(person);
16745
+ if (current && current !== basis.get(person)) {
16746
+ this.#succession(current.size > head.sth.size ? state : current.state, current.size > head.sth.size ? current.state : state);
16747
+ if (current.size > head.sth.size)
16748
+ continue;
16749
+ }
16750
+ const before = current?.state;
16783
16751
  this.#persons.set(person, { state, size: head.sth.size, fetchedAt: now });
16784
16752
  this.#cache.putPerson({ person, entries: [...state.log.entries()], keys: [...state.keys.values()], erased: state.erased, size: head.sth.size, fetchedAt: now });
16753
+ if (state.log.length !== (before?.log.length ?? 0) || state.erased !== (before?.erased ?? false))
16754
+ grew.push(person);
16785
16755
  }
16786
16756
  this.#changed();
16757
+ if (grew.length > 0)
16758
+ for (const listener of [...this.#listeners["device-log"]])
16759
+ listener(grew);
16760
+ const unwatched = persons.filter((person) => views.has(person) && !this.#watching.has(person));
16761
+ if (unwatched.length > 0)
16762
+ this.watch(unwatched);
16763
+ }
16764
+ async#headOf(person) {
16765
+ if (!this.#head)
16766
+ await this.#latest();
16767
+ let head = this.#head;
16768
+ let answer;
16769
+ try {
16770
+ answer = await this.#call((link) => link.heads([person], head.sth.size));
16771
+ } catch (error) {
16772
+ if (!(error instanceof MessagingError) || error.code !== "invalid_request")
16773
+ throw error;
16774
+ await this.#latest();
16775
+ head = this.#head;
16776
+ answer = await this.#call((link) => link.heads([person], head.sth.size));
16777
+ }
16778
+ if (answer.sth.size !== head.sth.size || answer.sth.root !== head.sth.root)
16779
+ throw new MessagingError("directory_inconsistent", "the directory answered at another tree head");
16780
+ const view = answer.persons.find((p) => p.person === person);
16781
+ if (!view)
16782
+ throw new MessagingError("directory_inconsistent", "the directory left out the person asked");
16783
+ try {
16784
+ return verifyPersonHead(view, { sth: head.sth, head: head.head });
16785
+ } catch (error) {
16786
+ this.#inconsistent(error);
16787
+ throw error;
16788
+ }
16787
16789
  }
16788
- async#verify(view, head) {
16789
- const known = this.#persons.get(view.person)?.state;
16790
+ async#verify(view, head, known) {
16790
16791
  const tokens = view.log.flatMap((entry) => entry.authority.kind === "jelly" ? [entry.authority.token] : []);
16791
16792
  const keys = await this.#keyring.prepare(tokens, this.#options.now());
16792
16793
  this.#saveArchive();
16793
16794
  try {
16794
16795
  return verifyPersonView(view, { ...this.#context(view.person, keys), sth: head.sth, head: head.head, ...known && !known.erased ? { known } : {} });
16795
16796
  } catch (error) {
16796
- if (error instanceof DirectoryInconsistency) {
16797
- this.#raise({ kind: "directory", reason: error.message, at: this.#options.now(), evidence: error.evidence });
16798
- } else if (error instanceof MessagingError && error.code === "directory_inconsistent") {
16799
- this.#raise({ kind: "directory", reason: error.message, at: this.#options.now() });
16800
- }
16797
+ this.#inconsistent(error);
16798
+ throw error;
16799
+ }
16800
+ }
16801
+ #succession(earlier, later) {
16802
+ try {
16803
+ checkPersonSuccession(earlier, later);
16804
+ } catch (error) {
16805
+ this.#inconsistent(error);
16801
16806
  throw error;
16802
16807
  }
16803
16808
  }
16809
+ #inconsistent(error) {
16810
+ if (error instanceof DirectoryInconsistency) {
16811
+ this.#raise({ kind: "directory", reason: error.message, at: this.#options.now(), evidence: error.evidence });
16812
+ } else if (error instanceof MessagingError && error.code === "directory_inconsistent") {
16813
+ this.#raise({ kind: "directory", reason: error.message, at: this.#options.now() });
16814
+ }
16815
+ }
16804
16816
  #saveArchive() {
16805
16817
  this.#cache.setArchive(this.#keyring.archive());
16806
16818
  }
@@ -16905,6 +16917,56 @@ ${this.#index.get(me.machine)?.enc ?? ""}`;
16905
16917
  return mine === theirs ? "same" : "other";
16906
16918
  }
16907
16919
  }
16920
+ function deviceChangesOf(log) {
16921
+ const vouch = new Map;
16922
+ const record = new Map;
16923
+ const out = [];
16924
+ for (const entry of log.entries()) {
16925
+ const token = log.token(entry.seq);
16926
+ const via = token === undefined ? undefined : token.claims.hst !== undefined ? "host" : "browser";
16927
+ if (entry.op === "bind") {
16928
+ const v = entry.authority.kind === "jelly" ? "jelly" : "device";
16929
+ vouch.set(entry.device, v);
16930
+ record.set(entry.device, { label: entry.label, kind: entry.kind, host: via === "host" });
16931
+ out.push({
16932
+ seq: entry.seq,
16933
+ change: "added",
16934
+ device: entry.device,
16935
+ label: entry.label,
16936
+ kind: entry.kind,
16937
+ vouch: v,
16938
+ ...via ? { via } : {},
16939
+ ...entry.authority.kind === "device" ? { approver: entry.authority.approver } : {},
16940
+ by: entry.by,
16941
+ at: entry.ts
16942
+ });
16943
+ } else if (entry.op === "attest") {
16944
+ const known = record.get(entry.device);
16945
+ const next = { label: entry.label ?? known?.label ?? "device", kind: entry.kind ?? known?.kind ?? "other", host: via === "host" };
16946
+ record.set(entry.device, next);
16947
+ const raised = vouch.get(entry.device) !== "jelly";
16948
+ vouch.set(entry.device, "jelly");
16949
+ if (raised)
16950
+ 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 });
16951
+ } else {
16952
+ const known = record.get(entry.device);
16953
+ const v = vouch.get(entry.device) ?? "device";
16954
+ out.push({
16955
+ seq: entry.seq,
16956
+ change: entry.reason === "replaced" ? "replaced" : "removed",
16957
+ device: entry.device,
16958
+ label: known?.label ?? "device",
16959
+ ...known ? { kind: known.kind } : {},
16960
+ vouch: v,
16961
+ ...v === "jelly" && known ? { via: known.host ? "host" : "browser" } : {},
16962
+ by: entry.by,
16963
+ reason: entry.reason,
16964
+ at: entry.ts
16965
+ });
16966
+ }
16967
+ }
16968
+ return out;
16969
+ }
16908
16970
  function orgOf(email, domains) {
16909
16971
  if (email === undefined)
16910
16972
  return;
@@ -16989,7 +17051,7 @@ class DirectoryHome {
16989
17051
  url: this.#options.config.url,
16990
17052
  realm: realm.realm,
16991
17053
  identity: this.#options.identity,
16992
- person: () => realm.binding().person,
17054
+ person: () => realm.linkPerson(),
16993
17055
  pushes: {
16994
17056
  changed: (persons, sth) => void realm.changed(persons, sth).catch((error) => this.#failed("directory.changed", error)),
16995
17057
  unbound: (entry, sth) => void realm.changed([entry.person], sth).catch((error) => this.#failed("directory.unbound", error)),
@@ -17123,7 +17185,6 @@ class DirectoryHome {
17123
17185
  const started = await this.#login.start({ purpose, label, kind });
17124
17186
  return {
17125
17187
  purpose,
17126
- verificationUriComplete: started.verificationUriComplete,
17127
17188
  verificationUri: started.verificationUri,
17128
17189
  userCode: started.userCode,
17129
17190
  fingerprint: started.fingerprint,
@@ -17141,7 +17202,26 @@ class DirectoryHome {
17141
17202
  this.#login.cancel();
17142
17203
  return { ok: true };
17143
17204
  }
17205
+ async signIn(purpose, args) {
17206
+ this.#removing = undefined;
17207
+ this.#binding = undefined;
17208
+ const started = await this.#login.start({ purpose, label: args.label, kind: args.kind });
17209
+ return {
17210
+ purpose,
17211
+ verificationUri: started.verificationUri,
17212
+ userCode: started.userCode,
17213
+ fingerprint: started.fingerprint,
17214
+ expires: started.expires,
17215
+ interval: started.interval
17216
+ };
17217
+ }
17144
17218
  async#completeLogin(token, purpose) {
17219
+ if (purpose === "replace" || purpose === "erase") {
17220
+ const step = purpose === "replace" ? this.#options.replaceStep : this.#options.eraseStep;
17221
+ if (!step)
17222
+ throw new MessagingError("invalid_state", `this daemon does not ${purpose} through a sign-in`);
17223
+ return step(token);
17224
+ }
17145
17225
  if (purpose === "remove") {
17146
17226
  const removing = this.#removing;
17147
17227
  if (!removing)
@@ -17207,6 +17287,28 @@ var init_directory_home = __esm(() => {
17207
17287
  init_directory_identity();
17208
17288
  });
17209
17289
 
17290
+ // packages/daemon/dist/realm/directory-cache.js
17291
+ function memoryDirectoryCache(from) {
17292
+ let head = from?.head();
17293
+ const persons = new Map((from?.persons() ?? []).map((p) => [p.person, p]));
17294
+ let archive = from?.archive() ?? [];
17295
+ let own = from?.own() ?? { keysSeq: -1, signed: [] };
17296
+ let alarm = from?.alarm();
17297
+ const copy = (value) => structuredClone(value);
17298
+ return {
17299
+ head: () => copy(head),
17300
+ setHead: (value) => void (head = copy(value)),
17301
+ persons: () => [...persons.values()].map(copy),
17302
+ putPerson: (value) => void persons.set(value.person, copy(value)),
17303
+ archive: () => copy(archive),
17304
+ setArchive: (keys) => void (archive = copy(keys)),
17305
+ own: () => copy(own),
17306
+ setOwn: (value) => void (own = copy(value)),
17307
+ alarm: () => copy(alarm),
17308
+ setAlarm: (value) => void (alarm = copy(value))
17309
+ };
17310
+ }
17311
+
17210
17312
  // packages/daemon/dist/jelly.js
17211
17313
  import { closeSync as closeSync6, constants as constants6, fstatSync as fstatSync3, readSync } from "fs";
17212
17314
  function jellyLabel(text) {
@@ -17537,12 +17639,12 @@ var init_answer_preview = __esm(() => {
17537
17639
  });
17538
17640
 
17539
17641
  // packages/daemon/dist/snapshot.js
17540
- import { createHash as createHash10 } from "crypto";
17642
+ import { createHash as createHash9 } from "crypto";
17541
17643
  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";
17542
17644
  import { join as join7 } from "path";
17543
17645
  function sha256File(path) {
17544
17646
  const data = readFileSync8(path);
17545
- return { bytes: data.length, sha256: createHash10("sha256").update(data).digest("hex") };
17647
+ return { bytes: data.length, sha256: createHash9("sha256").update(data).digest("hex") };
17546
17648
  }
17547
17649
 
17548
17650
  class Snapshots {
@@ -17646,6 +17748,12 @@ class Notices {
17646
17748
  alert(kind, at) {
17647
17749
  this.#file({ id: `a_${kind.replace(/_/g, "")}${at}`, category: "alert", source: kind, at });
17648
17750
  }
17751
+ device(seq, at) {
17752
+ this.#file({ id: `a_dev${seq}`, category: "alert", source: `device:${seq}`, at });
17753
+ }
17754
+ trust(grantId, person, until, at) {
17755
+ this.#file({ id: `a_trust${grantId}`, category: "alert", person, source: `trust:${until}`, at });
17756
+ }
17649
17757
  #file(row) {
17650
17758
  if (!this.#host.store.putNotice(row))
17651
17759
  return;
@@ -17711,7 +17819,15 @@ class Notices {
17711
17819
  const own = row.person === this.#host.me();
17712
17820
  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" };
17713
17821
  }
17714
- if (row.category === "alert")
17822
+ if (row.category === "alert" && row.source.startsWith("device:")) {
17823
+ const device = this.#host.device?.(Number(row.source.slice("device:".length)));
17824
+ if (device)
17825
+ notice.device = device;
17826
+ notice.line = device ? deviceLine(device) : "A device of your account changed.";
17827
+ } else if (row.category === "alert" && row.source.startsWith("trust:")) {
17828
+ const until = Number(row.source.slice("trust:".length));
17829
+ 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.`;
17830
+ } else if (row.category === "alert")
17715
17831
  notice.line = ALERT_TEXT[row.source] ?? "An alarm was raised on this device.";
17716
17832
  else if (row.category === "message" && lines) {
17717
17833
  const line = this.#host.store.line(row.source);
@@ -17721,6 +17837,18 @@ class Notices {
17721
17837
  return notice;
17722
17838
  }
17723
17839
  }
17840
+ function vouchWords2(d) {
17841
+ if (d.vouch === "admin")
17842
+ return "admitted by the team's admin";
17843
+ if (d.vouch === "device")
17844
+ return d.approver !== undefined ? `approved on '${d.approver}'` : "approved by link";
17845
+ return d.via === "browser" ? "signed in through the browser" : "vouched by Jelly ID on a Jelly host";
17846
+ }
17847
+ function deviceLine(d) {
17848
+ const what = `'${d.label}'${d.kind !== undefined ? ` (${d.kind})` : ""}`;
17849
+ 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.`;
17850
+ return line.length <= NOTICE_LINE_CHARS ? line : `${line.slice(0, NOTICE_LINE_CHARS - 1)}\u2026`;
17851
+ }
17724
17852
  function firstLine(text) {
17725
17853
  const first = cleanPeerText(text.split(/\r\n|\n|\r/, 1)[0] ?? "", { singleLine: true });
17726
17854
  if (first.length <= NOTICE_LINE_CHARS)
@@ -18301,46 +18429,44 @@ var init_storage_errors = __esm(() => {
18301
18429
  });
18302
18430
 
18303
18431
  // packages/daemon/dist/store.js
18304
- import { chmodSync as chmodSync5, closeSync as closeSync7, openSync as openSync3 } from "fs";
18432
+ import { chmodSync as chmodSync5, closeSync as closeSync7, existsSync as existsSync7, openSync as openSync3 } from "fs";
18305
18433
  import { DatabaseSync } from "node:sqlite";
18306
- function additiveColumn(decl) {
18307
- if (/\b(PRIMARY|UNIQUE|CHECK|REFERENCES|COLLATE|GENERATED|AS)\b/i.test(decl))
18308
- return false;
18309
- return !/\bNOT\s+NULL\b/i.test(decl) || /\bDEFAULT\b/i.test(decl);
18434
+ function checkStore(db, path) {
18435
+ if (db.prepare("SELECT 1 FROM sqlite_master LIMIT 1").get() === undefined)
18436
+ return true;
18437
+ const hasKv = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'kv'").get() !== undefined;
18438
+ const raw = hasKv ? kvGet(db, "generation") : undefined;
18439
+ if (raw === undefined) {
18440
+ throw new Error(`${path} was written before the store reset (it records no store generation), and this messagingd does not read it. ${RESET_HINT}`);
18441
+ }
18442
+ if (storeNumber(raw) !== STORE_GENERATION) {
18443
+ throw new Error(`${path} is of store generation ${raw.slice(0, 16)}; this messagingd reads generation ${STORE_GENERATION}. ${RESET_HINT}`);
18444
+ }
18445
+ const minReader = kvGet(db, "min_reader") ?? "";
18446
+ if ((storeNumber(minReader) ?? Number.POSITIVE_INFINITY) > STORE_SCHEMA) {
18447
+ throw new Error(`${path} needs a messagingd that reads store schema ${minReader.slice(0, 16) || "(unrecorded)"} or later; this one reads up to ${STORE_SCHEMA}. Run a newer messagingd`);
18448
+ }
18449
+ return false;
18450
+ }
18451
+ function checkStoreFile(path) {
18452
+ if (!existsSync7(path))
18453
+ return;
18454
+ const db = new DatabaseSync(path, { readOnly: true, timeout: 5000 });
18455
+ try {
18456
+ checkStore(db, path);
18457
+ } finally {
18458
+ db.close();
18459
+ }
18310
18460
  }
18311
- function upgradeStore(db, migrations) {
18461
+ function openStore(db, path) {
18312
18462
  db.exec("BEGIN IMMEDIATE");
18313
18463
  try {
18314
- db.exec("CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT NOT NULL) STRICT;");
18315
- const raw = kvGet(db, "schema");
18316
- const stored = schemaNumber(raw);
18317
- const minReader = schemaNumber(kvGet(db, "min_reader")) ?? STORE_MIN_READER;
18318
- const top = migrations.reduce((max, m) => Math.max(max, m.schema), STORE_SCHEMA);
18319
- if (raw !== undefined && (stored === undefined || stored < STORE_MIN_READER)) {
18320
- throw new Error(`messaging.db has schema ${raw.slice(0, 16)}, from before the first released store (schema ${STORE_MIN_READER}); move the Messaging home aside and set it up again`);
18321
- }
18322
- if (minReader !== undefined && minReader > top) {
18323
- throw new Error(`messaging.db needs a messagingd that reads store schema ${minReader} or later; this one reads up to ${top}. Run a newer messagingd`);
18324
- }
18325
- db.exec(BASELINE);
18326
- for (const migration of migrations) {
18327
- if (migration.ddl) {
18328
- if (/\b(DROP|RENAME|ALTER)\b|\bCREATE\s+UNIQUE\b/i.test(migration.ddl))
18329
- throw new Error(`store migration ${migration.schema} is not additive`);
18330
- db.exec(migration.ddl);
18331
- }
18332
- for (const { table, column, decl } of migration.columns ?? []) {
18333
- if (!/^[a-z_][a-z0-9_]*$/.test(table) || !/^[a-z_][a-z0-9_]*$/.test(column) || !additiveColumn(decl)) {
18334
- throw new Error(`store migration ${migration.schema} is not additive (${table}.${column})`);
18335
- }
18336
- const has = db.prepare(`SELECT name FROM pragma_table_info('${table}')`).all().some((c) => c.name === column);
18337
- if (!has)
18338
- db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${decl}`);
18339
- }
18340
- }
18341
- kvSet(db, "schema", String(Math.max(stored ?? 0, top)));
18342
- if (kvGet(db, "min_reader") === undefined)
18464
+ if (checkStore(db, path)) {
18465
+ db.exec(BASELINE);
18466
+ kvSet(db, "generation", String(STORE_GENERATION));
18467
+ kvSet(db, "schema", String(STORE_SCHEMA));
18343
18468
  kvSet(db, "min_reader", String(STORE_MIN_READER));
18469
+ }
18344
18470
  db.exec("COMMIT");
18345
18471
  } catch (error) {
18346
18472
  db.exec("ROLLBACK");
@@ -18395,7 +18521,7 @@ class Store {
18395
18521
  this.#db = new DatabaseSync(path, { timeout: 5000 });
18396
18522
  this.#db.exec("PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL;");
18397
18523
  try {
18398
- upgradeStore(this.#db, MIGRATIONS);
18524
+ openStore(this.#db, path);
18399
18525
  } catch (error) {
18400
18526
  this.#db.close();
18401
18527
  throw error;
@@ -18583,7 +18709,7 @@ class Store {
18583
18709
  }
18584
18710
  insertInbox(row) {
18585
18711
  const { key, stage, ...item } = row.item;
18586
- this.#run("INSERT INTO inbox (id, key, session, sender, item, message, stage, received_at, bytes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", item.id, key, row.session, row.sender, JSON.stringify({ ...item, from: storedPeer(item.from) }), JSON.stringify(storedMessage(row.message)), stage, item.receivedAt, item.bytes);
18712
+ this.#run("INSERT INTO inbox (id, key, session, sender, item, message, stage, received_at, bytes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", item.id, key, row.session, row.sender, JSON.stringify(item), JSON.stringify(row.message), stage, item.receivedAt, item.bytes);
18587
18713
  }
18588
18714
  inbox(key, unreadOnly, limit) {
18589
18715
  const sql = `SELECT * FROM inbox WHERE key = ?${unreadOnly ? " AND stage <> 'read'" : ""} ORDER BY received_at DESC, rowid DESC LIMIT ?`;
@@ -18984,7 +19110,7 @@ class Store {
18984
19110
  const prefix = `${peer}@`;
18985
19111
  const take = limit + 1;
18986
19112
  const [inboxBound, inboxArgs] = bound("received_at", "id");
18987
- const mail = this.#all(`SELECT * FROM inbox WHERE key = ? AND json_extract(item, '$.kind') = 'message' AND json_extract(item, '$.from.person') = ? AND json_extract(item, '$.from.session') = ?${inboxBound} ORDER BY received_at ${order}, id ${order} LIMIT ?`, key, person, session, ...inboxArgs, take).map((r) => ({ source: "inbox", at: Number(r.received_at), id: String(r.id), inbox: inboxRow2(r) }));
19113
+ const mail = this.#all(`SELECT * FROM inbox WHERE key = ? AND json_extract(item, '$.kind') = 'message' AND json_extract(item, '$.from.person') = ? AND json_extract(item, '$.from.agent') = ?${inboxBound} ORDER BY received_at ${order}, id ${order} LIMIT ?`, key, person, session, ...inboxArgs, take).map((r) => ({ source: "inbox", at: Number(r.received_at), id: String(r.id), inbox: inboxRow2(r) }));
18988
19114
  const [sentBound, sentArgs] = bound("created_at", "id");
18989
19115
  const sent = this.#all(`SELECT * FROM sent WHERE key = ? AND kind = 'message' AND substr(to_addr, 1, ?) = ?${sentBound} ORDER BY created_at ${order}, id ${order} LIMIT ?`, key, prefix.length, prefix, ...sentArgs, take).map((r) => ({ source: "sent", at: Number(r.created_at), id: String(r.id), sent: sentRow(r) }));
18990
19116
  const [taskBound, taskArgs] = bound("created_at", "task_id");
@@ -19039,7 +19165,7 @@ class Store {
19039
19165
  }
19040
19166
  agentPeers(key, limit) {
19041
19167
  return this.#all(`SELECT peer, SUM(unread) AS unread, MAX(at) AS at FROM (
19042
- SELECT json_extract(item, '$.from.person') || '/' || json_extract(item, '$.from.session') AS peer, stage <> 'read' AS unread, received_at AS at
19168
+ SELECT json_extract(item, '$.from.person') || '/' || json_extract(item, '$.from.agent') AS peer, stage <> 'read' AS unread, received_at AS at
19043
19169
  FROM inbox WHERE key = ? AND json_extract(item, '$.kind') = 'message'
19044
19170
  UNION ALL
19045
19171
  SELECT substr(to_addr, 1, instr(to_addr || '@', '@') - 1), 0, created_at FROM sent WHERE key = ? AND kind = 'message'
@@ -19066,7 +19192,7 @@ class Store {
19066
19192
  ON CONFLICT (task_id, role) DO UPDATE SET key = excluded.key, session = excluded.session, peer = excluded.peer,
19067
19193
  peer_machine = excluded.peer_machine, peer_address = excluded.peer_address, state = excluded.state,
19068
19194
  context_id = excluded.context_id, task = excluded.task, updated_at = excluded.updated_at,
19069
- request = COALESCE(excluded.request, tasks.request)`, r.task.id, r.role, r.key, row.session, JSON.stringify(storedPeer(r.peer)), r.peer.device, r.peer.address, r.task.status.state, opt(r.task.contextId), JSON.stringify(taskMessages(r.task, storedMessage)), r.createdAt, r.updatedAt, opt(row.request));
19195
+ request = COALESCE(excluded.request, tasks.request)`, r.task.id, r.role, r.key, row.session, JSON.stringify(r.peer), r.peer.device, r.peer.address, r.task.status.state, opt(r.task.contextId), JSON.stringify(r.task), r.createdAt, r.updatedAt, opt(row.request));
19070
19196
  if (r.chain !== undefined) {
19071
19197
  this.#run("INSERT OR IGNORE INTO delegations (task_id, role, grants, chain) VALUES (?, ?, ?, ?)", r.task.id, r.role, JSON.stringify(row.grants), JSON.stringify(r.chain));
19072
19198
  }
@@ -19169,35 +19295,12 @@ function directoryRow(r) {
19169
19295
  function inboxRow2(r) {
19170
19296
  const item = json(r.item);
19171
19297
  return {
19172
- item: { ...item, from: peerOf(item.from), key: String(r.key), stage: String(r.stage) },
19298
+ item: { ...item, key: String(r.key), stage: String(r.stage) },
19173
19299
  session: String(r.session),
19174
19300
  sender: String(r.sender),
19175
- message: messageOf2(json(r.message))
19176
- };
19177
- }
19178
- function storedPeer(peer) {
19179
- const { agent, device, ...rest } = peer;
19180
- return { ...rest, ...agent !== undefined ? { session: agent } : {}, machine: device };
19181
- }
19182
- function peerOf(stored) {
19183
- const { session, machine, ...rest } = stored;
19184
- return { ...rest, ...session !== undefined ? { agent: session } : {}, device: machine };
19185
- }
19186
- function storedMessage(message) {
19187
- const provenance = message.metadata?.[URI.provenance];
19188
- return provenance === undefined ? message : { ...message, metadata: { ...message.metadata, [URI.provenance]: storedPeer(provenance) } };
19189
- }
19190
- function taskMessages(task, convert) {
19191
- return {
19192
- ...task,
19193
- status: task.status.message !== undefined ? { ...task.status, message: convert(task.status.message) } : task.status,
19194
- ...task.history !== undefined ? { history: task.history.map(convert) } : {}
19301
+ message: json(r.message)
19195
19302
  };
19196
19303
  }
19197
- function messageOf2(message) {
19198
- const provenance = message.metadata?.[URI.provenance];
19199
- return provenance === undefined ? message : { ...message, metadata: { ...message.metadata, [URI.provenance]: peerOf(provenance) } };
19200
- }
19201
19304
  function outboxRow(r) {
19202
19305
  return {
19203
19306
  id: String(r.id),
@@ -19274,10 +19377,10 @@ function matchQuery(query) {
19274
19377
  function taskRow2(r) {
19275
19378
  const row = {
19276
19379
  record: {
19277
- task: taskMessages(json(r.task), messageOf2),
19380
+ task: json(r.task),
19278
19381
  role: String(r.role),
19279
19382
  key: String(r.key),
19280
- peer: peerOf(json(r.peer)),
19383
+ peer: json(r.peer),
19281
19384
  createdAt: Number(r.created_at),
19282
19385
  updatedAt: Number(r.updated_at)
19283
19386
  },
@@ -19293,7 +19396,7 @@ function taskRow2(r) {
19293
19396
  }
19294
19397
  return row;
19295
19398
  }
19296
- var CHAT_COUNTER = "chat", STORE_SCHEMA = 9, STORE_MIN_READER = 3, MIGRATIONS, BASELINE = `
19399
+ var CHAT_COUNTER = "chat", STORE_GENERATION = 2, STORE_SCHEMA = 1, STORE_MIN_READER = 1, BASELINE = `
19297
19400
  CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT NOT NULL) STRICT;
19298
19401
  CREATE TABLE IF NOT EXISTS counters (name TEXT PRIMARY KEY, value INTEGER NOT NULL) STRICT;
19299
19402
  CREATE TABLE IF NOT EXISTS roster (seq INTEGER PRIMARY KEY, hash TEXT NOT NULL, entry TEXT NOT NULL) STRICT;
@@ -19399,7 +19502,8 @@ CREATE TABLE IF NOT EXISTS chat_sent (
19399
19502
  person TEXT NOT NULL,
19400
19503
  stage TEXT NOT NULL,
19401
19504
  code TEXT,
19402
- exp INTEGER NOT NULL
19505
+ exp INTEGER NOT NULL,
19506
+ held_at INTEGER
19403
19507
  ) STRICT;
19404
19508
  CREATE INDEX IF NOT EXISTS chat_sent_by_line ON chat_sent (line, person);
19405
19509
  CREATE INDEX IF NOT EXISTS chat_sent_pending ON chat_sent (exp) WHERE stage = 'sent';
@@ -19442,6 +19546,7 @@ CREATE TABLE IF NOT EXISTS tasks (
19442
19546
  task TEXT NOT NULL,
19443
19547
  created_at INTEGER NOT NULL,
19444
19548
  updated_at INTEGER NOT NULL,
19549
+ request TEXT,
19445
19550
  PRIMARY KEY (task_id, role)
19446
19551
  ) STRICT;
19447
19552
  CREATE INDEX IF NOT EXISTS tasks_by_key ON tasks (key, updated_at DESC, task_id DESC, role DESC);
@@ -19470,37 +19575,20 @@ CREATE TABLE IF NOT EXISTS idempotency (
19470
19575
  created_at INTEGER NOT NULL,
19471
19576
  PRIMARY KEY (key, ikey)
19472
19577
  ) STRICT;
19473
- `, kvGet = (db, k) => {
19474
- const row = db.prepare("SELECT v FROM kv WHERE k = ?").get(k);
19475
- return row?.v === undefined || row.v === null ? undefined : String(row.v);
19476
- }, kvSet = (db, k, v) => {
19477
- db.prepare("INSERT INTO kv (k, v) VALUES (?, ?) ON CONFLICT (k) DO UPDATE SET v = excluded.v").run(k, v);
19478
- }, schemaNumber = (value) => value !== undefined && /^[1-9][0-9]{0,8}$/.test(value) ? Number(value) : undefined, TASKS = "tasks LEFT JOIN delegations USING (task_id, role)", text2 = (v) => v === null || v === undefined ? undefined : String(v), num = (v) => v === null || v === undefined ? undefined : Number(v), json = (v) => JSON.parse(String(v)), opt = (v) => v === undefined ? null : v, UNSPACED;
19479
- var init_store2 = __esm(() => {
19480
- init_dist();
19481
- MIGRATIONS = [
19482
- { schema: 4, columns: [{ table: "tasks", column: "request", decl: "TEXT" }] },
19483
- {
19484
- schema: 5,
19485
- ddl: `CREATE TABLE IF NOT EXISTS dir_persons (
19578
+ -- A directory realm's cache of verified persons (spec \xA715.9); its tree head, Jelly key archive, own record and alarm
19579
+ -- are kv rows (dir.*).
19580
+ CREATE TABLE IF NOT EXISTS dir_persons (
19486
19581
  person TEXT PRIMARY KEY,
19487
19582
  entries TEXT NOT NULL,
19488
19583
  keys TEXT NOT NULL,
19489
19584
  erased INTEGER NOT NULL DEFAULT 0,
19490
19585
  size INTEGER NOT NULL,
19491
19586
  fetched_at INTEGER NOT NULL
19492
- ) STRICT;`
19493
- },
19494
- {
19495
- schema: 6,
19496
- ddl: `
19587
+ ) STRICT;
19588
+ -- This person's grants, one per subject, as signed; and the widening levels that lapsed with their signer (spec \xA76.9).
19497
19589
  CREATE TABLE IF NOT EXISTS grants (subject TEXT PRIMARY KEY, grant TEXT NOT NULL) STRICT;
19498
19590
  CREATE TABLE IF NOT EXISTS grant_reviews (subject TEXT PRIMARY KEY, id TEXT NOT NULL, levels TEXT NOT NULL, at INTEGER NOT NULL) STRICT;
19499
- `
19500
- },
19501
- {
19502
- schema: 7,
19503
- ddl: `
19591
+ -- Requests held for this person to answer (spec \xA76.9, "Holds"): the envelope as received and its opened payload.
19504
19592
  CREATE TABLE IF NOT EXISTS held (
19505
19593
  id TEXT PRIMARY KEY,
19506
19594
  person TEXT NOT NULL,
@@ -19513,11 +19601,8 @@ CREATE TABLE IF NOT EXISTS held (
19513
19601
  ) STRICT;
19514
19602
  CREATE INDEX IF NOT EXISTS held_person ON held (person);
19515
19603
  CREATE INDEX IF NOT EXISTS held_exp ON held (exp);
19516
- `
19517
- },
19518
- {
19519
- schema: 8,
19520
- ddl: `
19604
+ -- Deployment "jelly" (spec \xA78.11): spent action-grant ids until their expiry, and the Jelly agent each session key
19605
+ -- was registered for. Notices (spec \xA78.2, "Notices") name their source (a line, a held request, an alarm).
19521
19606
  CREATE TABLE IF NOT EXISTS act_jti (jti TEXT PRIMARY KEY, exp INTEGER NOT NULL) STRICT;
19522
19607
  CREATE INDEX IF NOT EXISTS act_jti_exp ON act_jti (exp);
19523
19608
  CREATE TABLE IF NOT EXISTS session_agents (key TEXT PRIMARY KEY, agt TEXT NOT NULL) STRICT;
@@ -19532,11 +19617,8 @@ CREATE TABLE IF NOT EXISTS notices (
19532
19617
  acked_at INTEGER
19533
19618
  ) STRICT;
19534
19619
  CREATE INDEX IF NOT EXISTS notices_at ON notices (at);
19535
- `
19536
- },
19537
- {
19538
- schema: 9,
19539
- ddl: `
19620
+ -- Contact cards and message requests (spec \xA76.9): the latest card each other person sent this one; message requests
19621
+ -- held for the person to accept, as received and opened; and the contacts this person's agents suggested.
19540
19622
  CREATE TABLE IF NOT EXISTS contact_cards (person TEXT PRIMARY KEY, card TEXT NOT NULL, seq INTEGER NOT NULL, at INTEGER NOT NULL) STRICT;
19541
19623
  CREATE TABLE IF NOT EXISTS contact_requests (
19542
19624
  id TEXT PRIMARY KEY,
@@ -19549,10 +19631,14 @@ CREATE TABLE IF NOT EXISTS contact_requests (
19549
19631
  ) STRICT;
19550
19632
  CREATE INDEX IF NOT EXISTS contact_requests_person ON contact_requests (person);
19551
19633
  CREATE TABLE IF NOT EXISTS contact_suggestions (id TEXT PRIMARY KEY, session TEXT NOT NULL, email TEXT NOT NULL, at INTEGER NOT NULL) STRICT;
19552
- `,
19553
- columns: [{ table: "chat_sent", column: "held_at", decl: "INTEGER" }]
19554
- }
19555
- ];
19634
+ `, kvGet = (db, k) => {
19635
+ const row = db.prepare("SELECT v FROM kv WHERE k = ?").get(k);
19636
+ return row?.v === undefined || row.v === null ? undefined : String(row.v);
19637
+ }, kvSet = (db, k, v) => {
19638
+ db.prepare("INSERT INTO kv (k, v) VALUES (?, ?) ON CONFLICT (k) DO UPDATE SET v = excluded.v").run(k, v);
19639
+ }, storeNumber = (value) => value !== undefined && /^[1-9][0-9]{0,8}$/.test(value) ? Number(value) : undefined, RESET_HINT = "Nothing was changed. To start over, stop messagingd, move the whole Messaging home aside (it keeps the old messages, and the messagingd that wrote it still opens it), then set the home up again: messaging login, or messaging team create or team join for a self-hosted team", TASKS = "tasks LEFT JOIN delegations USING (task_id, role)", text2 = (v) => v === null || v === undefined ? undefined : String(v), num = (v) => v === null || v === undefined ? undefined : Number(v), json = (v) => JSON.parse(String(v)), opt = (v) => v === undefined ? null : v, UNSPACED;
19640
+ var init_store2 = __esm(() => {
19641
+ init_dist();
19556
19642
  UNSPACED = /([\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}\p{Script=Thai}])/gu;
19557
19643
  });
19558
19644
 
@@ -19945,6 +20031,16 @@ class Grants {
19945
20031
  hasGrant(person) {
19946
20032
  return this.#current(grantSubjectKey({ person })) !== undefined;
19947
20033
  }
20034
+ expiringTrust(withinMs) {
20035
+ const now = this.#host.now();
20036
+ const current = this.#currentDevice();
20037
+ return this.#all().flatMap(({ grant }) => {
20038
+ const until = grant.trustedUntil;
20039
+ if (!("person" in grant.subject) || until === undefined || until <= now || until > now + withinMs || !current(grant.by))
20040
+ return [];
20041
+ return [{ id: grant.id, person: grant.subject.person, until }];
20042
+ });
20043
+ }
19948
20044
  list(p) {
19949
20045
  const only = p.subject === undefined ? undefined : this.#subjectKey(p.subject, false);
19950
20046
  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));
@@ -20019,6 +20115,25 @@ class Grants {
20019
20115
  this.#host.log("grant.sync", { id: env.id, what: conv.what, merged });
20020
20116
  this.sweep();
20021
20117
  }
20118
+ resignFrom(predecessor) {
20119
+ const { identity } = this.#host.joined();
20120
+ const now = this.#host.now();
20121
+ const resigned = [];
20122
+ for (const { subject, grant } of this.#all()) {
20123
+ if (grant.by !== predecessor)
20124
+ continue;
20125
+ const copy = resignGrant(identity, grant, { now, keep: "all" });
20126
+ this.#store.putGrant(subject, copy);
20127
+ resigned.push(copy);
20128
+ if ("person" in copy.subject)
20129
+ this.#host.changed?.(copy.subject.person);
20130
+ }
20131
+ if (resigned.length > 0) {
20132
+ this.#sync("grant", resigned);
20133
+ this.#host.log("grant.resigned", { grants: resigned.length });
20134
+ }
20135
+ return resigned.length;
20136
+ }
20022
20137
  sweep() {
20023
20138
  const { identity, realm } = this.#host.joined();
20024
20139
  if (realm.removed())
@@ -20814,6 +20929,7 @@ class Daemon {
20814
20929
  const realm = this.#realm ?? this.#directory?.realm;
20815
20930
  return realm ? await realm.profile(person) : undefined;
20816
20931
  },
20932
+ device: (seq) => this.#noticeDevice(seq),
20817
20933
  emit: (event) => this.#emit(event)
20818
20934
  });
20819
20935
  this.#keyring = new Keyring({
@@ -20845,6 +20961,7 @@ class Daemon {
20845
20961
  static async start(options) {
20846
20962
  const paths = homePaths(options.home, options.jellyHost?.run);
20847
20963
  ensureHome(paths);
20964
+ checkStoreFile(paths.db);
20848
20965
  const jelly = options.jellyHost;
20849
20966
  if (jelly?.realm !== undefined && readConfig(paths.config).realm === undefined) {
20850
20967
  options = { ...options, jellyHost: { ...jelly, realm: await pinRealm(jelly.realm, options.wellKnown) } };
@@ -20919,6 +21036,7 @@ class Daemon {
20919
21036
  }
20920
21037
  this.#log("info", "daemon.started", { pid: process.pid, version: VERSION, joined: identity !== undefined, store: identityStore?.kind, ...jelly ? { deployment: "jelly" } : {} });
20921
21038
  this.#state = "running";
21039
+ this.#deviceNotices(true);
20922
21040
  this.#startTransport();
20923
21041
  this.#sweepTimer = setInterval(() => this.sweep(), this.#tuning.sweepMs);
20924
21042
  this.#sweepTimer.unref();
@@ -20958,7 +21076,12 @@ class Daemon {
20958
21076
  this.#alerts();
20959
21077
  },
20960
21078
  onDeliver: (raw) => this.#onDeliver(raw),
20961
- state: (connected) => this.#onHostedState(connected)
21079
+ state: (connected) => this.#onHostedState(connected),
21080
+ replaceStep: (token) => this.#replaceWith(token),
21081
+ eraseStep: async (token) => {
21082
+ const done = await this.#eraseWith(token);
21083
+ return { person: done.person, device: this.#identity.machine, seq: done.seq };
21084
+ }
20962
21085
  });
20963
21086
  this.#directory = home;
20964
21087
  const realm = home.realm;
@@ -20976,6 +21099,7 @@ class Daemon {
20976
21099
  else
20977
21100
  this.#presenceSoon();
20978
21101
  });
21102
+ realm.on("device-log", (persons) => this.#deviceLogGrew(persons));
20979
21103
  this.#removed = realm.removed();
20980
21104
  this.#transport = home.transport();
20981
21105
  home.start();
@@ -21076,7 +21200,11 @@ class Daemon {
21076
21200
  return {
21077
21201
  "daemon.status": () => this.status(),
21078
21202
  "device.status": () => this.#deviceStatus(),
21079
- "device.bind": (p) => this.#homeDirectory("device.bind").bind(this.#jellyHost ? { ...p, label: this.#jellyHost.label, kind: this.#jellyHost.kind } : p),
21203
+ "device.bind": async (p) => {
21204
+ if (this.#store.getKv(PREDECESSOR_KEY) !== undefined && this.#directory)
21205
+ return this.#replaceWith(p.token, p.label);
21206
+ return this.#homeDirectory("device.bind").bind(this.#jellyHost ? { ...p, label: this.#jellyHost.label, kind: this.#jellyHost.kind } : p);
21207
+ },
21080
21208
  "device.list": () => this.#directory?.list() ?? this.#rosterDevices(),
21081
21209
  "device.remove": (p) => this.#homeDirectory("device.remove").remove(p),
21082
21210
  "device.link": (p) => this.#homeDirectory("device.link").link(p),
@@ -21089,6 +21217,10 @@ class Daemon {
21089
21217
  "device.login.cancel": () => this.#homeDirectory("device.login.cancel").loginCancel(),
21090
21218
  "device.approve.preview": (p) => this.#homeDirectory("device.approve.preview").approvePreview(p),
21091
21219
  "device.approve": (p) => this.#homeDirectory("device.approve").approve(p),
21220
+ "identity.replace": (p) => this.#replace(p),
21221
+ "identity.replace.preview": (p) => this.#replacePreview(p),
21222
+ "directory.erase": (p) => this.#erase(p),
21223
+ "directory.erase.preview": (p) => this.#erasePreview(p),
21092
21224
  "daemon.stop": () => this.#stop(),
21093
21225
  "session.register": (p, ctx) => this.#register(p, ctx),
21094
21226
  "session.update": (p, ctx) => this.#updateSession(this.#bound(p, ctx)),
@@ -21189,10 +21321,30 @@ class Daemon {
21189
21321
  const out = [];
21190
21322
  for (const person of all) {
21191
21323
  const org = realm.kind === "roster" ? "same" : await realm.profile(person).then((profile) => profile.org, () => "none");
21192
- out.push(this.#cards.reach(person, org === "same"));
21324
+ const reach = this.#cards.reach(person, org === "same");
21325
+ const changedAt = this.#recentDeviceChanges(realm, person).at(0)?.at;
21326
+ out.push(changedAt === undefined ? reach : { ...reach, devicesChangedAt: changedAt });
21193
21327
  }
21194
21328
  return out;
21195
21329
  }
21330
+ #recentDeviceChanges(realm, person) {
21331
+ const since = this.#now() - DEVICES_CHANGED_MS;
21332
+ const changes = realm.deviceChanges(person);
21333
+ const labelOf = (device) => changes.filter((c) => c.device === device).at(-1)?.label;
21334
+ return changes.filter((change) => change.change !== "vouched" && change.at > since).reverse().slice(0, 10).map((change) => {
21335
+ const approver = change.approver !== undefined ? labelOf(change.approver) : undefined;
21336
+ return {
21337
+ change: change.change,
21338
+ device: change.device,
21339
+ label: MachineLabel.safeParse(change.label).success ? change.label : "device",
21340
+ ...change.kind !== undefined ? { kind: change.kind } : {},
21341
+ vouch: change.vouch === "service" ? "device" : change.vouch,
21342
+ ...change.via !== undefined ? { via: change.via } : {},
21343
+ ...approver !== undefined && MachineLabel.safeParse(approver).success ? { approver } : {},
21344
+ at: change.at
21345
+ };
21346
+ });
21347
+ }
21196
21348
  async#grantSubject(p) {
21197
21349
  const { realm } = this.#joined();
21198
21350
  if ("person" in p.subject)
@@ -21293,6 +21445,7 @@ class Daemon {
21293
21445
  now: this.#now
21294
21446
  });
21295
21447
  realm.on("devices-changed", () => this.#realmChanged());
21448
+ realm.on("device-log", (persons) => this.#deviceLogGrew(persons));
21296
21449
  return realm;
21297
21450
  }
21298
21451
  #admin() {
@@ -21491,7 +21644,7 @@ class Daemon {
21491
21644
  type: "presence",
21492
21645
  from: { person: me.person, device: identity.machine },
21493
21646
  to: { team: true },
21494
- 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?.(), {
21647
+ payload: presencePayload(final ? this.#store.sessions().map((s) => ({ ...s, unregisteredAt: s.unregisteredAt ?? this.#now() })) : this.#store.sessions(), me.label, this.#store.next("presence"), {
21495
21648
  wake: (s) => this.#wakes(s),
21496
21649
  share: new Set(this.#currentConfig().presence?.share ?? []),
21497
21650
  away: this.#away()
@@ -21641,7 +21794,6 @@ class Daemon {
21641
21794
  kind: p.kind,
21642
21795
  message,
21643
21796
  wake: p.wake ?? true,
21644
- ...p.external !== undefined ? { external: p.external } : {},
21645
21797
  ...chain !== undefined ? { delegation: chain.grants } : {}
21646
21798
  };
21647
21799
  const replyTo = p.replyTo ?? follow?.request;
@@ -21717,7 +21869,7 @@ class Daemon {
21717
21869
  }
21718
21870
  async#sendLine(p) {
21719
21871
  const session = this.#sessionOf(p.key);
21720
- const extra = ["contextId", "taskId", "messageId", "metadata", "extensions", "referenceTaskIds", "external", "onBehalfOf", "wake"].filter((name) => p[name] !== undefined);
21872
+ const extra = ["contextId", "taskId", "messageId", "metadata", "extensions", "referenceTaskIds", "onBehalfOf", "wake"].filter((name) => p[name] !== undefined);
21721
21873
  if (p.kind !== "message")
21722
21874
  throw new MessagingError("invalid_request", "a task goes to an agent, person/agent");
21723
21875
  if (extra.length > 0)
@@ -21790,7 +21942,9 @@ class Daemon {
21790
21942
  self: device.machine === identity.machine,
21791
21943
  ...away(device.machine) ? { away: true } : {}
21792
21944
  }));
21793
- return { person: p.person, ...profile, devices };
21945
+ const own = p.person === this.#ownPerson();
21946
+ const deviceChanges = own ? [] : this.#recentDeviceChanges(realm, p.person);
21947
+ return { person: p.person, ...profile, devices, ...deviceChanges.length > 0 ? { deviceChanges } : {} };
21794
21948
  }
21795
21949
  #seal(type, toMachine, session, payload, extra = {}) {
21796
21950
  const { identity, realm, me } = this.#joined();
@@ -22306,7 +22460,7 @@ ${row.receipt ? "receipt" : "envelope"}`;
22306
22460
  if (payload.kind === "task") {
22307
22461
  const { realm, me } = this.#joined();
22308
22462
  try {
22309
- inboundChain(payload.delegation, { realm, now, sender, fromSession, me, toSession, task: payload.message.taskId, external: payload.external });
22463
+ inboundChain(payload.delegation, { realm, now, sender, fromSession, me, toSession, task: payload.message.taskId });
22310
22464
  } catch (error) {
22311
22465
  return reject(codeOf(error));
22312
22466
  }
@@ -22336,10 +22490,9 @@ ${row.receipt ? "receipt" : "envelope"}`;
22336
22490
  const recipient = this.#store.sessionByName(toSession);
22337
22491
  const config = this.#currentConfig();
22338
22492
  const code = inboundPolicy({
22339
- sender: { machine: sender.machine, person: sender.person, role: sender.role, session: fromSession, agent: this.#agentOf(sender.machine, fromSession) },
22493
+ sender: { machine: sender.machine, session: fromSession, agent: this.#agentOf(sender.machine, fromSession) },
22340
22494
  ...recipient ? { recipient: { session: recipient.name, agent: recipient.agent } } : {},
22341
- task: payload.kind === "task",
22342
- external: payload.external !== undefined
22495
+ task: payload.kind === "task"
22343
22496
  }, this.#policy, now);
22344
22497
  if (code !== undefined || !recipient)
22345
22498
  return reject(code ?? "unknown_session");
@@ -22357,7 +22510,7 @@ ${row.receipt ? "receipt" : "envelope"}`;
22357
22510
  return reject("invalid_request");
22358
22511
  const { realm, me } = this.#joined();
22359
22512
  try {
22360
- chain = inboundChain(payload.delegation, { realm, now, sender, fromSession, me, toSession: recipient.name, task: taskId, external: payload.external });
22513
+ chain = inboundChain(payload.delegation, { realm, now, sender, fromSession, me, toSession: recipient.name, task: taskId });
22361
22514
  } catch (error) {
22362
22515
  return reject(codeOf(error));
22363
22516
  }
@@ -22374,7 +22527,7 @@ ${row.receipt ? "receipt" : "envelope"}`;
22374
22527
  return reject("too_large");
22375
22528
  }
22376
22529
  const from = peerRef(sender, fromSession);
22377
- const message = withProvenance(payload.message, provenanceOf3(env, sender, now, payload.external));
22530
+ const message = withProvenance(payload.message, provenanceOf3(env, sender, now));
22378
22531
  const item = {
22379
22532
  id: env.id,
22380
22533
  key: recipient.key,
@@ -22439,7 +22592,7 @@ ${row.receipt ? "receipt" : "envelope"}`;
22439
22592
  this.#dropped("not_found", env.id);
22440
22593
  return;
22441
22594
  }
22442
- if (row.request !== undefined && env.replyTo !== row.request) {
22595
+ if (env.replyTo !== row.request) {
22443
22596
  this.#dropped("invalid_request", env.id);
22444
22597
  return;
22445
22598
  }
@@ -22746,9 +22899,6 @@ ${row.receipt ? "receipt" : "envelope"}`;
22746
22899
  onPeer: (machine, online) => this.#onPeer(machine, online),
22747
22900
  onState: (state) => this.#onState(state),
22748
22901
  ...this.#tuning.reconnect ? { reconnect: this.#tuning.reconnect } : {},
22749
- ...this.#config.transport?.direct ? { direct: this.#config.transport.direct } : {},
22750
- member: (machine) => this.#removed ? undefined : this.#realm?.device(machine),
22751
- presence: (machine) => this.#store.directoryRow(machine)?.presence,
22752
22902
  log: this.#log
22753
22903
  });
22754
22904
  this.#transport = transport;
@@ -22881,7 +23031,6 @@ ${row.receipt ? "receipt" : "envelope"}`;
22881
23031
  this.#stopRemoved();
22882
23032
  return;
22883
23033
  }
22884
- this.#transport?.rosterChanged?.();
22885
23034
  this.#sealedTo.clear();
22886
23035
  const gone = hosted ? "unknown_device" : "not_member";
22887
23036
  this.#tx(() => {
@@ -22927,8 +23076,14 @@ ${row.receipt ? "receipt" : "envelope"}`;
22927
23076
  await transport?.stop().catch(() => {});
22928
23077
  }
22929
23078
  #deviceStatus() {
22930
- if (this.#directory)
22931
- return this.#directory.status();
23079
+ if (this.#directory) {
23080
+ const status = this.#directory.status();
23081
+ return this.#store.getKv(PREDECESSOR_KEY) !== undefined && status.binding !== "removed" ? { ...status, binding: "replace" } : status;
23082
+ }
23083
+ if (this.#config.realm) {
23084
+ const person = this.#store.directoryCache().own().person;
23085
+ return { realm: "directory", binding: "none", ...person ? { person } : {} };
23086
+ }
22932
23087
  const identity = this.#identity;
22933
23088
  const me = identity ? this.#realm?.device(identity.machine) : undefined;
22934
23089
  const binding = this.#removed ? "removed" : me ? "bound" : "none";
@@ -23065,6 +23220,232 @@ ${row.receipt ? "receipt" : "envelope"}`;
23065
23220
  const { rows, more } = this.#store.conversation(session.key, p.conversation.slice("agent:".length), { before: p.before }, p.limit ?? 30);
23066
23221
  return { kind: "entries", conversation: p.conversation, entries: rows.map(convEntry), more };
23067
23222
  }
23223
+ #heldRealm(config) {
23224
+ if (this.#directory)
23225
+ return this.#directory.realm;
23226
+ const throwaway = generateIdentity();
23227
+ return new DirectoryIdentity({ config, identity: () => throwaway, link: () => {
23228
+ return;
23229
+ }, cache: memoryDirectoryCache(this.#store.directoryCache()), jelly: this.#jelly ?? jellyFromIssuer(config.issuer), now: this.#now });
23230
+ }
23231
+ #predecessor() {
23232
+ return this.#store.getKv(PREDECESSOR_KEY) ?? this.#store.getKv(MACHINE_KEY) ?? this.#identity?.machine;
23233
+ }
23234
+ async#replacePreview(p) {
23235
+ const config = this.#config.realm;
23236
+ if (!config)
23237
+ 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");
23238
+ const person = this.#store.directoryCache().own().person;
23239
+ if (!person)
23240
+ throw new MessagingError("no_identity", "this home was never bound to an account: bind it instead (device.bind or messaging login)");
23241
+ const realm = this.#heldRealm(config);
23242
+ const pred = this.#predecessor();
23243
+ const record = realm.ownDevices().find((d) => d.device === pred);
23244
+ if (!record)
23245
+ throw new MessagingError("not_found", "this home's device is not in its account's log");
23246
+ const pending = this.#store.getKv(PREDECESSOR_KEY) !== undefined && this.#identity !== undefined && this.#identity.machine !== pred;
23247
+ const bound = pending && realm.binding().state !== "none";
23248
+ const signIns = !pending ? 2 : !bound ? 2 : record.current ? 1 : 0;
23249
+ const alarm = this.#keysMissing ? "keys_missing" : realm.alarm()?.kind === "restored" ? "restored" : null;
23250
+ const label = record.label;
23251
+ const why = alarm === "keys_missing" ? " (its key files are missing)" : alarm === "restored" ? " (it runs from an older copy of its home)" : "";
23252
+ 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}'.` : "");
23253
+ return {
23254
+ predecessor: {
23255
+ device: pred,
23256
+ label,
23257
+ kind: record.kind,
23258
+ vouch: record.vouch,
23259
+ ...record.vouch === "jelly" ? { via: record.host !== undefined ? "host" : "browser" } : {},
23260
+ since: record.since,
23261
+ current: record.current
23262
+ },
23263
+ alarm,
23264
+ ...pending ? { device: this.#identity.machine } : {},
23265
+ signIns,
23266
+ sentence: sentence.slice(0, 600)
23267
+ };
23268
+ }
23269
+ async#replace(p) {
23270
+ const config = this.#config.realm;
23271
+ if (!config)
23272
+ 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");
23273
+ if (this.#removed || this.#directory?.realm.removed())
23274
+ throw new MessagingError("invalid_state", "this device was removed from its account; it cannot replace anything");
23275
+ const person = this.#store.directoryCache().own().person;
23276
+ if (!person)
23277
+ throw new MessagingError("no_identity", "this home was never bound to an account: bind it instead (device.bind or messaging login)");
23278
+ const pred = this.#predecessor();
23279
+ if (!pred)
23280
+ throw new MessagingError("no_identity", "this home names no device to replace");
23281
+ const record = this.#heldRealm(config).ownDevices().find((d) => d.device === pred);
23282
+ if (!record)
23283
+ throw new MessagingError("not_found", "this home's device is not in its account's log");
23284
+ if (this.#store.getKv(PREDECESSOR_KEY) === undefined || this.#identity === undefined || this.#identity.machine === pred) {
23285
+ if (!record.current)
23286
+ throw new MessagingError("invalid_state", "this home's device was removed from its account; it cannot be replaced");
23287
+ await this.#newKeys(pred, config);
23288
+ }
23289
+ return this.#replaceNext(p);
23290
+ }
23291
+ async#replaceNext(p) {
23292
+ const home = this.#directory;
23293
+ const me = this.#identity.machine;
23294
+ if (p.token !== undefined) {
23295
+ const done = await this.#replaceWith(p.token, p.label);
23296
+ if (this.#store.getKv(PREDECESSOR_KEY) === undefined)
23297
+ return { state: "done", person: done.person, device: me, predecessor: done.predecessor, seq: done.seq };
23298
+ } else if (this.#replaceFinished()) {
23299
+ return this.#replaceDone();
23300
+ }
23301
+ if (this.#store.getKv(PREDECESSOR_KEY) === undefined)
23302
+ return this.#replaceDone();
23303
+ if (this.#jellyHost || p.token !== undefined)
23304
+ return { state: "token", device: me };
23305
+ const pred = this.#store.getKv(PREDECESSOR_KEY);
23306
+ const record = home.realm.ownDevices().find((d) => d.device === pred);
23307
+ const label = p.label ?? this.#store.getKv(REPLACE_LABEL_KEY) ?? record?.label ?? "device";
23308
+ const kind = record && record.kind !== "jelly-host" ? record.kind : "laptop";
23309
+ const started = await home.signIn("replace", { label, kind });
23310
+ return { state: "sign-in", device: me, ...started };
23311
+ }
23312
+ #replaceFinished() {
23313
+ const pred = this.#store.getKv(PREDECESSOR_KEY);
23314
+ if (pred === undefined)
23315
+ return true;
23316
+ const realm = this.#directory?.realm;
23317
+ if (!realm || realm.binding().state === "none")
23318
+ return false;
23319
+ return realm.ownDevices().find((d) => d.device === pred)?.current !== true;
23320
+ }
23321
+ #replaceDone() {
23322
+ const realm = this.#directory.realm;
23323
+ const pred = this.#store.getKv(PREDECESSOR_KEY) ?? this.#store.getKv(REPLACED_KEY);
23324
+ this.#store.deleteKv(PREDECESSOR_KEY);
23325
+ const me = this.#identity.machine;
23326
+ const binding = realm.binding();
23327
+ const record = pred !== undefined ? realm.ownDevices().find((d) => d.device === pred) : undefined;
23328
+ 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 };
23329
+ }
23330
+ async#replaceWith(token, label) {
23331
+ const home = this.#directory;
23332
+ const pred = this.#store.getKv(PREDECESSOR_KEY);
23333
+ if (!home || pred === undefined)
23334
+ throw new MessagingError("invalid_state", "no replacement is pending: start one with identity.replace");
23335
+ const realm = home.realm;
23336
+ const me = this.#identity.machine;
23337
+ const record = realm.ownDevices().find((d) => d.device === pred);
23338
+ let seq = 0;
23339
+ let spent = false;
23340
+ if (realm.binding().state === "none") {
23341
+ const kind = this.#jellyHost?.kind ?? (record && record.kind !== "jelly-host" ? record.kind : "laptop");
23342
+ const done = await home.bind({ token, label: this.#jellyHost?.label ?? label ?? this.#store.getKv(REPLACE_LABEL_KEY) ?? record?.label ?? "device", kind });
23343
+ seq = done.seq;
23344
+ spent = signInToken(token);
23345
+ this.#log("info", "identity.replace.bound", { seq });
23346
+ }
23347
+ await realm.awaitSelf();
23348
+ this.#tx(() => this.#grants.resignFrom(pred));
23349
+ let unbound = false;
23350
+ if (!spent && realm.ownDevices().find((d) => d.device === pred)?.current) {
23351
+ const done = await realm.unbind(pred, { reason: "replaced", token });
23352
+ seq = done.seq;
23353
+ unbound = true;
23354
+ this.#log("info", "identity.replace.done", { seq });
23355
+ }
23356
+ if (unbound || this.#replaceFinished()) {
23357
+ this.#store.setKv(REPLACED_KEY, pred);
23358
+ this.#store.deleteKv(PREDECESSOR_KEY);
23359
+ this.#store.deleteKv(REPLACE_LABEL_KEY);
23360
+ this.#emit({ type: "contacts" });
23361
+ }
23362
+ return { person: realm.binding().person, device: me, predecessor: pred, seq };
23363
+ }
23364
+ async#newKeys(pred, config) {
23365
+ const label = this.#heldRealm(config).ownDevices().find((d) => d.device === pred)?.label;
23366
+ this.#store.setKv(PREDECESSOR_KEY, pred);
23367
+ if (label !== undefined)
23368
+ this.#store.setKv(REPLACE_LABEL_KEY, label);
23369
+ const keys = { identity: generateIdentity(), retained: [] };
23370
+ let identityStore = this.#keyring.store;
23371
+ if (identityStore)
23372
+ identityStore.save(keys);
23373
+ else
23374
+ identityStore = saveNewIdentity(this.paths.identity, keys, identitySettings(this.#config), this.#storeDeps());
23375
+ this.#store.setKv(MACHINE_KEY, keys.identity.machine);
23376
+ this.#log("info", "identity.replace.keys", { store: identityStore.kind });
23377
+ const old = this.#directory;
23378
+ this.#directory = undefined;
23379
+ this.#realm = undefined;
23380
+ this.#transport = undefined;
23381
+ if (old) {
23382
+ await old.stop();
23383
+ this.#onHostedState(false);
23384
+ }
23385
+ const cache = this.#store.directoryCache();
23386
+ if (cache.alarm()?.kind === "restored")
23387
+ cache.setAlarm(undefined);
23388
+ for (const row of this.#store.outbox())
23389
+ if (row.env.from.device === pred)
23390
+ this.#store.deleteOutbox(row.id);
23391
+ this.#keysMissing = false;
23392
+ this.#keyring.adopt(keys, identityStore);
23393
+ this.#openDirectory(config, keys, identityStore);
23394
+ }
23395
+ #erasePreview(_p) {
23396
+ const home = this.#homeDirectory("directory.erase.preview");
23397
+ const binding = home.realm.binding();
23398
+ if (!binding.person || binding.state === "none" || binding.state === "removed")
23399
+ throw new MessagingError("no_identity", "this device is not bound to an account");
23400
+ const devices = home.realm.ownDevices().filter((d) => d.current);
23401
+ const me = this.#identity.machine;
23402
+ return {
23403
+ person: binding.person,
23404
+ devices: devices.map((d) => ({ device: d.device, label: d.label, kind: d.kind, self: d.device === me })),
23405
+ signIns: this.#jellyHost || binding.record?.host !== undefined ? 0 : 1,
23406
+ perDay: ERASURES_PER_DAY,
23407
+ lost: [
23408
+ `Every device of your account (${devices.length}, this one included) is removed from Messaging, and none can be bound again.`,
23409
+ "Your Jelly account can never be bound to Messaging again: the erasure is for good, and a new sign-in is refused.",
23410
+ "The directory deletes your name, email and handle, and every device record and key statement it held for you.",
23411
+ "Messages still queued at the relay for your devices are deleted.",
23412
+ "Your contacts see a deleted account, and drop the grants and cards they hold for you."
23413
+ ],
23414
+ 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."]
23415
+ };
23416
+ }
23417
+ async#erase(p) {
23418
+ const home = this.#homeDirectory("directory.erase");
23419
+ const binding = home.realm.binding();
23420
+ if (!binding.person || binding.state === "none" || binding.state === "removed")
23421
+ throw new MessagingError("no_identity", "this device is not bound to an account");
23422
+ if (binding.record?.vouch !== "jelly")
23423
+ throw new MessagingError("forbidden", "a device approved by link erases nothing; erase from a device Jelly ID vouched for");
23424
+ if (p.token !== undefined) {
23425
+ const done = await this.#eraseWith(p.token);
23426
+ return { state: "done", person: done.person, devices: done.devices };
23427
+ }
23428
+ if (this.#jellyHost)
23429
+ throw new MessagingError("invalid_request", "under a Jelly host, directory.erase needs a fresh token for this device from its host (token)");
23430
+ const record = binding.record;
23431
+ const started = await home.signIn("erase", { label: record.label, kind: record.kind !== "jelly-host" ? record.kind : "laptop" });
23432
+ return { state: "sign-in", ...started };
23433
+ }
23434
+ async#eraseWith(token) {
23435
+ const home = this.#homeDirectory("directory.erase");
23436
+ let done;
23437
+ try {
23438
+ done = await home.realm.erase(token);
23439
+ } catch (error) {
23440
+ if (!(error instanceof MessagingError) || error.code !== "rate_limited")
23441
+ throw error;
23442
+ const wait = error.retryAfterMs;
23443
+ const when = typeof wait === "number" && wait > 0 ? `; try again in about ${Math.max(1, Math.ceil(wait / 60000))} min` : "; try again tomorrow";
23444
+ throw new MessagingError("rate_limited", `the directory allows ${ERASURES_PER_DAY} erasures per person a day${when}`);
23445
+ }
23446
+ this.#log("warn", "directory.erased", { devices: done.devices });
23447
+ return done;
23448
+ }
23068
23449
  #removePreview(p) {
23069
23450
  const entry = this.#homeDirectory("device.remove.preview").list().devices.find((d) => d.device === p.device);
23070
23451
  if (!entry)
@@ -23078,6 +23459,95 @@ ${row.receipt ? "receipt" : "envelope"}`;
23078
23459
  ...entry.since !== undefined ? { since: entry.since } : {}
23079
23460
  };
23080
23461
  }
23462
+ #deviceLogGrew(persons) {
23463
+ try {
23464
+ this.#deviceNotices();
23465
+ } catch (error) {
23466
+ this.#log("error", "notice.device.failed", { code: codeOf(error) });
23467
+ }
23468
+ if (persons.some((person) => person !== this.#me()))
23469
+ this.#emit({ type: "contacts" });
23470
+ }
23471
+ #ownPerson() {
23472
+ return this.#directory?.person() ?? this.#me();
23473
+ }
23474
+ #deviceNotices(start = false) {
23475
+ const realm = this.#realm ?? this.#directory?.realm;
23476
+ const person = this.#ownPerson();
23477
+ const me = this.#identity?.machine;
23478
+ if (!realm || !person || !me)
23479
+ return;
23480
+ const changes = realm.deviceChanges(person);
23481
+ const head = changes.at(-1)?.seq ?? -1;
23482
+ let last = this.#store.getKv(DEVICE_NOTICES_KEY);
23483
+ if (last === undefined) {
23484
+ const admitted = changes.find((change) => change.device === me && change.change === "added");
23485
+ if (!start && changes.length === 0)
23486
+ return;
23487
+ if (start || admitted === undefined) {
23488
+ this.#store.setKv(DEVICE_NOTICES_KEY, String(head));
23489
+ return;
23490
+ }
23491
+ last = String(admitted.seq);
23492
+ }
23493
+ const fresh = changes.filter((change) => change.seq > Number(last));
23494
+ if (fresh.length === 0)
23495
+ return;
23496
+ const replacers = new Set(fresh.filter((change) => change.change === "replaced").map((change) => change.by));
23497
+ for (const change of fresh) {
23498
+ if (change.device === me || change.by === me)
23499
+ continue;
23500
+ if (change.change === "added" && replacers.has(change.device))
23501
+ continue;
23502
+ this.#notices.device(change.seq, change.at);
23503
+ }
23504
+ this.#store.setKv(DEVICE_NOTICES_KEY, String(Math.max(head, Number(last))));
23505
+ }
23506
+ #noticeDevice(seq) {
23507
+ const realm = this.#realm ?? this.#directory?.realm;
23508
+ const person = this.#ownPerson();
23509
+ if (!realm || !person)
23510
+ return;
23511
+ const changes = realm.deviceChanges(person);
23512
+ const change = changes.find((c) => c.seq === seq);
23513
+ if (!change)
23514
+ return;
23515
+ const labelOf = (device) => changes.filter((c) => c.device === device).at(-1)?.label;
23516
+ const gone = (device) => changes.some((c) => c.device === device && (c.change === "removed" || c.change === "replaced"));
23517
+ const subject = change.change === "replaced" ? change.by : change.device;
23518
+ const latest = change.change === "replaced" ? changes.filter((c) => c.device === subject && c.change !== "removed" && c.change !== "replaced").at(-1) : change;
23519
+ const label = latest?.label ?? labelOf(subject) ?? "device";
23520
+ const current = !gone(subject);
23521
+ const view = {
23522
+ change: change.change,
23523
+ device: subject,
23524
+ label: MachineLabel.safeParse(label).success ? label : "device",
23525
+ ...latest?.kind !== undefined ? { kind: latest.kind } : {},
23526
+ vouch: ((v) => v === "service" ? "device" : v)(latest?.vouch ?? change.vouch),
23527
+ ...(latest?.via ?? change.via) !== undefined ? { via: latest?.via ?? change.via } : {},
23528
+ ...change.approver !== undefined && labelOf(change.approver) !== undefined ? { approver: labelOf(change.approver) } : {},
23529
+ ...change.change === "replaced" ? { replaces: { device: change.device, label: change.label } } : {},
23530
+ current,
23531
+ at: change.at
23532
+ };
23533
+ const remove = current ? this.#removeWith() : undefined;
23534
+ if (remove)
23535
+ view.remove = remove;
23536
+ return view;
23537
+ }
23538
+ #removeWith() {
23539
+ const home = this.#directory;
23540
+ if (!home)
23541
+ return this.#roster?.isAdmin(this.#identity?.machine ?? "") ? "team" : undefined;
23542
+ const self = home.list().devices.find((device) => device.self && device.current);
23543
+ if (!self || self.vouched !== "jelly")
23544
+ return;
23545
+ return self.via === "browser" ? "sign-in" : "token";
23546
+ }
23547
+ #trustNotices() {
23548
+ for (const grant of this.#grants.expiringTrust(TRUST_NOTICE_MS))
23549
+ this.#notices.trust(grant.id, grant.person, grant.until, this.#now());
23550
+ }
23081
23551
  #alerts() {
23082
23552
  const realmAlarm = this.#directory?.status().alarm;
23083
23553
  const roster = this.#keysMissing ? "keys_missing" : this.#keyring.restored() ? "restored" : undefined;
@@ -23095,6 +23565,16 @@ ${row.receipt ? "receipt" : "envelope"}`;
23095
23565
  }
23096
23566
  if (realmAlarm)
23097
23567
  this.#notices.alert(realmAlarm.kind, realmAlarm.at);
23568
+ this.#deviceNotices();
23569
+ if (this.#joinedOrUndefined())
23570
+ this.#trustNotices();
23571
+ }
23572
+ #joinedOrUndefined() {
23573
+ try {
23574
+ return this.#joined();
23575
+ } catch {
23576
+ return;
23577
+ }
23098
23578
  }
23099
23579
  #homeDirectory(method) {
23100
23580
  if (this.#directory)
@@ -23203,7 +23683,7 @@ ${row.receipt ? "receipt" : "envelope"}`;
23203
23683
  throw new MessagingError("relay_unavailable", "config.json has no relay url");
23204
23684
  let result;
23205
23685
  await this.#append((roster) => {
23206
- const made = createInvite(roster, identity, { person: p.person, role: p.role ?? "member", ttlMs: p.ttlMs ?? DEFAULT_INVITE_TTL_MS, relay, now: this.#now() });
23686
+ const made = createInvite(roster, identity, { person: p.person, ttlMs: p.ttlMs ?? DEFAULT_INVITE_TTL_MS, relay, now: this.#now() });
23207
23687
  result = { ticket: made.ticket, inviteId: made.entry.body.inviteId, expires: made.entry.body.expires };
23208
23688
  return made.entry;
23209
23689
  });
@@ -23223,8 +23703,8 @@ ${row.receipt ? "receipt" : "envelope"}`;
23223
23703
  const invites = roster.entries().flatMap((entry) => {
23224
23704
  if (entry.kind !== "invite.create")
23225
23705
  return [];
23226
- const { inviteId, person, role, expires } = entry.body;
23227
- return [{ inviteId, person, role, expires, state: roster.invite(inviteId, now)?.state ?? "expired" }];
23706
+ const { inviteId, person, expires } = entry.body;
23707
+ return [{ inviteId, person, expires, state: roster.invite(inviteId, now)?.state ?? "expired" }];
23228
23708
  });
23229
23709
  return { team: { id: roster.team, name: roster.name }, members, invites };
23230
23710
  }
@@ -23247,7 +23727,7 @@ ${row.receipt ? "receipt" : "envelope"}`;
23247
23727
  }
23248
23728
  async#teamAdd(p) {
23249
23729
  const { identity } = this.#admin();
23250
- await this.#append((roster) => createEntry(roster, identity, "member.add", { machine: p.machine, enc: p.enc, person: p.person, role: p.role ?? "member", label: p.label }, this.#now()));
23730
+ await this.#append((roster) => createEntry(roster, identity, "member.add", { machine: p.machine, enc: p.enc, person: p.person, role: "member", label: p.label }, this.#now()));
23251
23731
  this.#log("info", "team.added", { machine: p.machine, person: p.person });
23252
23732
  return { ok: true };
23253
23733
  }
@@ -23364,7 +23844,7 @@ function chainPersons(sender, payload) {
23364
23844
  function peerRef(member, session) {
23365
23845
  return { address: formatAddress(member.person, session, member.label), person: member.person, agent: session, device: member.machine, label: member.label };
23366
23846
  }
23367
- function provenanceOf3(env, sender, now, external) {
23847
+ function provenanceOf3(env, sender, now) {
23368
23848
  const session = env.from.agent;
23369
23849
  return {
23370
23850
  envelopeId: env.id,
@@ -23374,8 +23854,7 @@ function provenanceOf3(env, sender, now, external) {
23374
23854
  device: sender.machine,
23375
23855
  label: sender.label,
23376
23856
  verified: "signature",
23377
- receivedAt: now,
23378
- ...external !== undefined ? { external: { principal: external.principal, gateway: sender.machine } } : {}
23857
+ receivedAt: now
23379
23858
  };
23380
23859
  }
23381
23860
  function withProvenance(message, provenance) {
@@ -23391,7 +23870,7 @@ function plaintextBytes(env) {
23391
23870
  }
23392
23871
  function requestOf(row) {
23393
23872
  if (row.request === undefined)
23394
- throw new MessagingError("invalid_state", "this task began before this version of messagingd; its updates cannot be sent");
23873
+ throw new MessagingError("invalid_state", "this task has no request to answer");
23395
23874
  return row.request;
23396
23875
  }
23397
23876
  function recipientSession(env) {
@@ -23463,7 +23942,15 @@ function jellyConfig(path, jelly) {
23463
23942
  function isLineTarget(to) {
23464
23943
  return to.startsWith("#") || !to.includes("/");
23465
23944
  }
23466
- 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;
23945
+ function signInToken(token) {
23946
+ try {
23947
+ const claims = parseWire(Buffer.from(token.split(".")[1] ?? "", "base64url").toString("utf8"));
23948
+ return typeof claims === "object" && claims !== null && claims.src === "device";
23949
+ } catch {
23950
+ return false;
23951
+ }
23952
+ }
23953
+ 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;
23467
23954
  var init_daemon = __esm(() => {
23468
23955
  init_dist();
23469
23956
  init_dist2();
@@ -23480,6 +23967,7 @@ var init_daemon = __esm(() => {
23480
23967
  init_policy();
23481
23968
  init_roster_identity();
23482
23969
  init_directory_home();
23970
+ init_directory_identity();
23483
23971
  init_jelly3();
23484
23972
  init_jelly_env();
23485
23973
  init_answer_preview();
@@ -23509,644 +23997,11 @@ var init_daemon = __esm(() => {
23509
23997
  };
23510
23998
  KEPT_MS = DAY3;
23511
23999
  INPUT_TIMEOUT_MS = DAY3;
24000
+ TRUST_NOTICE_MS = 7 * DAY3;
24001
+ DEVICES_CHANGED_MS = 7 * DAY3;
23512
24002
  DEFAULT_INVITE_TTL_MS = DAY3;
23513
24003
  });
23514
24004
 
23515
- // packages/daemon/dist/direct-link.js
23516
- import { createServer } from "http";
23517
- import { WebSocket, WebSocketServer } from "ws";
23518
- function unavailable2(message) {
23519
- return new MessagingError("relay_unavailable", message);
23520
- }
23521
-
23522
- class Bucket {
23523
- capacity;
23524
- #tokens;
23525
- #at = Date.now();
23526
- constructor(capacity) {
23527
- this.capacity = capacity;
23528
- this.#tokens = capacity;
23529
- }
23530
- take(amount = 1) {
23531
- const now = Date.now();
23532
- this.#tokens = Math.min(this.capacity, this.#tokens + (now - this.#at) * this.capacity / 60000);
23533
- this.#at = now;
23534
- if (this.#tokens < amount)
23535
- return false;
23536
- this.#tokens -= amount;
23537
- return true;
23538
- }
23539
- }
23540
-
23541
- class Link {
23542
- peer;
23543
- role;
23544
- #ws;
23545
- #tx;
23546
- #rx;
23547
- #owner;
23548
- #pending = new Map;
23549
- #deliveries = Promise.resolve();
23550
- #closed = false;
23551
- constructor(peer, role, ws, keys, owner) {
23552
- this.peer = peer;
23553
- this.role = role;
23554
- this.#ws = ws;
23555
- this.#owner = owner;
23556
- const other = role === "initiator" ? "responder" : "initiator";
23557
- this.#tx = new DirectCipher(keys.record[role]);
23558
- this.#rx = new DirectCipher(keys.record[other]);
23559
- ws.removeAllListeners("message");
23560
- ws.on("message", (data, isBinary) => {
23561
- try {
23562
- this.#onMessage(data, isBinary);
23563
- } catch {
23564
- this.close(DIRECT_CLOSE.protocolError);
23565
- }
23566
- });
23567
- ws.on("close", () => this.#onClose());
23568
- }
23569
- get open() {
23570
- return !this.#closed && this.#ws.readyState === WebSocket.OPEN;
23571
- }
23572
- send(env, ackMs) {
23573
- const existing = this.#pending.get(env.id);
23574
- if (existing)
23575
- return existing.promise;
23576
- if (!this.open)
23577
- return Promise.reject(unavailable2("direct link closed"));
23578
- const text = JSON.stringify({ t: "send", env });
23579
- if (Buffer.byteLength(text) > LIMITS.frameBytes)
23580
- return Promise.reject(new MessagingError("too_large", "envelope exceeds the frame limit"));
23581
- let entry;
23582
- const promise = new Promise((resolve, reject) => {
23583
- const timer = setTimeout(() => this.#settle(env.id, unavailable2("direct peer did not answer in time")), ackMs);
23584
- timer.unref();
23585
- entry = { resolve, reject, timer, promise: undefined };
23586
- });
23587
- entry.promise = promise;
23588
- this.#pending.set(env.id, entry);
23589
- this.#write({ t: "send", env }, text);
23590
- return promise;
23591
- }
23592
- close(code) {
23593
- if (this.#closed)
23594
- return;
23595
- this.#closed = true;
23596
- this.#failAll();
23597
- this.#ws.close(code);
23598
- setTimeout(() => this.#ws.terminate(), 2000).unref();
23599
- }
23600
- #onClose() {
23601
- this.#closed = true;
23602
- this.#failAll();
23603
- this.#owner.forget(this);
23604
- }
23605
- #failAll() {
23606
- for (const id of [...this.#pending.keys()])
23607
- this.#settle(id, unavailable2("direct link closed"));
23608
- }
23609
- #settle(id, error) {
23610
- const entry = this.#pending.get(id);
23611
- if (!entry)
23612
- return;
23613
- this.#pending.delete(id);
23614
- clearTimeout(entry.timer);
23615
- if (error)
23616
- entry.reject(error);
23617
- else
23618
- entry.resolve();
23619
- }
23620
- #write(record, text = JSON.stringify(record)) {
23621
- if (this.open)
23622
- this.#ws.send(this.#tx.seal(Buffer.from(text, "utf8")));
23623
- }
23624
- #onMessage(data, isBinary) {
23625
- if (this.#closed)
23626
- return;
23627
- if (!isBinary)
23628
- return this.close(DIRECT_CLOSE.protocolError);
23629
- if (!this.#owner.isMember(this.peer))
23630
- return this.close(DIRECT_CLOSE.notMember);
23631
- const bytes = bufferOf(data);
23632
- let raw;
23633
- try {
23634
- raw = parseWire(this.#rx.open(bytes).toString("utf8"), { maxBytes: LIMITS.frameBytes });
23635
- } catch {
23636
- return this.close(DIRECT_CLOSE.protocolError);
23637
- }
23638
- const within = this.#owner.withinRate(this.peer, bytes.length);
23639
- const parsed = DirectRecord.safeParse(raw);
23640
- if (!parsed.success) {
23641
- const id = raw?.env?.id;
23642
- if (raw?.t === "send" && Ulid.safeParse(id).success)
23643
- this.#write({ t: "nack", id, code: within ? "invalid_request" : "rate_limited" });
23644
- return;
23645
- }
23646
- const record = parsed.data;
23647
- if (record.t === "send")
23648
- return this.#onSend(raw.env, record.env, within);
23649
- if (!within)
23650
- return;
23651
- if (record.t === "ack")
23652
- this.#settle(record.id);
23653
- else
23654
- this.#settle(record.id, new MessagingError(record.code, "refused by the direct peer"));
23655
- }
23656
- #onSend(raw, env, within) {
23657
- const nack = (code) => this.#write({ t: "nack", id: env.id, code });
23658
- if (!within)
23659
- return nack("rate_limited");
23660
- if (env.from.device !== this.peer || env.to.device !== this.#owner.self || env.type === "presence")
23661
- return nack("invalid_request");
23662
- this.#deliveries = this.#deliveries.then(async () => {
23663
- try {
23664
- await this.#owner.deliver(raw);
23665
- } catch (error) {
23666
- return nack(error instanceof MessagingError ? error.code : "internal");
23667
- }
23668
- this.#write({ t: "ack", id: env.id });
23669
- });
23670
- }
23671
- }
23672
-
23673
- class DirectLinks {
23674
- #options;
23675
- #limits;
23676
- #in = new Map;
23677
- #out = new Map;
23678
- #dials = new Map;
23679
- #rates = new Map;
23680
- #pendingByAddress = new Map;
23681
- #handshakes;
23682
- #sockets = new Set;
23683
- #dialing = new Set;
23684
- #pending = 0;
23685
- #server;
23686
- #wss;
23687
- #closed = false;
23688
- stats = { inbound: 0, outbound: 0, refused: 0, dialFailed: 0, delivered: 0 };
23689
- constructor(options) {
23690
- this.#options = options;
23691
- this.#limits = { ...DEFAULT_DIRECT_LIMITS, ...options.limits };
23692
- this.#handshakes = new Bucket(this.#limits.handshakesPerMinute);
23693
- }
23694
- get self() {
23695
- return this.#options.identity.machine;
23696
- }
23697
- isMember(machine) {
23698
- return machine !== this.self && this.#options.member(machine) !== undefined;
23699
- }
23700
- listen(host, port) {
23701
- if (this.#server)
23702
- return Promise.reject(new MessagingError("invalid_state", "already listening"));
23703
- const server = createServer((_req, res) => {
23704
- res.writeHead(426, { connection: "close" });
23705
- res.end();
23706
- });
23707
- server.headersTimeout = this.#limits.handshakeMs;
23708
- server.requestTimeout = this.#limits.handshakeMs;
23709
- server.maxConnections = this.#limits.pending + 256;
23710
- const wss = new WebSocketServer({ noServer: true, clientTracking: false, ...SOCKET_OPTIONS });
23711
- server.on("connection", (socket) => {
23712
- this.#sockets.add(socket);
23713
- socket.once("close", () => this.#sockets.delete(socket));
23714
- });
23715
- server.on("upgrade", (req, socket, head) => this.#upgrade(wss, req, socket, head));
23716
- this.#server = server;
23717
- this.#wss = wss;
23718
- return new Promise((resolve, reject) => {
23719
- server.once("error", reject);
23720
- server.listen({ host, port, exclusive: true }, () => {
23721
- server.off("error", reject);
23722
- server.on("error", () => {});
23723
- const bound = server.address();
23724
- if (bound === null || typeof bound === "string")
23725
- return reject(new MessagingError("internal", "listener has no address"));
23726
- resolve({ host: bound.address, port: bound.port });
23727
- });
23728
- });
23729
- }
23730
- async close() {
23731
- this.#closed = true;
23732
- for (const link of [...this.#in.values(), ...this.#out.values()])
23733
- link.close(1001);
23734
- for (const ws of this.#dialing)
23735
- ws.terminate();
23736
- this.#in.clear();
23737
- this.#out.clear();
23738
- const server = this.#server;
23739
- this.#server = undefined;
23740
- this.#wss?.close();
23741
- if (!server)
23742
- return;
23743
- await new Promise((resolve) => {
23744
- server.close(() => resolve());
23745
- for (const socket of this.#sockets)
23746
- socket.destroy();
23747
- });
23748
- }
23749
- hasLink(peer) {
23750
- return this.#linkTo(peer) !== undefined;
23751
- }
23752
- async send(peer, endpoints, env) {
23753
- if (this.#closed)
23754
- throw unavailable2("direct links are closed");
23755
- if (!this.isMember(peer))
23756
- throw new MessagingError("not_member", "not a current member");
23757
- let link = this.#linkTo(peer);
23758
- if (!link) {
23759
- if (endpoints.length === 0)
23760
- throw unavailable2("no direct link to this machine");
23761
- link = await this.#dialOnce(peer, endpoints);
23762
- }
23763
- await link.send(env, this.#limits.ackMs);
23764
- }
23765
- prune() {
23766
- for (const link of [...this.#in.values(), ...this.#out.values()])
23767
- if (!this.isMember(link.peer))
23768
- link.close(DIRECT_CLOSE.notMember);
23769
- }
23770
- forget(link) {
23771
- if (this.#in.get(link.peer) === link)
23772
- this.#in.delete(link.peer);
23773
- if (this.#out.get(link.peer) === link)
23774
- this.#out.delete(link.peer);
23775
- }
23776
- withinRate(peer, bytes) {
23777
- let rate = this.#rates.get(peer);
23778
- if (!rate) {
23779
- rate = { frames: new Bucket(this.#limits.framesPerMinute), bytes: new Bucket(this.#limits.bytesPerMinute) };
23780
- this.#rates.set(peer, rate);
23781
- }
23782
- return rate.frames.take() && rate.bytes.take(bytes);
23783
- }
23784
- async deliver(raw) {
23785
- await this.#options.onDeliver(raw);
23786
- this.stats.delivered++;
23787
- }
23788
- #linkTo(peer) {
23789
- for (const link of [this.#out.get(peer), this.#in.get(peer)])
23790
- if (link?.open)
23791
- return link;
23792
- return;
23793
- }
23794
- #upgrade(wss, req, socket, head) {
23795
- const address = req.socket.remoteAddress ?? "?";
23796
- const fromAddress = this.#pendingByAddress.get(address) ?? 0;
23797
- const path = new URL(req.url ?? "/", "http://direct").pathname;
23798
- if (this.#closed || path !== DIRECT_PATH || this.#pending >= this.#limits.pending || fromAddress >= this.#limits.pendingPerAddress || !this.#handshakes.take()) {
23799
- this.stats.refused++;
23800
- socket.destroy();
23801
- return;
23802
- }
23803
- this.#pending++;
23804
- this.#pendingByAddress.set(address, fromAddress + 1);
23805
- let released = false;
23806
- const release = () => {
23807
- if (released)
23808
- return;
23809
- released = true;
23810
- this.#pending--;
23811
- const left = (this.#pendingByAddress.get(address) ?? 1) - 1;
23812
- if (left > 0)
23813
- this.#pendingByAddress.set(address, left);
23814
- else
23815
- this.#pendingByAddress.delete(address);
23816
- };
23817
- const timer = setTimeout(() => {
23818
- release();
23819
- socket.destroy();
23820
- }, this.#limits.handshakeMs);
23821
- timer.unref();
23822
- socket.once("close", () => {
23823
- clearTimeout(timer);
23824
- release();
23825
- });
23826
- wss.handleUpgrade(req, socket, head, (ws) => this.#respond(ws, () => {
23827
- clearTimeout(timer);
23828
- release();
23829
- }));
23830
- }
23831
- #respond(ws, done) {
23832
- const { identity, team } = this.#options;
23833
- let state = { step: "hello" };
23834
- const refuse = () => {
23835
- state = { step: "done" };
23836
- this.stats.refused++;
23837
- done();
23838
- ws.close(DIRECT_CLOSE.authFailed);
23839
- setTimeout(() => ws.terminate(), 2000).unref();
23840
- };
23841
- ws.on("error", () => {});
23842
- ws.on("message", (data, isBinary) => {
23843
- try {
23844
- step(data, isBinary);
23845
- } catch {
23846
- if (state.step !== "done")
23847
- refuse();
23848
- }
23849
- });
23850
- ws.on("close", () => done());
23851
- const step = (data, isBinary) => {
23852
- if (state.step === "done")
23853
- return;
23854
- if (state.step === "hello") {
23855
- const hello = handshakeFrame(DirectHelloFrame, data, isBinary);
23856
- if (!hello)
23857
- return refuse();
23858
- const own = directEphemeral();
23859
- const transcript = directTranscript(hello, own.half);
23860
- let keys;
23861
- try {
23862
- keys = directKeys(own.key, hello.epk, transcript);
23863
- } catch {
23864
- return refuse();
23865
- }
23866
- const sig = sign(identity, DOMAIN.directAuth, directAuthPayload({ transcript, team, role: "responder", machine: identity.machine }));
23867
- state = { step: "auth", transcript, keys };
23868
- ws.send(JSON.stringify({ t: "accept", ...own.half, box: sealDirectBox(keys.box.responder, transcript, { team, machine: identity.machine, sig }) }));
23869
- return;
23870
- }
23871
- const { transcript, keys } = state;
23872
- const auth = handshakeFrame(DirectAuthFrame, data, isBinary);
23873
- const peer = auth && openIdentity(keys.box.initiator, transcript, auth.box);
23874
- if (!peer || peer.team !== team || !this.isMember(peer.machine))
23875
- return refuse();
23876
- const signed = directAuthPayload({ transcript, team, role: "initiator", machine: peer.machine, peer: identity.machine });
23877
- if (!verify(peer.machine, DOMAIN.directAuth, signed, peer.sig))
23878
- return refuse();
23879
- state = { step: "done" };
23880
- done();
23881
- if (this.#closed)
23882
- return void ws.close(1001);
23883
- const link = new Link(peer.machine, "responder", ws, keys, this);
23884
- this.#in.get(peer.machine)?.close(DIRECT_CLOSE.replaced);
23885
- this.#in.set(peer.machine, link);
23886
- this.stats.inbound++;
23887
- };
23888
- }
23889
- #dialOnce(peer, endpoints) {
23890
- let dial = this.#dials.get(peer);
23891
- if (!dial) {
23892
- dial = this.#dialAny(peer, endpoints).finally(() => this.#dials.delete(peer));
23893
- this.#dials.set(peer, dial);
23894
- }
23895
- return dial;
23896
- }
23897
- async#dialAny(peer, endpoints) {
23898
- let last;
23899
- for (const endpoint of endpoints) {
23900
- try {
23901
- const link = await this.#dial(peer, endpoint);
23902
- if (this.#closed) {
23903
- link.close(1001);
23904
- break;
23905
- }
23906
- this.#out.get(peer)?.close(1000);
23907
- this.#out.set(peer, link);
23908
- this.stats.outbound++;
23909
- return link;
23910
- } catch (error) {
23911
- last = error;
23912
- }
23913
- }
23914
- this.stats.dialFailed++;
23915
- throw last instanceof MessagingError ? last : unavailable2("could not reach the direct peer");
23916
- }
23917
- #dial(peer, endpoint) {
23918
- const { identity, team } = this.#options;
23919
- const host = endpoint.host.includes(":") ? `[${endpoint.host}]` : endpoint.host;
23920
- return new Promise((resolve, reject) => {
23921
- const ws = new WebSocket(`ws://${host}:${endpoint.port}${DIRECT_PATH}`, { ...SOCKET_OPTIONS, handshakeTimeout: this.#limits.dialMs, followRedirects: false });
23922
- this.#dialing.add(ws);
23923
- let settled = false;
23924
- const fail = (message) => {
23925
- if (settled)
23926
- return;
23927
- settled = true;
23928
- clearTimeout(timer);
23929
- this.#dialing.delete(ws);
23930
- ws.removeAllListeners("message");
23931
- if (ws.readyState === WebSocket.OPEN)
23932
- ws.close(DIRECT_CLOSE.authFailed);
23933
- else
23934
- ws.terminate();
23935
- reject(unavailable2(message));
23936
- };
23937
- const timer = setTimeout(() => fail("direct handshake timed out"), this.#limits.dialMs);
23938
- timer.unref();
23939
- const own = directEphemeral();
23940
- ws.on("error", () => fail("cannot reach the direct peer"));
23941
- ws.on("close", () => fail("the direct peer closed the link"));
23942
- ws.on("unexpected-response", () => fail("the direct peer refused the upgrade"));
23943
- ws.on("open", () => ws.send(JSON.stringify({ t: "hello", proto: DIRECT_PROTOCOL, ...own.half })));
23944
- ws.on("message", (data, isBinary) => {
23945
- try {
23946
- onAccept(data, isBinary);
23947
- } catch {
23948
- fail("bad handshake");
23949
- }
23950
- });
23951
- const onAccept = (data, isBinary) => {
23952
- if (settled)
23953
- return;
23954
- const accept = handshakeFrame(DirectAcceptFrame, data, isBinary);
23955
- if (!accept)
23956
- return fail("bad handshake frame");
23957
- const transcript = directTranscript(own.half, accept);
23958
- let keys;
23959
- try {
23960
- keys = directKeys(own.key, accept.epk, transcript);
23961
- } catch {
23962
- return fail("unusable handshake key");
23963
- }
23964
- const them = openIdentity(keys.box.responder, transcript, accept.box);
23965
- if (!them || them.team !== team || them.machine !== peer || !this.isMember(peer))
23966
- return fail("the direct peer is not the expected member");
23967
- if (!verify(peer, DOMAIN.directAuth, directAuthPayload({ transcript, team, role: "responder", machine: peer }), them.sig))
23968
- return fail("bad handshake signature");
23969
- const sig = sign(identity, DOMAIN.directAuth, directAuthPayload({ transcript, team, role: "initiator", machine: identity.machine, peer }));
23970
- ws.send(JSON.stringify({ t: "auth", box: sealDirectBox(keys.box.initiator, transcript, { team, machine: identity.machine, sig }) }));
23971
- settled = true;
23972
- clearTimeout(timer);
23973
- this.#dialing.delete(ws);
23974
- ws.removeAllListeners("close");
23975
- resolve(new Link(peer, "initiator", ws, keys, this));
23976
- };
23977
- });
23978
- }
23979
- }
23980
- function handshakeFrame(schema, data, isBinary) {
23981
- if (isBinary)
23982
- return;
23983
- const bytes = bufferOf(data);
23984
- if (bytes.length > DIRECT_HANDSHAKE_BYTES)
23985
- return;
23986
- try {
23987
- const parsed = schema.safeParse(parseWire(bytes.toString("utf8"), { maxBytes: DIRECT_HANDSHAKE_BYTES }));
23988
- return parsed.success ? parsed.data : undefined;
23989
- } catch {
23990
- return;
23991
- }
23992
- }
23993
- function openIdentity(key, transcript, box) {
23994
- try {
23995
- const parsed = DirectIdentity.safeParse(openDirectBox(key, transcript, box));
23996
- return parsed.success ? parsed.data : undefined;
23997
- } catch {
23998
- return;
23999
- }
24000
- }
24001
- function bufferOf(data) {
24002
- if (Buffer.isBuffer(data))
24003
- return data;
24004
- if (Array.isArray(data))
24005
- return Buffer.concat(data);
24006
- return Buffer.from(data);
24007
- }
24008
- var DIRECT_PATH = "/v1/direct", DIRECT_CLOSE, DEFAULT_DIRECT_LIMITS, RECORD_BYTES, SOCKET_OPTIONS;
24009
- var init_direct_link = __esm(() => {
24010
- init_dist();
24011
- DIRECT_CLOSE = { authFailed: 4001, protocolError: 4002, notMember: 4003, replaced: 4004 };
24012
- DEFAULT_DIRECT_LIMITS = {
24013
- pending: 16,
24014
- pendingPerAddress: 4,
24015
- handshakeMs: 5000,
24016
- handshakesPerMinute: 120,
24017
- dialMs: 2000,
24018
- ackMs: 5000,
24019
- framesPerMinute: LIMITS.relayFramesPerMinute,
24020
- bytesPerMinute: LIMITS.relayBytesPerMinute
24021
- };
24022
- RECORD_BYTES = LIMITS.frameBytes + 16;
24023
- SOCKET_OPTIONS = { maxPayload: RECORD_BYTES, perMessageDeflate: false };
24024
- });
24025
-
24026
- // packages/daemon/dist/direct-transport.js
24027
- function withDirect(relay, tuning = {}) {
24028
- return (options) => options.direct?.enabled === true && options.team !== undefined ? new DirectTransport(relay, options, options.direct, tuning) : relay(options);
24029
- }
24030
-
24031
- class DirectTransport {
24032
- #relay;
24033
- #links;
24034
- #options;
24035
- #config;
24036
- #listen;
24037
- #backoffMs;
24038
- #maxBackoffMs;
24039
- #backoff = new Map;
24040
- #advertised;
24041
- #running = false;
24042
- #counts = { direct: 0, relay: 0, fallback: 0 };
24043
- constructor(relay, options, config, tuning = {}) {
24044
- this.#options = options;
24045
- this.#config = config;
24046
- const listen = parseHostPort(config.listen ?? DEFAULT_DIRECT_LISTEN);
24047
- this.#listen = { host: listen.host, port: listen.port ?? 0 };
24048
- this.#backoffMs = tuning.backoffMs ?? 30000;
24049
- this.#maxBackoffMs = tuning.maxBackoffMs ?? 600000;
24050
- this.#links = new DirectLinks({
24051
- identity: options.identity,
24052
- team: options.team,
24053
- member: (machine) => options.member?.(machine),
24054
- onDeliver: (raw) => options.onDeliver(raw),
24055
- ...tuning.limits ? { limits: tuning.limits } : {}
24056
- });
24057
- this.#relay = relay(options);
24058
- }
24059
- start() {
24060
- if (this.#running)
24061
- return;
24062
- this.#running = true;
24063
- this.#links.listen(this.#listen.host, this.#listen.port).then((bound) => {
24064
- if (!this.#running)
24065
- return;
24066
- this.#advertised = advertisedEndpoints(this.#config, bound);
24067
- this.#options.log?.("info", "direct.listening", { host: bound.host, port: bound.port, advertised: this.#advertised.length });
24068
- }).catch((error) => this.#options.log?.("warn", "direct.listen.failed", { code: codeOf(error) })).finally(() => {
24069
- if (this.#running)
24070
- this.#relay.start();
24071
- });
24072
- }
24073
- async stop() {
24074
- this.#running = false;
24075
- this.#advertised = undefined;
24076
- await this.#links.close();
24077
- await this.#relay.stop();
24078
- }
24079
- get connected() {
24080
- return this.#relay.connected;
24081
- }
24082
- get stats() {
24083
- return { ...this.#links.stats, ...this.#counts };
24084
- }
24085
- async send(env) {
24086
- const to = this.#running && env.type !== "presence" && "device" in env.to ? env.to.device : undefined;
24087
- if (to !== undefined) {
24088
- const endpoints = this.#dialable(to);
24089
- if (endpoints.length > 0 || this.#links.hasLink(to)) {
24090
- try {
24091
- await this.#links.send(to, endpoints, env);
24092
- this.#backoff.delete(to);
24093
- this.#counts.direct++;
24094
- return;
24095
- } catch (error) {
24096
- this.#counts.fallback++;
24097
- this.#fail(to);
24098
- this.#options.log?.("info", "direct.fallback", { id: env.id, code: codeOf(error) });
24099
- }
24100
- }
24101
- }
24102
- await this.#relay.send(env);
24103
- this.#counts.relay++;
24104
- }
24105
- presence(env) {
24106
- this.#relay.presence(env);
24107
- }
24108
- retryNow() {
24109
- this.#relay.retryNow?.();
24110
- }
24111
- appendRoster(entry) {
24112
- return this.#relay.appendRoster(entry);
24113
- }
24114
- syncRoster(after) {
24115
- return this.#relay.syncRoster(after);
24116
- }
24117
- rosterChanged() {
24118
- this.#links.prune();
24119
- }
24120
- hasLink(machine) {
24121
- return this.#links.hasLink(machine);
24122
- }
24123
- advertise() {
24124
- const direct = this.#advertised;
24125
- return direct && direct.length > 0 ? { features: ["direct"], direct } : undefined;
24126
- }
24127
- #dialable(machine) {
24128
- const backoff = this.#backoff.get(machine);
24129
- if (backoff && Date.now() < backoff.until)
24130
- return [];
24131
- if (!this.#options.member?.(machine))
24132
- return [];
24133
- const presence = this.#options.presence?.(machine);
24134
- if (!presence?.features?.includes("direct") || !presence.direct)
24135
- return [];
24136
- return presence.direct.filter((endpoint) => mayDial(endpoint, isLoopback(this.#listen.host)));
24137
- }
24138
- #fail(machine) {
24139
- const prior = this.#backoff.get(machine);
24140
- const ms = prior ? Math.min(prior.ms * 2, this.#maxBackoffMs) : this.#backoffMs;
24141
- this.#backoff.set(machine, { until: Date.now() + ms, ms });
24142
- }
24143
- }
24144
- var init_direct_transport = __esm(() => {
24145
- init_direct_config();
24146
- init_direct_link();
24147
- init_log();
24148
- });
24149
-
24150
24005
  // packages/relay/dist/frames.js
24151
24006
  function text3(data) {
24152
24007
  if (Buffer.isBuffer(data))
@@ -24176,21 +24031,37 @@ var init_pages = __esm(() => {
24176
24031
  ROSTER_PAGE_BYTES = LIMITS.frameBytes - 4096;
24177
24032
  PAGE_CACHE_BYTES = 8 * MIB;
24178
24033
  });
24034
+ // packages/relay/dist/metrics.js
24035
+ var init_metrics = () => {};
24036
+
24179
24037
  // packages/relay/dist/store.js
24180
24038
  var init_store3 = __esm(() => {
24181
24039
  init_dist();
24040
+ init_metrics();
24182
24041
  });
24183
24042
 
24043
+ // packages/relay/dist/backoff.js
24044
+ function backoffDelay(attempt, minMs, maxMs, random = Math.random) {
24045
+ const ceiling = Math.min(maxMs, minMs * 2 ** Math.min(attempt, 30));
24046
+ return Math.round(ceiling / 2 + random() * (ceiling / 2));
24047
+ }
24048
+
24184
24049
  // packages/relay/dist/directory/jelly.js
24185
24050
  var init_jelly4 = __esm(() => {
24186
24051
  init_dist();
24187
24052
  });
24188
24053
 
24054
+ // packages/relay/dist/directory/witness.js
24055
+ var init_witness = __esm(() => {
24056
+ init_dist();
24057
+ });
24058
+
24189
24059
  // packages/relay/dist/directory/directory.js
24190
24060
  var WARM_MAX_MS;
24191
24061
  var init_directory3 = __esm(() => {
24192
24062
  init_dist();
24193
24063
  init_jelly4();
24064
+ init_witness();
24194
24065
  WARM_MAX_MS = 5 * 60000;
24195
24066
  });
24196
24067
 
@@ -24199,6 +24070,7 @@ var DAY_MS4 = 86400000, FEED_RETENTION_MS;
24199
24070
  var init_feed = __esm(() => {
24200
24071
  init_dist();
24201
24072
  init_jelly4();
24073
+ init_store3();
24202
24074
  FEED_RETENTION_MS = 90 * DAY_MS4;
24203
24075
  });
24204
24076
 
@@ -24231,6 +24103,7 @@ var init_module = __esm(() => {
24231
24103
  "dir.sth",
24232
24104
  "dir.lookup",
24233
24105
  "dir.device",
24106
+ "dir.heads",
24234
24107
  "dir.resolve",
24235
24108
  "dir.search",
24236
24109
  "dir.watch",
@@ -24247,7 +24120,7 @@ var init_store4 = __esm(() => {
24247
24120
  });
24248
24121
 
24249
24122
  // packages/relay/dist/server.js
24250
- var HIGH_WATER, MAX_BUFFERED;
24123
+ var FRAME_TYPES, HIGH_WATER, MAX_BUFFERED;
24251
24124
  var init_server2 = __esm(() => {
24252
24125
  init_dist();
24253
24126
  init_frames();
@@ -24255,20 +24128,43 @@ var init_server2 = __esm(() => {
24255
24128
  init_limits();
24256
24129
  init_pages();
24257
24130
  init_store3();
24131
+ init_metrics();
24258
24132
  init_module();
24259
24133
  init_store4();
24134
+ FRAME_TYPES = new Set([
24135
+ "challenge",
24136
+ "hello",
24137
+ "welcome",
24138
+ "ping",
24139
+ "pong",
24140
+ "join",
24141
+ "roster.append",
24142
+ "roster.sync",
24143
+ "roster",
24144
+ "roster.ok",
24145
+ "peer",
24146
+ "send",
24147
+ "presence",
24148
+ "recv.ack",
24149
+ "deliver",
24150
+ "ack",
24151
+ "nack",
24152
+ "error",
24153
+ "dir.ok",
24154
+ "dir.persons",
24155
+ "dir.headsAre",
24156
+ "dir.sthIs",
24157
+ "dir.resolved",
24158
+ "dir.changed",
24159
+ "dir.unbound",
24160
+ ...DIRECTORY_FRAMES
24161
+ ]);
24260
24162
  HIGH_WATER = 1024 * 1024;
24261
24163
  MAX_BUFFERED = 16 * 1024 * 1024;
24262
24164
  });
24263
24165
 
24264
- // packages/relay/dist/backoff.js
24265
- function backoffDelay(attempt, minMs, maxMs, random = Math.random) {
24266
- const ceiling = Math.min(maxMs, minMs * 2 ** Math.min(attempt, 30));
24267
- return Math.round(ceiling / 2 + random() * (ceiling / 2));
24268
- }
24269
-
24270
24166
  // packages/relay/dist/client.js
24271
- import { WebSocket as WebSocket2 } from "ws";
24167
+ import { WebSocket } from "ws";
24272
24168
 
24273
24169
  class RelayClient {
24274
24170
  #options;
@@ -24325,7 +24221,7 @@ class RelayClient {
24325
24221
  if (!ws)
24326
24222
  return;
24327
24223
  await new Promise((resolve) => {
24328
- if (ws.readyState === WebSocket2.CLOSED)
24224
+ if (ws.readyState === WebSocket.CLOSED)
24329
24225
  return resolve();
24330
24226
  const force = setTimeout(() => ws.terminate(), 2000);
24331
24227
  ws.once("close", () => {
@@ -24347,7 +24243,7 @@ class RelayClient {
24347
24243
  return existing.promise;
24348
24244
  const ws = this.#socket();
24349
24245
  if (!ws)
24350
- return Promise.reject(unavailable3("not connected to the relay"));
24246
+ return Promise.reject(unavailable2("not connected to the relay"));
24351
24247
  const text = frameText({ t: "send", env });
24352
24248
  if (text instanceof MessagingError)
24353
24249
  return Promise.reject(text);
@@ -24378,6 +24274,9 @@ class RelayClient {
24378
24274
  dirSth(args = {}) {
24379
24275
  return this.#dir("dir.sthIs", { t: "dir.sth", ...args });
24380
24276
  }
24277
+ dirHeads(persons, size) {
24278
+ return this.#dir("dir.headsAre", { t: "dir.heads", persons, ...size !== undefined ? { size } : {} });
24279
+ }
24381
24280
  dirLookup(persons, size) {
24382
24281
  return this.#dir("dir.persons", { t: "dir.lookup", persons, ...size !== undefined ? { size } : {} });
24383
24282
  }
@@ -24407,7 +24306,7 @@ class RelayClient {
24407
24306
  return Promise.reject(new MessagingError("invalid_request", "this relay link is not to a directory relay"));
24408
24307
  const ws = this.#socket();
24409
24308
  if (!ws)
24410
- return Promise.reject(unavailable3("not connected to the relay"));
24309
+ return Promise.reject(unavailable2("not connected to the relay"));
24411
24310
  const req = `r${++this.#req}`;
24412
24311
  const text = frameText({ ...frame, req });
24413
24312
  if (text instanceof MessagingError)
@@ -24415,7 +24314,7 @@ class RelayClient {
24415
24314
  const timer = setTimeout(() => {
24416
24315
  const request = this.#dirRequests.get(req);
24417
24316
  this.#dirRequests.delete(req);
24418
- request?.pending.reject(unavailable3("relay did not answer in time"));
24317
+ request?.pending.reject(unavailable2("relay did not answer in time"));
24419
24318
  }, REQUEST_MS);
24420
24319
  timer.unref();
24421
24320
  const pending = deferred();
@@ -24447,7 +24346,7 @@ class RelayClient {
24447
24346
  this.#helloSent = false;
24448
24347
  this.#error = undefined;
24449
24348
  this.#lastSeen = Date.now();
24450
- const ws = new WebSocket2(this.#options.url, SOCKET_OPTIONS2);
24349
+ const ws = new WebSocket(this.#options.url, SOCKET_OPTIONS);
24451
24350
  this.#ws = ws;
24452
24351
  this.#welcomeTimer = setTimeout(() => {
24453
24352
  if (this.#ws !== ws || this.#connected)
@@ -24469,7 +24368,7 @@ class RelayClient {
24469
24368
  clearInterval(this.#pingTimer);
24470
24369
  clearTimeout(this.#welcomeTimer);
24471
24370
  this.#connected = false;
24472
- const lost = unavailable3("relay connection closed");
24371
+ const lost = unavailable2("relay connection closed");
24473
24372
  for (const pending of this.#sends.values())
24474
24373
  pending.reject(lost);
24475
24374
  for (const request of [...this.#requests.values(), ...this.#dirRequests.values()]) {
@@ -24658,12 +24557,12 @@ ${id}`;
24658
24557
  #request(expect, frame) {
24659
24558
  const ws = this.#socket();
24660
24559
  if (!ws)
24661
- return Promise.reject(unavailable3("not connected to the relay"));
24560
+ return Promise.reject(unavailable2("not connected to the relay"));
24662
24561
  const req = `r${++this.#req}`;
24663
24562
  const text = frameText({ ...frame, req });
24664
24563
  if (text instanceof MessagingError)
24665
24564
  return Promise.reject(text);
24666
- const timer = setTimeout(() => this.#take(req)?.pending.reject(unavailable3("relay did not answer in time")), REQUEST_MS);
24565
+ const timer = setTimeout(() => this.#take(req)?.pending.reject(unavailable2("relay did not answer in time")), REQUEST_MS);
24667
24566
  timer.unref();
24668
24567
  const request = expect === "roster" ? { expect, pending: deferred(), timer } : { expect, pending: deferred(), timer };
24669
24568
  this.#requests.set(req, request);
@@ -24671,7 +24570,7 @@ ${id}`;
24671
24570
  return request.pending.promise;
24672
24571
  }
24673
24572
  #socket() {
24674
- return this.#connected && this.#ws?.readyState === WebSocket2.OPEN ? this.#ws : undefined;
24573
+ return this.#connected && this.#ws?.readyState === WebSocket.OPEN ? this.#ws : undefined;
24675
24574
  }
24676
24575
  }
24677
24576
  function createTeamOnRelay(url, genesis) {
@@ -24698,7 +24597,7 @@ function directoryOneShot(url, frame, expect) {
24698
24597
  }
24699
24598
  let settled = false;
24700
24599
  let sent = false;
24701
- const ws = new WebSocket2(url, SOCKET_OPTIONS2);
24600
+ const ws = new WebSocket(url, SOCKET_OPTIONS);
24702
24601
  const finish = (error, value) => {
24703
24602
  if (settled)
24704
24603
  return;
@@ -24710,7 +24609,7 @@ function directoryOneShot(url, frame, expect) {
24710
24609
  else
24711
24610
  resolve(value);
24712
24611
  };
24713
- const timer = setTimeout(() => finish(unavailable3("relay did not answer")), ONE_SHOT_MS);
24612
+ const timer = setTimeout(() => finish(unavailable2("relay did not answer")), ONE_SHOT_MS);
24714
24613
  ws.on("message", (data, isBinary) => {
24715
24614
  if (isBinary)
24716
24615
  return;
@@ -24722,7 +24621,10 @@ function directoryOneShot(url, frame, expect) {
24722
24621
  }
24723
24622
  if (raw?.t === "challenge" && !sent) {
24724
24623
  sent = true;
24725
- const outgoing = frameText({ ...frame, req: "r1" }, LIMITS.preAuthFrameBytes);
24624
+ if (typeof frame === "function" && raw.relay !== relayOrigin(url))
24625
+ return finish(unavailable2("relay id does not match the relay URL origin"));
24626
+ const body = typeof frame === "function" ? frame({ relay: String(raw.relay), nonce: String(raw.nonce) }) : frame;
24627
+ const outgoing = frameText({ ...body, req: "r1" }, LIMITS.preAuthFrameBytes);
24726
24628
  if (outgoing instanceof MessagingError)
24727
24629
  return finish(outgoing);
24728
24630
  ws.send(outgoing);
@@ -24735,18 +24637,26 @@ function directoryOneShot(url, frame, expect) {
24735
24637
  }
24736
24638
  }
24737
24639
  });
24738
- ws.on("error", (error) => finish(unavailable3(`cannot reach the relay: ${error.message}`)));
24739
- ws.on("close", () => finish(unavailable3("relay closed the connection")));
24640
+ ws.on("error", (error) => finish(unavailable2(`cannot reach the relay: ${error.message}`)));
24641
+ ws.on("close", () => finish(unavailable2("relay closed the connection")));
24740
24642
  });
24741
24643
  }
24742
24644
  function sthOnRelay(url, args = {}) {
24743
24645
  return directoryOneShot(url, { t: "dir.sth", ...args }, "dir.sthIs");
24744
24646
  }
24745
- function lookupOnRelay(url, persons, size) {
24746
- return directoryOneShot(url, { t: "dir.lookup", persons, ...size !== undefined ? { size } : {} }, "dir.persons");
24647
+ function lookupOnRelay(url, persons, size, identity, realm) {
24648
+ return directoryOneShot(url, (challenge) => ({
24649
+ t: "dir.lookup",
24650
+ persons,
24651
+ ...size !== undefined ? { size } : {},
24652
+ proof: { device: identity.machine, sig: sign(identity, DOMAIN.dirRead, dirReadPayload({ relay: challenge.relay, realm, device: identity.machine, nonce: challenge.nonce })) }
24653
+ }), "dir.persons");
24654
+ }
24655
+ function headsOnRelay(url, persons, size) {
24656
+ return directoryOneShot(url, { t: "dir.heads", persons, ...size !== undefined ? { size } : {} }, "dir.headsAre");
24747
24657
  }
24748
24658
  function deviceOnRelay(url, device) {
24749
- return directoryOneShot(url, { t: "dir.device", device }, "dir.persons");
24659
+ return directoryOneShot(url, { t: "dir.device", device }, "dir.headsAre");
24750
24660
  }
24751
24661
  function redeemOnRelay(url, entry) {
24752
24662
  return oneShot(url, { t: "roster.append", entry }, "roster.ok");
@@ -24764,7 +24674,7 @@ function fetchRosterAsMember(url, team, identity) {
24764
24674
  let welcomed = false;
24765
24675
  let req = 0;
24766
24676
  let page;
24767
- const ws = new WebSocket2(url, SOCKET_OPTIONS2);
24677
+ const ws = new WebSocket(url, SOCKET_OPTIONS);
24768
24678
  const finish = (error, value) => {
24769
24679
  if (settled)
24770
24680
  return;
@@ -24780,7 +24690,7 @@ function fetchRosterAsMember(url, team, identity) {
24780
24690
  page?.pending.reject(error);
24781
24691
  finish(error);
24782
24692
  };
24783
- const timer = setTimeout(() => fail(unavailable3("relay did not answer")), ONE_SHOT_MS);
24693
+ const timer = setTimeout(() => fail(unavailable2("relay did not answer")), ONE_SHOT_MS);
24784
24694
  const fetchPage = (after) => {
24785
24695
  const pending = deferred();
24786
24696
  const outgoing = frameText({ t: "roster.sync", req: `r${++req}`, after });
@@ -24829,8 +24739,8 @@ function fetchRosterAsMember(url, team, identity) {
24829
24739
  fail(new MessagingError(reply.code, reply.message));
24830
24740
  }
24831
24741
  });
24832
- ws.on("error", (error) => fail(unavailable3(`cannot reach the relay: ${error.message}`)));
24833
- ws.on("close", () => fail(unavailable3("relay closed the connection")));
24742
+ ws.on("error", (error) => fail(unavailable2(`cannot reach the relay: ${error.message}`)));
24743
+ ws.on("close", () => fail(unavailable2("relay closed the connection")));
24834
24744
  });
24835
24745
  }
24836
24746
  function oneShot(url, frame, expect) {
@@ -24844,7 +24754,7 @@ function oneShot(url, frame, expect) {
24844
24754
  const req = "r1";
24845
24755
  let settled = false;
24846
24756
  let sent = false;
24847
- const ws = new WebSocket2(url, SOCKET_OPTIONS2);
24757
+ const ws = new WebSocket(url, SOCKET_OPTIONS);
24848
24758
  const finish = (error, value) => {
24849
24759
  if (settled)
24850
24760
  return;
@@ -24856,7 +24766,7 @@ function oneShot(url, frame, expect) {
24856
24766
  else
24857
24767
  resolve(value);
24858
24768
  };
24859
- const timer = setTimeout(() => finish(unavailable3("relay did not answer")), ONE_SHOT_MS);
24769
+ const timer = setTimeout(() => finish(unavailable2("relay did not answer")), ONE_SHOT_MS);
24860
24770
  ws.on("message", (data, isBinary) => {
24861
24771
  if (isBinary)
24862
24772
  return;
@@ -24893,8 +24803,8 @@ function oneShot(url, frame, expect) {
24893
24803
  finish(new MessagingError(reply.code, reply.message));
24894
24804
  }
24895
24805
  });
24896
- ws.on("error", (error) => finish(unavailable3(`cannot reach the relay: ${error.message}`)));
24897
- ws.on("close", () => finish(unavailable3("relay closed the connection")));
24806
+ ws.on("error", (error) => finish(unavailable2(`cannot reach the relay: ${error.message}`)));
24807
+ ws.on("close", () => finish(unavailable2("relay closed the connection")));
24898
24808
  });
24899
24809
  }
24900
24810
  async function collectPages(after, pages, fetch2) {
@@ -24930,7 +24840,7 @@ function frameText(frame, limit = LIMITS.frameBytes) {
24930
24840
  return new MessagingError("too_large", `frame exceeds the relay's ${limit}-byte limit`);
24931
24841
  return json;
24932
24842
  }
24933
- function unavailable3(message) {
24843
+ function unavailable2(message) {
24934
24844
  return new MessagingError("relay_unavailable", message);
24935
24845
  }
24936
24846
  function deferred() {
@@ -24948,12 +24858,12 @@ function callback(fn) {
24948
24858
  fn();
24949
24859
  } catch {}
24950
24860
  }
24951
- var PING_MS = 20000, DEAD_MS, HANDSHAKE_MS = 1e4, WELCOME_MS = 15000, ONE_SHOT_MS = 15000, REQUEST_MS = 15000, MAX_PAGES = 256, SOCKET_OPTIONS2, RelayRefusal;
24861
+ var PING_MS = 20000, DEAD_MS, HANDSHAKE_MS = 1e4, WELCOME_MS = 15000, ONE_SHOT_MS = 15000, REQUEST_MS = 15000, MAX_PAGES = 256, SOCKET_OPTIONS, RelayRefusal;
24952
24862
  var init_client2 = __esm(() => {
24953
24863
  init_dist();
24954
24864
  init_frames();
24955
24865
  DEAD_MS = 2 * PING_MS + 5000;
24956
- SOCKET_OPTIONS2 = { maxPayload: LIMITS.frameBytes, perMessageDeflate: false, handshakeTimeout: HANDSHAKE_MS };
24866
+ SOCKET_OPTIONS = { maxPayload: LIMITS.frameBytes, perMessageDeflate: false, handshakeTimeout: HANDSHAKE_MS };
24957
24867
  RelayRefusal = class RelayRefusal extends MessagingError {
24958
24868
  retryAfterMs;
24959
24869
  constructor(code, message, retryAfterMs) {
@@ -25072,6 +24982,9 @@ class RelayDirectoryLink {
25072
24982
  throw shape();
25073
24983
  return { seq: reply.seq, sth: reply.sth };
25074
24984
  }
24985
+ async erase(entries, token) {
24986
+ parse5(DirOkFrame, await link((await this.#authed()).dirErase(entries, token)));
24987
+ }
25075
24988
  async keys(statement) {
25076
24989
  const reply = parse5(DirOkFrame, await link((await this.#authed()).dirKeys(statement)));
25077
24990
  if (reply.sth === undefined)
@@ -25090,11 +25003,21 @@ class RelayDirectoryLink {
25090
25003
  }
25091
25004
  async lookup(persons, size) {
25092
25005
  const asks = persons.map((p) => ({ person: p.person, ...p.knownSeq !== undefined ? { knownSeq: p.knownSeq } : {} }));
25093
- const reply = parse5(DirPersonsFrame, await this.#read((client) => client.dirLookup(asks, size), () => lookupOnRelay(this.#options.url, asks, size)));
25006
+ const { url, identity, realm } = this.#options;
25007
+ const own = this.#options.person();
25008
+ const reply = parse5(DirPersonsFrame, await this.#read((client) => client.dirLookup(asks, size), () => lookupOnRelay(url, asks, size, identity, realm), asks.some((ask) => ask.person !== own)));
25094
25009
  return { sth: reply.sth, persons: reply.persons };
25095
25010
  }
25096
- async#read(authed, preAuth) {
25097
- const client = this.#connect();
25011
+ async heads(persons, size) {
25012
+ const reply = parse5(DirHeadsAreFrame, await this.#read((client) => client.dirHeads([...persons], size), () => headsOnRelay(this.#options.url, [...persons], size)));
25013
+ return { sth: reply.sth, head: reply.head, persons: reply.persons };
25014
+ }
25015
+ async#read(authed, preAuth, wait = false) {
25016
+ let client = this.#connect();
25017
+ if (wait && client && !client.connected)
25018
+ client = await this.#authed().catch(() => {
25019
+ return;
25020
+ });
25098
25021
  if (client?.connected) {
25099
25022
  try {
25100
25023
  return await authed(client);
@@ -25106,7 +25029,7 @@ class RelayDirectoryLink {
25106
25029
  return link(preAuth());
25107
25030
  }
25108
25031
  async device(device) {
25109
- const reply = parse5(DirPersonsFrame, await link(deviceOnRelay(this.#options.url, device)));
25032
+ const reply = parse5(DirHeadsAreFrame, await link(deviceOnRelay(this.#options.url, device)));
25110
25033
  return { sth: reply.sth, persons: reply.persons };
25111
25034
  }
25112
25035
  async resolve(query) {
@@ -25196,18 +25119,15 @@ var init_relay_transport = __esm(() => {
25196
25119
  function startDaemon2(options) {
25197
25120
  return startDaemon({
25198
25121
  ...options,
25199
- transportFactory: options.transportFactory ?? withDirect(relayTransport),
25122
+ transportFactory: options.transportFactory ?? relayTransport,
25200
25123
  relayOps: options.relayOps ?? relayOps,
25201
25124
  directoryLinkFactory: options.directoryLinkFactory ?? relayDirectoryLink
25202
25125
  });
25203
25126
  }
25204
25127
  var init_dist4 = __esm(() => {
25205
25128
  init_daemon();
25206
- init_direct_transport();
25207
25129
  init_relay_transport();
25208
25130
  init_relay_transport();
25209
- init_direct_transport();
25210
- init_direct_config();
25211
25131
  init_home();
25212
25132
  init_lock();
25213
25133
  init_log();
@@ -26454,6 +26374,12 @@ var people = {
26454
26374
  }
26455
26375
  ctx.out.line(`${ctx.out.style.bold(inline(person.name, 64))} handle ${inline(person.handle, 64)} org ${person.org} vouched ${person.vouched}`);
26456
26376
  ctx.out.lines(table(["DEVICE", "LABEL", ""], person.devices.map((d) => [d.device, inline(d.label, 32), d.self ? "this device" : ""]), ctx.out.style));
26377
+ if (person.deviceChanges !== undefined) {
26378
+ const now = ctx.now();
26379
+ ctx.out.line(`${ctx.out.style.yellow(`${inline(person.name, 64)}'s devices changed:`)}`);
26380
+ for (const change of person.deviceChanges)
26381
+ ctx.out.line(` ${change.change} '${inline(change.label, 32)}' (${vouchWords(change)}) ${ago(change.at, now)}`);
26382
+ }
26457
26383
  return;
26458
26384
  }
26459
26385
  if (ctx.json) {
@@ -26518,7 +26444,7 @@ var devices = {
26518
26444
  "directory, removed ones last.",
26519
26445
  "link: on a new device Jelly does not enrol, shows its link request and a six-digit code (design \xA72.5).",
26520
26446
  "approve: on a device of yours that Jelly bound, shows what the link asks for, then approves it once you type",
26521
- "the code the new device shows. A device approved by link approves none. Removing devices is not in this CLI."
26447
+ "the code the new device shows. A device approved by link approves none. To remove a device: messaging login --remove <device>."
26522
26448
  ].join(`
26523
26449
  `),
26524
26450
  options: {
@@ -26570,8 +26496,9 @@ var notices = {
26570
26496
  usage: "[--lines] [--ack]",
26571
26497
  details: [
26572
26498
  "Each line is a message for you (a DM or channel line from someone else, or your own agent's line to you),",
26573
- "a request held for you to answer (`messaging requests`), or an alert about this device. --lines adds a",
26574
- "message's first line (peer text). --ack marks every notice shown as seen."
26499
+ "a request held for you to answer (`messaging requests`), or an alert: about this device, a device added to or",
26500
+ "removed from your account (with how to remove one that is not yours), or trust you gave that ends within 7 days.",
26501
+ "--lines adds a message's first line (peer text). --ack marks every notice shown as seen."
26575
26502
  ].join(`
26576
26503
  `),
26577
26504
  options: {
@@ -26598,13 +26525,25 @@ var notices = {
26598
26525
  ago(n.at, now),
26599
26526
  n.category,
26600
26527
  n.from ? inline(n.from.name, 40) : "",
26601
- n.conversation !== undefined ? inline(n.conversation, 60) : "",
26528
+ n.conversation !== undefined ? inline(n.conversation, 60) : n.device !== undefined ? `device ${n.device.device}` : "",
26602
26529
  n.line !== undefined ? inline(n.line, 120) : ""
26603
26530
  ]), ctx.out.style));
26531
+ const removable = list.flatMap((n) => n.device?.remove !== undefined && n.device.current ? [n.device] : []);
26532
+ for (const d of removable) {
26533
+ 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";
26534
+ ctx.out.note(`Not yours? '${inline(d.label, 32)}': ${how}`);
26535
+ }
26604
26536
  if (values.ack === true)
26605
26537
  ctx.out.line(ctx.out.style.dim(`${list.length} marked seen`));
26606
26538
  }
26607
26539
  };
26540
+ function vouchWords(d) {
26541
+ if (d.vouch === "admin")
26542
+ return "admitted by the team's admin";
26543
+ if (d.vouch === "device")
26544
+ return d.approver !== undefined ? `approved on '${inline(d.approver, 32)}'` : "approved by link";
26545
+ return d.via === "browser" ? "signed in through the browser" : "vouched by Jelly ID";
26546
+ }
26608
26547
  var DIRECTORY_COMMANDS = [status, who2, people, devices, notices];
26609
26548
 
26610
26549
  // packages/daemon/dist/cli/commands/identity.js
@@ -26874,14 +26813,62 @@ function aborted2(signal) {
26874
26813
  signal.addEventListener("abort", () => resolve(), { once: true });
26875
26814
  });
26876
26815
  }
26816
+ async function showSignIn(ctx, started, open) {
26817
+ if (!ctx.json) {
26818
+ const { bold } = ctx.out.style;
26819
+ 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:";
26820
+ ctx.out.line(lead);
26821
+ ctx.out.line(` ${started.verificationUri}`);
26822
+ ctx.out.line(`and type the code ${bold(started.userCode)} there. Approve only if the page shows this device's key ${bold(started.fingerprint)}.`);
26823
+ }
26824
+ if (open) {
26825
+ const opened = await (ctx.io.openUrl ?? openInBrowser)(started.verificationUri);
26826
+ if (!opened)
26827
+ ctx.out.note("Could not open a browser here; open the page on any device and type the code.");
26828
+ } else if (!ctx.json && (ctx.io.env.SSH_CONNECTION !== undefined || ctx.io.env.SSH_TTY !== undefined)) {
26829
+ ctx.out.note("Over SSH: open the page on your laptop or phone and type the code; the code ties it to this machine.");
26830
+ }
26831
+ }
26832
+ async function waitSignIn(ctx, client, started) {
26833
+ const minutes = Math.max(1, Math.round((started.expires - ctx.now()) / 60000));
26834
+ if (!ctx.json)
26835
+ ctx.out.note(`Waiting for your approval (the code expires in ${minutes} min; Ctrl-C cancels)...`);
26836
+ const interrupted = aborted2(ctx.io.signal);
26837
+ let state = { ...started, state: "pending" };
26838
+ while (state.state === "pending") {
26839
+ const asked = client.call("device.login.status", { waitMs: STATUS_WAIT_MS }, { timeoutMs: STATUS_WAIT_MS + 15000 });
26840
+ const next = await Promise.race([asked, interrupted.then(() => {
26841
+ return;
26842
+ })]);
26843
+ if (next === undefined) {
26844
+ asked.catch(() => {});
26845
+ await client.call("device.login.cancel", {}).catch(() => {});
26846
+ ctx.out.note("Cancelled.");
26847
+ return;
26848
+ }
26849
+ state = next;
26850
+ }
26851
+ if (state.state === "failed")
26852
+ throw new MessagingError(state.code, state.message);
26853
+ return state;
26854
+ }
26855
+ async function confirmed(ctx, yes, question, word = "y") {
26856
+ if (yes)
26857
+ return true;
26858
+ if (ctx.json)
26859
+ throw new UsageError("with --json there is no prompt: confirm with --yes");
26860
+ ctx.out.prompt(`${question} Type ${word} to confirm: `);
26861
+ const answer = (await readLine(ctx.io.stdin) ?? "").trim().toLowerCase();
26862
+ return word === "y" ? /^y(es)?$/.test(answer) : answer === word;
26863
+ }
26877
26864
  var login = {
26878
26865
  name: "login",
26879
26866
  summary: "Sign this device in to your account through the browser (hosted realm)",
26880
26867
  usage: "[--label <label>] [--kind laptop|server|other] [--open] [--no-wait] [--remove <device> [--reason removed|lost]]",
26881
26868
  details: [
26882
- "Prints a link to Jelly ID and a code. Open the link on any device (with --open, this machine's browser opens it),",
26883
- "check that the page shows the same code and key, and approve. This device then binds to your account: the first",
26884
- "device of your account, or one more. Over SSH, open the link on your laptop or phone.",
26869
+ "Prints Jelly ID's page and a code. Open the page on any device (with --open, this machine's browser opens it),",
26870
+ "type the code, check that the page shows the same key, and approve. This device then binds to your account: the",
26871
+ "first device of your account, or one more. Over SSH, open the page on your laptop or phone.",
26885
26872
  "With --remove, the sign-in removes another device of yours instead (a lost one, with --reason lost).",
26886
26873
  "Needs Jelly ID's device sign-in; until Jelly ID offers it, this says so and does nothing."
26887
26874
  ].join(`
@@ -26889,8 +26876,8 @@ var login = {
26889
26876
  options: {
26890
26877
  label: { type: "string", value: "<label>", description: "This device's label (default: its current one, else device)" },
26891
26878
  kind: { type: "string", value: "<kind>", description: "laptop (default), server or other" },
26892
- open: { type: "boolean", description: "Open the link in this machine's browser" },
26893
- "no-wait": { type: "boolean", description: "Print the link and code, and leave the daemon waiting" },
26879
+ open: { type: "boolean", description: "Open Jelly ID's page in this machine's browser" },
26880
+ "no-wait": { type: "boolean", description: "Print the page and code, and leave the daemon waiting" },
26894
26881
  remove: { type: "string", value: "<device>", description: "Remove that device of yours with this sign-in" },
26895
26882
  reason: { type: "string", value: "<reason>", description: "--remove: removed (default) or lost" }
26896
26883
  },
@@ -26917,45 +26904,15 @@ var login = {
26917
26904
  ctx.out.json(started);
26918
26905
  return;
26919
26906
  }
26920
- if (!ctx.json) {
26921
- const { bold } = ctx.out.style;
26922
- ctx.out.line(started.purpose === "remove" ? "To remove that device, sign in at Jelly ID:" : "To sign this device in, open:");
26923
- ctx.out.line(` ${started.verificationUriComplete}`);
26924
- ctx.out.line(`and check that the page shows the code ${bold(started.userCode)} and the key ${bold(started.fingerprint)}.`);
26925
- ctx.out.line(`(Or open ${started.verificationUri} and type the code.)`);
26926
- }
26927
- if (values.open === true) {
26928
- const opened = await (ctx.io.openUrl ?? openInBrowser)(started.verificationUriComplete);
26929
- if (!opened)
26930
- ctx.out.note("Could not open a browser here; open the link on any device.");
26931
- } else if (!ctx.json && (ctx.io.env.SSH_CONNECTION !== undefined || ctx.io.env.SSH_TTY !== undefined)) {
26932
- ctx.out.note("Over SSH: open the link on your laptop or phone; the code ties it to this machine.");
26933
- }
26907
+ await showSignIn(ctx, started, values.open === true);
26934
26908
  const minutes = Math.max(1, Math.round((started.expires - ctx.now()) / 60000));
26935
26909
  if (!wait) {
26936
26910
  ctx.out.note(`The daemon waits ${minutes} min for your approval. See it with: messaging devices`);
26937
26911
  return;
26938
26912
  }
26939
- if (!ctx.json)
26940
- ctx.out.note(`Waiting for your approval (the code expires in ${minutes} min; Ctrl-C cancels)...`);
26941
- const signal = ctx.io.signal;
26942
- const interrupted = aborted2(signal);
26943
- let state = { ...started, state: "pending" };
26944
- while (state.state === "pending") {
26945
- const asked = client.call("device.login.status", { waitMs: STATUS_WAIT_MS }, { timeoutMs: STATUS_WAIT_MS + 15000 });
26946
- const next = await Promise.race([asked, interrupted.then(() => {
26947
- return;
26948
- })]);
26949
- if (next === undefined) {
26950
- asked.catch(() => {});
26951
- await client.call("device.login.cancel", {}).catch(() => {});
26952
- ctx.out.note("Cancelled.");
26953
- return 130;
26954
- }
26955
- state = next;
26956
- }
26957
- if (state.state === "failed")
26958
- throw new MessagingError(state.code, state.message);
26913
+ const state = await waitSignIn(ctx, client, started);
26914
+ if (state === undefined)
26915
+ return 130;
26959
26916
  if (ctx.json) {
26960
26917
  ctx.out.json(state);
26961
26918
  return state.state === "done" ? undefined : 1;
@@ -26972,7 +26929,128 @@ var login = {
26972
26929
  }
26973
26930
  }
26974
26931
  };
26975
- var LOGIN_COMMANDS = [login];
26932
+ var replace = {
26933
+ name: "identity replace",
26934
+ summary: "Replace this home's device with new keys after a restore (hosted realm)",
26935
+ usage: "[--label <label>] [--open] [--yes]",
26936
+ details: [
26937
+ "For a home restored from a backup or a snapshot (alarm keys_missing or restored), or for a fresh key on purpose.",
26938
+ "This device gets a new key, binds it to your account and removes the old device (reason replaced): your",
26939
+ "conversations, grants and history stay, and tasks continue. Mail still queued at the relay for the old device is",
26940
+ "lost. Through the browser you approve two sign-ins at Jelly ID: one binds the new key, one removes the old."
26941
+ ].join(`
26942
+ `),
26943
+ options: {
26944
+ label: { type: "string", value: "<label>", description: "The new device's label (default: the old one's)" },
26945
+ open: { type: "boolean", description: "Open Jelly ID's page in this machine's browser for each sign-in" },
26946
+ yes: { type: "boolean", short: "y", description: "Do not ask for confirmation" }
26947
+ },
26948
+ args: { min: 0, max: 0 },
26949
+ async run(ctx, _positionals, values) {
26950
+ const label = stringValue(values, "label");
26951
+ const client = await ctx.client();
26952
+ const preview = await client.call("identity.replace.preview", {});
26953
+ if (!ctx.json)
26954
+ ctx.out.line(inline(preview.sentence, 600));
26955
+ if (!await confirmed(ctx, values.yes === true, `Replace '${inline(preview.predecessor.label, 32)}' (device ${preview.predecessor.device}) with this device?`)) {
26956
+ ctx.out.note("Not replaced.");
26957
+ return 1;
26958
+ }
26959
+ const params = label !== undefined ? { label: parseWith2(MachineLabel, label, "--label must be a lowercase label, e.g. desk") } : {};
26960
+ for (let step = 0;step < 3; step++) {
26961
+ const result = await client.call("identity.replace", params);
26962
+ if (result.state === "done") {
26963
+ if (ctx.json)
26964
+ ctx.out.json(result);
26965
+ else
26966
+ ctx.out.line(`Replaced: this device (${result.device}) replaces ${result.predecessor} on ${inline(result.person, 64)}.`);
26967
+ return;
26968
+ }
26969
+ if (result.state === "token") {
26970
+ if (ctx.json)
26971
+ ctx.out.json(result);
26972
+ else
26973
+ ctx.out.note(`This device (${result.device}) waits for its Jelly host to bind the new key; the host's app finishes the replacement.`);
26974
+ return;
26975
+ }
26976
+ await showSignIn(ctx, result, values.open === true);
26977
+ const state = await waitSignIn(ctx, client, result);
26978
+ if (state === undefined)
26979
+ return 130;
26980
+ if (state.state !== "done") {
26981
+ ctx.out.note("The sign-in was cancelled (another sign-in started, or the daemon stopped).");
26982
+ return 1;
26983
+ }
26984
+ if ((await client.call("device.status", {})).binding !== "replace") {
26985
+ const done = { state: "done", person: state.person, device: result.device, predecessor: preview.predecessor.device, seq: state.seq };
26986
+ if (ctx.json)
26987
+ ctx.out.json(done);
26988
+ else
26989
+ ctx.out.line(`Replaced: this device (${result.device}) replaces ${preview.predecessor.device} on ${inline(state.person, 64)}.`);
26990
+ return;
26991
+ }
26992
+ if (!ctx.json)
26993
+ ctx.out.note("The new key is bound. One more sign-in removes the old device.");
26994
+ }
26995
+ throw new MessagingError("internal", "the replacement did not finish; run messaging identity replace again");
26996
+ }
26997
+ };
26998
+ var erase = {
26999
+ name: "erase",
27000
+ summary: "Erase your account from Messaging, for good (hosted realm)",
27001
+ usage: "[--open] [--yes]",
27002
+ details: [
27003
+ "Removes every device of your account and deletes what the directory holds about you. Your Jelly account can",
27004
+ "never be bound to Messaging again. The directory allows 10 erasures per person a day. Shows what is lost and",
27005
+ "asks you to type erase, unless --yes. A device approved by link cannot erase; one signed in through the browser",
27006
+ "approves one more sign-in at Jelly ID."
27007
+ ].join(`
27008
+ `),
27009
+ options: {
27010
+ open: { type: "boolean", description: "Open Jelly ID's page in this machine's browser" },
27011
+ yes: { type: "boolean", short: "y", description: "Do not ask for confirmation" }
27012
+ },
27013
+ args: { min: 0, max: 0 },
27014
+ async run(ctx, _positionals, values) {
27015
+ const client = await ctx.client();
27016
+ const preview = await client.call("directory.erase.preview", {});
27017
+ if (!ctx.json) {
27018
+ ctx.out.line(`Erasing ${inline(preview.person, 64)} from Messaging. What is lost:`);
27019
+ for (const line of preview.lost)
27020
+ ctx.out.line(` - ${inline(line, 300)}`);
27021
+ ctx.out.line(`Devices removed: ${preview.devices.map((d) => `'${inline(d.label, 32)}'${d.self ? " (this one)" : ""}`).join(", ")}`);
27022
+ ctx.out.line("What stays:");
27023
+ for (const line of preview.kept)
27024
+ ctx.out.line(` - ${inline(line, 300)}`);
27025
+ ctx.out.note(`The directory allows ${preview.perDay} erasures per person a day.`);
27026
+ }
27027
+ if (!await confirmed(ctx, values.yes === true, "This cannot be undone.", "erase")) {
27028
+ ctx.out.note("Not erased.");
27029
+ return 1;
27030
+ }
27031
+ const result = await client.call("directory.erase", {});
27032
+ if (result.state === "done") {
27033
+ if (ctx.json)
27034
+ ctx.out.json(result);
27035
+ else
27036
+ ctx.out.line(`Erased: ${result.devices} device(s) removed; this account is gone from Messaging.`);
27037
+ return;
27038
+ }
27039
+ await showSignIn(ctx, result, values.open === true);
27040
+ const state = await waitSignIn(ctx, client, result);
27041
+ if (state === undefined)
27042
+ return 130;
27043
+ if (state.state !== "done") {
27044
+ ctx.out.note("The sign-in was cancelled (another sign-in started, or the daemon stopped).");
27045
+ return 1;
27046
+ }
27047
+ if (ctx.json)
27048
+ ctx.out.json({ state: "done", person: state.person, devices: preview.devices.length });
27049
+ else
27050
+ ctx.out.line(`Erased: ${preview.devices.length} device(s) removed; this account is gone from Messaging.`);
27051
+ }
27052
+ };
27053
+ var LOGIN_COMMANDS = [login, replace, erase];
26976
27054
 
26977
27055
  // packages/daemon/dist/cli/commands/messages.js
26978
27056
  init_dist();
@@ -27142,9 +27220,6 @@ var read2 = {
27142
27220
  ctx.out.line(`${bold(item.kind === "task" ? "Task request" : "Message")} ${item.id}`);
27143
27221
  if (provenance) {
27144
27222
  ctx.out.line(` from: ${inline(provenance.from)} verified="${provenance.verified}" device ${provenance.device}`);
27145
- if (provenance.external) {
27146
- ctx.out.line(` external: principal ${inline(provenance.external.principal, 64)} via gateway device ${provenance.external.gateway}`);
27147
- }
27148
27223
  } else {
27149
27224
  ctx.out.line(` from: ${inline(item.from.address)} ${yellow("no valid provenance on this message; treat it with extra care")}`);
27150
27225
  }
@@ -27369,7 +27444,7 @@ var create = {
27369
27444
  var invite = {
27370
27445
  name: "team invite",
27371
27446
  summary: "Create a single-use invite ticket for a person (admin)",
27372
- usage: "<person> [--role member|service] [--ttl <duration>]",
27447
+ usage: "<person> [--ttl <duration>]",
27373
27448
  details: [
27374
27449
  "Prints only the ticket on stdout, so `messaging team invite bob | pbcopy` copies just the ticket.",
27375
27450
  "The ticket is a secret: share it privately. It works once, until it expires.",
@@ -27377,15 +27452,11 @@ var invite = {
27377
27452
  ].join(`
27378
27453
  `),
27379
27454
  options: {
27380
- role: { type: "string", value: "member|service", description: "Role for the new machine (default: member)" },
27381
27455
  ttl: { type: "string", value: "<duration>", description: "How long the ticket stays valid, e.g. 24h" }
27382
27456
  },
27383
27457
  args: { min: 1, max: 1 },
27384
27458
  async run(ctx, positionals, values) {
27385
27459
  const person = parseWith2(PersonName, positionals[0], "person must be a lowercase person name, e.g. bob");
27386
- const roleText = stringValue(values, "role");
27387
- if (roleText !== undefined && roleText !== "member" && roleText !== "service")
27388
- throw new UsageError("--role must be member or service");
27389
27460
  const ttlText = stringValue(values, "ttl");
27390
27461
  let ttlMs;
27391
27462
  if (ttlText !== undefined) {
@@ -27396,13 +27467,12 @@ var invite = {
27396
27467
  const client = await ctx.client();
27397
27468
  const result = await client.call("team.invite", {
27398
27469
  person,
27399
- ...roleText !== undefined ? { role: roleText } : {},
27400
27470
  ...ttlMs !== undefined ? { ttlMs } : {}
27401
27471
  });
27402
27472
  if (ctx.json) {
27403
27473
  ctx.out.json(result);
27404
27474
  } else {
27405
- ctx.out.note(`Invite ${result.inviteId} for ${person} (${roleText ?? "member"}), expires ${iso2(result.expires)} (${until(result.expires, ctx.now())}).`);
27475
+ ctx.out.note(`Invite ${result.inviteId} for ${person}, expires ${iso2(result.expires)} (${until(result.expires, ctx.now())}).`);
27406
27476
  ctx.out.line(result.ticket);
27407
27477
  }
27408
27478
  ctx.out.warn(`this ticket is a secret. Share it privately with ${person}, never in a public channel or a shared log.`);
@@ -27461,7 +27531,7 @@ var members2 = {
27461
27531
  const now = ctx.now();
27462
27532
  ctx.out.line();
27463
27533
  ctx.out.line(`Invites (${result.invites.length})`);
27464
- ctx.out.lines(table(["INVITE", "PERSON", "ROLE", "STATE", "EXPIRES"], result.invites.map((i) => [i.inviteId, i.person, i.role, i.state, i.state === "open" ? until(i.expires, now) : iso2(i.expires)]), style));
27534
+ ctx.out.lines(table(["INVITE", "PERSON", "STATE", "EXPIRES"], result.invites.map((i) => [i.inviteId, i.person, i.state, i.state === "open" ? until(i.expires, now) : iso2(i.expires)]), style));
27465
27535
  }
27466
27536
  }
27467
27537
  };
@@ -27523,10 +27593,10 @@ var remove = {
27523
27593
  var role = {
27524
27594
  name: "team role",
27525
27595
  summary: "Change a member's role (admin)",
27526
- usage: "<machine|person/label> <admin|member|service>",
27596
+ usage: "<machine|person/label> <admin|member>",
27527
27597
  args: { min: 2, max: 2 },
27528
27598
  async run(ctx, positionals) {
27529
- const newRole = parseWith2(MemberRole, positionals[1], "role must be admin, member or service");
27599
+ const newRole = parseWith2(MemberRole, positionals[1], "role must be admin or member");
27530
27600
  const { member } = await resolveMember(ctx, positionals[0]);
27531
27601
  const client = await ctx.client();
27532
27602
  const result = await client.call("team.role", { machine: member.machine, role: newRole });
@@ -27574,17 +27644,13 @@ var card2 = {
27574
27644
  var add = {
27575
27645
  name: "team add",
27576
27646
  summary: "Admit a machine by its public admit card (admin)",
27577
- usage: "<card|-> [--role member|service] [--yes]",
27647
+ usage: "<card|-> [--yes]",
27578
27648
  details: "Pass `-` to read the card from stdin (then pass --yes). The card has public keys only. You are admitting that exact machine. Asks for confirmation unless --yes.",
27579
27649
  options: {
27580
- role: { type: "string", value: "member|service", description: "Role for the new machine (default: member)" },
27581
27650
  yes: { type: "boolean", short: "y", description: "Do not ask for confirmation" }
27582
27651
  },
27583
27652
  args: { min: 1, max: 1 },
27584
27653
  async run(ctx, positionals, values) {
27585
- const roleText = stringValue(values, "role");
27586
- if (roleText !== undefined && roleText !== "member" && roleText !== "service")
27587
- throw new UsageError("--role must be member or service");
27588
27654
  const fromStdin = positionals[0] === "-";
27589
27655
  const text = (fromStdin ? await readAll(ctx.io.stdin, 8192) : positionals[0]).trim();
27590
27656
  const card = decodeAdmitCard(text);
@@ -27601,8 +27667,7 @@ var add = {
27601
27667
  machine: card.machine,
27602
27668
  enc: card.enc,
27603
27669
  person: card.person,
27604
- label: card.label,
27605
- ...roleText !== undefined ? { role: roleText } : {}
27670
+ label: card.label
27606
27671
  });
27607
27672
  if (ctx.json)
27608
27673
  ctx.out.json({ ok: true, person: card.person, label: card.label, machine: card.machine });
@@ -27724,13 +27789,13 @@ var HANDLES_INTERRUPT = new Set(["listen", "login"]);
27724
27789
  var GROUPS = {
27725
27790
  daemon: "Run the local daemon, messagingd",
27726
27791
  team: "Manage the team: create, invite, join, members, roles (only the CLI may do this)",
27727
- identity: "Show, move or rotate this machine's keys (only the CLI may do this)",
27792
+ identity: "Show, move, rotate or replace this machine's keys (only the CLI may do this)",
27728
27793
  chat: "Your DMs and channels: list, read, post, search"
27729
27794
  };
27730
27795
  var SECTIONS = [
27731
27796
  ["Daemon", ["daemon start", "daemon stop", "status"]],
27732
27797
  ["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"]],
27733
- ["Identity", ["identity show", "identity migrate", "identity rotate-enc"]],
27798
+ ["Identity", ["identity show", "identity migrate", "identity rotate-enc", "identity replace", "erase"]],
27734
27799
  ["People", ["login", "people", "devices", "grants", "grant", "requests", "contact", "card", "notices"]],
27735
27800
  ["Messages", ["who", "send", "inbox", "read", "message-status"]],
27736
27801
  ["Chats", ["chat list", "chat read", "chat post", "chat search"]],
@@ -27964,6 +28029,37 @@ function cliMain(argv, start) {
27964
28029
  // packages/daemon/dist/bin/daemon-main.js
27965
28030
  init_dist2();
27966
28031
  import { resolve as resolve4 } from "path";
28032
+
28033
+ // packages/daemon/dist/bin/fatal.js
28034
+ import { writeSync as writeSync2 } from "fs";
28035
+ var sink;
28036
+ function setFatalLog(log) {
28037
+ sink = log;
28038
+ }
28039
+ function installFatalPolicy(name) {
28040
+ const fatal = (origin) => (error) => {
28041
+ const fields = { origin, error: error instanceof Error ? error.name : typeof error, ...frameOf(error) };
28042
+ try {
28043
+ sink?.("error", "daemon.fatal", fields);
28044
+ } catch {}
28045
+ const frames = error instanceof Error ? error.stack?.split(`
28046
+ `).slice(1).join(`
28047
+ `) ?? "" : "";
28048
+ writeSync2(2, `${name}: fatal ${fields.origin}: ${fields.error}
28049
+ ${frames}
28050
+ `);
28051
+ process.exit(70);
28052
+ };
28053
+ process.on("uncaughtException", fatal("uncaughtException"));
28054
+ process.on("unhandledRejection", fatal("unhandledRejection"));
28055
+ }
28056
+ function frameOf(error) {
28057
+ const frame = error instanceof Error ? error.stack?.split(`
28058
+ `).find((line) => line.trimStart().startsWith("at ")) : undefined;
28059
+ return frame ? { at: frame.trim().slice(3, 240) } : {};
28060
+ }
28061
+
28062
+ // packages/daemon/dist/bin/daemon-main.js
27967
28063
  var USAGE = `usage: messagingd [--home <dir>]
27968
28064
  messagingd --deployment jelly --home <dir> --act-key <path> --label <label>
27969
28065
  [--kind jelly-host] [--run <dir>] [--socket-group <gid>]
@@ -28094,6 +28190,7 @@ ${USAGE}`);
28094
28190
  stderr: process.stderr.isTTY === true,
28095
28191
  level: level === "debug" || level === "warn" || level === "error" ? level : "info"
28096
28192
  });
28193
+ setFatalLog(log);
28097
28194
  let daemon;
28098
28195
  try {
28099
28196
  const jellyHost = fromEnv ? {
@@ -28164,6 +28261,8 @@ function filterSqliteWarning() {
28164
28261
  // packages/daemon/dist/bin/standalone.js
28165
28262
  filterSqliteWarning();
28166
28263
  var { mode, args } = dispatch(process.argv0, process.argv.slice(2));
28264
+ if (mode !== "cli")
28265
+ installFatalPolicy("messagingd");
28167
28266
  var done = mode === "cli" ? cliMain(args, { daemonCommand: [process.execPath] }) : daemonMain(args);
28168
28267
  done.then((code) => {
28169
28268
  process.exitCode = code;