@joe-broadhead/messaging-bin-linux-arm64-gnu 0.2.0 → 0.3.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.
@@ -6118,6 +6118,8 @@ var init_common = __esm(() => {
6118
6118
  delegation: `messaging/1/delegation
6119
6119
  `,
6120
6120
  grant: `messaging/1/grant
6121
+ `,
6122
+ card: `messaging/1/card
6121
6123
  `,
6122
6124
  seal: "messaging/1/seal",
6123
6125
  directAuth: `messaging/1/direct-auth
@@ -6210,6 +6212,7 @@ var init_common = __esm(() => {
6210
6212
  deletionPollMs: 6 * 60 * 60 * 1000,
6211
6213
  dormantDays: 180,
6212
6214
  requestsPerDay: 20,
6215
+ requestChars: 500,
6213
6216
  unsolicitedPerPairPerDay: 20,
6214
6217
  unsolicitedTargetsPerDay: 50,
6215
6218
  dirLookupPersons: 64,
@@ -6225,6 +6228,7 @@ var init_common = __esm(() => {
6225
6228
  trustDefaultMs: 30 * 24 * 60 * 60 * 1000,
6226
6229
  trustMaxMs: 90 * 24 * 60 * 60 * 1000,
6227
6230
  grantBytes: 2048,
6231
+ cardBytes: 2048,
6228
6232
  grantsPerPerson: 5000,
6229
6233
  heldPerSender: 20,
6230
6234
  heldTotal: 200,
@@ -6688,7 +6692,14 @@ function grantProblem(grant) {
6688
6692
  return "resignedAt is before ts";
6689
6693
  return;
6690
6694
  }
6691
- var ChatLevel, AgentMailLevel, AskLevel, GrantSubject, ConsentGrant, GRANTS_PER_SYNC = 100, GrantSync;
6695
+ function cardProblem(card) {
6696
+ if (card.from === card.to)
6697
+ return "a card is about another person";
6698
+ if (card.can.trustedUntil !== undefined && card.can.ask !== "open")
6699
+ return "trustedUntil needs ask open";
6700
+ return;
6701
+ }
6702
+ var ChatLevel, AgentMailLevel, AskLevel, GrantSubject, ConsentGrant, GRANTS_PER_SYNC = 100, GrantSync, CardCan, ContactCard, CardData, CardGetData;
6692
6703
  var init_consent = __esm(() => {
6693
6704
  init_zod();
6694
6705
  init_common();
@@ -6718,6 +6729,30 @@ var init_consent = __esm(() => {
6718
6729
  ctx.addIssue({ code: "custom", message: problem });
6719
6730
  });
6720
6731
  GrantSync = strictObject({ grants: array(record(string2(), unknown())).min(1).max(GRANTS_PER_SYNC) });
6732
+ CardCan = strictObject({
6733
+ chat: boolean2(),
6734
+ agentMail: boolean2(),
6735
+ ask: AskLevel,
6736
+ trustedUntil: Millis.optional()
6737
+ });
6738
+ ContactCard = object({
6739
+ v: literal(PROTOCOL),
6740
+ realm: RealmId,
6741
+ from: PersonId,
6742
+ to: PersonId,
6743
+ by: MachineId,
6744
+ seq: number2().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
6745
+ ts: Millis,
6746
+ grantId: Ulid.optional(),
6747
+ can: CardCan,
6748
+ sig: Base64Url
6749
+ }).superRefine((card, ctx) => {
6750
+ const problem = cardProblem(card);
6751
+ if (problem)
6752
+ ctx.addIssue({ code: "custom", message: problem });
6753
+ });
6754
+ CardData = strictObject({ card: record(string2(), unknown()) });
6755
+ CardGetData = strictObject({ get: literal(true) });
6721
6756
  });
6722
6757
 
6723
6758
  // packages/core/dist/schema/delegation.js
@@ -6754,7 +6789,14 @@ var init_delegation = __esm(() => {
6754
6789
  });
6755
6790
 
6756
6791
  // packages/core/dist/schema/envelope.js
6757
- var EnvelopeType, SEAL_ALG = "x25519-hkdf-sha256-a256gcm", EnvelopeClass, Party, Sender, Recipient, Envelope, ChannelPersons, ChannelConv, SelfConv, Conv, MessagePayload, TaskStatusPayload, TaskArtifactPayload, TaskCancelPayload, ReceiptPayload, SessionInfo, DirectEndpoint, PresencePayload, PAYLOAD_BY_TYPE, Provenance;
6792
+ function requestMessage(message) {
6793
+ const text = message.parts[0]?.text;
6794
+ return bare(message, "text") && typeof text === "string" && text.length >= 1 && text.length <= LIMITS.requestChars;
6795
+ }
6796
+ function cardMessage(message, what) {
6797
+ return bare(message, "data") && (what === "card" ? CardData : CardGetData).safeParse(message.parts[0].data).success;
6798
+ }
6799
+ 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;
6758
6800
  var init_envelope = __esm(() => {
6759
6801
  init_zod();
6760
6802
  init_a2a();
@@ -6764,7 +6806,7 @@ var init_envelope = __esm(() => {
6764
6806
  init_errors3();
6765
6807
  init_realm();
6766
6808
  EnvelopeType = _enum(["message", "task.status", "task.artifact", "task.cancel", "receipt", "presence"]);
6767
- EnvelopeClass = _enum(["self", "contact"]);
6809
+ EnvelopeClass = _enum(["self", "contact", "request"]);
6768
6810
  Party = object({
6769
6811
  person: PersonId,
6770
6812
  device: MachineId,
@@ -6802,7 +6844,9 @@ var init_envelope = __esm(() => {
6802
6844
  object({ kind: literal("channel"), op: literal("post"), id: Ulid, name: ChannelName })
6803
6845
  ]);
6804
6846
  SelfConv = object({ kind: literal("self"), what: _enum(["grant", "snapshot"]) });
6805
- Conv = discriminatedUnion("kind", [ChannelConv, SelfConv]);
6847
+ RequestConv = object({ kind: literal("request") });
6848
+ CardConv = object({ kind: literal("card"), what: _enum(["card", "get"]) });
6849
+ Conv = discriminatedUnion("kind", [ChannelConv, SelfConv, RequestConv, CardConv]);
6806
6850
  MessagePayload = object({
6807
6851
  kind: _enum(["message", "task"]),
6808
6852
  message: Message,
@@ -6812,9 +6856,14 @@ var init_envelope = __esm(() => {
6812
6856
  conv: Conv.optional()
6813
6857
  }).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, {
6814
6858
  message: "a self sync message carries exactly one data part with its grants"
6859
+ }).refine((p) => p.conv?.kind !== "request" || requestMessage(p.message), {
6860
+ message: `a message request carries one text part of 1 to ${LIMITS.requestChars} characters, and nothing else`
6861
+ }).refine((p) => p.conv?.kind !== "card" || cardMessage(p.message, p.conv.what), {
6862
+ message: "a card message carries one data part, the card or the request for one, and nothing else"
6815
6863
  }).refine((p) => p.message.taskId === undefined || Ulid.safeParse(p.message.taskId).success, {
6816
6864
  message: "Messaging task ids are ULIDs"
6817
6865
  });
6866
+ BARE_MESSAGE = new Set(["messageId", "role", "parts"]);
6818
6867
  TaskStatusPayload = TaskStatusUpdateEvent;
6819
6868
  TaskArtifactPayload = TaskArtifactUpdateEvent;
6820
6869
  TaskCancelPayload = object({
@@ -7304,7 +7353,7 @@ var init_shapes = __esm(() => {
7304
7353
  direction: _enum(["in", "out"]),
7305
7354
  body: ChatText,
7306
7355
  thread: Ulid.optional(),
7307
- state: _enum(["sent", "delivered", "refused"]).optional(),
7356
+ state: _enum(["sent", "held", "delivered", "refused"]).optional(),
7308
7357
  refused: array(PersonId).max(LIMITS.channelPersons).optional(),
7309
7358
  notice: literal(true).optional()
7310
7359
  });
@@ -7375,6 +7424,95 @@ var init_shapes = __esm(() => {
7375
7424
  });
7376
7425
  });
7377
7426
 
7427
+ // packages/core/dist/schema/ipc/consent.js
7428
+ var GrantTarget, Levels, GrantView, GrantReview, GrantSetParams, AskRequest, ContactRequestView, ContactSuggestion, HeldRequest, RequestAnswer, RequestText, PersonReach, CONSENT_METHODS;
7429
+ var init_consent2 = __esm(() => {
7430
+ init_zod();
7431
+ init_common();
7432
+ init_consent();
7433
+ init_directory_frames();
7434
+ GrantTarget = union([strictObject({ person: PersonId }), strictObject({ channel: ChannelName })]);
7435
+ Levels = {
7436
+ chat: ChatLevel.optional(),
7437
+ agentMail: AgentMailLevel.optional(),
7438
+ ask: AskLevel.optional(),
7439
+ trustedUntil: Millis.optional(),
7440
+ discoverable: boolean2().optional()
7441
+ };
7442
+ GrantView = object({
7443
+ subject: union([object({ person: PersonId }), object({ channel: Ulid, name: ChannelName.optional() })]),
7444
+ ...Levels,
7445
+ id: Ulid,
7446
+ ts: Millis,
7447
+ by: MachineId,
7448
+ resignedAt: Millis.optional()
7449
+ });
7450
+ GrantReview = object({
7451
+ subject: GrantView.shape.subject,
7452
+ chat: literal("open").optional(),
7453
+ agentMail: literal("open").optional(),
7454
+ ask: _enum(["open", "request"]).optional(),
7455
+ trustedUntil: Millis.optional(),
7456
+ discoverable: literal(true).optional(),
7457
+ at: Millis
7458
+ });
7459
+ GrantSetParams = strictObject({ subject: GrantTarget, ...Levels, trust: literal(true).optional() });
7460
+ AskRequest = object({
7461
+ id: Ulid,
7462
+ kind: _enum(["message", "task"]),
7463
+ from: object({ person: PersonId, device: MachineId, label: MachineLabel, agent: SessionName, address: SessionAddress }),
7464
+ to: SessionName,
7465
+ preview: string2().max(LIMITS.previewChars),
7466
+ at: Millis,
7467
+ exp: Millis
7468
+ });
7469
+ ContactRequestView = object({
7470
+ id: Ulid,
7471
+ kind: literal("contact"),
7472
+ from: object({
7473
+ person: PersonId,
7474
+ device: MachineId,
7475
+ label: MachineLabel,
7476
+ name: string2().max(200),
7477
+ email: string2().max(320).optional(),
7478
+ org: _enum(["same", "other", "none"])
7479
+ }),
7480
+ text: string2().max(LIMITS.requestChars),
7481
+ at: Millis,
7482
+ exp: Millis
7483
+ });
7484
+ ContactSuggestion = object({
7485
+ id: Ulid,
7486
+ kind: literal("suggestion"),
7487
+ agent: SessionName,
7488
+ email: Email,
7489
+ at: Millis
7490
+ });
7491
+ HeldRequest = union([AskRequest, ContactRequestView, ContactSuggestion]);
7492
+ RequestAnswer = _enum(["once", "always", "accept", "decline", "block"]);
7493
+ RequestText = string2().min(1).refine((text) => text.length <= LIMITS.requestChars, { message: `at most ${LIMITS.requestChars} UTF-16 code units` });
7494
+ PersonReach = object({ person: PersonId, can: CardCan, source: _enum(["card", "default"]), at: Millis.optional() });
7495
+ CONSENT_METHODS = {
7496
+ "grant.list": {
7497
+ params: strictObject({ subject: GrantTarget.optional() }),
7498
+ result: object({ grants: array(GrantView), review: array(GrantReview) })
7499
+ },
7500
+ "grant.set": { params: GrantSetParams, result: object({ grant: GrantView }) },
7501
+ "grant.set.preview": { params: GrantSetParams, result: object({ sentence: string2().max(500) }) },
7502
+ "request.list": { params: strictObject({}), result: object({ requests: array(HeldRequest).max(LIMITS.heldTotal * 3) }) },
7503
+ "request.answer": {
7504
+ params: strictObject({ id: Ulid, answer: RequestAnswer, all: boolean2().optional() }),
7505
+ result: object({ answered: array(Ulid).max(LIMITS.heldTotal) })
7506
+ },
7507
+ "contact.request": {
7508
+ params: strictObject({ email: Email, text: RequestText, suggestion: Ulid.optional() }),
7509
+ result: object({ id: Ulid, conversation: RoomRef, person: PersonId })
7510
+ },
7511
+ "contact.request.preview": { params: strictObject({ email: Email }), result: object({ found: boolean2() }) },
7512
+ "card.get": { params: strictObject({ person: PersonId }), result: object({ sent: boolean2() }) }
7513
+ };
7514
+ });
7515
+
7378
7516
  // packages/core/dist/schema/ipc/surfaces.js
7379
7517
  var METADATA_BYTES = 16384, ConversationRef, ConvPlace, ConvSummary, PersonSummary, SURFACE_METHODS;
7380
7518
  var init_surfaces = __esm(() => {
@@ -7382,7 +7520,9 @@ var init_surfaces = __esm(() => {
7382
7520
  init_a2a();
7383
7521
  init_common();
7384
7522
  init_delegation();
7523
+ init_directory_frames();
7385
7524
  init_errors3();
7525
+ init_consent2();
7386
7526
  init_shapes();
7387
7527
  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>");
7388
7528
  ConvPlace = union([ChatSeq, ConvCursor]);
@@ -7405,7 +7545,7 @@ var init_surfaces = __esm(() => {
7405
7545
  SURFACE_METHODS = {
7406
7546
  "contacts.list": {
7407
7547
  params: strictObject({ person: PersonId.optional(), includeOffline: boolean2().optional() }),
7408
- result: object({ entries: array(DirectoryEntry), conversations: array(ConvSummary) })
7548
+ result: object({ entries: array(DirectoryEntry), conversations: array(ConvSummary), reach: array(PersonReach) })
7409
7549
  },
7410
7550
  "msg.send": {
7411
7551
  params: strictObject({
@@ -7422,6 +7562,7 @@ var init_surfaces = __esm(() => {
7422
7562
  extensions: array(string2().min(1).max(256)).max(16).optional(),
7423
7563
  referenceTaskIds: array(Ulid).max(16).optional(),
7424
7564
  wake: boolean2().optional().describe("hint that the recipient may wake an idle session; default true"),
7565
+ request: strictObject({ contact: Email }).optional(),
7425
7566
  external: strictObject({ principal: string2().min(1).max(64) }).optional(),
7426
7567
  onBehalfOf: strictObject({
7427
7568
  taskId: Ulid.describe("a task this session executes"),
@@ -7491,63 +7632,6 @@ var init_surfaces = __esm(() => {
7491
7632
  };
7492
7633
  });
7493
7634
 
7494
- // packages/core/dist/schema/ipc/consent.js
7495
- var GrantTarget, Levels, GrantView, GrantReview, GrantSetParams, HeldRequest, RequestAnswer, CONSENT_METHODS;
7496
- var init_consent2 = __esm(() => {
7497
- init_zod();
7498
- init_common();
7499
- init_consent();
7500
- GrantTarget = union([strictObject({ person: PersonId }), strictObject({ channel: ChannelName })]);
7501
- Levels = {
7502
- chat: ChatLevel.optional(),
7503
- agentMail: AgentMailLevel.optional(),
7504
- ask: AskLevel.optional(),
7505
- trustedUntil: Millis.optional(),
7506
- discoverable: boolean2().optional()
7507
- };
7508
- GrantView = object({
7509
- subject: union([object({ person: PersonId }), object({ channel: Ulid, name: ChannelName.optional() })]),
7510
- ...Levels,
7511
- id: Ulid,
7512
- ts: Millis,
7513
- by: MachineId,
7514
- resignedAt: Millis.optional()
7515
- });
7516
- GrantReview = object({
7517
- subject: GrantView.shape.subject,
7518
- chat: literal("open").optional(),
7519
- agentMail: literal("open").optional(),
7520
- ask: _enum(["open", "request"]).optional(),
7521
- trustedUntil: Millis.optional(),
7522
- discoverable: literal(true).optional(),
7523
- at: Millis
7524
- });
7525
- GrantSetParams = strictObject({ subject: GrantTarget, ...Levels, trust: literal(true).optional() });
7526
- HeldRequest = object({
7527
- id: Ulid,
7528
- from: object({ person: PersonId, device: MachineId, label: MachineLabel, agent: SessionName, address: SessionAddress }),
7529
- to: SessionName,
7530
- kind: _enum(["message", "task"]),
7531
- preview: string2().max(LIMITS.previewChars),
7532
- at: Millis,
7533
- exp: Millis
7534
- });
7535
- RequestAnswer = _enum(["once", "always", "decline", "block"]);
7536
- CONSENT_METHODS = {
7537
- "grant.list": {
7538
- params: strictObject({ subject: GrantTarget.optional() }),
7539
- result: object({ grants: array(GrantView), review: array(GrantReview) })
7540
- },
7541
- "grant.set": { params: GrantSetParams, result: object({ grant: GrantView }) },
7542
- "grant.set.preview": { params: GrantSetParams, result: object({ sentence: string2().max(500) }) },
7543
- "request.list": { params: strictObject({}), result: object({ requests: array(HeldRequest).max(LIMITS.heldTotal) }) },
7544
- "request.answer": {
7545
- params: strictObject({ id: Ulid, answer: RequestAnswer, all: boolean2().optional() }),
7546
- result: object({ answered: array(Ulid).max(LIMITS.heldTotal) })
7547
- }
7548
- };
7549
- });
7550
-
7551
7635
  // packages/core/dist/schema/ipc/jelly.js
7552
7636
  var NoticeId, NOTICE_LINE_CHARS = 120, Notice, NoticeCounts, DaemonHealth, AgentSummary, AgentConversation, JELLY_METHODS;
7553
7637
  var init_jelly2 = __esm(() => {
@@ -7929,7 +8013,7 @@ var init_b64u = __esm(() => {
7929
8013
  });
7930
8014
 
7931
8015
  // packages/core/dist/version.js
7932
- var VERSION = "0.2.0";
8016
+ var VERSION = "0.3.0";
7933
8017
 
7934
8018
  // packages/core/dist/jcs.js
7935
8019
  function hasLoneSurrogate(text) {
@@ -8494,16 +8578,17 @@ function seal(args, randomness) {
8494
8578
  const plaintext = utf8(canonicalize(payload));
8495
8579
  if (plaintext.length > LIMITS.plaintextBytes)
8496
8580
  throw new MessagingError("too_large", `payload exceeds ${LIMITS.plaintextBytes} bytes`);
8497
- assertMeaning(type, args.from, args.to, payload);
8581
+ const klass = type === "message" && payload.conv?.kind === "request" ? "request" : classOf(args.from, args.to);
8582
+ assertMeaning(type, args.from, args.to, klass, payload);
8498
8583
  if (type !== "presence")
8499
- return sealBytes(args, type, plaintext, randomness);
8584
+ return sealBytes(args, type, klass, plaintext, randomness);
8500
8585
  if (args.recipientEnc !== undefined)
8501
8586
  throw new MessagingError("invalid_request", "presence is not sealed");
8502
- const header = parseWith(Header, { ...base2(args, type), enc: "none" }, "envelope header");
8587
+ const header = parseWith(Header, { ...base2(args, type, undefined), enc: "none" }, "envelope header");
8503
8588
  assertShape(header);
8504
8589
  return signed(args.identity, { ...header, body: payload });
8505
8590
  }
8506
- function base2(args, type) {
8591
+ function base2(args, type, klass) {
8507
8592
  if (args.from?.device !== args.identity.machine)
8508
8593
  throw new MessagingError("invalid_request", "from.device must be the signing identity");
8509
8594
  const ttl = args.ttlMs ?? LIMITS.ttlMs;
@@ -8520,15 +8605,15 @@ function base2(args, type) {
8520
8605
  ts: now,
8521
8606
  exp: now + ttl,
8522
8607
  anchor: args.anchor?.kind === "epoch" ? { kind: "epoch", size: args.anchor.size, root: args.anchor.root } : { kind: args.anchor?.kind, seq: args.anchor?.seq, hash: args.anchor?.hash },
8523
- ...type === "presence" ? {} : { class: classOf(args.from, args.to) },
8608
+ ...klass === undefined ? {} : { class: klass },
8524
8609
  ...args.replyTo === undefined ? {} : { replyTo: args.replyTo }
8525
8610
  };
8526
8611
  }
8527
8612
  function classOf(from, to) {
8528
8613
  return to !== undefined && "person" in to && to.person === from?.person ? "self" : "contact";
8529
8614
  }
8530
- function sealBytes(args, type, plaintext, randomness) {
8531
- const head = base2(args, type);
8615
+ function sealBytes(args, type, klass, plaintext, randomness) {
8616
+ const head = base2(args, type, klass);
8532
8617
  const recipient = recipientKey(args.recipientEnc);
8533
8618
  const { ephemeral, nonce } = randomness();
8534
8619
  const epk = rawPublicKey(ephemeral);
@@ -8629,13 +8714,15 @@ function open2(env, identity) {
8629
8714
  const value = parseWire(text, { maxBytes: LIMITS.plaintextBytes });
8630
8715
  canonicalize(value);
8631
8716
  const payload = parseWith(schema, value, `${type} payload`);
8632
- assertMeaning(type, env.from, env.to, payload);
8717
+ assertMeaning(type, env.from, env.to, env.class, payload);
8633
8718
  return payload;
8634
8719
  }
8635
- function assertMeaning(type, from, to, payload) {
8720
+ function assertMeaning(type, from, to, klass, payload) {
8636
8721
  if (type !== "message" || to === undefined || !("device" in to))
8637
8722
  return;
8638
8723
  const p = payload;
8724
+ if (klass === "request" !== (p.conv?.kind === "request"))
8725
+ throw new MessagingError("invalid_request", "a message request is exactly an envelope of class request");
8639
8726
  if (to.agent !== undefined) {
8640
8727
  if (p.conv !== undefined)
8641
8728
  throw new MessagingError("invalid_request", "mail to an agent has no conv");
@@ -8643,6 +8730,8 @@ function assertMeaning(type, from, to, payload) {
8643
8730
  }
8644
8731
  if (p.conv?.kind === "self" && from?.person !== to.person)
8645
8732
  throw new MessagingError("invalid_request", "a self sync goes between devices of one person");
8733
+ if (p.conv?.kind === "card" && (from?.person === to.person || from?.agent !== undefined))
8734
+ throw new MessagingError("invalid_request", "a card goes from a person to another person");
8646
8735
  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);
8647
8736
  if (!line)
8648
8737
  throw new MessagingError("invalid_request", "a chat line is kind message, outside any task, with ULID messageId and contextId");
@@ -8701,6 +8790,17 @@ function shapeProblem(env) {
8701
8790
  return "a sealed envelope needs epk and nonce";
8702
8791
  if (env.class === undefined)
8703
8792
  return "a sealed envelope needs class";
8793
+ if (env.class === "request") {
8794
+ if (env.type !== "message")
8795
+ return "only a message is of class request";
8796
+ if (classOf(env.from, env.to) !== "contact")
8797
+ return "a message request goes to another person";
8798
+ if (env.from.agent !== undefined || "agent" in env.to && env.to.agent !== undefined)
8799
+ return "a message request goes from a person to a person";
8800
+ if (env.replyTo !== undefined)
8801
+ return "a message request answers nothing";
8802
+ return;
8803
+ }
8704
8804
  if (env.class !== classOf(env.from, env.to))
8705
8805
  return `class ${env.class} does not match the ends`;
8706
8806
  return;
@@ -10468,6 +10568,10 @@ function decide(args) {
10468
10568
  if (policy.chat === "request_required")
10469
10569
  return { action: "refuse", code: "request_required" };
10470
10570
  return policy.chat === "muted" ? { action: "deliver", consent: "guarded", silent: true } : { action: "deliver", consent: "guarded" };
10571
+ case "request":
10572
+ if (stranger)
10573
+ return { action: "hold" };
10574
+ return policy.chat === "muted" ? { action: "deliver", consent: "guarded", silent: true } : { action: "deliver", consent: "guarded" };
10471
10575
  case "ask":
10472
10576
  if (policy.ask === "open")
10473
10577
  return { action: "deliver", consent: trusted(inbound.chain) ? "trusted" : "guarded" };
@@ -10497,6 +10601,67 @@ var init_consent3 = __esm(() => {
10497
10601
  ASK_RANK = { no: 0, request: 1, open: 2 };
10498
10602
  });
10499
10603
 
10604
+ // packages/core/dist/contact-card.js
10605
+ function signContactCard(identity, args) {
10606
+ const unsigned = {
10607
+ v: PROTOCOL,
10608
+ realm: args.realm,
10609
+ from: args.from,
10610
+ to: args.to,
10611
+ by: identity.machine,
10612
+ seq: args.seq,
10613
+ ts: args.now,
10614
+ ...args.grantId !== undefined ? { grantId: args.grantId } : {},
10615
+ can: { chat: args.can.chat, agentMail: args.can.agentMail, ask: args.can.ask, ...args.can.trustedUntil !== undefined ? { trustedUntil: args.can.trustedUntil } : {} }
10616
+ };
10617
+ const problem = cardProblem(unsigned);
10618
+ if (problem)
10619
+ throw new MessagingError("invalid_request", problem);
10620
+ const card = parseWith(ContactCard, { ...unsigned, sig: sign(identity, DOMAIN.card, unsigned) }, "card");
10621
+ if (utf8(canonicalize(card)).length > LIMITS.cardBytes)
10622
+ throw new MessagingError("too_large", `a card is at most ${LIMITS.cardBytes} bytes`);
10623
+ return card;
10624
+ }
10625
+ function verifyContactCard(raw, ctx) {
10626
+ try {
10627
+ const card = parseWith(ContactCard, raw, "card");
10628
+ if (utf8(canonicalize(raw)).length > LIMITS.cardBytes)
10629
+ throw new MessagingError("too_large", `a card is at most ${LIMITS.cardBytes} bytes`);
10630
+ const { sig, ...unsigned } = raw;
10631
+ const input = signingInput(DOMAIN.card, unsigned);
10632
+ if (card.realm !== ctx.realm)
10633
+ throw new MessagingError("invalid_request", "the card is for another realm");
10634
+ if (card.from !== ctx.from)
10635
+ throw new MessagingError("invalid_request", "the card is not its sender's");
10636
+ if (card.to !== ctx.to)
10637
+ throw new MessagingError("invalid_request", "the card is for another person");
10638
+ if (typeof sig !== "string" || !verifyInput(card.by, input, sig))
10639
+ throw new MessagingError("bad_signature", "the card's signature is invalid");
10640
+ return card;
10641
+ } catch (error) {
10642
+ throw asMessagingError(error, "card");
10643
+ }
10644
+ }
10645
+ function cardCan(relation, grant, now) {
10646
+ const policy = effectivePolicy(relation, grant, [], now);
10647
+ return {
10648
+ chat: policy.chat === "open" || policy.chat === "muted",
10649
+ agentMail: policy.agentMail === "open",
10650
+ ask: policy.ask,
10651
+ ...policy.trustedUntil !== undefined ? { trustedUntil: policy.trustedUntil } : {}
10652
+ };
10653
+ }
10654
+ var init_contact_card = __esm(() => {
10655
+ init_b64u();
10656
+ init_check();
10657
+ init_consent3();
10658
+ init_jcs();
10659
+ init_common();
10660
+ init_consent();
10661
+ init_errors3();
10662
+ init_sign();
10663
+ });
10664
+
10500
10665
  // packages/core/dist/delegation.js
10501
10666
  function grantHash(grant) {
10502
10667
  return sha256(canonicalize(grant));
@@ -11925,6 +12090,7 @@ var TaskStateName, ToolErrorBody, fenced = (what) => string2().describe(`${what}
11925
12090
  var init_agent_tools = __esm(() => {
11926
12091
  init_zod();
11927
12092
  init_common();
12093
+ init_directory_frames();
11928
12094
  init_errors3();
11929
12095
  init_ipc();
11930
12096
  TaskStateName = _enum(["submitted", "working", "completed", "failed", "canceled", "input_required", "rejected", "auth_required"]);
@@ -11960,14 +12126,15 @@ var init_agent_tools = __esm(() => {
11960
12126
  ContactsInput = strictObject({
11961
12127
  query: string2().min(1).max(100).optional().describe("Only contacts whose person, agent or channel name contains this text"),
11962
12128
  kind: _enum(["person", "agent", "channel"]).optional().describe("Only persons (each with their agents), only agents, or only channels. Omit for persons and channels."),
11963
- can: _enum(["chat", "ask"]).optional().describe("Only those you can chat with (a line to a person or channel) or ask (a task to an agent)"),
12129
+ can: _enum(["chat", "ask"]).optional().describe("Only those you can chat with (a line to a person or channel) or ask (a task to an agent, at once or after their person agrees)"),
11964
12130
  limit: number2().int().min(1).max(50).optional().describe("At most this many (default 20)")
11965
12131
  });
11966
12132
  Can = object({
11967
12133
  chat: boolean2().describe("A chat line reaches them: a person other than you, or a channel you are in"),
11968
12134
  agentMail: boolean2().describe("Agent mail reaches them"),
11969
- ask: _enum(["open", "request", "no"]).describe("open: a task request goes straight to the agent, which runs it as a guarded peer turn unless its person trusts you"),
11970
- source: _enum(["card", "default"]).describe("default: today's team policy; nobody has granted or narrowed it")
12135
+ ask: _enum(["open", "request", "no"]).describe("open: a task request goes straight to the agent, which runs it as a guarded peer turn unless its person trusts you; request: their person sees a request card first; no: refused"),
12136
+ source: _enum(["card", "default"]).describe("card: what their person's card says; default: the policy for your org, or for a stranger outside it"),
12137
+ card: iso.optional().describe("With source card: when their person signed it. It is advice: their person may have changed it since")
11971
12138
  });
11972
12139
  Agent = object({
11973
12140
  address: string2().describe("person/agent@label"),
@@ -12007,7 +12174,8 @@ var init_agent_tools = __esm(() => {
12007
12174
  ask: boolean2().optional().describe("true: a task request, which the agent runs and reports on; follow it with messaging_tasks and messaging_read {id}. Only to an agent."),
12008
12175
  replyTo: Ulid.optional().describe("What this answers: a message or task id from messaging_inbox or messaging_read (to an agent), or a line id (to a person or channel, a reply in that thread). Answering a task you asked for, while it waits for input, goes to that task."),
12009
12176
  onBehalfOf: Ulid.optional().describe("With ask: a task you are running that this new task is part of, so the recipient sees who asked whom. Without it, the task starts a new chain."),
12010
- key: string2().min(1).max(64).optional().describe("Any id of yours for this send: repeating a call with the same key sends once")
12177
+ key: string2().min(1).max(64).optional().describe("Any id of yours for this send: repeating a call with the same key sends once"),
12178
+ request: strictObject({ contact: Email.describe("Their exact email, lower-case") }).optional().describe('Only with to: "me": ask your person to send a message request to someone outside your org. Only your person can send one; you are not told what they decide.')
12011
12179
  });
12012
12180
  SendOutput = object({
12013
12181
  id: string2().describe("The message, task request or line id; messaging_read {id} shows how far mail got"),
@@ -12196,6 +12364,7 @@ var init_dist = __esm(() => {
12196
12364
  init_address();
12197
12365
  init_cards();
12198
12366
  init_consent3();
12367
+ init_contact_card();
12199
12368
  init_delegation2();
12200
12369
  init_task();
12201
12370
  init_direct2();
@@ -14430,6 +14599,17 @@ class Chats {
14430
14599
  });
14431
14600
  return { id, conversation: room };
14432
14601
  }
14602
+ sendRequest(person, text) {
14603
+ const { realm, me } = this.#host.joined();
14604
+ if (person === me.person || !realm.persons().includes(person))
14605
+ throw new MessagingError("not_found", "no Messaging user with that email accepts requests");
14606
+ const now = this.#host.now();
14607
+ this.#paced([person], now);
14608
+ const id = ulid2(now);
14609
+ this.#fan([person], { kind: "request" }, line(id, text));
14610
+ this.#file({ id, room: `dm:${person}`, direction: "out", person: me.person, body: text, at: now });
14611
+ return { id };
14612
+ }
14433
14613
  create(p) {
14434
14614
  const { realm, me } = this.#host.joined();
14435
14615
  if (this.#store.channelByLocal(p.name))
@@ -14665,6 +14845,12 @@ class Chats {
14665
14845
  return false;
14666
14846
  if (payload.stage === "delivered" || payload.stage === "rejected")
14667
14847
  this.settle(row.envelope, payload.stage, payload.code);
14848
+ if (payload.stage === "held" && this.#store.holdChatSent(row.envelope, this.#host.now())) {
14849
+ const sent = this.#store.line(row.line);
14850
+ const rev = sent && this.#store.touchLine(sent.id);
14851
+ if (sent && rev !== undefined)
14852
+ this.#emit(sent.room, sent.id, this.#host.joined().me.person, rev);
14853
+ }
14668
14854
  return true;
14669
14855
  }
14670
14856
  settle(envelope, stage, code) {
@@ -16515,7 +16701,8 @@ class Notices {
16515
16701
  return { ok: true };
16516
16702
  }
16517
16703
  counts() {
16518
- return { unread: this.#host.store.unreadLines(), requests: this.#host.store.heldCount() };
16704
+ const store = this.#host.store;
16705
+ return { unread: store.unreadLines(), requests: store.heldCount() + store.contactRequestCount() + store.contactSuggestionCount() };
16519
16706
  }
16520
16707
  disconnected(id) {
16521
16708
  this.#watchers.delete(id);
@@ -17190,6 +17377,17 @@ function upgradeStore(db, migrations) {
17190
17377
  throw error;
17191
17378
  }
17192
17379
  }
17380
+ function contactRequestRow(row) {
17381
+ return {
17382
+ id: String(row.id),
17383
+ person: String(row.person),
17384
+ machine: String(row.machine),
17385
+ envelope: JSON.parse(String(row.envelope)),
17386
+ payload: JSON.parse(String(row.payload)),
17387
+ at: Number(row.at),
17388
+ exp: Number(row.exp)
17389
+ };
17390
+ }
17193
17391
  function heldRow(row) {
17194
17392
  return {
17195
17393
  id: String(row.id),
@@ -17619,6 +17817,58 @@ class Store {
17619
17817
  deleteExpiredHeld(now) {
17620
17818
  return this.#run("DELETE FROM held WHERE exp <= ?", now);
17621
17819
  }
17820
+ contactCard(person) {
17821
+ const row = this.#get("SELECT card, seq, at FROM contact_cards WHERE person = ?", person);
17822
+ return row && { card: JSON.parse(String(row.card)), seq: Number(row.seq), at: Number(row.at) };
17823
+ }
17824
+ contactCards() {
17825
+ return this.#all("SELECT person, card FROM contact_cards ORDER BY person").map((row) => ({ person: String(row.person), card: JSON.parse(String(row.card)) }));
17826
+ }
17827
+ putContactCard(person, card, seq, at) {
17828
+ return this.#run("INSERT INTO contact_cards (person, card, seq, at) VALUES (?, ?, ?, ?) ON CONFLICT (person) DO UPDATE SET card = excluded.card, seq = excluded.seq, at = excluded.at WHERE excluded.seq > contact_cards.seq", person, JSON.stringify(card), seq, at) > 0;
17829
+ }
17830
+ deleteContactCard(person) {
17831
+ this.#run("DELETE FROM contact_cards WHERE person = ?", person);
17832
+ }
17833
+ putContactRequest(row) {
17834
+ this.#run("INSERT INTO contact_requests (id, person, machine, envelope, payload, at, exp) VALUES (?, ?, ?, ?, ?, ?, ?)", row.id, row.person, row.machine, JSON.stringify(row.envelope), JSON.stringify(row.payload), row.at, row.exp);
17835
+ }
17836
+ contactRequest(id) {
17837
+ const row = this.#get("SELECT * FROM contact_requests WHERE id = ?", id);
17838
+ return row && contactRequestRow(row);
17839
+ }
17840
+ contactRequests(person) {
17841
+ const rows = person === undefined ? this.#all("SELECT * FROM contact_requests ORDER BY at, id") : this.#all("SELECT * FROM contact_requests WHERE person = ? ORDER BY at, id", person);
17842
+ return rows.map(contactRequestRow);
17843
+ }
17844
+ contactRequestCount() {
17845
+ return Number(this.#get("SELECT COUNT(*) AS n FROM contact_requests")?.n ?? 0);
17846
+ }
17847
+ deleteContactRequest(id) {
17848
+ this.#run("DELETE FROM contact_requests WHERE id = ?", id);
17849
+ }
17850
+ deleteExpiredContactRequests(now) {
17851
+ return this.#run("DELETE FROM contact_requests WHERE exp <= ?", now);
17852
+ }
17853
+ putContactSuggestion(row) {
17854
+ this.#run("INSERT INTO contact_suggestions (id, session, email, at) VALUES (?, ?, ?, ?)", row.id, row.session, row.email, row.at);
17855
+ }
17856
+ contactSuggestion(id) {
17857
+ const row = this.#get("SELECT * FROM contact_suggestions WHERE id = ?", id);
17858
+ return row && { id: String(row.id), session: String(row.session), email: String(row.email), at: Number(row.at) };
17859
+ }
17860
+ contactSuggestions() {
17861
+ return this.#all("SELECT * FROM contact_suggestions ORDER BY at, id").map((row) => ({ id: String(row.id), session: String(row.session), email: String(row.email), at: Number(row.at) }));
17862
+ }
17863
+ contactSuggestionCount() {
17864
+ return Number(this.#get("SELECT COUNT(*) AS n FROM contact_suggestions")?.n ?? 0);
17865
+ }
17866
+ deleteContactSuggestion(id) {
17867
+ this.#run("DELETE FROM contact_suggestions WHERE id = ?", id);
17868
+ }
17869
+ deleteContactSuggestionsBefore(before) {
17870
+ return this.#run("DELETE FROM contact_suggestions WHERE at < ?", before);
17871
+ }
17622
17872
  putGrant(subject, grant) {
17623
17873
  this.#run("INSERT INTO grants (subject, grant) VALUES (?, ?) ON CONFLICT (subject) DO UPDATE SET grant = excluded.grant", subject, JSON.stringify(grant));
17624
17874
  }
@@ -17711,6 +17961,9 @@ class Store {
17711
17961
  settleChatSent(envelope, stage, code) {
17712
17962
  return this.#run("UPDATE chat_sent SET stage = ?, code = ? WHERE envelope = ? AND stage = 'sent'", stage, opt(code), envelope) > 0;
17713
17963
  }
17964
+ holdChatSent(envelope, at) {
17965
+ return this.#run("UPDATE chat_sent SET held_at = ? WHERE envelope = ? AND stage = 'sent' AND held_at IS NULL", at, envelope) > 0;
17966
+ }
17714
17967
  chatSentDue(now) {
17715
17968
  return this.#all("SELECT * FROM chat_sent WHERE stage = 'sent' AND exp <= ?", now).map(chatSentRow);
17716
17969
  }
@@ -17725,19 +17978,23 @@ class Store {
17725
17978
  const out = new Map;
17726
17979
  if (lines.length === 0)
17727
17980
  return out;
17728
- const rows = this.#all(`SELECT line, person, MAX(stage = 'delivered') AS d, MIN(stage IN ('rejected', 'expired')) AS r
17981
+ const rows = this.#all(`SELECT line, person, MAX(stage = 'delivered') AS d, MIN(stage IN ('rejected', 'expired')) AS r, MAX(stage = 'sent' AND held_at IS NOT NULL) AS h
17729
17982
  FROM chat_sent WHERE line IN (SELECT value FROM json_each(?)) AND person <> ? GROUP BY line, person ORDER BY person`, JSON.stringify(lines), me);
17730
17983
  for (const row of rows) {
17731
17984
  const line = String(row.line);
17732
- const entry = out.get(line) ?? { state: "sent", refused: [], pending: false };
17985
+ const entry = out.get(line) ?? { state: "sent", refused: [], pending: false, waiting: false };
17733
17986
  if (Number(row.r) === 1)
17734
17987
  entry.refused.push(String(row.person));
17735
- else if (Number(row.d) !== 1)
17736
- entry.pending = true;
17988
+ else if (Number(row.d) !== 1) {
17989
+ if (Number(row.h) === 1)
17990
+ entry.waiting = true;
17991
+ else
17992
+ entry.pending = true;
17993
+ }
17737
17994
  out.set(line, entry);
17738
17995
  }
17739
17996
  for (const entry of out.values())
17740
- entry.state = entry.refused.length > 0 ? "refused" : entry.pending ? "sent" : "delivered";
17997
+ entry.state = entry.refused.length > 0 ? "refused" : entry.pending ? "sent" : entry.waiting ? "held" : "delivered";
17741
17998
  return out;
17742
17999
  }
17743
18000
  sentUndelivered(now) {
@@ -18053,7 +18310,7 @@ function taskRow2(r) {
18053
18310
  }
18054
18311
  return row;
18055
18312
  }
18056
- var CHAT_COUNTER = "chat", STORE_SCHEMA = 8, STORE_MIN_READER = 3, MIGRATIONS, BASELINE = `
18313
+ var CHAT_COUNTER = "chat", STORE_SCHEMA = 9, STORE_MIN_READER = 3, MIGRATIONS, BASELINE = `
18057
18314
  CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT NOT NULL) STRICT;
18058
18315
  CREATE TABLE IF NOT EXISTS counters (name TEXT PRIMARY KEY, value INTEGER NOT NULL) STRICT;
18059
18316
  CREATE TABLE IF NOT EXISTS roster (seq INTEGER PRIMARY KEY, hash TEXT NOT NULL, entry TEXT NOT NULL) STRICT;
@@ -18293,6 +18550,24 @@ CREATE TABLE IF NOT EXISTS notices (
18293
18550
  ) STRICT;
18294
18551
  CREATE INDEX IF NOT EXISTS notices_at ON notices (at);
18295
18552
  `
18553
+ },
18554
+ {
18555
+ schema: 9,
18556
+ ddl: `
18557
+ CREATE TABLE IF NOT EXISTS contact_cards (person TEXT PRIMARY KEY, card TEXT NOT NULL, seq INTEGER NOT NULL, at INTEGER NOT NULL) STRICT;
18558
+ CREATE TABLE IF NOT EXISTS contact_requests (
18559
+ id TEXT PRIMARY KEY,
18560
+ person TEXT NOT NULL,
18561
+ machine TEXT NOT NULL,
18562
+ envelope TEXT NOT NULL,
18563
+ payload TEXT NOT NULL,
18564
+ at INTEGER NOT NULL,
18565
+ exp INTEGER NOT NULL
18566
+ ) STRICT;
18567
+ CREATE INDEX IF NOT EXISTS contact_requests_person ON contact_requests (person);
18568
+ CREATE TABLE IF NOT EXISTS contact_suggestions (id TEXT PRIMARY KEY, session TEXT NOT NULL, email TEXT NOT NULL, at INTEGER NOT NULL) STRICT;
18569
+ `,
18570
+ columns: [{ table: "chat_sent", column: "held_at", decl: "INTEGER" }]
18296
18571
  }
18297
18572
  ];
18298
18573
  UNSPACED = /([\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}\p{Script=Thai}])/gu;
@@ -18693,7 +18968,7 @@ class Grants {
18693
18968
  const review = this.#store.grantReviews().filter(({ subject }) => only === undefined || subject === only).map(({ subject, levels, at }) => ({ subject: this.#subjectView(subject), ...levels, at }));
18694
18969
  return { grants, review };
18695
18970
  }
18696
- set(p) {
18971
+ set(p, opts = {}) {
18697
18972
  const { identity, realm, me } = this.#host.joined();
18698
18973
  const now = this.#host.now();
18699
18974
  const key = this.#subjectKey(p.subject, true);
@@ -18708,13 +18983,15 @@ class Grants {
18708
18983
  this.#sync("grant", [grant]);
18709
18984
  });
18710
18985
  this.#host.log("grant.set", { subject: "person" in subject ? "person" : "channel" });
18986
+ if ("person" in subject && opts.card !== false)
18987
+ this.#host.changed?.(subject.person);
18711
18988
  return { grant: this.#view(grant) };
18712
18989
  }
18713
- setPerson(person, change) {
18990
+ setPerson(person, change, opts = {}) {
18714
18991
  const levels = { ...this.personGrant(person), ...change };
18715
18992
  if (levels.ask !== "open")
18716
18993
  delete levels.trustedUntil;
18717
- return this.set({ subject: { person }, ...levels });
18994
+ return this.set({ subject: { person }, ...levels }, opts);
18718
18995
  }
18719
18996
  check(p) {
18720
18997
  const { identity, realm, me } = this.#host.joined();
@@ -18753,6 +19030,8 @@ class Grants {
18753
19030
  if (this.#store.grantReview(key)?.id !== grant.id)
18754
19031
  this.#store.deleteGrantReview(key);
18755
19032
  merged++;
19033
+ if ("person" in grant.subject)
19034
+ this.#host.merged?.(grant.subject.person);
18756
19035
  }
18757
19036
  this.#host.log("grant.sync", { id: env.id, what: conv.what, merged });
18758
19037
  this.sweep();
@@ -18774,6 +19053,8 @@ class Grants {
18774
19053
  if (copy) {
18775
19054
  this.#store.putGrant(subject, copy);
18776
19055
  resigned.push(copy);
19056
+ if ("person" in copy.subject)
19057
+ this.#host.changed?.(copy.subject.person);
18777
19058
  } else {
18778
19059
  this.#store.deleteGrant(subject);
18779
19060
  }
@@ -18931,18 +19212,23 @@ class Holds {
18931
19212
  return { requests };
18932
19213
  }
18933
19214
  answer(p) {
19215
+ if (p.answer === "accept" && !this.#store.held(p.id))
19216
+ throw new MessagingError("not_found", "no request with that id is held here");
18934
19217
  const now = this.#host.now();
18935
19218
  const row = this.#store.held(p.id);
18936
19219
  if (!row || row.exp <= now)
18937
19220
  throw new MessagingError("not_found", "no request with that id is held here");
19221
+ if (p.answer === "accept")
19222
+ throw new MessagingError("invalid_request", "an ask takes once, always, decline or block; accept answers a message request");
18938
19223
  if (p.answer === "always")
18939
19224
  this.#host.grants.setPerson(row.person, { ask: "open" });
18940
19225
  if (p.answer === "block")
18941
19226
  this.#host.grants.setPerson(row.person, { chat: "blocked" });
18942
19227
  const rows = p.all ? this.#store.helds(row.person).filter((held) => held.exp > now) : [row];
18943
19228
  const answered = [];
19229
+ const answer = p.answer;
18944
19230
  for (const held of rows) {
18945
- this.#host.tx(() => this.#settle(held, p.answer));
19231
+ this.#host.tx(() => this.#settle(held, answer));
18946
19232
  answered.push(held.id);
18947
19233
  }
18948
19234
  this.#host.log("request.answered", { answer: p.answer, count: answered.length });
@@ -18968,6 +19254,351 @@ var init_holds = __esm(() => {
18968
19254
  init_dist();
18969
19255
  });
18970
19256
 
19257
+ // packages/daemon/dist/consent/cards.js
19258
+ class Cards {
19259
+ #host;
19260
+ #answered = new Map;
19261
+ #asked = new Map;
19262
+ constructor(host) {
19263
+ this.#host = host;
19264
+ }
19265
+ changed(person) {
19266
+ this.#issueSoon(person).catch((error) => this.#host.log("card.failed", { code: error.code ?? "internal" }));
19267
+ }
19268
+ async#issueSoon(person) {
19269
+ await this.#host.prefetch(person);
19270
+ await this.#host.fresh([person], () => this.#issue(person));
19271
+ }
19272
+ #issue(person) {
19273
+ const { identity, realm, me } = this.#host.joined();
19274
+ if (person === me.person)
19275
+ return;
19276
+ const relation = this.#host.relation(person);
19277
+ if (relation === "self" || relation === "stranger")
19278
+ return;
19279
+ const grant = this.#host.personGrant(person);
19280
+ if (grant?.chat === "blocked")
19281
+ return;
19282
+ const now = this.#host.now();
19283
+ const seq = this.#nextSeq(now);
19284
+ const held = this.#host.store.grant(`person:${person}`);
19285
+ const card = signContactCard(identity, {
19286
+ realm: realm.realm,
19287
+ from: me.person,
19288
+ to: person,
19289
+ seq,
19290
+ now,
19291
+ ...grant !== undefined && typeof held?.id === "string" ? { grantId: held.id } : {},
19292
+ can: cardCan(relation, grant, now)
19293
+ });
19294
+ for (const device of realm.known(person).devices) {
19295
+ this.#host.route(this.#host.seal(device.machine, cardMessage2({ card }, "card")));
19296
+ }
19297
+ this.#host.log("card.sent", { seq });
19298
+ }
19299
+ #nextSeq(now) {
19300
+ const store = this.#host.store;
19301
+ const last = Number(store.getKv(SEQ_KEY) ?? 0);
19302
+ const seq = Math.max(now, last + 1);
19303
+ store.setKv(SEQ_KEY, String(seq));
19304
+ return seq;
19305
+ }
19306
+ receive(env, sender, payload) {
19307
+ const conv = payload.conv;
19308
+ const data = payload.message.parts[0].data;
19309
+ if (conv.what === "get")
19310
+ return this.#answer(sender.person);
19311
+ const { realm, me } = this.#host.joined();
19312
+ const drop = (code) => this.#host.log("card.dropped", { id: env.id, code });
19313
+ const relation = this.#host.relation(sender.person);
19314
+ if (relation === "stranger")
19315
+ return drop("request_required");
19316
+ if (this.#host.personGrant(sender.person)?.chat === "blocked")
19317
+ return drop("refused");
19318
+ let card;
19319
+ try {
19320
+ card = verifyContactCard(data.card, { realm: realm.realm, from: sender.person, to: me.person });
19321
+ } catch (error) {
19322
+ return drop(error.code ?? "invalid_request");
19323
+ }
19324
+ if (!realm.known(sender.person).devices.some((device) => device.machine === card.by))
19325
+ return drop("unknown_device");
19326
+ if (this.#host.store.putContactCard(sender.person, data.card, card.seq, this.#host.now()))
19327
+ this.#host.log("card.kept", { seq: card.seq });
19328
+ }
19329
+ #answer(person) {
19330
+ const now = this.#host.now();
19331
+ const last = this.#answered.get(person);
19332
+ if (last !== undefined && now - last < DAY_MS2)
19333
+ return;
19334
+ if (this.#host.personGrant(person) === undefined)
19335
+ return;
19336
+ this.#answered.set(person, now);
19337
+ this.changed(person);
19338
+ }
19339
+ async get(person) {
19340
+ const { me } = this.#host.joined();
19341
+ if (person === me.person)
19342
+ return false;
19343
+ const now = this.#host.now();
19344
+ const last = this.#asked.get(person);
19345
+ if (last !== undefined && now - last < DAY_MS2)
19346
+ return false;
19347
+ this.#asked.set(person, now);
19348
+ await this.#host.fresh([person], () => {
19349
+ const { realm } = this.#host.joined();
19350
+ for (const device of realm.known(person).devices)
19351
+ this.#host.route(this.#host.seal(device.machine, cardMessage2({ get: true }, "get")));
19352
+ });
19353
+ return true;
19354
+ }
19355
+ refused(person) {
19356
+ this.#host.store.deleteContactCard(person);
19357
+ this.get(person).catch(() => {
19358
+ return;
19359
+ });
19360
+ }
19361
+ reach(person, sameOrg) {
19362
+ const card = this.#card(person);
19363
+ if (card)
19364
+ return { person, can: card.can, source: "card", at: card.ts };
19365
+ return { person, can: cardCan(sameOrg ? "org" : "stranger", undefined, this.#host.now()), source: "default" };
19366
+ }
19367
+ cardPersons() {
19368
+ return this.#host.store.contactCards().map((row) => row.person);
19369
+ }
19370
+ #card(person) {
19371
+ const held = this.#host.store.contactCard(person);
19372
+ if (!held)
19373
+ return;
19374
+ const { realm, me } = this.#host.joined();
19375
+ try {
19376
+ const card = verifyContactCard(held.card, { realm: realm.realm, from: person, to: me.person });
19377
+ if (!realm.known(person).devices.some((device) => device.machine === card.by))
19378
+ return;
19379
+ const { trustedUntil, ...rest } = card.can;
19380
+ return { can: trustedUntil !== undefined && trustedUntil > this.#host.now() ? card.can : rest, ts: card.ts };
19381
+ } catch {
19382
+ this.#host.store.deleteContactCard(person);
19383
+ return;
19384
+ }
19385
+ }
19386
+ }
19387
+ function cardMessage2(data, what) {
19388
+ return { kind: "message", message: { messageId: ulid2(), role: "ROLE_USER", parts: [{ data }] }, conv: { kind: "card", what } };
19389
+ }
19390
+ var SEQ_KEY = "cards.seq", DAY_MS2 = 86400000;
19391
+ var init_cards2 = __esm(() => {
19392
+ init_dist();
19393
+ });
19394
+
19395
+ // packages/daemon/dist/consent/requests.js
19396
+ class Requests {
19397
+ #host;
19398
+ #store;
19399
+ #waiting = new Map;
19400
+ #day = -1;
19401
+ #today = new Set;
19402
+ constructor(host) {
19403
+ this.#host = host;
19404
+ this.#store = host.store;
19405
+ }
19406
+ async preview(p) {
19407
+ this.#hosted();
19408
+ return { found: await this.#resolve(p.email) !== undefined };
19409
+ }
19410
+ async request(p) {
19411
+ this.#hosted();
19412
+ const { me } = this.#host.joined();
19413
+ if (p.suggestion !== undefined && !this.#store.contactSuggestion(p.suggestion))
19414
+ throw new MessagingError("not_found", "no suggestion with that id is held here");
19415
+ const person = await this.#resolve(p.email);
19416
+ if (person === undefined)
19417
+ throw new MessagingError("not_found", "no Messaging user with that email accepts requests");
19418
+ if (person === me.person)
19419
+ throw new MessagingError("invalid_request", "that email is your own");
19420
+ if (this.#host.grants.personGrant(person)?.chat === "blocked")
19421
+ throw new MessagingError("invalid_state", "you blocked that person; unblock them with grant.set first");
19422
+ const now = this.#host.now();
19423
+ this.#paced(person, now);
19424
+ await this.#host.grants.prefetch(person);
19425
+ const line = await this.#host.fresh([person], () => this.#host.tx(() => {
19426
+ if (this.#host.grants.personGrant(person)?.chat === undefined)
19427
+ this.#host.grants.setPerson(person, { chat: "open" }, { card: false });
19428
+ const sent = this.#host.chats.sendRequest(person, p.text);
19429
+ if (p.suggestion !== undefined)
19430
+ this.#store.deleteContactSuggestion(p.suggestion);
19431
+ return sent;
19432
+ }));
19433
+ this.#waiting.set(person, now);
19434
+ this.#today.add(person);
19435
+ this.#host.log("contact.request.sent", {});
19436
+ return { id: line.id, conversation: `dm:${person}`, person };
19437
+ }
19438
+ async#resolve(email) {
19439
+ const { realm } = this.#host.joined();
19440
+ const found = await realm.resolve(email, "exact-email");
19441
+ if (found.length > 1)
19442
+ throw new MessagingError("ambiguous_address", "more than one person has that email");
19443
+ return found[0];
19444
+ }
19445
+ #hosted() {
19446
+ if (this.#host.realmKind() !== "directory") {
19447
+ throw new MessagingError("invalid_request", "message requests are for a hosted realm: on a team, every person is in your org and can be written to");
19448
+ }
19449
+ }
19450
+ #paced(person, now) {
19451
+ const day = Math.floor(now / DAY_MS3);
19452
+ if (day !== this.#day) {
19453
+ this.#day = day;
19454
+ this.#today.clear();
19455
+ }
19456
+ const waiting = this.#waiting.get(person);
19457
+ if (waiting !== undefined && now - waiting < LIMITS.ttlMs)
19458
+ throw new MessagingError("rate_limited", "a request to that person is waiting for their answer");
19459
+ if (!this.#today.has(person) && this.#today.size >= LIMITS.requestsPerDay) {
19460
+ throw new MessagingError("rate_limited", `you sent message requests to ${LIMITS.requestsPerDay} persons today; try again tomorrow`);
19461
+ }
19462
+ }
19463
+ answered(person) {
19464
+ this.#waiting.delete(person);
19465
+ }
19466
+ receive(env, sender, payload, now) {
19467
+ const reject = (code) => {
19468
+ this.#host.log("contact.request.rejected", { id: env.id, code });
19469
+ this.#host.receipt(sender.machine, env.id, "rejected", code);
19470
+ };
19471
+ if (this.#host.realmKind() !== "directory")
19472
+ return reject("invalid_request");
19473
+ const decision = this.#host.grants.decide(sender.person, { kind: "request" });
19474
+ if (decision.action === "refuse")
19475
+ return reject(decision.code);
19476
+ if (decision.action === "deliver")
19477
+ return this.#host.chats.receive(env, sender, asLine(payload), now);
19478
+ if (this.#store.contactRequest(env.id))
19479
+ return reject("invalid_request");
19480
+ if (this.#store.contactRequests(sender.person).length > 0 || this.#store.contactRequestCount() >= LIMITS.heldTotal)
19481
+ return reject("rate_limited");
19482
+ this.#store.putContactRequest({ id: env.id, person: sender.person, machine: sender.machine, envelope: env, payload, at: now, exp: env.exp });
19483
+ this.#host.held({ id: env.id, person: sender.person, agent: undefined, at: now });
19484
+ this.#host.log("contact.request.held", { id: env.id });
19485
+ this.#host.receipt(sender.machine, env.id, "held");
19486
+ }
19487
+ async list() {
19488
+ const { realm } = this.#host.joined();
19489
+ const now = this.#host.now();
19490
+ const out = [];
19491
+ for (const row of this.#store.contactRequests()) {
19492
+ if (row.exp <= now)
19493
+ continue;
19494
+ const device = realm.device(row.machine);
19495
+ const profile = await this.#host.profile(row.person).catch(() => {
19496
+ return;
19497
+ });
19498
+ out.push({
19499
+ id: row.id,
19500
+ kind: "contact",
19501
+ from: {
19502
+ person: row.person,
19503
+ device: row.machine,
19504
+ label: device?.label ?? "gone",
19505
+ name: (profile?.name ?? row.person).slice(0, 200),
19506
+ ...profile?.email !== undefined ? { email: profile.email.slice(0, 320) } : {},
19507
+ org: profile?.org ?? "none"
19508
+ },
19509
+ text: requestText(row.payload),
19510
+ at: row.at,
19511
+ exp: row.exp
19512
+ });
19513
+ }
19514
+ for (const row of this.#store.contactSuggestions())
19515
+ out.push({ id: row.id, kind: "suggestion", agent: row.session, email: row.email, at: row.at });
19516
+ return out;
19517
+ }
19518
+ holds(id) {
19519
+ return this.#store.contactRequest(id) !== undefined || this.#store.contactSuggestion(id) !== undefined;
19520
+ }
19521
+ answer(p) {
19522
+ const suggestion = this.#store.contactSuggestion(p.id);
19523
+ if (suggestion) {
19524
+ if (p.answer !== "decline")
19525
+ throw new MessagingError("invalid_request", "a suggestion is dismissed with decline; send the request itself with contact.request");
19526
+ this.#host.tx(() => this.#store.deleteContactSuggestion(p.id));
19527
+ return { answered: [p.id] };
19528
+ }
19529
+ const now = this.#host.now();
19530
+ const row = this.#store.contactRequest(p.id);
19531
+ if (!row || row.exp <= now)
19532
+ throw new MessagingError("not_found", "no request with that id is held here");
19533
+ if (p.answer !== "accept" && p.answer !== "decline" && p.answer !== "block") {
19534
+ throw new MessagingError("invalid_request", "a message request takes accept, decline or block");
19535
+ }
19536
+ const answer = p.answer;
19537
+ const rows = p.all ? this.#store.contactRequests(row.person).filter((held) => held.exp > now) : [row];
19538
+ this.#host.tx(() => {
19539
+ if (answer === "accept")
19540
+ this.#host.grants.setPerson(row.person, { chat: "open" });
19541
+ if (answer === "block")
19542
+ this.#host.grants.setPerson(row.person, { chat: "blocked" });
19543
+ for (const held of rows)
19544
+ this.#settle(held, answer === "accept" ? "accept" : "decline");
19545
+ });
19546
+ this.#host.log("contact.request.answered", { answer, count: rows.length });
19547
+ return { answered: rows.map((held) => held.id) };
19548
+ }
19549
+ granted(person) {
19550
+ const chat = this.#host.grants.personGrant(person)?.chat;
19551
+ if (chat !== "open" && chat !== "blocked")
19552
+ return;
19553
+ for (const row of this.#store.contactRequests(person))
19554
+ this.#settle(row, chat === "open" ? "accept" : "decline");
19555
+ }
19556
+ #settle(row, answer) {
19557
+ this.#store.deleteContactRequest(row.id);
19558
+ const { realm } = this.#host.joined();
19559
+ const sender = realm.device(row.machine);
19560
+ if (!sender || sender.person !== row.person)
19561
+ return;
19562
+ if (answer === "decline")
19563
+ return this.#host.receipt(sender.machine, row.id, "rejected", "declined");
19564
+ this.#host.chats.receive(row.envelope, sender, asLine(row.payload), this.#host.now());
19565
+ }
19566
+ suggest(session, email) {
19567
+ const { me } = this.#host.joined();
19568
+ const now = this.#host.now();
19569
+ if (this.#store.contactSuggestionCount() >= LIMITS.heldTotal)
19570
+ throw new MessagingError("rate_limited", "your person has too many suggestions waiting");
19571
+ const id = ulid2(now);
19572
+ this.#host.tx(() => {
19573
+ this.#store.putContactSuggestion({ id, session, email, at: now });
19574
+ this.#host.held({ id, person: me.person, agent: session, at: now });
19575
+ });
19576
+ return id;
19577
+ }
19578
+ sweep(now) {
19579
+ const requests = this.#store.deleteExpiredContactRequests(now);
19580
+ const suggestions = this.#store.deleteContactSuggestionsBefore(now - SUGGESTION_MS);
19581
+ if (requests + suggestions > 0)
19582
+ this.#host.log("contact.request.expired", { requests, suggestions });
19583
+ for (const [person, at] of this.#waiting)
19584
+ if (now - at >= LIMITS.ttlMs)
19585
+ this.#waiting.delete(person);
19586
+ }
19587
+ }
19588
+ function asLine(payload) {
19589
+ const { conv: _conv, ...line } = payload;
19590
+ return line;
19591
+ }
19592
+ function requestText(payload) {
19593
+ const text = payload.message.parts[0]?.text;
19594
+ return typeof text === "string" ? text.slice(0, LIMITS.requestChars) : "";
19595
+ }
19596
+ var DAY_MS3 = 86400000, SUGGESTION_MS;
19597
+ var init_requests = __esm(() => {
19598
+ init_dist();
19599
+ SUGGESTION_MS = LIMITS.ttlMs;
19600
+ });
19601
+
18971
19602
  // packages/daemon/dist/consent/sentence.js
18972
19603
  function grantSentence(p, me, now) {
18973
19604
  const levels = levelsFrom(p, now);
@@ -19063,6 +19694,8 @@ class Daemon {
19063
19694
  #chats;
19064
19695
  #grants;
19065
19696
  #holds;
19697
+ #cards;
19698
+ #requests;
19066
19699
  #notices;
19067
19700
  #jellyHost;
19068
19701
  #jellyAuth;
@@ -19128,6 +19761,36 @@ class Daemon {
19128
19761
  seal: (machine, payload) => this.#seal("message", machine, undefined, payload),
19129
19762
  route: (env) => this.#route(env),
19130
19763
  tx: (fn) => this.#tx(fn),
19764
+ log: (event, fields) => this.#log("info", event, fields),
19765
+ changed: (person) => this.#cards.changed(person),
19766
+ merged: (person) => this.#requests.granted(person)
19767
+ });
19768
+ this.#cards = new Cards({
19769
+ store,
19770
+ now: this.#now,
19771
+ joined: () => this.#joined(),
19772
+ relation: (person) => this.#grants.relation(person),
19773
+ personGrant: (person) => this.#grants.personGrant(person),
19774
+ prefetch: (person) => this.#grants.prefetch(person),
19775
+ fresh: async (persons, fn) => {
19776
+ await this.#withFresh(persons, () => this.#tx(fn), { first: false });
19777
+ },
19778
+ seal: (machine, payload) => this.#seal("message", machine, undefined, payload),
19779
+ route: (env) => this.#route(env),
19780
+ log: (event, fields) => this.#log("info", event, fields)
19781
+ });
19782
+ this.#requests = new Requests({
19783
+ store,
19784
+ grants: this.#grants,
19785
+ chats: this.#chats,
19786
+ now: this.#now,
19787
+ joined: () => this.#joined(),
19788
+ realmKind: () => this.#realm?.kind,
19789
+ fresh: (persons, fn) => this.#withFresh(persons, fn),
19790
+ profile: async (person) => await this.#joined().realm.profile(person),
19791
+ receipt: (machine, ref, stage, code) => this.#receipt(machine, ref, undefined, stage, code),
19792
+ tx: (fn) => this.#tx(fn),
19793
+ held: (row) => this.#notices.held(row),
19131
19794
  log: (event, fields) => this.#log("info", event, fields)
19132
19795
  });
19133
19796
  this.#holds = new Holds({
@@ -19414,10 +20077,12 @@ class Daemon {
19414
20077
  "session.register": (p, ctx) => this.#register(p, ctx),
19415
20078
  "session.update": (p, ctx) => this.#updateSession(this.#bound(p, ctx)),
19416
20079
  "session.unregister": (p, ctx) => this.#unregister(this.#bound(p, ctx)),
19417
- "contacts.list": (p) => ({
19418
- entries: directory2(this.#view(this.#joined()), p, this.#now(), { wake: (s) => this.#wakes(s) }),
19419
- conversations: this.#chats.list({ limit: 200 }).conversations
19420
- }),
20080
+ "contacts.list": async (p) => {
20081
+ const entries = directory2(this.#view(this.#joined()), p, this.#now(), { wake: (s) => this.#wakes(s) });
20082
+ const conversations = this.#chats.list({ limit: 200 }).conversations;
20083
+ const dms = conversations.flatMap((c) => c.conversation.startsWith("dm:") ? [c.conversation.slice("dm:".length)] : []);
20084
+ return { entries, conversations, reach: await this.#reach([...entries.map((entry) => entry.person), ...dms]) };
20085
+ },
19421
20086
  "msg.send": (p, ctx) => isLineTarget(p.to) ? this.#sendLine(this.#bound(p, ctx)) : this.#send(this.#bound(p, ctx)),
19422
20087
  "item.read": (p, ctx) => this.#itemRead(this.#bound(p, ctx)),
19423
20088
  "inbox.list": (p, ctx) => ({ items: this.#store.inbox(this.#bound(p, ctx).key, p.unreadOnly ?? false, p.limit ?? 50) }),
@@ -19478,8 +20143,15 @@ class Daemon {
19478
20143
  this.#grants.check(p);
19479
20144
  return { sentence: grantSentence(p, this.#joined().me.person, this.#now()) };
19480
20145
  },
19481
- "request.list": () => this.#holds.list(),
19482
- "request.answer": (p) => this.#holds.answer(p),
20146
+ "request.list": async () => ({ requests: [...this.#holds.list().requests, ...await this.#requests.list()] }),
20147
+ "request.answer": (p) => this.#requests.holds(p.id) ? this.#requests.answer(p) : this.#holds.answer(p),
20148
+ "contact.request": (p) => this.#requests.request(p),
20149
+ "contact.request.preview": (p) => this.#requests.preview(p),
20150
+ "card.get": async (p) => {
20151
+ const { realm } = this.#joined();
20152
+ await realm.profile(p.person);
20153
+ return { sent: await this.#cards.get(p.person) };
20154
+ },
19483
20155
  "daemon.health": () => this.#health(),
19484
20156
  "agents.list": () => this.#agentsList(),
19485
20157
  "agents.read": (p) => this.#agentsRead(p),
@@ -19489,6 +20161,16 @@ class Daemon {
19489
20161
  "device.remove.preview": (p) => this.#removePreview(p)
19490
20162
  };
19491
20163
  }
20164
+ async#reach(persons) {
20165
+ const { realm, me } = this.#joined();
20166
+ const all = [...new Set([...persons, ...this.#cards.cardPersons()])].filter((person) => person !== me.person).sort();
20167
+ const out = [];
20168
+ for (const person of all) {
20169
+ const org = realm.kind === "roster" ? "same" : await realm.profile(person).then((profile) => profile.org, () => "none");
20170
+ out.push(this.#cards.reach(person, org === "same"));
20171
+ }
20172
+ return out;
20173
+ }
19492
20174
  async#grantSubject(p) {
19493
20175
  const { realm } = this.#joined();
19494
20176
  if ("person" in p.subject)
@@ -19997,7 +20679,12 @@ class Daemon {
19997
20679
  throw new MessagingError("invalid_request", "to is person/agent[@label], a person, or #<channel>");
19998
20680
  }
19999
20681
  const conversation = p.to.startsWith("#") ? `channel:${name}` : `dm:${name}`;
20682
+ if (p.request !== undefined && conversation !== `dm:${this.#joined().me.person}`) {
20683
+ throw new MessagingError("invalid_request", "request.contact goes only with a line to your own person");
20684
+ }
20000
20685
  const line = await this.#withFresh(this.#linePersons(conversation), () => this.#chats.send({ conversation, text, ...p.replyTo !== undefined ? { thread: p.replyTo } : {}, ...p.idempotencyKey !== undefined ? { clientId: p.idempotencyKey } : {} }, session, p.key));
20686
+ if (p.request !== undefined)
20687
+ this.#requests.suggest(session, p.request.contact);
20001
20688
  return { results: [{ to: p.to, envelopeId: line.id, messageId: line.id, contextId: line.id, stage: "accepted" }] };
20002
20689
  }
20003
20690
  #linePersons(conversation) {
@@ -20065,7 +20752,7 @@ class Daemon {
20065
20752
  this.#freshBound.set(env.id, this.#staleBound);
20066
20753
  this.#routed = true;
20067
20754
  }
20068
- async#withFresh(persons, fn) {
20755
+ async#withFresh(persons, fn, opts = {}) {
20069
20756
  const realm = this.#realm;
20070
20757
  if (realm?.kind !== "directory")
20071
20758
  return fn();
@@ -20086,8 +20773,9 @@ class Daemon {
20086
20773
  this.#staleBound = stale;
20087
20774
  try {
20088
20775
  const result = fn();
20089
- for (const person of others)
20090
- this.#sealedTo.add(person);
20776
+ if (opts.first !== false)
20777
+ for (const person of others)
20778
+ this.#sealedTo.add(person);
20091
20779
  return result;
20092
20780
  } finally {
20093
20781
  this.#staleBound = undefined;
@@ -20197,7 +20885,9 @@ ${row.receipt ? "receipt" : "envelope"}`;
20197
20885
  this.#releaseWaiters();
20198
20886
  return;
20199
20887
  }
20200
- if (code === undefined || code === "rate_limited" || code === "internal") {
20888
+ if (code === "rate_limited" && row.env.class === "request")
20889
+ this.#nacked(row, code);
20890
+ else if (code === undefined || code === "rate_limited" || code === "internal") {
20201
20891
  this.#retryLater(row);
20202
20892
  waiting.add(lane);
20203
20893
  } else
@@ -20529,6 +21219,14 @@ ${row.receipt ? "receipt" : "envelope"}`;
20529
21219
  const data = payload.message.parts[0].data;
20530
21220
  return this.#grants.receive(env, payload.conv, data.grants);
20531
21221
  }
21222
+ if (payload.conv?.kind === "request")
21223
+ return this.#requests.receive(env, sender, payload, now);
21224
+ if (payload.conv?.kind === "card") {
21225
+ this.#requests.answered(sender.person);
21226
+ return this.#cards.receive(env, sender, payload);
21227
+ }
21228
+ if (toSession === undefined && sender.person !== this.#joined().me.person)
21229
+ this.#requests.answered(sender.person);
20532
21230
  if (toSession === undefined)
20533
21231
  return this.#chats.receive(env, sender, payload, now);
20534
21232
  if (fromSession === undefined)
@@ -20730,6 +21428,9 @@ ${row.receipt ? "receipt" : "envelope"}`;
20730
21428
  this.#sendStatus(row);
20731
21429
  }
20732
21430
  #onReceipt(env, sender, payload) {
21431
+ if (payload.stage === "rejected" && sender.person !== this.#joined().me.person && (payload.code === "refused" || payload.code === "request_required" || payload.code === "declined")) {
21432
+ this.#cards.refused(sender.person);
21433
+ }
20733
21434
  const sent = this.#store.sent(payload.ref);
20734
21435
  if (sent && this.#counterpart(sent.toMachine, sender))
20735
21436
  return void this.#advance(sent, payload.stage, payload.code);
@@ -20907,6 +21608,7 @@ ${row.receipt ? "receipt" : "envelope"}`;
20907
21608
  this.#advance(sent, "expired");
20908
21609
  this.#chats.sweep(now);
20909
21610
  this.#holds.sweep(now);
21611
+ this.#requests.sweep(now);
20910
21612
  sessions = this.#store.expireKept(now - KEPT_MS);
20911
21613
  for (const row of this.#store.interruptedTasks(now - INPUT_TIMEOUT_MS))
20912
21614
  this.#timeOut(row, now);
@@ -21627,6 +22329,8 @@ var init_daemon = __esm(() => {
21627
22329
  init_wake_bridge();
21628
22330
  init_grants();
21629
22331
  init_holds();
22332
+ init_cards2();
22333
+ init_requests();
21630
22334
  init_sentence();
21631
22335
  DEFAULT_TUNING = {
21632
22336
  presenceMinMs: 2000,
@@ -22322,18 +23026,18 @@ var init_directory3 = __esm(() => {
22322
23026
  });
22323
23027
 
22324
23028
  // packages/relay/dist/directory/feed.js
22325
- var DAY_MS2 = 86400000, FEED_RETENTION_MS;
23029
+ var DAY_MS4 = 86400000, FEED_RETENTION_MS;
22326
23030
  var init_feed = __esm(() => {
22327
23031
  init_dist();
22328
23032
  init_jelly4();
22329
- FEED_RETENTION_MS = 90 * DAY_MS2;
23033
+ FEED_RETENTION_MS = 90 * DAY_MS4;
22330
23034
  });
22331
23035
 
22332
23036
  // packages/relay/dist/directory/pacing.js
22333
- var DAY_MS3 = 86400000, PAIR_WINDOW_MS;
23037
+ var DAY_MS5 = 86400000, PAIR_WINDOW_MS;
22334
23038
  var init_pacing = __esm(() => {
22335
23039
  init_dist();
22336
- PAIR_WINDOW_MS = 30 * DAY_MS3;
23040
+ PAIR_WINDOW_MS = 30 * DAY_MS5;
22337
23041
  });
22338
23042
 
22339
23043
  // packages/relay/dist/directory/module.js
@@ -23321,7 +24025,7 @@ var HINTS = {
23321
24025
  unknown_address: "find a person, agent or channel with `messaging who` or `messaging people`.",
23322
24026
  refused: "do not retry; the recipient's policy refuses this from you.",
23323
24027
  declined: "do not retry: the recipient's person declined this request.",
23324
- request_required: "send them a message request first: they are outside your org and have not accepted you.",
24028
+ request_required: "send them a message request first, with messaging contact <email> <text>: they are outside your org and have not accepted you.",
23325
24029
  rate_limited: "wait a minute, then retry.",
23326
24030
  loop_guard: "wait before sending more: the agent-to-agent loop guard tripped.",
23327
24031
  too_large: "shorten the content or split it.",
@@ -24021,24 +24725,27 @@ function optional2(key, value) {
24021
24725
  }
24022
24726
  var requests = {
24023
24727
  name: "requests",
24024
- summary: "List the requests waiting for you, or answer one: once, always, decline or block",
24025
- usage: "[answer <id> once|always|decline|block [--all]]",
24728
+ summary: "List the requests waiting for you, or answer one: once, always, accept, decline or block",
24729
+ usage: "[answer <id> once|always|accept|decline|block [--all]]",
24026
24730
  details: [
24027
- "A request is mail or a task from someone's agent to one of yours that waits for you (their ask level is request).",
24028
- "once: deliver this one. always: let their agents ask yours from now on, then deliver it.",
24029
- "decline: refuse it. block: block their lines and decline it. --all: answer every request from that person.",
24030
- "Previews are peer text, shown in a frame."
24731
+ "An ask is mail or a task from someone's agent to one of yours that waits for you (their ask level is request):",
24732
+ "once delivers this one; always lets their agents ask yours from now on, then delivers it.",
24733
+ "A message request is a person of another org asking to write to you: accept lets them chat with you and files",
24734
+ "their text as their first line. A suggestion is one of your agents asking you to contact someone: send it with",
24735
+ "`messaging contact`, or dismiss it with decline.",
24736
+ "decline: refuse it. block: block their lines and decline it. --all: answer every request of that kind from that person.",
24737
+ "Previews and request text are peer text, shown in a frame; a request's name and email are the directory's, never the text's."
24031
24738
  ].join(`
24032
24739
  `),
24033
- options: { all: { type: "boolean", description: "With answer: every request from that person" } },
24740
+ options: { all: { type: "boolean", description: "With answer: every request of that kind from that person" } },
24034
24741
  args: { min: 0, max: 3 },
24035
24742
  async run(ctx, positionals, values) {
24036
24743
  const client = await ctx.client();
24037
24744
  if (positionals.length > 0) {
24038
24745
  if (positionals[0] !== "answer" || positionals.length !== 3)
24039
- throw new UsageError("answer with: messaging requests answer <id> once|always|decline|block [--all]");
24746
+ throw new UsageError("answer with: messaging requests answer <id> once|always|accept|decline|block [--all]");
24040
24747
  const id = parseWith2(Ulid, positionals[1], "<id> must be a request id from `messaging requests`");
24041
- const answer = parseWith2(RequestAnswer, positionals[2], "the answer must be once, always, decline or block");
24748
+ const answer = parseWith2(RequestAnswer, positionals[2], "the answer must be once, always, accept, decline or block");
24042
24749
  const result = await client.call("request.answer", { id, answer, ...values.all === true ? { all: true } : {} });
24043
24750
  if (ctx.json) {
24044
24751
  ctx.out.json(result);
@@ -24060,13 +24767,80 @@ var requests = {
24060
24767
  }
24061
24768
  const now = ctx.now();
24062
24769
  for (const r of held) {
24770
+ if (r.kind === "suggestion") {
24771
+ ctx.out.line(`${ctx.out.style.bold("Suggestion")} ${r.id} from your agent ${inline(r.agent, 64)}: contact ${inline(r.email, 320)} ${ago(r.at, now)}`);
24772
+ continue;
24773
+ }
24774
+ if (r.kind === "contact") {
24775
+ const who = `${inline(r.from.name, 64)}${r.from.email !== undefined ? ` <${inline(r.from.email, 320)}>` : ""}`;
24776
+ const org = r.from.org === "same" ? "your org" : r.from.org === "other" ? "another org" : "no org";
24777
+ ctx.out.line(`${ctx.out.style.bold("Message request")} ${r.id} from ${who} (${org}) ${ago(r.at, now)}, expires ${iso2(r.exp)}`);
24778
+ ctx.out.lines(peerBlock(r.from.name, [r.text]));
24779
+ continue;
24780
+ }
24063
24781
  ctx.out.line(`${ctx.out.style.bold(r.kind === "task" ? "Task request" : "Message")} ${r.id} from ${inline(r.from.address)} to ${inline(r.to, 64)} ${ago(r.at, now)}, expires ${iso2(r.exp)}`);
24064
24782
  ctx.out.lines(peerBlock(r.from.address, [r.preview]));
24065
24783
  }
24066
- ctx.out.note("Answer with: messaging requests answer <id> once|always|decline|block [--all]");
24784
+ ctx.out.note("Answer with: messaging requests answer <id> once|always|accept|decline|block [--all]");
24067
24785
  }
24068
24786
  };
24069
- var CONSENT_COMMANDS = [grants, grant2, requests];
24787
+ var contact = {
24788
+ name: "contact",
24789
+ summary: "Send a message request to a person of another org, by their exact email",
24790
+ usage: "<email> <text\u2026> [--dry-run] [--suggestion <id>]",
24791
+ details: [
24792
+ "A hosted realm only. The email must be exact; nothing tells an unknown email from one that does not accept requests.",
24793
+ `The text is all they see before they answer: at most ${LIMITS.requestChars} characters. One request waits per person.`,
24794
+ "--dry-run only checks that the email finds someone. --suggestion answers your agent's suggestion of this contact."
24795
+ ].join(`
24796
+ `),
24797
+ options: {
24798
+ "dry-run": { type: "boolean", description: "Check the email, send nothing" },
24799
+ suggestion: { type: "string", description: "The suggestion this answers, from `messaging requests`" }
24800
+ },
24801
+ args: { min: 1, max: 200 },
24802
+ async run(ctx, positionals, values) {
24803
+ const email = parseWith2(Email, positionals[0].toLowerCase(), "<email> must be an exact email address");
24804
+ const client = await ctx.client();
24805
+ if (values["dry-run"] === true) {
24806
+ const { found } = await client.call("contact.request.preview", { email });
24807
+ if (ctx.json)
24808
+ return ctx.out.json({ found });
24809
+ ctx.out.line(found ? "That email finds a Messaging user who accepts requests." : "No Messaging user with that email accepts requests.");
24810
+ return;
24811
+ }
24812
+ const text = positionals.slice(1).join(" ");
24813
+ if (text.length === 0)
24814
+ throw new UsageError("say who you are and why you write: messaging contact <email> <text\u2026>");
24815
+ if (text.length > LIMITS.requestChars)
24816
+ throw new UsageError(`a message request is at most ${LIMITS.requestChars} characters`);
24817
+ const suggestion = stringValue(values, "suggestion");
24818
+ const result = await client.call("contact.request", {
24819
+ email,
24820
+ text,
24821
+ ...suggestion !== undefined ? { suggestion: parseWith2(Ulid, suggestion, "--suggestion must be a suggestion id from `messaging requests`") } : {}
24822
+ });
24823
+ if (ctx.json)
24824
+ return ctx.out.json(result);
24825
+ ctx.out.line(`Sent. They see your request first; it is ${result.conversation} once they accept.`);
24826
+ }
24827
+ };
24828
+ var card = {
24829
+ name: "card",
24830
+ summary: "Ask a person for their contact card: what they let you send them",
24831
+ usage: "<who>",
24832
+ details: "At most once a day per person. Their card arrives later; `messaging who` and the tools show it.",
24833
+ args: { min: 1, max: 1 },
24834
+ async run(ctx, positionals) {
24835
+ const person = parseWith2(PersonId, positionals[0], "<who> is a person (a lowercase name or a p_\u2026 id)");
24836
+ const client = await ctx.client();
24837
+ const { sent } = await client.call("card.get", { person });
24838
+ if (ctx.json)
24839
+ return ctx.out.json({ sent });
24840
+ ctx.out.line(sent ? "Asked. Their card comes when one of their devices answers." : "You asked them in the last day; their card comes when they answer.");
24841
+ }
24842
+ };
24843
+ var CONSENT_COMMANDS = [grants, grant2, requests, contact, card];
24070
24844
 
24071
24845
  // packages/daemon/dist/cli/commands/daemon.js
24072
24846
  init_dist();
@@ -25401,7 +26175,7 @@ var revokeInvite = {
25401
26175
  ctx.out.line(`Revoked invite ${inviteId}.`);
25402
26176
  }
25403
26177
  };
25404
- var card = {
26178
+ var card2 = {
25405
26179
  name: "team card",
25406
26180
  summary: "Print this team's public card (relay, team id, name)",
25407
26181
  usage: "",
@@ -25550,7 +26324,7 @@ var prepare = {
25550
26324
  ctx.out.note("Give this machine id to the relay operator for --allow-team-create, then run `messaging team create`.");
25551
26325
  }
25552
26326
  };
25553
- var TEAM_COMMANDS = [prepare, create, invite, join5, members2, remove, role, revokeInvite, card, add, pair, claim2, pairCancel];
26327
+ var TEAM_COMMANDS = [prepare, create, invite, join5, members2, remove, role, revokeInvite, card2, add, pair, claim2, pairCancel];
25554
26328
 
25555
26329
  // packages/daemon/dist/cli/version.js
25556
26330
  init_dist();
@@ -25580,7 +26354,7 @@ var SECTIONS = [
25580
26354
  ["Daemon", ["daemon start", "daemon stop", "status"]],
25581
26355
  ["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"]],
25582
26356
  ["Identity", ["identity show", "identity migrate", "identity rotate-enc"]],
25583
- ["People", ["people", "devices", "grants", "grant", "requests", "notices"]],
26357
+ ["People", ["people", "devices", "grants", "grant", "requests", "contact", "card", "notices"]],
25584
26358
  ["Messages", ["who", "send", "inbox", "read", "message-status"]],
25585
26359
  ["Chats", ["chat list", "chat read", "chat post", "chat search"]],
25586
26360
  ["Tasks", ["tasks", "task", "cancel"]],