@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.
- package/CHANGELOG.md +259 -2
- package/dist/bin/messaging-mcp.js +5484 -3858
- package/dist/bin/messaging-relay.js +4728 -1136
- package/dist/bin/messaging.js +4616 -2807
- package/dist/bin/messagingd.js +16451 -9444
- package/dist/lib/modules/core/agent/operations.js +19 -327
- package/dist/lib/modules/core/agent/present.js +15 -55
- package/dist/lib/modules/core/agent-tools.js +46 -143
- package/dist/lib/modules/core/consent.js +1 -1
- package/dist/lib/modules/core/conversation/context.js +80 -0
- package/dist/lib/modules/core/conversation/event.js +125 -0
- package/dist/lib/modules/core/conversation/guest-invitation.js +42 -0
- package/dist/lib/modules/core/conversation/ids.js +38 -0
- package/dist/lib/modules/core/conversation/link.js +35 -0
- package/dist/lib/modules/core/conversation/log.js +988 -0
- package/dist/lib/modules/core/conversation/ratchet.js +243 -0
- package/dist/lib/modules/core/conversation/request-log.js +158 -0
- package/dist/lib/modules/core/conversation/request-seal.js +41 -0
- package/dist/lib/modules/core/conversation/requests.js +87 -0
- package/dist/lib/modules/core/conversation/seal.js +251 -0
- package/dist/lib/modules/core/conversation-agent-tools.js +69 -0
- package/dist/lib/modules/core/federation/directory-proof.js +83 -0
- package/dist/lib/modules/core/federation/proof-state.js +167 -0
- package/dist/lib/modules/core/federation/roster-proof.js +74 -0
- package/dist/lib/modules/core/index.js +16 -2
- package/dist/lib/modules/core/jelly-host.js +5 -5
- package/dist/lib/modules/core/schema/common.js +85 -3
- package/dist/lib/modules/core/schema/consent.js +5 -4
- package/dist/lib/modules/core/schema/contact-request.js +22 -0
- package/dist/lib/modules/core/schema/conv-frames.js +94 -0
- package/dist/lib/modules/core/schema/conversation.js +326 -0
- package/dist/lib/modules/core/schema/envelope.js +8 -24
- package/dist/lib/modules/core/schema/errors.js +42 -2
- package/dist/lib/modules/core/schema/guest-invitation.js +13 -0
- package/dist/lib/modules/core/schema/index.js +6 -0
- package/dist/lib/modules/core/schema/ipc/consent.js +13 -9
- package/dist/lib/modules/core/schema/ipc/conversation-views.js +80 -0
- package/dist/lib/modules/core/schema/ipc/conversations.js +128 -0
- package/dist/lib/modules/core/schema/ipc/device.js +2 -0
- package/dist/lib/modules/core/schema/ipc/jelly.js +3 -38
- package/dist/lib/modules/core/schema/ipc/shapes.js +14 -72
- package/dist/lib/modules/core/schema/ipc/surfaces.js +12 -195
- package/dist/lib/modules/core/schema/ipc/tasks.js +31 -0
- package/dist/lib/modules/core/schema/ipc.js +18 -44
- package/dist/lib/modules/core/testing.js +1 -0
- package/dist/lib/modules/core/version.js +1 -1
- package/dist/lib/modules/ipc/client.js +10 -2
- package/dist/lib/modules/ipc/server.js +14 -5
- package/dist/lib/modules/ipc/testing.js +13 -23
- package/dist/spec/messaging-v1.md +835 -380
- package/dist/third-party-notices.md +1 -1
- package/dist/third-party.json +1 -1
- package/dist/types/core/agent/operations.d.ts +5 -52
- package/dist/types/core/agent/present.d.ts +6 -21
- package/dist/types/core/agent-tools.d.ts +277 -438
- package/dist/types/core/conversation/context.d.ts +56 -0
- package/dist/types/core/conversation/event.d.ts +36 -0
- package/dist/types/core/conversation/guest-invitation.d.ts +8 -0
- package/dist/types/core/conversation/ids.d.ts +18 -0
- package/dist/types/core/conversation/link.d.ts +16 -0
- package/dist/types/core/conversation/log.d.ts +157 -0
- package/dist/types/core/conversation/ratchet.d.ts +95 -0
- package/dist/types/core/conversation/request-log.d.ts +24 -0
- package/dist/types/core/conversation/request-seal.d.ts +6 -0
- package/dist/types/core/conversation/requests.d.ts +14 -0
- package/dist/types/core/conversation/seal.d.ts +70 -0
- package/dist/types/core/conversation-agent-tools.d.ts +918 -0
- package/dist/types/core/federation/directory-proof.d.ts +8 -0
- package/dist/types/core/federation/proof-state.d.ts +114 -0
- package/dist/types/core/federation/roster-proof.d.ts +7 -0
- package/dist/types/core/index.d.ts +15 -3
- package/dist/types/core/jelly-host.d.ts +1 -1
- package/dist/types/core/schema/common.d.ts +85 -4
- package/dist/types/core/schema/consent.d.ts +7 -7
- package/dist/types/core/schema/contact-request.d.ts +264 -0
- package/dist/types/core/schema/conv-frames.d.ts +1200 -0
- package/dist/types/core/schema/conversation.d.ts +2803 -0
- package/dist/types/core/schema/envelope.d.ts +89 -130
- package/dist/types/core/schema/errors.d.ts +40 -0
- package/dist/types/core/schema/guest-invitation.d.ts +51 -0
- package/dist/types/core/schema/index.d.ts +6 -0
- package/dist/types/core/schema/ipc/consent.d.ts +89 -49
- package/dist/types/core/schema/ipc/conversation-views.d.ts +1023 -0
- package/dist/types/core/schema/ipc/conversations.d.ts +3987 -0
- package/dist/types/core/schema/ipc/device.d.ts +10 -0
- package/dist/types/core/schema/ipc/jelly.d.ts +2 -105
- package/dist/types/core/schema/ipc/shapes.d.ts +16 -95
- package/dist/types/core/schema/ipc/surfaces.d.ts +28 -424
- package/dist/types/core/schema/ipc/tasks.d.ts +208 -0
- package/dist/types/core/schema/ipc.d.ts +2684 -523
- package/dist/types/core/schema/relay.d.ts +20 -4
- package/dist/types/core/testing.d.ts +1 -0
- package/dist/types/core/version.d.ts +1 -1
- package/dist/types/ipc/client.d.ts +4 -1
- package/dist/types/ipc/server.d.ts +10 -1
- package/dist/types/ipc/testing.d.ts +21 -8
- package/package.json +1 -1
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversations (spec §16): one log per conversation, sequenced by the relay, hash-chained and signed per event;
|
|
3
|
+
* sender keys for message bodies; pairwise sealed key shares; a person's `self` conversation; and the person's
|
|
4
|
+
* context rules for their agents. Every object is strict: an unknown member fails the schema.
|
|
5
|
+
*
|
|
6
|
+
* sig = Ed25519(by.device, DOMAIN.convEvent + JCS(event without sig))
|
|
7
|
+
* h(0) = SHA-256(DOMAIN.convGenesis + JCS(cid))
|
|
8
|
+
* h(seq) = SHA-256(h(seq − 1) ‖ SHA-256(JCS(event))), over the raw 32-byte digests
|
|
9
|
+
* dm id = "d_" + base32lower(SHA-256(DOMAIN.dm + JCS([a, b] sorted)))[0..26]
|
|
10
|
+
* self id= "s_" + base32lower(SHA-256(DOMAIN.self + JCS(person)))[0..26]
|
|
11
|
+
*
|
|
12
|
+
* The authority rules over these shapes are `ConvLog.apply`'s (spec §16.7).
|
|
13
|
+
*/
|
|
14
|
+
import { z } from "../../vendor/zod/index.js";
|
|
15
|
+
import { Message, TaskArtifactUpdateEvent, TaskStatusUpdateEvent, Part } from "./a2a.js";
|
|
16
|
+
import { Base64Url, ChannelName, EncKey, Hash, Key32, LIMITS, MachineId, Millis, PersonId, PROTOCOL, SessionName, Ulid } from "./common.js";
|
|
17
|
+
import { DelegationChain, DelegationGrant } from "./delegation.js";
|
|
18
|
+
import { RealmId } from "./realm.js";
|
|
19
|
+
import { ActivationProof } from "./contact-request.js";
|
|
20
|
+
import { GuestInvitation } from "./guest-invitation.js";
|
|
21
|
+
// ---- identifiers (spec §16.1) ----
|
|
22
|
+
/** A person, or one agent of a person (`person/agent`). Every agent belongs to one person. */
|
|
23
|
+
export const ParticipantId = z
|
|
24
|
+
.string()
|
|
25
|
+
.regex(/^(?:[a-z][a-z0-9-]{0,31}|p_[a-z2-7]{26})(?:\/[a-z0-9][a-z0-9._-]{0,63})?$/, "person or person/agent");
|
|
26
|
+
/** `d_…` a dm (derived), `s_…` a person's `self` conversation (derived), `c_` + ULID a group or channel. */
|
|
27
|
+
export const ConvId = z.string().regex(/^(?:[ds]_[a-z2-7]{26}|c_[0-7][0-9A-HJKMNP-TV-Z]{25})$/, "conversation id");
|
|
28
|
+
export const ConvKind = z.enum(["dm", "group", "channel", "self"]);
|
|
29
|
+
/** Channels only; every other kind is private. */
|
|
30
|
+
export const Visibility = z.enum(["public", "private"]);
|
|
31
|
+
/** What a late joiner may read: `shared` gets earlier chain keys, `none` reads from its add onwards. */
|
|
32
|
+
export const HistoryPolicy = z.enum(["none", "shared"]);
|
|
33
|
+
export const ConvRole = z.enum(["owner", "admin", "member", "guest"]);
|
|
34
|
+
/** Who may do a thing in a group or channel. */
|
|
35
|
+
export const Permit = z.enum(["all", "admins"]);
|
|
36
|
+
/** Which mentions wake a seated agent: its own person's, any member's (under consent), or none. */
|
|
37
|
+
export const RespondTo = z.enum(["self", "members", "none"]);
|
|
38
|
+
/** A conversation's policy for members' agents mining it as context (spec §16.8). */
|
|
39
|
+
export const AgentContext = z.enum(["members", "seats", "none"]);
|
|
40
|
+
/** Why a sender epoch ends. */
|
|
41
|
+
export const RekeyCause = z.enum(["removed", "device", "added", "settings", "schedule"]);
|
|
42
|
+
const Seq = z.number().int().min(0);
|
|
43
|
+
const RetainDays = z.number().int().min(1).max(LIMITS.retainDaysMax);
|
|
44
|
+
/** A conversation event's signature: 64 bytes. */
|
|
45
|
+
const Sig = z.string().regex(/^[A-Za-z0-9_-]{86}$/, "Ed25519 signature, base64url");
|
|
46
|
+
/** An AES-GCM nonce: 12 bytes. */
|
|
47
|
+
const Nonce = z.string().regex(/^[A-Za-z0-9_-]{16}$/, "12-byte nonce, base64url");
|
|
48
|
+
const isAgent = (p) => p.includes("/");
|
|
49
|
+
// ---- linked channels (spec §16.10; reserved: validated, no daemon behaviour yet) ----
|
|
50
|
+
/** An outside channel a person may link as an alias of themselves. Telegram first, then Slack. */
|
|
51
|
+
export const LinkChannel = z.enum(["telegram", "slack"]);
|
|
52
|
+
/** What a linked channel may do: Jelly's `chat` and `view` action classes (spec §8.11), never more. */
|
|
53
|
+
export const LinkAuthority = z.enum(["chat", "view"]);
|
|
54
|
+
/** Who may mirror a conversation to a linked channel: nobody (the default), its owners and admins, or any member. */
|
|
55
|
+
export const BridgePolicy = z.enum(["none", "admins", "members"]);
|
|
56
|
+
/** The kinds of a `self` conversation's `setting` op: the person's settings, which never expire (spec §16.3). */
|
|
57
|
+
export const SettingKind = z.enum(["prefs", "grant", "context", "localRetention", "link"]);
|
|
58
|
+
/** A sealed op content: base64url(nonce ‖ AES-256-GCM ciphertext ‖ tag), at most a share's size. */
|
|
59
|
+
const SealedState = Base64Url.min(38).max(Math.ceil(((LIMITS.convPlaintextBytes + 28) * 4) / 3));
|
|
60
|
+
// ---- ops (spec §16.2, "Ops") ----
|
|
61
|
+
/** A participant record as an op states it. `device` and `respondTo` are for agent participants only. */
|
|
62
|
+
export const Seat = z
|
|
63
|
+
.strictObject({ p: ParticipantId, role: ConvRole, device: MachineId.optional(), respondTo: RespondTo.optional() })
|
|
64
|
+
.refine((s) => isAgent(s.p) || (s.device === undefined && s.respondTo === undefined), { message: "device and respondTo are for agents only" });
|
|
65
|
+
export const ConvOp = z.discriminatedUnion("op", [
|
|
66
|
+
z.strictObject({
|
|
67
|
+
op: z.literal("create"),
|
|
68
|
+
kind: ConvKind,
|
|
69
|
+
visibility: Visibility.optional(),
|
|
70
|
+
history: HistoryPolicy,
|
|
71
|
+
retainDays: RetainDays,
|
|
72
|
+
/** A channel: the org it is open to when public. */
|
|
73
|
+
org: z.string().min(1).max(128).optional(),
|
|
74
|
+
participants: z.array(Seat).min(1).max(LIMITS.convParticipants),
|
|
75
|
+
/** Absent: `all`. */
|
|
76
|
+
post: Permit.optional(),
|
|
77
|
+
invite: Permit.optional(),
|
|
78
|
+
pins: Permit.optional(),
|
|
79
|
+
/** JCS(`ConvMeta`) sealed under the creator's epoch-0 message key 0 (spec §16.5). */
|
|
80
|
+
sealedMeta: Base64Url.max(2048).optional(),
|
|
81
|
+
/** External human DM: no content or shares until the permanent activation proof. */
|
|
82
|
+
admission: z.literal("request").optional(),
|
|
83
|
+
}),
|
|
84
|
+
z.strictObject({ op: z.literal("add"), participants: z.array(Seat).min(1).max(LIMITS.convOpParticipants) }),
|
|
85
|
+
z.strictObject({ op: z.literal("accept"), invitation: GuestInvitation.optional() }),
|
|
86
|
+
z.strictObject({ op: z.literal("activate"), proof: ActivationProof }),
|
|
87
|
+
z.strictObject({ op: z.literal("request.complete"), request: Ulid, seq: z.number().int().min(2), digest: Hash }),
|
|
88
|
+
z.strictObject({ op: z.literal("join") }),
|
|
89
|
+
z.strictObject({ op: z.literal("leave") }),
|
|
90
|
+
z.strictObject({ op: z.literal("remove"), participants: z.array(ParticipantId).min(1).max(LIMITS.convOpParticipants) }),
|
|
91
|
+
z.strictObject({ op: z.literal("role"), p: ParticipantId, role: ConvRole }),
|
|
92
|
+
z.strictObject({ op: z.literal("seat"), p: ParticipantId, device: MachineId.optional(), respondTo: RespondTo.optional() }),
|
|
93
|
+
z.strictObject({
|
|
94
|
+
op: z.literal("settings"),
|
|
95
|
+
visibility: Visibility.optional(),
|
|
96
|
+
history: HistoryPolicy.optional(),
|
|
97
|
+
retainDays: RetainDays.optional(),
|
|
98
|
+
post: Permit.optional(),
|
|
99
|
+
invite: Permit.optional(),
|
|
100
|
+
pins: Permit.optional(),
|
|
101
|
+
}),
|
|
102
|
+
z.strictObject({ op: z.literal("archive"), on: z.boolean() }),
|
|
103
|
+
z.strictObject({ op: z.literal("rekey"), epoch: z.number().int().min(1), cause: RekeyCause, device: MachineId.optional(), until: Seq.optional() })
|
|
104
|
+
.refine((op) => op.cause === "device" ? op.device !== undefined : op.device === undefined && op.until === undefined, { message: "only a device rekey names a device and optional cutoff" }),
|
|
105
|
+
/**
|
|
106
|
+
* The agent-context policy and the linked-channel `bridges` policy (spec §16.8, §16.10), in plaintext: an op, so it
|
|
107
|
+
* never expires, and a joiner who holds no earlier keys still knows it. In a dm or a group, the author's vote.
|
|
108
|
+
*/
|
|
109
|
+
z
|
|
110
|
+
.strictObject({ op: z.literal("policy"), agentContext: AgentContext.optional(), bridges: BridgePolicy.optional() })
|
|
111
|
+
.refine((p) => p.agentContext !== undefined || p.bridges !== undefined, { message: "a policy sets agentContext, bridges or both" }),
|
|
112
|
+
/** A group's or channel's name, topic and purpose: JCS(`ConvMeta`) sealed under the author's key (`epoch`, `idx`). */
|
|
113
|
+
z.strictObject({ op: z.literal("meta"), epoch: Seq, idx: Seq, sealed: SealedState }),
|
|
114
|
+
/** A setting of the person's `self` conversation: JCS(`SettingBody` of `kind`) sealed under (`epoch`, `idx`). */
|
|
115
|
+
z.strictObject({ op: z.literal("setting"), kind: SettingKind, epoch: Seq, idx: Seq, sealed: SealedState }),
|
|
116
|
+
]);
|
|
117
|
+
/** The plaintext of `create.sealedMeta` and of a `meta` body's fields. */
|
|
118
|
+
export const ConvMeta = z.strictObject({
|
|
119
|
+
name: ChannelName.optional(),
|
|
120
|
+
topic: z.string().max(250).optional(),
|
|
121
|
+
purpose: z.string().max(250).optional(),
|
|
122
|
+
});
|
|
123
|
+
// ---- message bodies (spec §16.3) ----
|
|
124
|
+
export const Mention = z.union([
|
|
125
|
+
z.strictObject({ p: ParticipantId }),
|
|
126
|
+
/** Every participant person. Never wakes an agent. */
|
|
127
|
+
z.strictObject({ all: z.literal("channel") }),
|
|
128
|
+
/** Every participant person online now. Never wakes an agent. */
|
|
129
|
+
z.strictObject({ all: z.literal("here") }),
|
|
130
|
+
]);
|
|
131
|
+
/** A file in the relay's blob store, sealed under its own AES-256-GCM key. The shape only; files land later. */
|
|
132
|
+
export const Attachment = z.strictObject({
|
|
133
|
+
blob: z.string().regex(/^b_[A-Za-z0-9_-]{43}$/, "blob id"),
|
|
134
|
+
key: Key32,
|
|
135
|
+
nonce: Nonce,
|
|
136
|
+
sha256: Hash,
|
|
137
|
+
bytes: z.number().int().positive().max(LIMITS.blobBytes),
|
|
138
|
+
mime: z.string().max(100),
|
|
139
|
+
name: z.string().max(255),
|
|
140
|
+
thumb: Base64Url.max(16_384).optional(),
|
|
141
|
+
});
|
|
142
|
+
/** A line's provenance when its person wrote it from a linked channel. Signed and sealed with the event. */
|
|
143
|
+
export const Via = z.strictObject({ channel: LinkChannel });
|
|
144
|
+
/**
|
|
145
|
+
* A person's linked channel, as a human-tier device records it in the person's `self` conversation (never the
|
|
146
|
+
* device log, which is public). `account` is the channel's own id for the person's account (Telegram `from.id`;
|
|
147
|
+
* Slack `team/user`); it exists only inside the sealed `self` conversation. The proof is the one-time code's digest
|
|
148
|
+
* and the six-digit code the person saw match on both sides. `revokedAt` ends it; the last writer wins by `seq`.
|
|
149
|
+
*/
|
|
150
|
+
export const LinkRecord = z.strictObject({
|
|
151
|
+
id: Ulid,
|
|
152
|
+
channel: LinkChannel,
|
|
153
|
+
account: z.string().regex(/^[A-Za-z0-9._:/-]{1,128}$/, "channel account id"),
|
|
154
|
+
/** The person's device that runs the bridge and wrote this record. */
|
|
155
|
+
host: MachineId,
|
|
156
|
+
proof: z.strictObject({ method: z.literal("code-sas"), codeDigest: Hash, sas: z.string().regex(/^[0-9]{6}$/, "six digits") }),
|
|
157
|
+
authority: z
|
|
158
|
+
.array(LinkAuthority)
|
|
159
|
+
.min(1)
|
|
160
|
+
.max(2)
|
|
161
|
+
.refine((a) => new Set(a).size === a.length, { message: "each authority once" }),
|
|
162
|
+
linkedAt: Millis,
|
|
163
|
+
revokedAt: Millis.optional(),
|
|
164
|
+
});
|
|
165
|
+
/**
|
|
166
|
+
* Who can read a piece of content, for the audience guard: a person, or a linked channel's provider as
|
|
167
|
+
* `ext:<channel>:<link id>`. Not on the wire; the guard's reader sets are typed with it.
|
|
168
|
+
*/
|
|
169
|
+
export const ReaderId = z.union([PersonId, z.string().regex(/^ext:(?:telegram|slack):[0-7][0-9A-HJKMNP-TV-Z]{25}$/, "ext:<channel>:<link id>")]);
|
|
170
|
+
export const ReaderSet = z.array(ReaderId).max(LIMITS.convParticipants + LIMITS.convParticipants);
|
|
171
|
+
export const ReceiptStage = z.enum(["delivered", "handed_to_agent", "read"]);
|
|
172
|
+
/**
|
|
173
|
+
* A task as a thread (spec §16.3): `request` is the thread root, from the requester to an agent participant;
|
|
174
|
+
* `status` and `artifact` come from that agent; `cancel` and `followUp` from the requester.
|
|
175
|
+
*/
|
|
176
|
+
export const TaskEventBody = z.discriminatedUnion("k", [
|
|
177
|
+
z.strictObject({ k: z.literal("request"), to: ParticipantId.refine(isAgent, "a task goes to an agent"), message: Message, delegation: DelegationChain }),
|
|
178
|
+
z.strictObject({ k: z.literal("status"), root: Ulid, update: TaskStatusUpdateEvent }),
|
|
179
|
+
z.strictObject({ k: z.literal("artifact"), root: Ulid, update: TaskArtifactUpdateEvent }),
|
|
180
|
+
z.strictObject({ k: z.literal("cancel"), root: Ulid, reason: z.string().max(1000).optional() }),
|
|
181
|
+
z.strictObject({ k: z.literal("followUp"), root: Ulid, message: Message }),
|
|
182
|
+
]);
|
|
183
|
+
export const ContextAccess = z.enum(["allow", "ask", "off"]);
|
|
184
|
+
export const ContextKind = z.enum(["own", "dm", "group", "channel.private", "channel.public"]);
|
|
185
|
+
export const ContextScope = z.union([z.strictObject({ cid: ConvId }), z.strictObject({ kind: ContextKind })]);
|
|
186
|
+
/** One of a person's rules for which of their agents may mine which conversations (spec §16.8). */
|
|
187
|
+
export const ContextRule = z.strictObject({
|
|
188
|
+
agent: z.union([SessionName, z.literal("*")]),
|
|
189
|
+
scope: ContextScope,
|
|
190
|
+
access: ContextAccess,
|
|
191
|
+
/** Absent: the conversation's retention. */
|
|
192
|
+
lookbackDays: RetainDays.optional(),
|
|
193
|
+
});
|
|
194
|
+
const scopeKey = (r) => `${r.agent} ${"cid" in r.scope ? `cid:${r.scope.cid}` : `kind:${r.scope.kind}`}`;
|
|
195
|
+
/** A person's whole rule set: at most one rule per (agent, scope). */
|
|
196
|
+
export const ContextRules = z
|
|
197
|
+
.array(ContextRule)
|
|
198
|
+
.max(LIMITS.contextRules)
|
|
199
|
+
.refine((rules) => new Set(rules.map(scopeKey)).size === rules.length, { message: "at most one rule per agent and scope" });
|
|
200
|
+
/** One sender chain at one index: `ck` is the chain key for message `idx` of `device`'s chain in `epoch`. */
|
|
201
|
+
export const KeyShare = z.strictObject({ epoch: Seq, device: MachineId, idx: Seq, ck: Key32 });
|
|
202
|
+
/** The plaintext of one recipient's sealed share in a `keys` event. */
|
|
203
|
+
export const KeySharePayload = z.strictObject({ shares: z.array(KeyShare).min(1).max(LIMITS.keyShares) });
|
|
204
|
+
/**
|
|
205
|
+
* The `self` conversation's passing state (spec §16.3, "The self conversation"), in `sync` message bodies: it expires
|
|
206
|
+
* with the conversation's retention. The last writer wins by `seq`.
|
|
207
|
+
*/
|
|
208
|
+
export const SyncBody = z.discriminatedUnion("s", [
|
|
209
|
+
z.strictObject({ s: z.literal("read"), cid: ConvId, upTo: Seq, thread: Ulid.optional() }),
|
|
210
|
+
z.strictObject({ s: z.literal("unread"), cid: ConvId, from: z.number().int().min(1) }),
|
|
211
|
+
z.strictObject({ s: z.literal("follow"), cid: ConvId, thread: Ulid, on: z.boolean() }),
|
|
212
|
+
z.strictObject({ s: z.literal("save"), cid: ConvId, target: Ulid, on: z.boolean() }),
|
|
213
|
+
z.strictObject({ s: z.literal("draft"), cid: ConvId, thread: Ulid.optional(), text: z.string().max(8000) }),
|
|
214
|
+
z.strictObject({ s: z.literal("joined"), cid: ConvId, on: z.boolean() }),
|
|
215
|
+
z.strictObject({ s: z.literal("keys"), cid: ConvId, shares: z.array(KeyShare).min(1).max(LIMITS.keyShares) }),
|
|
216
|
+
]);
|
|
217
|
+
/**
|
|
218
|
+
* The person's settings, each sealed in a `setting` op of their `self` conversation (spec §16.3): ops never expire,
|
|
219
|
+
* so a setting outlives the retention. `s` is the op's `kind`. The last writer wins by `seq`.
|
|
220
|
+
*/
|
|
221
|
+
export const SettingBody = z.discriminatedUnion("s", [
|
|
222
|
+
z.strictObject({
|
|
223
|
+
s: z.literal("prefs"),
|
|
224
|
+
cid: ConvId,
|
|
225
|
+
notify: z.enum(["all", "mentions", "none"]),
|
|
226
|
+
muteUntil: Millis.optional(),
|
|
227
|
+
/** dms only; absent is off. */
|
|
228
|
+
receipts: z.boolean().optional(),
|
|
229
|
+
}),
|
|
230
|
+
/** A consent grant (spec §6.9), verified with `verifyConsentGrant` over this raw value. */
|
|
231
|
+
z.strictObject({ s: z.literal("grant"), grant: z.record(z.string(), z.unknown()) }),
|
|
232
|
+
z.strictObject({ s: z.literal("context"), rules: ContextRules }),
|
|
233
|
+
z.strictObject({ s: z.literal("localRetention"), cid: ConvId, days: RetainDays }),
|
|
234
|
+
z.strictObject({ s: z.literal("link"), link: LinkRecord }),
|
|
235
|
+
]);
|
|
236
|
+
/** The plaintext of a `msg` event. */
|
|
237
|
+
export const MsgBody = z.discriminatedUnion("t", [
|
|
238
|
+
z.strictObject({
|
|
239
|
+
t: z.literal("line"),
|
|
240
|
+
text: z.string().max(LIMITS.lineChars),
|
|
241
|
+
format: z.enum(["plain", "md"]),
|
|
242
|
+
/** The thread root's event id. */
|
|
243
|
+
thread: Ulid.optional(),
|
|
244
|
+
/** A thread reply also shown in the main view. */
|
|
245
|
+
broadcast: z.boolean().optional(),
|
|
246
|
+
mentions: z.array(Mention).max(LIMITS.mentions).optional(),
|
|
247
|
+
files: z.array(Attachment).max(LIMITS.files).optional(),
|
|
248
|
+
/** A2A data parts, rendered as cards. */
|
|
249
|
+
parts: z.array(Part).max(8).optional(),
|
|
250
|
+
/** The mention this line answers. */
|
|
251
|
+
answers: Ulid.optional(),
|
|
252
|
+
grants: z.array(DelegationGrant).max(LIMITS.agentMentions).optional(),
|
|
253
|
+
/** Written by its person from a linked channel. Never on an agent's line. */
|
|
254
|
+
via: Via.optional(),
|
|
255
|
+
}),
|
|
256
|
+
z.strictObject({ t: z.literal("edit"), target: Ulid, text: z.string().max(LIMITS.lineChars), mentions: z.array(Mention).max(LIMITS.mentions).optional() }),
|
|
257
|
+
z.strictObject({ t: z.literal("delete"), target: Ulid }),
|
|
258
|
+
z.strictObject({ t: z.literal("react"), target: Ulid, key: z.string().min(1).max(64), on: z.boolean() }),
|
|
259
|
+
z.strictObject({ t: z.literal("pin"), target: Ulid, on: z.boolean() }),
|
|
260
|
+
z.strictObject({ t: z.literal("task"), task: TaskEventBody }),
|
|
261
|
+
z.strictObject({ t: z.literal("receipt"), target: Ulid, stage: ReceiptStage }),
|
|
262
|
+
z.strictObject({ t: z.literal("sync"), sync: SyncBody }),
|
|
263
|
+
]);
|
|
264
|
+
// ---- events and the log (spec §16.2) ----
|
|
265
|
+
const Position = z.strictObject({ seq: Seq, hash: Hash });
|
|
266
|
+
const header = {
|
|
267
|
+
v: z.literal(PROTOCOL),
|
|
268
|
+
realm: RealmId,
|
|
269
|
+
cid: ConvId,
|
|
270
|
+
/** Chosen by the author; unique in the conversation. */
|
|
271
|
+
id: Ulid,
|
|
272
|
+
by: z.strictObject({ person: PersonId, device: MachineId, agent: SessionName.optional() }),
|
|
273
|
+
ts: Millis,
|
|
274
|
+
/** The last op the author applied: its seq and h(seq), or {0, h(0)} before any. */
|
|
275
|
+
opHead: Position,
|
|
276
|
+
/** The head the author had, for gossip. */
|
|
277
|
+
seen: z.strictObject({ seq: z.number().int().min(1), hash: Hash }).optional(),
|
|
278
|
+
};
|
|
279
|
+
/** A control op, in plaintext: the relay routes and checks authority by it. */
|
|
280
|
+
export const ConvOpEvent = z.strictObject({ ...header, class: z.literal("op"), body: ConvOp, sig: Sig });
|
|
281
|
+
/** base64url(nonce ‖ AES-256-GCM ciphertext ‖ tag) of JCS(`MsgBody`), sealed under one message key. */
|
|
282
|
+
export const SealedBody = Base64Url.min(38).max(Math.ceil(((LIMITS.convPlaintextBytes + 28) * 4) / 3));
|
|
283
|
+
/** Everything else, sealed under the author device's sender key at (`epoch`, `idx`). */
|
|
284
|
+
export const ConvMsgEvent = z.strictObject({
|
|
285
|
+
...header,
|
|
286
|
+
class: z.literal("msg"),
|
|
287
|
+
epoch: Seq,
|
|
288
|
+
idx: Seq,
|
|
289
|
+
/** Own-person wake hint only: sessions of `by.person` that this event may wake. */
|
|
290
|
+
wake: z.array(SessionName).min(1).max(LIMITS.wakeHints).optional(),
|
|
291
|
+
/** The accepted request reservation. Only a sealed request.import can use it. */
|
|
292
|
+
request: Ulid.optional(),
|
|
293
|
+
body: SealedBody,
|
|
294
|
+
sig: Sig,
|
|
295
|
+
});
|
|
296
|
+
/** One recipient device's share, sealed to it under the envelope seal (spec §5, "Sealing"). */
|
|
297
|
+
export const SealedShare = z.strictObject({
|
|
298
|
+
to: z.strictObject({ person: PersonId, device: MachineId }),
|
|
299
|
+
epk: EncKey,
|
|
300
|
+
nonce: Nonce,
|
|
301
|
+
ct: Base64Url.max(Math.ceil(((LIMITS.keyShares * 160 + 16) * 4) / 3)),
|
|
302
|
+
});
|
|
303
|
+
/** A `keys` event's body: one share, to one recipient device. */
|
|
304
|
+
export const KeysBody = SealedShare;
|
|
305
|
+
/**
|
|
306
|
+
* A sender-key or history share to one recipient device (spec §16.6). It is signed like every event but is not an
|
|
307
|
+
* entry of the log: the relay keeps it for its recipient alone and serves it to no one else (`ConvShareEntry`).
|
|
308
|
+
*/
|
|
309
|
+
export const ConvKeysEvent = z.strictObject({ ...header, class: z.literal("keys"), body: KeysBody, sig: Sig });
|
|
310
|
+
/** The events of the log: ops and messages. */
|
|
311
|
+
export const ConvEvent = z.discriminatedUnion("class", [ConvOpEvent, ConvMsgEvent]);
|
|
312
|
+
/** Every signed conversation event: the log's, and key shares. */
|
|
313
|
+
export const ConvAnyEvent = z.discriminatedUnion("class", [ConvOpEvent, ConvMsgEvent, ConvKeysEvent]);
|
|
314
|
+
/** An event as the relay stores and serves it. The relay does not sign it; members verify the chain. */
|
|
315
|
+
export const ConvLogEntry = z.strictObject({ seq: z.number().int().min(1), prev: Hash, event: ConvEvent });
|
|
316
|
+
/**
|
|
317
|
+
* A `msg` entry whose body the relay pruned after the retention (spec §16.4): its seq, its prev and the digest
|
|
318
|
+
* SHA-256(JCS(event)), so the chain stays verifiable (`ConvLog.applyExpired`). An op is never served this way.
|
|
319
|
+
*/
|
|
320
|
+
export const ConvExpiredEntry = z.strictObject({ seq: z.number().int().min(2), prev: Hash, digest: Hash });
|
|
321
|
+
/** What `conv.fetch` serves for one seq: the entry, or its digest once expired. */
|
|
322
|
+
export const ConvServedEntry = z.union([ConvLogEntry, ConvExpiredEntry]);
|
|
323
|
+
/** A key share as the relay serves it to its recipient: `n` orders the recipient's shares of one conversation. */
|
|
324
|
+
export const ConvShareEntry = z.strictObject({ n: z.number().int().min(1), event: ConvKeysEvent });
|
|
325
|
+
/** A log position: a seq and h(seq). */
|
|
326
|
+
export const ConvHead = Position;
|
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { z } from "../../vendor/zod/index.js";
|
|
7
7
|
import { Message, TaskArtifactUpdateEvent, TaskStatusUpdateEvent } from "./a2a.js";
|
|
8
|
-
import { AgentKind, Base64Url,
|
|
8
|
+
import { AgentKind, Base64Url, DeliveryTier, MachineId, MachineLabel, Millis, PersonId, PROTOCOL, SessionAddress, SessionName, SessionState, Ulid, LIMITS, } from "./common.js";
|
|
9
9
|
import { CardData, CardGetData, GrantSync } from "./consent.js";
|
|
10
10
|
import { DelegationChain } from "./delegation.js";
|
|
11
11
|
import { ErrorCode } from "./errors.js";
|
|
12
12
|
import { Anchor, RealmId } from "./realm.js";
|
|
13
|
+
import { PortableRequest } from "./contact-request.js";
|
|
13
14
|
export const EnvelopeType = z.enum(["message", "task.status", "task.artifact", "task.cancel", "receipt", "presence"]);
|
|
14
15
|
export const SEAL_ALG = "x25519-hkdf-sha256-a256gcm";
|
|
15
16
|
/**
|
|
@@ -62,22 +63,6 @@ export const Envelope = z.object({
|
|
|
62
63
|
sig: Base64Url,
|
|
63
64
|
});
|
|
64
65
|
// ---- Decrypted body payloads, by envelope type ----
|
|
65
|
-
/** A channel's persons: roster person names, or hosted `p_…` ids in a directory realm (spec §6.8). */
|
|
66
|
-
const ChannelPersons = z.array(PersonId).min(1).max(LIMITS.channelPersons);
|
|
67
|
-
/**
|
|
68
|
-
* A channel change or post (spec §6.8), on a `kind: "message"` envelope sealed to a device with no recipient agent.
|
|
69
|
-
* `create`: the full channel record with the person who created it, to every member device and to each later
|
|
70
|
-
* invitee. `invite`: persons added, to the existing members. `leave`: the sender left. `remove`: persons removed by
|
|
71
|
-
* the creator or an admin, to every member device. `post`: a line in the channel. `name` is always the channel's own
|
|
72
|
-
* name. A DM line has no `conv` (spec §5, "Meaning").
|
|
73
|
-
*/
|
|
74
|
-
export const ChannelConv = z.discriminatedUnion("op", [
|
|
75
|
-
z.object({ kind: z.literal("channel"), op: z.literal("create"), id: Ulid, name: ChannelName, persons: ChannelPersons, creator: PersonId }),
|
|
76
|
-
z.object({ kind: z.literal("channel"), op: z.literal("invite"), id: Ulid, name: ChannelName, persons: ChannelPersons }),
|
|
77
|
-
z.object({ kind: z.literal("channel"), op: z.literal("leave"), id: Ulid, name: ChannelName }),
|
|
78
|
-
z.object({ kind: z.literal("channel"), op: z.literal("remove"), id: Ulid, name: ChannelName, persons: ChannelPersons }),
|
|
79
|
-
z.object({ kind: z.literal("channel"), op: z.literal("post"), id: Ulid, name: ChannelName }),
|
|
80
|
-
]);
|
|
81
66
|
/**
|
|
82
67
|
* Sync between two devices of one person (spec §6.9, "Sync"): `grant` carries grants the person set or re-signed,
|
|
83
68
|
* `snapshot` every current grant, for a device of the person that is new to the sender. The message carries exactly
|
|
@@ -88,14 +73,14 @@ export const SelfConv = z.object({ kind: z.literal("self"), what: z.enum(["grant
|
|
|
88
73
|
* A message request (spec §6.9, "Message requests"), on an envelope of class `request` from a person to a person:
|
|
89
74
|
* its message carries exactly one text part of at most `LIMITS.requestChars`.
|
|
90
75
|
*/
|
|
91
|
-
export const RequestConv = z.object({ kind: z.literal("request") });
|
|
76
|
+
export const RequestConv = z.object({ kind: z.literal("request"), proof: PortableRequest.optional() });
|
|
92
77
|
/**
|
|
93
78
|
* A contact card between two persons (spec §6.9, "Contact cards"): `card` carries one data part `{card}`, the signed
|
|
94
79
|
* card; `get` asks for the receiver's card and carries one data part `{get: true}`.
|
|
95
80
|
*/
|
|
96
81
|
export const CardConv = z.object({ kind: z.literal("card"), what: z.enum(["card", "get"]) });
|
|
97
82
|
/** The conversation a `message` belongs to when its ends alone do not say (spec §5, "Meaning"). */
|
|
98
|
-
export const Conv = z.discriminatedUnion("kind", [
|
|
83
|
+
export const Conv = z.discriminatedUnion("kind", [SelfConv, RequestConv, CardConv]);
|
|
99
84
|
/**
|
|
100
85
|
* type "message". `kind: "task"` asks the recipient to run `message` as a new A2A task;
|
|
101
86
|
* the sender assigns `message.taskId` (a ULID) and `message.contextId`.
|
|
@@ -109,11 +94,7 @@ export const MessagePayload = z
|
|
|
109
94
|
wake: z.boolean().optional(),
|
|
110
95
|
/** Required on `kind: "task"`, absent otherwise: the signed chain from the root requester to this hop (spec §6.7). Receivers run `verifyChain`. */
|
|
111
96
|
delegation: DelegationChain.optional(),
|
|
112
|
-
/**
|
|
113
|
-
* The conversation, when the ends do not say it (spec §5, "Meaning"): a channel change or post (§6.8).
|
|
114
|
-
* `message.messageId` is a ULID shared by every copy; `message.contextId`, when set, is the ULID `messageId` of
|
|
115
|
-
* the thread's first line.
|
|
116
|
-
*/
|
|
97
|
+
/** Device-addressed consent sync, portable contact request or contact card. */
|
|
117
98
|
conv: Conv.optional(),
|
|
118
99
|
})
|
|
119
100
|
.refine((p) => (p.kind === "task" ? p.message.taskId !== undefined : true), { message: "a task message needs message.taskId" })
|
|
@@ -123,6 +104,9 @@ export const MessagePayload = z
|
|
|
123
104
|
})
|
|
124
105
|
.refine((p) => p.conv?.kind !== "request" || requestMessage(p.message), {
|
|
125
106
|
message: `a message request carries one text part of 1 to ${LIMITS.requestChars} characters, and nothing else`,
|
|
107
|
+
})
|
|
108
|
+
.refine((p) => p.conv?.kind !== "request" || p.conv.proof === undefined || (p.message.messageId === p.conv.proof.descriptor.id && p.message.parts[0]?.text === p.conv.proof.opening.text), {
|
|
109
|
+
message: "a portable request's message id and text match its signed opening",
|
|
126
110
|
})
|
|
127
111
|
.refine((p) => p.conv?.kind !== "card" || cardMessage(p.message, p.conv.what), {
|
|
128
112
|
message: "a card message carries one data part, the card or the request for one, and nothing else",
|
|
@@ -11,6 +11,7 @@ export const ErrorCode = z.enum([
|
|
|
11
11
|
"refused", // recipient policy refuses this sender
|
|
12
12
|
"declined", // the recipient's person declined a held request (spec §6.9)
|
|
13
13
|
"request_required", // the recipient's person has not accepted this sender: a message request comes first (spec §6.9)
|
|
14
|
+
"peer_outdated", // the other person's devices run a Messaging from before conversations: they need to update first
|
|
14
15
|
"rate_limited", // too many envelopes; retry later
|
|
15
16
|
"loop_guard", // agent-to-agent loop guard tripped
|
|
16
17
|
"too_large", // size limit exceeded
|
|
@@ -24,10 +25,12 @@ export const ErrorCode = z.enum([
|
|
|
24
25
|
"guarded_turn", // a guarded peer turn tried to send outside its allowance (client-side; never on the wire)
|
|
25
26
|
"roster_conflict", // roster entry does not extend the current head; resync and retry
|
|
26
27
|
"invite_invalid", // unknown, expired, revoked, used or mismatched invite
|
|
27
|
-
"no_identity", // daemon has no
|
|
28
|
+
"no_identity", // daemon has no usable team identity or bound directory device with published keys
|
|
28
29
|
"binding_invalid", // a device binding, log entry or its authority failed verification (hosted realms)
|
|
29
|
-
"jelly_unavailable", // Jelly ID
|
|
30
|
+
"jelly_unavailable", // Jelly ID unreachable or an unusable response for keys or sign-in; never reported as invalid
|
|
30
31
|
"log_conflict", // a device-log entry does not extend the log's head; resync and retry
|
|
32
|
+
"conv_conflict", // a conversation event does not extend the log, or its op head is not the current one; re-read and retry
|
|
33
|
+
"stale_epoch", // a conversation message is sealed under an epoch that is not the current one, or a rekey is due; re-seal and retry
|
|
31
34
|
"device_limit", // the person already has LIMITS.devicesPerPerson current devices
|
|
32
35
|
"admission_required", // dir.bind without a Jelly-vouched bind of the connecting device
|
|
33
36
|
"admission_invalid", // dir.bind whose Jelly token is not live, or not for this directory's issuer and resource
|
|
@@ -42,8 +45,23 @@ export const ErrorCode = z.enum([
|
|
|
42
45
|
"ticket_invalid", // deployment "jelly": the session ticket did not verify, expired, or names another session or agent
|
|
43
46
|
"daemon_unavailable", // client could not reach the local daemon socket
|
|
44
47
|
"daemon_mismatch", // the daemon on the socket is another version or IPC protocol than the client (spec §8.2)
|
|
48
|
+
"store_refused", // messagingd refused the home's store and changed nothing; StoreRefusedError.reason says why: pre-reset or newer (spec §8.8)
|
|
45
49
|
"internal", // unexpected failure; details are logged locally, never sent
|
|
46
50
|
]);
|
|
51
|
+
/**
|
|
52
|
+
* The exit status of a messagingd or messaging-relay that refuses its store and changes nothing (spec §8.8): `EX_CONFIG`
|
|
53
|
+
* of sysexits(3), for either reason. messagingd also prints one stderr line,
|
|
54
|
+
* `messagingd: store_refused: <reason>: <why and the remedy>`. A client matches this status, the code and the
|
|
55
|
+
* reason, never the wording.
|
|
56
|
+
*/
|
|
57
|
+
export const STORE_REFUSED_EXIT = 78;
|
|
58
|
+
/**
|
|
59
|
+
* Why a store is refused (spec §8.8):
|
|
60
|
+
* - `pre-reset`: it records no store generation, or another one. Starting over (moving the home aside) is the remedy.
|
|
61
|
+
* - `newer`: its `min_reader` is above this reader's schema: a newer messagingd wrote it. Upgrading is the remedy;
|
|
62
|
+
* moving the home aside would set a newer home aside.
|
|
63
|
+
*/
|
|
64
|
+
export const StoreRefusedReason = z.enum(["pre-reset", "newer"]);
|
|
47
65
|
export const ErrorBody = z.object({
|
|
48
66
|
code: ErrorCode,
|
|
49
67
|
message: z.string().max(500),
|
|
@@ -60,6 +78,28 @@ export class MessagingError extends Error {
|
|
|
60
78
|
return { code: this.code, message: this.message.slice(0, 500) };
|
|
61
79
|
}
|
|
62
80
|
}
|
|
81
|
+
/** `store_refused`, with its reason as a field: clients branch on `reason`, never on the message. */
|
|
82
|
+
export class StoreRefusedError extends MessagingError {
|
|
83
|
+
reason;
|
|
84
|
+
constructor(reason, message, options) {
|
|
85
|
+
super("store_refused", message, options);
|
|
86
|
+
this.name = "StoreRefusedError";
|
|
87
|
+
this.reason = reason;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** The stderr line of a messagingd that refuses its store: `messagingd: store_refused: <reason>: <text>`. */
|
|
91
|
+
export function storeRefusedLine(reason, text) {
|
|
92
|
+
return `messagingd: store_refused: ${reason}: ${text}`;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The reason on a messagingd refusal line, or undefined for any other line. Reads the tokens only; the text after
|
|
96
|
+
* them may change between releases.
|
|
97
|
+
*/
|
|
98
|
+
export function storeRefusedReason(line) {
|
|
99
|
+
const match = /^messagingd: store_refused: ([a-z-]+): /.exec(line);
|
|
100
|
+
const parsed = StoreRefusedReason.safeParse(match?.[1]);
|
|
101
|
+
return parsed.success ? parsed.data : undefined;
|
|
102
|
+
}
|
|
63
103
|
export function isMessagingError(value) {
|
|
64
104
|
return value instanceof MessagingError;
|
|
65
105
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** A bounded, sealed preview. Its signature is portable; the guest accepts exactly this op head and exposure. */
|
|
2
|
+
import { z } from "../../vendor/zod/index.js";
|
|
3
|
+
import { Hash, LIMITS, MachineId, Millis, PersonId, PROTOCOL, Ulid } from "./common.js";
|
|
4
|
+
import { RealmId } from "./realm.js";
|
|
5
|
+
export const GuestInvitation = z.strictObject({
|
|
6
|
+
v: z.literal(PROTOCOL), realm: RealmId, cid: z.string().regex(/^c_[0-7][0-9A-HJKMNP-TV-Z]{25}$/),
|
|
7
|
+
id: Ulid, to: PersonId, inviter: PersonId, by: MachineId,
|
|
8
|
+
head: z.strictObject({ seq: z.number().int().min(1), hash: Hash }), issuedAt: Millis, expiresAt: Millis,
|
|
9
|
+
kind: z.enum(["channel", "group"]), history: z.enum(["shared", "none"]),
|
|
10
|
+
agentContext: z.enum(["members", "seats", "none"]), bridges: z.enum(["members", "admins", "none"]),
|
|
11
|
+
audience: z.array(z.strictObject({ p: z.string().min(1).max(100), person: PersonId, role: z.enum(["owner", "admin", "member", "guest"]), state: z.enum(["member", "invited"]) })).min(1).max(LIMITS.convParticipants),
|
|
12
|
+
sig: z.string().regex(/^[A-Za-z0-9_-]{86}$/),
|
|
13
|
+
});
|
|
@@ -13,3 +13,9 @@ export * from "./directory-frames.js";
|
|
|
13
13
|
export * from "./ipc.js";
|
|
14
14
|
export * from "./ipc/device.js";
|
|
15
15
|
export * from "./ipc/consent.js";
|
|
16
|
+
export * from "./conversation.js";
|
|
17
|
+
export * from "./conv-frames.js";
|
|
18
|
+
export * from "./contact-request.js";
|
|
19
|
+
export * from "./guest-invitation.js";
|
|
20
|
+
export * from "./ipc/conversation-views.js";
|
|
21
|
+
export * from "./ipc/conversations.js";
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
* socket refuses them, and agents never set or read a person's grants. Spread into `IPC_METHODS`.
|
|
4
4
|
*/
|
|
5
5
|
import { z } from "../../../vendor/zod/index.js";
|
|
6
|
-
import {
|
|
6
|
+
import { LIMITS, MachineId, MachineLabel, Millis, PersonId, SessionAddress, SessionName, Ulid } from "../common.js";
|
|
7
|
+
import { ConvId, ParticipantId } from "../conversation.js";
|
|
7
8
|
import { AgentMailLevel, AskLevel, CardCan, ChatLevel } from "../consent.js";
|
|
8
9
|
import { Email } from "../directory-frames.js";
|
|
9
|
-
/** A grant's subject
|
|
10
|
-
export const GrantTarget = z.union([z.strictObject({ person: PersonId }), z.strictObject({
|
|
10
|
+
/** A grant's subject: a person, or a verified conversation id. */
|
|
11
|
+
export const GrantTarget = z.union([z.strictObject({ person: PersonId }), z.strictObject({ conv: ConvId })]);
|
|
11
12
|
const Levels = {
|
|
12
13
|
chat: ChatLevel.optional(),
|
|
13
14
|
agentMail: AgentMailLevel.optional(),
|
|
@@ -17,7 +18,7 @@ const Levels = {
|
|
|
17
18
|
};
|
|
18
19
|
/** A grant as this device holds it. A channel subject carries the channel's id and, while this person is in it, its name here. */
|
|
19
20
|
export const GrantView = z.object({
|
|
20
|
-
subject:
|
|
21
|
+
subject: GrantTarget,
|
|
21
22
|
...Levels,
|
|
22
23
|
id: Ulid,
|
|
23
24
|
ts: Millis,
|
|
@@ -61,13 +62,15 @@ export const AskRequest = z.object({
|
|
|
61
62
|
* write to them. `name`, `email` and `org` are the realm's verified profile of the sender, never the request's text.
|
|
62
63
|
*/
|
|
63
64
|
export const ContactRequestView = z.object({
|
|
64
|
-
/** The
|
|
65
|
+
/** The portable request id; old unsigned requests explicitly require a resend. */
|
|
65
66
|
id: Ulid,
|
|
66
67
|
kind: z.literal("contact"),
|
|
68
|
+
cid: ConvId.optional(),
|
|
69
|
+
state: z.enum(["pending", "answering", "accepted", "resend"]),
|
|
67
70
|
from: z.object({
|
|
68
71
|
person: PersonId,
|
|
69
72
|
device: MachineId,
|
|
70
|
-
label: MachineLabel,
|
|
73
|
+
label: MachineLabel.optional(),
|
|
71
74
|
name: z.string().max(200),
|
|
72
75
|
email: z.string().max(320).optional(),
|
|
73
76
|
org: z.enum(["same", "other", "none"]),
|
|
@@ -77,6 +80,7 @@ export const ContactRequestView = z.object({
|
|
|
77
80
|
at: Millis,
|
|
78
81
|
exp: Millis,
|
|
79
82
|
});
|
|
83
|
+
export const AgentInvitationRequest = z.strictObject({ kind: z.literal("agent-invitation"), id: Ulid, cid: ConvId, from: ParticipantId, to: ParticipantId, at: Millis });
|
|
80
84
|
/**
|
|
81
85
|
* A contact one of this person's agents asks them to make (spec §6.9, "Message requests"): `send {to: "me", request:
|
|
82
86
|
* {contact}}`. Only the person sends a message request, with `contact.request`; answering this one `decline`
|
|
@@ -91,7 +95,7 @@ export const ContactSuggestion = z.object({
|
|
|
91
95
|
at: Millis,
|
|
92
96
|
});
|
|
93
97
|
/** What `request.list` lists: asks to the person's agents, message requests, and contacts their agents suggest. */
|
|
94
|
-
export const HeldRequest = z.union([AskRequest, ContactRequestView, ContactSuggestion]);
|
|
98
|
+
export const HeldRequest = z.union([AskRequest, ContactRequestView, ContactSuggestion, AgentInvitationRequest]);
|
|
95
99
|
/**
|
|
96
100
|
* For an ask: `once` delivers it, guarded; `always` sets the sender's grant to `ask: "open"` (keeping its other
|
|
97
101
|
* levels), then delivers it, guarded. For a message request: `accept` sets the sender's grant to `chat: "open"`
|
|
@@ -152,7 +156,7 @@ export const CONSENT_METHODS = {
|
|
|
152
156
|
"request.answer.preview": {
|
|
153
157
|
params: RequestAnswerParams,
|
|
154
158
|
result: z.object({
|
|
155
|
-
kind: z.enum(["message", "task", "contact", "suggestion"]),
|
|
159
|
+
kind: z.enum(["message", "task", "contact", "suggestion", "agent-invitation"]),
|
|
156
160
|
from: z.object({ name: z.string().max(200), email: z.string().max(320).optional(), org: z.enum(["same", "other", "none"]) }),
|
|
157
161
|
agent: SessionName.optional(),
|
|
158
162
|
to: SessionName.optional(),
|
|
@@ -169,7 +173,7 @@ export const CONSENT_METHODS = {
|
|
|
169
173
|
*/
|
|
170
174
|
"contact.request": {
|
|
171
175
|
params: ContactRequestParams,
|
|
172
|
-
result: z.object({ id: Ulid,
|
|
176
|
+
result: z.object({ id: Ulid, cid: ConvId, person: PersonId }),
|
|
173
177
|
},
|
|
174
178
|
/** Whether `contact.request` would find a person for `email`; counts against the directory's lookup limits. Sends nothing. */
|
|
175
179
|
"contact.request.preview": { params: ContactRequestParams.partial({ text: true }), result: z.object({ found: z.boolean() }) },
|