@joe-broadhead/messaging 0.7.0 → 0.8.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.
Files changed (97) hide show
  1. package/CHANGELOG.md +259 -2
  2. package/dist/bin/messaging-mcp.js +5484 -3858
  3. package/dist/bin/messaging-relay.js +4728 -1136
  4. package/dist/bin/messaging.js +4616 -2807
  5. package/dist/bin/messagingd.js +16451 -9444
  6. package/dist/lib/modules/core/agent/operations.js +19 -327
  7. package/dist/lib/modules/core/agent/present.js +15 -55
  8. package/dist/lib/modules/core/agent-tools.js +46 -143
  9. package/dist/lib/modules/core/consent.js +1 -1
  10. package/dist/lib/modules/core/conversation/context.js +80 -0
  11. package/dist/lib/modules/core/conversation/event.js +125 -0
  12. package/dist/lib/modules/core/conversation/guest-invitation.js +42 -0
  13. package/dist/lib/modules/core/conversation/ids.js +38 -0
  14. package/dist/lib/modules/core/conversation/link.js +35 -0
  15. package/dist/lib/modules/core/conversation/log.js +988 -0
  16. package/dist/lib/modules/core/conversation/ratchet.js +243 -0
  17. package/dist/lib/modules/core/conversation/request-log.js +158 -0
  18. package/dist/lib/modules/core/conversation/request-seal.js +41 -0
  19. package/dist/lib/modules/core/conversation/requests.js +87 -0
  20. package/dist/lib/modules/core/conversation/seal.js +251 -0
  21. package/dist/lib/modules/core/conversation-agent-tools.js +69 -0
  22. package/dist/lib/modules/core/federation/directory-proof.js +83 -0
  23. package/dist/lib/modules/core/federation/proof-state.js +167 -0
  24. package/dist/lib/modules/core/federation/roster-proof.js +74 -0
  25. package/dist/lib/modules/core/index.js +16 -2
  26. package/dist/lib/modules/core/jelly-host.js +5 -5
  27. package/dist/lib/modules/core/schema/common.js +85 -3
  28. package/dist/lib/modules/core/schema/consent.js +5 -4
  29. package/dist/lib/modules/core/schema/contact-request.js +22 -0
  30. package/dist/lib/modules/core/schema/conv-frames.js +94 -0
  31. package/dist/lib/modules/core/schema/conversation.js +326 -0
  32. package/dist/lib/modules/core/schema/envelope.js +8 -24
  33. package/dist/lib/modules/core/schema/errors.js +42 -2
  34. package/dist/lib/modules/core/schema/guest-invitation.js +13 -0
  35. package/dist/lib/modules/core/schema/index.js +6 -0
  36. package/dist/lib/modules/core/schema/ipc/consent.js +13 -9
  37. package/dist/lib/modules/core/schema/ipc/conversation-views.js +80 -0
  38. package/dist/lib/modules/core/schema/ipc/conversations.js +128 -0
  39. package/dist/lib/modules/core/schema/ipc/device.js +2 -0
  40. package/dist/lib/modules/core/schema/ipc/jelly.js +3 -38
  41. package/dist/lib/modules/core/schema/ipc/shapes.js +14 -72
  42. package/dist/lib/modules/core/schema/ipc/surfaces.js +12 -195
  43. package/dist/lib/modules/core/schema/ipc/tasks.js +31 -0
  44. package/dist/lib/modules/core/schema/ipc.js +18 -44
  45. package/dist/lib/modules/core/testing.js +1 -0
  46. package/dist/lib/modules/core/version.js +1 -1
  47. package/dist/lib/modules/ipc/client.js +10 -2
  48. package/dist/lib/modules/ipc/server.js +14 -5
  49. package/dist/lib/modules/ipc/testing.js +13 -23
  50. package/dist/spec/messaging-v1.md +835 -380
  51. package/dist/third-party-notices.md +1 -1
  52. package/dist/third-party.json +1 -1
  53. package/dist/types/core/agent/operations.d.ts +5 -52
  54. package/dist/types/core/agent/present.d.ts +6 -21
  55. package/dist/types/core/agent-tools.d.ts +277 -438
  56. package/dist/types/core/conversation/context.d.ts +56 -0
  57. package/dist/types/core/conversation/event.d.ts +36 -0
  58. package/dist/types/core/conversation/guest-invitation.d.ts +8 -0
  59. package/dist/types/core/conversation/ids.d.ts +18 -0
  60. package/dist/types/core/conversation/link.d.ts +16 -0
  61. package/dist/types/core/conversation/log.d.ts +157 -0
  62. package/dist/types/core/conversation/ratchet.d.ts +95 -0
  63. package/dist/types/core/conversation/request-log.d.ts +24 -0
  64. package/dist/types/core/conversation/request-seal.d.ts +6 -0
  65. package/dist/types/core/conversation/requests.d.ts +14 -0
  66. package/dist/types/core/conversation/seal.d.ts +70 -0
  67. package/dist/types/core/conversation-agent-tools.d.ts +918 -0
  68. package/dist/types/core/federation/directory-proof.d.ts +8 -0
  69. package/dist/types/core/federation/proof-state.d.ts +114 -0
  70. package/dist/types/core/federation/roster-proof.d.ts +7 -0
  71. package/dist/types/core/index.d.ts +15 -3
  72. package/dist/types/core/jelly-host.d.ts +1 -1
  73. package/dist/types/core/schema/common.d.ts +85 -4
  74. package/dist/types/core/schema/consent.d.ts +7 -7
  75. package/dist/types/core/schema/contact-request.d.ts +264 -0
  76. package/dist/types/core/schema/conv-frames.d.ts +1200 -0
  77. package/dist/types/core/schema/conversation.d.ts +2803 -0
  78. package/dist/types/core/schema/envelope.d.ts +89 -130
  79. package/dist/types/core/schema/errors.d.ts +40 -0
  80. package/dist/types/core/schema/guest-invitation.d.ts +51 -0
  81. package/dist/types/core/schema/index.d.ts +6 -0
  82. package/dist/types/core/schema/ipc/consent.d.ts +89 -49
  83. package/dist/types/core/schema/ipc/conversation-views.d.ts +1023 -0
  84. package/dist/types/core/schema/ipc/conversations.d.ts +3987 -0
  85. package/dist/types/core/schema/ipc/device.d.ts +10 -0
  86. package/dist/types/core/schema/ipc/jelly.d.ts +2 -105
  87. package/dist/types/core/schema/ipc/shapes.d.ts +16 -95
  88. package/dist/types/core/schema/ipc/surfaces.d.ts +28 -424
  89. package/dist/types/core/schema/ipc/tasks.d.ts +208 -0
  90. package/dist/types/core/schema/ipc.d.ts +2684 -523
  91. package/dist/types/core/schema/relay.d.ts +20 -4
  92. package/dist/types/core/testing.d.ts +1 -0
  93. package/dist/types/core/version.d.ts +1 -1
  94. package/dist/types/ipc/client.d.ts +4 -1
  95. package/dist/types/ipc/server.d.ts +10 -1
  96. package/dist/types/ipc/testing.d.ts +21 -8
  97. package/package.json +1 -1
@@ -1,10 +1,15 @@
1
1
  // Wire contracts (frozen; change only with a spec update and new test vectors).
2
2
  export * from "./schema/index.js";
3
+ export * from "./conversation-agent-tools.js";
3
4
  // Protocol logic; see docs/spec/messaging-v1.md §9.
4
5
  export { b64u } from "./b64u.js";
5
6
  export { VERSION } from "./version.js";
6
7
  export { canonicalize } from "./jcs.js";
7
8
  export { parseWire } from "./wire.js";
9
+ export * from "./conversation/requests.js";
10
+ export * from "./conversation/request-log.js";
11
+ export * from "./conversation/request-seal.js";
12
+ export * from "./conversation/guest-invitation.js";
8
13
  export { sha256 } from "./hash.js";
9
14
  export { ulid } from "./ulid.js";
10
15
  export { Identity, IdentityFile, generateIdentity, exportIdentity, importIdentity } from "./identity.js";
@@ -37,8 +42,17 @@ export { canTransition, isTerminal, isInterrupted } from "./task.js";
37
42
  export { TERMINAL_STATES } from "./schema/a2a.js";
38
43
  export { cleanPeerText, createFence, terminalSafe, displayWidth, HOST_TAGS, PEER_FENCE_TAG, PEER_DATA_NOTICE, provenanceOf } from "./peer-text.js";
39
44
  // The agent tool contract shared by the OpenCode plugin and messaging-mcp (spec §11, "Agent tools").
40
- export { AgentTool, TOOL_VERBS, TOOL_DESCRIPTIONS, ToolExpect, UNTRUSTED_SENTENCE, TOOL_REMEDIES, DONE_STATES, TaskStateName, ToolErrorBody, ToolFiles, a2aTaskState, conversationOf, decodeToolCursor, encodeToolCursor, formatToolAddress, parseToolAddress, taskStateName, toolErrorBody, toolName, } from "./agent-tools.js";
45
+ export { AgentTool, TOOL_VERBS, TOOL_DESCRIPTIONS, ToolExpect, UNTRUSTED_SENTENCE, TOOL_REMEDIES, STORE_REFUSED_REMEDIES, DONE_STATES, TaskStateName, ToolErrorBody, ToolFiles, a2aTaskState, formatToolAddress, parseToolAddress, taskStateName, toolErrorBody, toolName, } from "./agent-tools.js";
41
46
  // The agent tools' operations and presenters, shared by the plugin and messaging-mcp (spec §11).
42
47
  export { Operations, } from "./agent/operations.js";
43
- export { agentCan, consentOf, conversationOfPeer, followUpRead, isoOf, presentAgent, presentConvEntry, presentInboxItem, presentLine, presentMail, presentSent, presentTask, presentTaskItem, presentTaskView, } from "./agent/present.js";
48
+ export { agentCan, consentOf, followUpRead, isoOf, presentAgent, presentInboxItem, presentTaskEnvelope, presentTask, presentTaskItem, presentTaskView, } from "./agent/present.js";
44
49
  export { chainPath, compact, delegated, delegationPrompt, EXPECT_TEXT, isSafeId, messageAttributes, validatedAddress, } from "./agent/text.js";
50
+ // Conversations (spec §16): ids, events and the log's hash chain, sender keys and sealing, the log as a pure
51
+ // reducer, and context-rule resolution. Fixed-randomness sealing for vectors lives in `@messaging/core/testing`.
52
+ export { dmConvId, selfConvId, participantOf, participantPerson, participantAgent } from "./conversation/ids.js";
53
+ export { admitConvEvent, convEntryHash, convEventDigest, convEventHeader, convEventSigned, convGenesisHash, convLinkHash, signConvEvent, verifyConvEvent, } from "./conversation/event.js";
54
+ export { chainStep, ReceiverChains, ReceiverChainsState, SenderChain, SenderChainState } from "./conversation/ratchet.js";
55
+ export { openConvMeta, openConvMsg, openConvState, openKeyShares, sealConvMeta, sealConvMsg, sealConvState, sealKeyShare, } from "./conversation/seal.js";
56
+ export { ConvLog, } from "./conversation/log.js";
57
+ export { checkVia, currentLinks, linkReader } from "./conversation/link.js";
58
+ export { CONTEXT_DEFAULTS, contextKindOf, matchContextRule, resolveContext, seatReads, } from "./conversation/context.js";
@@ -22,7 +22,7 @@ export const SESSION_TICKET_MAX_S = 7 * 24 * 60 * 60;
22
22
  /** The clock skew allowed on `iat` and `exp`. */
23
23
  export const JELLY_HOST_SKEW_S = 5;
24
24
  /** `chat`: the person's own lines and read state. */
25
- const CHAT_METHODS = ["person.send", "conv.markRead", "notice.ack"];
25
+ const CHAT_METHODS = ["conv.send", "conv.edit", "conv.delete", "conv.react", "notice.ack"];
26
26
  /**
27
27
  * `view`: the read-only methods a Jelly section reads with (`section.rpc`): the read-only human-tier methods, and
28
28
  * `device.list`, the one admin-tier read a section shows. A preview is not one of them: it takes its action's class
@@ -32,13 +32,13 @@ const CHAT_METHODS = ["person.send", "conv.markRead", "notice.ack"];
32
32
  const VIEW_METHODS = [
33
33
  "conv.list",
34
34
  "conv.read",
35
- "conv.subscribe",
35
+ "conv.sync", "conv.get", "conv.members", "conv.threads", "conv.search", "conv.context", "conv.access",
36
+ "request.waiting",
36
37
  "people.search",
37
38
  "people.get",
38
39
  "grant.list",
39
40
  "request.list",
40
- "agents.list",
41
- "agents.read",
41
+ "sessions.list",
42
42
  "notice.subscribe",
43
43
  "notice.list",
44
44
  "device.list",
@@ -46,7 +46,7 @@ const VIEW_METHODS = [
46
46
  const classOf = (method) => CHAT_METHODS.includes(method) ? "chat" : VIEW_METHODS.includes(method) ? "view" : "local";
47
47
  /**
48
48
  * The class a method grant must name in `cls` for each human- and admin-tier method (spec §8.11): `chat` for
49
- * `person.send`, `conv.markRead` and `notice.ack`, `view` for the read-only human-tier methods, `local` for every
49
+ * line mutations and `notice.ack`, `view` for the read-only human-tier methods, `local` for every
50
50
  * other. A plugin's `jelly` declaration gives each action and read the same class; a test there checks one against
51
51
  * the other.
52
52
  */
@@ -42,6 +42,24 @@ export const DOMAIN = {
42
42
  epoch: "messaging/1/epoch\n",
43
43
  /** Hosted realms (spec §15.6): an epoch-key rotation, signed by the old key and by the new over JCS(rotation without sigs). */
44
44
  dirKey: "messaging/1/dir-key\n",
45
+ /** Conversations (spec §16.4): a conversation event, signed by `by.device` over JCS(event without sig). */
46
+ convEvent: "messaging/1/conv-event\n",
47
+ /** Conversations (spec §16.4): the prefix of a conversation log's genesis hash h(0), over JCS(cid). */
48
+ convGenesis: "messaging/1/conv-genesis\n",
49
+ /** Conversations (spec §16.1): the prefix of the hash that derives a dm's id, over JCS of its two sorted participants. */
50
+ dm: "messaging/1/dm\n",
51
+ /** Conversations (spec §16.1): the prefix of the hash that derives a person's `self` conversation id, over JCS(person). */
52
+ self: "messaging/1/self\n",
53
+ /** Conversations (spec §16.5): HKDF `info` for a sender chain's message key. */
54
+ convMsg: "messaging/1/conv-msg",
55
+ /** Conversations (spec §16.5): HKDF `info` for a sender chain's next chain key. */
56
+ convChain: "messaging/1/conv-chain",
57
+ request: "messaging/1/contact-request\n",
58
+ requestContent: "messaging/1/contact-request-content\n",
59
+ requestDigest: "messaging/1/contact-request-digest\n",
60
+ requestDecision: "messaging/1/contact-request-decision\n",
61
+ contactAuthorization: "messaging/1/contact-authorization\n",
62
+ guestInvitation: "messaging/1/guest-invitation\n",
45
63
  };
46
64
  /** A2A extension and binding URIs owned by Messaging. */
47
65
  export const URI = {
@@ -69,8 +87,6 @@ export const HostedPersonId = z.string().regex(/^p_[a-z2-7]{26}$/, "hosted perso
69
87
  export const PersonId = z.union([PersonName, HostedPersonId]);
70
88
  /** A channel's name (spec §6.8). Unique among the channels one machine holds, not across the team. */
71
89
  export const ChannelName = z.string().regex(/^[a-z][a-z0-9-]{0,31}$/, "lowercase channel name");
72
- /** A chat room (spec §6.8): `dm:<person>` for a direct human message, `channel:<name>` for a channel. */
73
- export const RoomRef = z.string().regex(/^(?:dm:(?:[a-z][a-z0-9-]{0,31}|p_[a-z2-7]{26})|channel:[a-z][a-z0-9-]{0,31})$/, "dm:<person> or channel:<name>");
74
90
  export const SessionName = z.string().regex(/^[a-z0-9][a-z0-9._-]{0,63}$/, "lowercase session name");
75
91
  export const MachineLabel = z.string().regex(/^[a-z0-9][a-z0-9-]{0,31}$/, "lowercase machine label");
76
92
  export const TeamName = z.string().min(1).max(64);
@@ -78,7 +94,7 @@ export const TeamName = z.string().min(1).max(64);
78
94
  export const AgentKind = z.string().regex(/^[a-z0-9][a-z0-9._-]{0,63}$/, "lowercase agent kind");
79
95
  /**
80
96
  * The agent kind of the human `messaging` command line (spec §8.2, §8.3): never kept or expired on
81
- * disconnect, refused as a task executor, and the only sender of group addresses.
97
+ * disconnect and refused as a task executor. No sender can use group addresses.
82
98
  */
83
99
  export const CLI_AGENT = "cli";
84
100
  /** Milliseconds since the Unix epoch. */
@@ -122,6 +138,8 @@ export const LIMITS = {
122
138
  plaintextBytes: 262_144,
123
139
  /** Max characters in a single text part. */
124
140
  textPartChars: 65_536,
141
+ /** Existing model-visible per-part cap, shared with fenced conversation events. */
142
+ toolTextChars: 32_768,
125
143
  /** Max parts in one message. */
126
144
  partsPerMessage: 16,
127
145
  /** Default and max envelope TTL, and the max invite lifetime (`expires − ts`). */
@@ -232,6 +250,70 @@ export const LIMITS = {
232
250
  a2hPerHour: 30,
233
251
  /** Max lines one person's agents send to another person in a day, summed over the agents (spec §6.9, "Pacing"). */
234
252
  a2hPerDay: 100,
253
+ /** Conversations (spec §16): max participants (persons and agents) of a channel. */
254
+ convParticipants: 1000,
255
+ /** Max participants (persons and agents) of a group. A group has at least 3. */
256
+ groupParticipants: 50,
257
+ /** Max participants one `add` or `remove` op names. */
258
+ convOpParticipants: 100,
259
+ /** Max UTF-8 bytes of one conversation event (JCS), signature and body included. */
260
+ convEventBytes: 262_144,
261
+ /** Max UTF-8 bytes of one message body's plaintext (JCS) before it is sealed. */
262
+ convPlaintextBytes: 65_536,
263
+ /** Max UTF-16 code units of one conversation line's text. */
264
+ lineChars: 40_000,
265
+ /** Max mentions in one line or edit. */
266
+ mentions: 50,
267
+ /** Max attachments in one line. */
268
+ files: 10,
269
+ /** Max delegation grants one line carries for the agents it mentions (spec §16.3). */
270
+ agentMentions: 5,
271
+ /** Max bytes of one attached file, encrypted. */
272
+ blobBytes: 26_214_400,
273
+ /** Max distinct reaction keys on one event. */
274
+ reactionsPerEvent: 50,
275
+ /** Max pinned events in one conversation. */
276
+ pins: 100,
277
+ /** Retention of a conversation that sets none, in days. */
278
+ retainDaysDefault: 90,
279
+ /** Max `retainDays` of a conversation, and max lookback of a context rule. */
280
+ retainDaysMax: 365,
281
+ /** Max message keys a receiver keeps for one sender chain to open messages that arrive out of order. */
282
+ skippedKeys: 1000,
283
+ /** A sender starts a new chain after this many messages on one chain... */
284
+ chainMessages: 500,
285
+ /** ...or this long after it started the chain, whichever comes first. */
286
+ chainMaxMs: 7 * 24 * 60 * 60 * 1000,
287
+ /** An epoch older than this may be ended by a `rekey` with cause `schedule`. */
288
+ rekeyScheduleMs: 30 * 24 * 60 * 60 * 1000,
289
+ /** Max chain keys in one sealed key share, and in one `keys` sync of the `self` conversation. */
290
+ keyShares: 512,
291
+ /** Max agents one event's `wake` hint names. */
292
+ wakeHints: 5,
293
+ /** Max context rules a person holds (spec §16.8). */
294
+ contextRules: 500,
295
+ /** Relay (spec §7, "Conversation frames"): appends one device makes to one conversation, a burst of this many... */
296
+ convAppendBurst: 20,
297
+ /** ...refilled at this many a minute. */
298
+ convAppendsPerMinute: 60,
299
+ /** Appends to one conversation from all its devices, a minute (burst and refill). */
300
+ convAppendsPerConvPerMinute: 600,
301
+ /** `rekey` ops with a cause the log cannot prove (`device`, `schedule`) per device per conversation, an hour. */
302
+ convRekeysPerHour: 4,
303
+ /** Conversations one device takes part in, through its person (create, add and join refuse past it). */
304
+ convsPerDevice: 5000,
305
+ /** Conversations one device creates, an hour (burst and refill). */
306
+ convCreatesPerHour: 60,
307
+ /** Key-share events in one `conv.share` frame. */
308
+ convSharesPerFrame: 100,
309
+ /** Key shares one sender device holds at the relay for one recipient device in one conversation. */
310
+ convSharesPerPair: 256,
311
+ /** Key shares one device sends to one conversation, a burst of this many, refilled per minute. */
312
+ convSharesPerMinute: 10_000,
313
+ /** Entries (and shares) in one `conv.entries` reply. */
314
+ convFetchEntries: 500,
315
+ /** Conversations named in one `conv.head`, and heads in one `conv.heads` reply. */
316
+ convHeads: 500,
235
317
  };
236
318
  const LOOPBACK_HOST = /^(?:localhost|127(?:\.\d{1,3}){3}|\[::1\])$/;
237
319
  /**
@@ -6,14 +6,15 @@
6
6
  import { z } from "../../vendor/zod/index.js";
7
7
  import { Base64Url, LIMITS, MachineId, Millis, PersonId, PROTOCOL, Ulid } from "./common.js";
8
8
  import { RealmId } from "./realm.js";
9
+ import { ConvId } from "./conversation.js";
9
10
  /** H2H lines from the subject: `muted` delivers with no notification and no unread mark; `blocked` refuses every class from them. */
10
11
  export const ChatLevel = z.enum(["open", "muted", "blocked"]);
11
12
  /** Lines from the subject's agents to the person (A2H). */
12
13
  export const AgentMailLevel = z.enum(["open", "blocked"]);
13
14
  /** Mail and tasks from the subject or their agents to the person's agents: `request` holds each one for the person to answer. */
14
15
  export const AskLevel = z.enum(["open", "request", "no"]);
15
- /** Whom a grant is about: one person, or every member of one channel (by its id). */
16
- export const GrantSubject = z.union([z.strictObject({ person: PersonId }), z.strictObject({ channel: Ulid })]);
16
+ /** Whom a grant is about: one person, or the verified participants of one conversation. */
17
+ export const GrantSubject = z.union([z.strictObject({ person: PersonId }), z.strictObject({ conv: ConvId })]);
17
18
  /**
18
19
  * A grant, signed by `by`, a device of `person`: `sig = Ed25519(by, "messaging/1/grant\n" || JCS(grant without sig))`.
19
20
  * Every level is optional: an absent level is the default for the relation (spec §6.9, "Effective policy"), and a
@@ -54,8 +55,8 @@ export const GrantSync = z.strictObject({ grants: z.array(z.record(z.string(), z
54
55
  /** The rules of a grant that its schema alone cannot say. */
55
56
  export function grantProblem(grant) {
56
57
  const own = "person" in grant.subject && grant.subject.person === grant.person;
57
- if ("channel" in grant.subject && (grant.chat !== undefined || grant.trustedUntil !== undefined || grant.discoverable !== undefined)) {
58
- return "a channel grant sets only ask and agentMail";
58
+ if ("conv" in grant.subject && (grant.chat !== undefined || grant.trustedUntil !== undefined || grant.discoverable !== undefined)) {
59
+ return "a conversation grant sets only ask and agentMail";
59
60
  }
60
61
  if (own && (grant.chat !== undefined || grant.agentMail !== undefined || grant.ask !== undefined || grant.trustedUntil !== undefined)) {
61
62
  return "a grant about its own person sets only discoverable";
@@ -0,0 +1,22 @@
1
+ /** Portable same-realm contact requests (spec §16.11). All proofs are strict and domain-separated. */
2
+ import { z } from "../../vendor/zod/index.js";
3
+ import { Hash, Key32, LIMITS, MachineId, Millis, PersonId, PROTOCOL, Ulid } from "./common.js";
4
+ import { RealmId } from "./realm.js";
5
+ const Dm = z.string().regex(/^d_[a-z2-7]{26}$/);
6
+ const Sig = z.string().regex(/^[A-Za-z0-9_-]{86}$/);
7
+ const Req = z.string().min(1).max(64);
8
+ const route = { v: z.literal(PROTOCOL), realm: RealmId, cid: Dm, from: PersonId, to: PersonId };
9
+ export const RequestDescriptor = z.strictObject({ ...route, id: Ulid, issuedAt: Millis, expiresAt: Millis, commitment: Hash, by: MachineId, sig: Sig });
10
+ export const RequestOpening = z.strictObject({ salt: Key32, text: z.string().min(1).max(LIMITS.requestChars) });
11
+ export const PortableRequest = z.strictObject({ descriptor: RequestDescriptor, opening: RequestOpening });
12
+ /** A block is conveyed as decline. The first valid terminal answer is sequenced in the recipient mailbox. */
13
+ export const RequestDecision = z.strictObject({ ...route, id: Ulid, descriptor: Hash, answer: z.enum(["accept", "decline"]), at: Millis, by: MachineId, sig: Sig });
14
+ export const RequestActivation = z.strictObject({ descriptor: RequestDescriptor, decision: RequestDecision });
15
+ /** Existing contacts authorize the exact human DM on both sides, under their current local grants. */
16
+ export const ContactAuthorization = z.strictObject({ ...route, at: Millis, by: MachineId, sig: Sig });
17
+ export const ActivationProof = z.union([RequestActivation, z.strictObject({ contacts: z.tuple([ContactAuthorization, ContactAuthorization]) })]);
18
+ export const RequestImport = z.strictObject({ t: z.literal("request.import"), ...RequestActivation.shape, opening: RequestOpening });
19
+ /** No opening or request text is sent to the relay. */
20
+ export const RequestSettleFrame = z.strictObject({ t: z.literal("request.settle"), req: Req, ...RequestActivation.shape });
21
+ export const RequestStateFrame = z.strictObject({ t: z.literal("request.state"), req: Req, descriptor: RequestDescriptor });
22
+ export const RequestSettledFrame = z.strictObject({ t: z.literal("request.settled"), req: Req, id: Ulid, decision: RequestDecision.optional() });
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Conversation frames (spec §7, "Conversation frames"; spec §16): the frames a relay speaks after `hello` to carry
3
+ * conversation logs, in a roster realm and in a directory realm alike. They follow the frame rules of §7: one JSON
4
+ * object per text frame, discriminated by `t`; requests carry `req`, replies echo it, and a refusal is the relay
5
+ * binding's `nack {id: req, code, message}` (with `retryAfterMs` for `rate_limited`). Every frame is strict.
6
+ *
7
+ * conv.append {req, event} → conv.ok {req, cid, seq, hash} an op or a msg, sequenced
8
+ * conv.share {req, events} → conv.shared {req, cid, count} key shares, one recipient each
9
+ * conv.fetch {req, cid, after, limit?, sharesAfter?} → conv.entries {req, cid, head, entries, shares, more}
10
+ * conv.head {req, cids? | after?} → conv.heads {req, heads, more}
11
+ * conv.caps {req, persons} → conv.capabilities {req, outdated} who cannot read conversations yet
12
+ * relay → conv.push {cid, entry} | {cid, share} a new entry to the members' online devices; a share to its recipient
13
+ *
14
+ * The relay sequences and filters; members verify everything (spec §16.4, §16.7). It never signs an entry.
15
+ */
16
+ import { z } from "../../vendor/zod/index.js";
17
+ import { LIMITS, PersonId } from "./common.js";
18
+ import { ConvEvent, ConvHead, ConvId, ConvKeysEvent, ConvLogEntry, ConvServedEntry, ConvShareEntry } from "./conversation.js";
19
+ const Req = z.string().min(1).max(64);
20
+ const Count = z.number().int().min(0);
21
+ // ---- client → relay ----
22
+ /** Appends an op or a msg to its conversation (`event.cid`). The relay answers `conv.ok` once the entry is stored. */
23
+ export const ConvAppendFrame = z.strictObject({ t: z.literal("conv.append"), req: Req, event: ConvEvent });
24
+ /**
25
+ * Stores key shares (spec §16.6): each event to one recipient device, all of one conversation and by the sending
26
+ * device. All or none are stored; the relay answers `conv.shared` once they are.
27
+ */
28
+ export const ConvShareFrame = z.strictObject({
29
+ t: z.literal("conv.share"),
30
+ req: Req,
31
+ events: z.array(ConvKeysEvent).min(1).max(LIMITS.convSharesPerFrame),
32
+ });
33
+ /**
34
+ * The entries of `cid` after `after`, in seq order, at most `limit` (and `LIMITS.convFetchEntries`) and as many as
35
+ * fit in a frame. `sharesAfter`: also the shares to this device after that `n`; absent, no shares.
36
+ */
37
+ export const ConvFetchFrame = z.strictObject({
38
+ t: z.literal("conv.fetch"),
39
+ req: Req,
40
+ cid: ConvId,
41
+ after: Count,
42
+ limit: z.number().int().min(1).max(LIMITS.convFetchEntries).optional(),
43
+ sharesAfter: Count.optional(),
44
+ });
45
+ /**
46
+ * The heads of the named conversations, or, without `cids`, of every conversation this device's person takes part in,
47
+ * by cid after `after`, at most `LIMITS.convHeads`.
48
+ */
49
+ export const ConvHeadFrame = z
50
+ .strictObject({
51
+ t: z.literal("conv.head"),
52
+ req: Req,
53
+ cids: z.array(ConvId).min(1).max(LIMITS.convHeads).optional(),
54
+ after: ConvId.optional(),
55
+ })
56
+ .refine((f) => f.cids === undefined || f.after === undefined, { message: "cids or after, not both" });
57
+ /**
58
+ * Which of these persons cannot read conversations yet: every current device of theirs whose latest presence the
59
+ * relay holds names no `conversations` feature (a daemon before the cut-over). A person with no such presence is not
60
+ * named. A hint from the relay: it can only make a sender refuse, never admit anything.
61
+ */
62
+ export const ConvCapsFrame = z.strictObject({ t: z.literal("conv.caps"), req: Req, persons: z.array(PersonId).min(1).max(LIMITS.convOpParticipants) });
63
+ // ---- relay → client ----
64
+ /** `conv.append` stored: the entry's seq and h(seq), which the author recomputes. */
65
+ export const ConvOkFrame = z.strictObject({ t: z.literal("conv.ok"), req: Req, cid: ConvId, seq: z.number().int().min(1), hash: ConvHead.shape.hash });
66
+ /** `conv.share` stored: how many shares. */
67
+ export const ConvSharedFrame = z.strictObject({ t: z.literal("conv.shared"), req: Req, cid: ConvId, count: z.number().int().min(1).max(LIMITS.convSharesPerFrame) });
68
+ /**
69
+ * `conv.fetch`'s answer: the relay's head, the entries (an expired `msg` as its digest, `ConvExpiredEntry`) and this
70
+ * device's shares. `more`: ask again after the last entry (and the last share).
71
+ */
72
+ export const ConvEntriesFrame = z.strictObject({
73
+ t: z.literal("conv.entries"),
74
+ req: Req,
75
+ cid: ConvId,
76
+ head: ConvHead,
77
+ entries: z.array(ConvServedEntry).max(LIMITS.convFetchEntries),
78
+ shares: z.array(ConvShareEntry).max(LIMITS.convFetchEntries),
79
+ more: z.boolean(),
80
+ });
81
+ /** One conversation's position at the relay: its head, its op head, and the last share `n` for this device (0: none). */
82
+ export const ConvHeadView = z.strictObject({ cid: ConvId, head: ConvHead, opHead: ConvHead, shares: Count });
83
+ /** `conv.head`'s answer. A named conversation this device's person is not in is left out, as one that does not exist. */
84
+ export const ConvHeadsFrame = z.strictObject({ t: z.literal("conv.heads"), req: Req, heads: z.array(ConvHeadView).max(LIMITS.convHeads), more: z.boolean() });
85
+ /** `conv.caps`'s answer: the named persons that cannot read conversations yet. */
86
+ export const ConvCapabilitiesFrame = z.strictObject({ t: z.literal("conv.capabilities"), req: Req, outdated: z.array(PersonId).max(LIMITS.convOpParticipants) });
87
+ /** A new entry, to the online devices of the conversation's participant persons; or a new share, to its recipient. */
88
+ export const ConvPushFrame = z.union([
89
+ z.strictObject({ t: z.literal("conv.push"), cid: ConvId, entry: ConvLogEntry }),
90
+ z.strictObject({ t: z.literal("conv.push"), cid: ConvId, share: ConvShareEntry }),
91
+ ]);
92
+ /** The conversation frames' types, for the relay's dispatch and `/metrics`. */
93
+ export const CONV_FRAMES = ["conv.append", "conv.share", "conv.fetch", "conv.head", "conv.caps"];
94
+ export const CONV_REPLIES = ["conv.ok", "conv.shared", "conv.entries", "conv.heads", "conv.capabilities", "conv.push"];