@joe-broadhead/messaging 0.7.1 → 0.9.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 +364 -2
- package/dist/bin/messaging-mcp.js +5498 -4338
- package/dist/bin/messaging-relay.js +8626 -5540
- package/dist/bin/messaging.js +4492 -3158
- package/dist/bin/messagingd.js +19672 -12412
- package/dist/lib/modules/core/agent/operations.js +19 -327
- package/dist/lib/modules/core/agent/present.js +20 -55
- package/dist/lib/modules/core/agent-tools.js +35 -139
- package/dist/lib/modules/core/consent.js +54 -14
- package/dist/lib/modules/core/contact-card.js +3 -3
- package/dist/lib/modules/core/conversation/event.js +29 -4
- package/dist/lib/modules/core/conversation/guest-invitation.js +42 -0
- package/dist/lib/modules/core/conversation/log.js +287 -65
- 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 +90 -37
- 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 +9 -4
- package/dist/lib/modules/core/jelly-host.js +8 -5
- package/dist/lib/modules/core/schema/common.js +33 -5
- package/dist/lib/modules/core/schema/consent.js +23 -5
- package/dist/lib/modules/core/schema/contact-request.js +22 -0
- package/dist/lib/modules/core/schema/conv-frames.js +101 -0
- package/dist/lib/modules/core/schema/conversation.js +67 -33
- package/dist/lib/modules/core/schema/directory-frames.js +8 -3
- package/dist/lib/modules/core/schema/envelope.js +8 -24
- package/dist/lib/modules/core/schema/errors.js +7 -5
- package/dist/lib/modules/core/schema/guest-invitation.js +13 -0
- package/dist/lib/modules/core/schema/index.js +5 -0
- package/dist/lib/modules/core/schema/ipc/consent.js +97 -18
- package/dist/lib/modules/core/schema/ipc/conversation-views.js +87 -0
- package/dist/lib/modules/core/schema/ipc/conversations.js +132 -0
- package/dist/lib/modules/core/schema/ipc/device.js +9 -0
- package/dist/lib/modules/core/schema/ipc/jelly.js +17 -45
- package/dist/lib/modules/core/schema/ipc/shapes.js +74 -73
- 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 +68 -46
- package/dist/lib/modules/core/testing.js +1 -1
- 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 +854 -445
- 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 +295 -442
- package/dist/types/core/consent.d.ts +30 -7
- package/dist/types/core/contact-card.d.ts +3 -3
- package/dist/types/core/conversation/event.d.ts +6 -6
- package/dist/types/core/conversation/guest-invitation.d.ts +8 -0
- package/dist/types/core/conversation/log.d.ts +28 -13
- 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 +32 -9
- package/dist/types/core/conversation-agent-tools.d.ts +939 -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 +11 -6
- package/dist/types/core/jelly-host.d.ts +1 -1
- package/dist/types/core/schema/common.d.ts +33 -6
- package/dist/types/core/schema/consent.d.ts +29 -8
- package/dist/types/core/schema/contact-request.d.ts +264 -0
- package/dist/types/core/schema/conv-frames.d.ts +1204 -0
- package/dist/types/core/schema/conversation.d.ts +1410 -264
- package/dist/types/core/schema/directory-frames.d.ts +8 -3
- package/dist/types/core/schema/envelope.d.ts +85 -130
- package/dist/types/core/schema/errors.d.ts +5 -1
- package/dist/types/core/schema/guest-invitation.d.ts +51 -0
- package/dist/types/core/schema/index.d.ts +5 -0
- package/dist/types/core/schema/ipc/consent.d.ts +464 -62
- package/dist/types/core/schema/ipc/conversation-views.d.ts +1199 -0
- package/dist/types/core/schema/ipc/conversations.d.ts +4355 -0
- package/dist/types/core/schema/ipc/device.d.ts +10 -0
- package/dist/types/core/schema/ipc/jelly.d.ts +16 -111
- package/dist/types/core/schema/ipc/shapes.d.ts +211 -95
- package/dist/types/core/schema/ipc/surfaces.d.ts +26 -427
- package/dist/types/core/schema/ipc/tasks.d.ts +210 -0
- package/dist/types/core/schema/ipc.d.ts +3859 -890
- package/dist/types/core/schema/relay.d.ts +12 -4
- package/dist/types/core/testing.d.ts +1 -1
- 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 +16 -1
- package/dist/types/ipc/testing.d.ts +21 -8
- package/package.json +1 -1
|
@@ -16,6 +16,8 @@ import { Message, TaskArtifactUpdateEvent, TaskStatusUpdateEvent, Part } from ".
|
|
|
16
16
|
import { Base64Url, ChannelName, EncKey, Hash, Key32, LIMITS, MachineId, Millis, PersonId, PROTOCOL, SessionName, Ulid } from "./common.js";
|
|
17
17
|
import { DelegationChain, DelegationGrant } from "./delegation.js";
|
|
18
18
|
import { RealmId } from "./realm.js";
|
|
19
|
+
import { ActivationProof } from "./contact-request.js";
|
|
20
|
+
import { GuestInvitation } from "./guest-invitation.js";
|
|
19
21
|
// ---- identifiers (spec §16.1) ----
|
|
20
22
|
/** A person, or one agent of a person (`person/agent`). Every agent belongs to one person. */
|
|
21
23
|
export const ParticipantId = z
|
|
@@ -44,6 +46,17 @@ const Sig = z.string().regex(/^[A-Za-z0-9_-]{86}$/, "Ed25519 signature, base64ur
|
|
|
44
46
|
/** An AES-GCM nonce: 12 bytes. */
|
|
45
47
|
const Nonce = z.string().regex(/^[A-Za-z0-9_-]{16}$/, "12-byte nonce, base64url");
|
|
46
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));
|
|
47
60
|
// ---- ops (spec §16.2, "Ops") ----
|
|
48
61
|
/** A participant record as an op states it. `device` and `respondTo` are for agent participants only. */
|
|
49
62
|
export const Seat = z
|
|
@@ -65,9 +78,13 @@ export const ConvOp = z.discriminatedUnion("op", [
|
|
|
65
78
|
pins: Permit.optional(),
|
|
66
79
|
/** JCS(`ConvMeta`) sealed under the creator's epoch-0 message key 0 (spec §16.5). */
|
|
67
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(),
|
|
68
83
|
}),
|
|
69
84
|
z.strictObject({ op: z.literal("add"), participants: z.array(Seat).min(1).max(LIMITS.convOpParticipants) }),
|
|
70
|
-
z.strictObject({ op: z.literal("accept") }),
|
|
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 }),
|
|
71
88
|
z.strictObject({ op: z.literal("join") }),
|
|
72
89
|
z.strictObject({ op: z.literal("leave") }),
|
|
73
90
|
z.strictObject({ op: z.literal("remove"), participants: z.array(ParticipantId).min(1).max(LIMITS.convOpParticipants) }),
|
|
@@ -83,7 +100,19 @@ export const ConvOp = z.discriminatedUnion("op", [
|
|
|
83
100
|
pins: Permit.optional(),
|
|
84
101
|
}),
|
|
85
102
|
z.strictObject({ op: z.literal("archive"), on: z.boolean() }),
|
|
86
|
-
z.strictObject({ op: z.literal("rekey"), epoch: z.number().int().min(1), cause: RekeyCause })
|
|
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 }),
|
|
87
116
|
]);
|
|
88
117
|
/** The plaintext of `create.sealedMeta` and of a `meta` body's fields. */
|
|
89
118
|
export const ConvMeta = z.strictObject({
|
|
@@ -110,13 +139,6 @@ export const Attachment = z.strictObject({
|
|
|
110
139
|
name: z.string().max(255),
|
|
111
140
|
thumb: Base64Url.max(16_384).optional(),
|
|
112
141
|
});
|
|
113
|
-
// ---- linked channels (spec §16.10; reserved: validated, no daemon behaviour yet) ----
|
|
114
|
-
/** An outside channel a person may link as an alias of themselves. Telegram first, then Slack. */
|
|
115
|
-
export const LinkChannel = z.enum(["telegram", "slack"]);
|
|
116
|
-
/** What a linked channel may do: Jelly's `chat` and `view` action classes (spec §8.11), never more. */
|
|
117
|
-
export const LinkAuthority = z.enum(["chat", "view"]);
|
|
118
|
-
/** Who may mirror a conversation to a linked channel: nobody (the default), its owners and admins, or any member. */
|
|
119
|
-
export const BridgePolicy = z.enum(["none", "admins", "members"]);
|
|
120
142
|
/** A line's provenance when its person wrote it from a linked channel. Signed and sealed with the event. */
|
|
121
143
|
export const Via = z.strictObject({ channel: LinkChannel });
|
|
122
144
|
/**
|
|
@@ -179,10 +201,24 @@ export const ContextRules = z
|
|
|
179
201
|
export const KeyShare = z.strictObject({ epoch: Seq, device: MachineId, idx: Seq, ck: Key32 });
|
|
180
202
|
/** The plaintext of one recipient's sealed share in a `keys` event. */
|
|
181
203
|
export const KeySharePayload = z.strictObject({ shares: z.array(KeyShare).min(1).max(LIMITS.keyShares) });
|
|
182
|
-
/**
|
|
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
|
+
*/
|
|
183
208
|
export const SyncBody = z.discriminatedUnion("s", [
|
|
184
209
|
z.strictObject({ s: z.literal("read"), cid: ConvId, upTo: Seq, thread: Ulid.optional() }),
|
|
185
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", [
|
|
186
222
|
z.strictObject({
|
|
187
223
|
s: z.literal("prefs"),
|
|
188
224
|
cid: ConvId,
|
|
@@ -191,15 +227,10 @@ export const SyncBody = z.discriminatedUnion("s", [
|
|
|
191
227
|
/** dms only; absent is off. */
|
|
192
228
|
receipts: z.boolean().optional(),
|
|
193
229
|
}),
|
|
194
|
-
z.strictObject({ s: z.literal("follow"), cid: ConvId, thread: Ulid, on: z.boolean() }),
|
|
195
|
-
z.strictObject({ s: z.literal("save"), cid: ConvId, target: Ulid, on: z.boolean() }),
|
|
196
|
-
z.strictObject({ s: z.literal("draft"), cid: ConvId, thread: Ulid.optional(), text: z.string().max(8000) }),
|
|
197
|
-
z.strictObject({ s: z.literal("joined"), cid: ConvId, on: z.boolean() }),
|
|
198
230
|
/** A consent grant (spec §6.9), verified with `verifyConsentGrant` over this raw value. */
|
|
199
231
|
z.strictObject({ s: z.literal("grant"), grant: z.record(z.string(), z.unknown()) }),
|
|
200
232
|
z.strictObject({ s: z.literal("context"), rules: ContextRules }),
|
|
201
233
|
z.strictObject({ s: z.literal("localRetention"), cid: ConvId, days: RetainDays }),
|
|
202
|
-
z.strictObject({ s: z.literal("keys"), cid: ConvId, shares: z.array(KeyShare).min(1).max(LIMITS.keyShares) }),
|
|
203
234
|
z.strictObject({ s: z.literal("link"), link: LinkRecord }),
|
|
204
235
|
]);
|
|
205
236
|
/** The plaintext of a `msg` event. */
|
|
@@ -228,15 +259,6 @@ export const MsgBody = z.discriminatedUnion("t", [
|
|
|
228
259
|
z.strictObject({ t: z.literal("pin"), target: Ulid, on: z.boolean() }),
|
|
229
260
|
z.strictObject({ t: z.literal("task"), task: TaskEventBody }),
|
|
230
261
|
z.strictObject({ t: z.literal("receipt"), target: Ulid, stage: ReceiptStage }),
|
|
231
|
-
z.strictObject({
|
|
232
|
-
t: z.literal("meta"),
|
|
233
|
-
name: ChannelName.optional(),
|
|
234
|
-
topic: z.string().max(250).optional(),
|
|
235
|
-
purpose: z.string().max(250).optional(),
|
|
236
|
-
}),
|
|
237
|
-
z
|
|
238
|
-
.strictObject({ t: z.literal("policy"), agentContext: AgentContext.optional(), bridges: BridgePolicy.optional() })
|
|
239
|
-
.refine((p) => p.agentContext !== undefined || p.bridges !== undefined, { message: "a policy sets agentContext, bridges or both" }),
|
|
240
262
|
z.strictObject({ t: z.literal("sync"), sync: SyncBody }),
|
|
241
263
|
]);
|
|
242
264
|
// ---- events and the log (spec §16.2) ----
|
|
@@ -266,6 +288,8 @@ export const ConvMsgEvent = z.strictObject({
|
|
|
266
288
|
idx: Seq,
|
|
267
289
|
/** Own-person wake hint only: sessions of `by.person` that this event may wake. */
|
|
268
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(),
|
|
269
293
|
body: SealedBody,
|
|
270
294
|
sig: Sig,
|
|
271
295
|
});
|
|
@@ -276,17 +300,27 @@ export const SealedShare = z.strictObject({
|
|
|
276
300
|
nonce: Nonce,
|
|
277
301
|
ct: Base64Url.max(Math.ceil(((LIMITS.keyShares * 160 + 16) * 4) / 3)),
|
|
278
302
|
});
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
});
|
|
286
|
-
/** Sender-key and history shares, sealed pairwise; the relay serves each share only to its recipient. */
|
|
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
|
+
*/
|
|
287
309
|
export const ConvKeysEvent = z.strictObject({ ...header, class: z.literal("keys"), body: KeysBody, sig: Sig });
|
|
288
|
-
|
|
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]);
|
|
289
314
|
/** An event as the relay stores and serves it. The relay does not sign it; members verify the chain. */
|
|
290
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 });
|
|
291
325
|
/** A log position: a seq and h(seq). */
|
|
292
326
|
export const ConvHead = Position;
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* dir.watch {req, persons} → dir.ok {req}; then dir.changed {persons, sth} pushes
|
|
24
24
|
* presence.watch {req, persons} → dir.ok {req}
|
|
25
25
|
* dir.erase {req, entries, token} → dir.ok {req}
|
|
26
|
-
* wake.arm {req, url, ticket, agents, awaiting} → dir.ok {req} a Jelly host's device, before it sleeps (§7, "Wake tickets")
|
|
26
|
+
* wake.arm {req, url, ticket, agents, awaiting, dms?} → dir.ok {req} a Jelly host's device, before it sleeps (§7, "Wake tickets")
|
|
27
27
|
* wake.disarm {req} → dir.ok {req}
|
|
28
28
|
* relay → dir.unbound {entry, sth} this device's own unbind, just before close 4003
|
|
29
29
|
*
|
|
@@ -36,6 +36,7 @@ import { Base64Url, Hash, HostedPersonId, LIMITS, Millis, RELAY_PROTOCOL, Sessio
|
|
|
36
36
|
import { EpochProof } from "./directory.js";
|
|
37
37
|
import { RealmId } from "./realm.js";
|
|
38
38
|
import { DeviceId, KeyRotation, KeysStatement, LogEntry, PersonHead, PersonView, STH } from "./directory.js";
|
|
39
|
+
import { ConvId } from "./conversation.js";
|
|
39
40
|
import { Jws } from "./jelly.js";
|
|
40
41
|
const Req = z.string().min(1).max(64);
|
|
41
42
|
const Size = z.number().int().positive();
|
|
@@ -150,6 +151,8 @@ export const DirEraseFrame = z.object({
|
|
|
150
151
|
/** Most agents a wake ticket's filter names, and most awaited task requests (spec §7, "Wake tickets"). */
|
|
151
152
|
export const WAKE_AGENTS = 64;
|
|
152
153
|
export const WAKE_AWAITING = 500;
|
|
154
|
+
/** Most dm conversations a wake ticket's filter names (spec §7, "Wake tickets", "a peer's dm"). */
|
|
155
|
+
export const WAKE_DMS = 256;
|
|
153
156
|
/** Most bytes of a wake ticket, and its longest life: Jelly's `jelly-wake+jwt` lives at most seven days. */
|
|
154
157
|
export const WAKE_TICKET_BYTES = 4096;
|
|
155
158
|
export const WAKE_TICKET_MAX_S = 7 * 24 * 60 * 60;
|
|
@@ -159,8 +162,9 @@ export const WakeUrl = z.string().max(2048).regex(/^https?:\/\/[^/?#@\s]+\/hosts
|
|
|
159
162
|
* A Jelly host's device arms a wake ticket before the host sleeps (spec §7, "Wake tickets"). `url` is Jelly ID's wake
|
|
160
163
|
* endpoint for this device's host; `ticket` the host's single-use `jelly-wake+jwt`, which the relay never verifies
|
|
161
164
|
* (Jelly ID does) and reads only for its `exp`; `agents` the local sessions whose own person's mail may wake the host;
|
|
162
|
-
* `awaiting` the request envelope ids of the tasks those agents wait on, whose updates (`replyTo`) may wake it
|
|
163
|
-
*
|
|
165
|
+
* `awaiting` the request envelope ids of the tasks those agents wait on, whose updates (`replyTo`) may wake it; `dms`
|
|
166
|
+
* the dm conversations whose other participant's events may wake it (absent: none), which the host's daemon chose. A
|
|
167
|
+
* new arm replaces the device's previous one.
|
|
164
168
|
*/
|
|
165
169
|
export const WakeArmFrame = z.object({
|
|
166
170
|
t: z.literal("wake.arm"),
|
|
@@ -169,6 +173,7 @@ export const WakeArmFrame = z.object({
|
|
|
169
173
|
ticket: Jws.refine((ticket) => ticket.length <= WAKE_TICKET_BYTES, { message: `at most ${WAKE_TICKET_BYTES} bytes` }),
|
|
170
174
|
agents: z.array(SessionName).max(WAKE_AGENTS),
|
|
171
175
|
awaiting: z.array(Ulid).max(WAKE_AWAITING),
|
|
176
|
+
dms: z.array(ConvId).max(WAKE_DMS).optional(),
|
|
172
177
|
});
|
|
173
178
|
/** The relay forgets this device's wake ticket. */
|
|
174
179
|
export const WakeDisarmFrame = z.object({ t: z.literal("wake.disarm"), req: Req });
|
|
@@ -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,11 +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
|
|
31
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
|
+
"conflict", // a person's setting changed since the caller read it (a stale revision, reach.set); nothing changed: read again
|
|
32
34
|
"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
|
|
33
35
|
"device_limit", // the person already has LIMITS.devicesPerPerson current devices
|
|
34
36
|
"admission_required", // dir.bind without a Jelly-vouched bind of the connecting device
|
|
@@ -38,8 +40,8 @@ export const ErrorCode = z.enum([
|
|
|
38
40
|
"directory_unavailable", // the key directory could not be reached to verify a person's devices within the freshness bound
|
|
39
41
|
"login_unsupported", // Jelly ID offers no device sign-in for this realm's resource (no v1.2 device sign-in in its metadata)
|
|
40
42
|
"login_denied", // the person denied the sign-in at Jelly ID, or the account is disabled or being deleted
|
|
41
|
-
"grant_required", // deployment "jelly": a human- or admin-tier call carried no action grant (spec §8.11)
|
|
42
|
-
"grant_invalid", // deployment "jelly"
|
|
43
|
+
"grant_required", // deployment "jelly": a human- or admin-tier call carried no action grant (spec §8.11), or request.answer no action token (§8.2)
|
|
44
|
+
"grant_invalid", // the action grant (deployment "jelly") or an action token (§8.2) did not verify, is for another call, expired or was spent
|
|
43
45
|
"ticket_required", // deployment "jelly": session.register carried no session ticket
|
|
44
46
|
"ticket_invalid", // deployment "jelly": the session ticket did not verify, expired, or names another session or agent
|
|
45
47
|
"daemon_unavailable", // client could not reach the local daemon socket
|
|
@@ -50,7 +52,7 @@ export const ErrorCode = z.enum([
|
|
|
50
52
|
/**
|
|
51
53
|
* The exit status of a messagingd or messaging-relay that refuses its store and changes nothing (spec §8.8): `EX_CONFIG`
|
|
52
54
|
* of sysexits(3), for either reason. messagingd also prints one stderr line,
|
|
53
|
-
* `messagingd: store_refused: <reason>: <why and
|
|
55
|
+
* `messagingd: store_refused: <reason>: <why and the remedy>`. A client matches this status, the code and the
|
|
54
56
|
* reason, never the wording.
|
|
55
57
|
*/
|
|
56
58
|
export const STORE_REFUSED_EXIT = 78;
|
|
@@ -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
|
+
});
|
|
@@ -14,3 +14,8 @@ export * from "./ipc.js";
|
|
|
14
14
|
export * from "./ipc/device.js";
|
|
15
15
|
export * from "./ipc/consent.js";
|
|
16
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,13 @@
|
|
|
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
|
-
|
|
10
|
-
|
|
10
|
+
import { ActionToken } from "./shapes.js";
|
|
11
|
+
/** A grant's subject: a person, or a verified conversation id. */
|
|
12
|
+
export const GrantTarget = z.union([z.strictObject({ person: PersonId }), z.strictObject({ conv: ConvId })]);
|
|
11
13
|
const Levels = {
|
|
12
14
|
chat: ChatLevel.optional(),
|
|
13
15
|
agentMail: AgentMailLevel.optional(),
|
|
@@ -17,7 +19,7 @@ const Levels = {
|
|
|
17
19
|
};
|
|
18
20
|
/** 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
21
|
export const GrantView = z.object({
|
|
20
|
-
subject:
|
|
22
|
+
subject: GrantTarget,
|
|
21
23
|
...Levels,
|
|
22
24
|
id: Ulid,
|
|
23
25
|
ts: Millis,
|
|
@@ -42,6 +44,21 @@ export const GrantReview = z.object({
|
|
|
42
44
|
* level it is a clear. `trust: true` trusts for `LIMITS.trustDefaultMs`; `trustedUntil` names the end instead.
|
|
43
45
|
*/
|
|
44
46
|
export const GrantSetParams = z.strictObject({ subject: GrantTarget, ...Levels, trust: z.literal(true).optional() });
|
|
47
|
+
/**
|
|
48
|
+
* For an ask: `once` delivers it, guarded; `always` sets the sender's grant to `ask: "open"` (keeping its other
|
|
49
|
+
* levels), then delivers it, guarded. For a message request: `accept` sets the sender's grant to `chat: "open"`
|
|
50
|
+
* (keeping its other levels), files the request as their first line and sends them a card. For both: `decline`
|
|
51
|
+
* refuses it (`declined`); `block` sets the sender's grant to `chat: "blocked"` (keeping its other levels), then
|
|
52
|
+
* declines it. For a suggestion: `decline` dismisses it.
|
|
53
|
+
*/
|
|
54
|
+
export const RequestAnswer = z.enum(["once", "always", "accept", "decline", "block"]);
|
|
55
|
+
/**
|
|
56
|
+
* A source-issued action on a held request (spec §8.2, "Action tokens"): `name` is the answer it gives, redeemed with
|
|
57
|
+
* `request.answer {id, answer: name, token}` on the connection that read it.
|
|
58
|
+
*/
|
|
59
|
+
export const RequestAction = z.strictObject({ name: RequestAnswer, token: ActionToken });
|
|
60
|
+
/** One per answer the request's kind takes; only on the human tier, only while the request is pending. */
|
|
61
|
+
const Actions = { actions: z.array(RequestAction).max(4).optional() };
|
|
45
62
|
/** A request this device holds for its person to answer (spec §6.9, "Holds"): mail or a task to one of their agents. */
|
|
46
63
|
export const AskRequest = z.object({
|
|
47
64
|
/** The envelope id. */
|
|
@@ -55,19 +72,22 @@ export const AskRequest = z.object({
|
|
|
55
72
|
at: Millis,
|
|
56
73
|
/** When the request expires, unanswered. */
|
|
57
74
|
exp: Millis,
|
|
75
|
+
...Actions,
|
|
58
76
|
});
|
|
59
77
|
/**
|
|
60
78
|
* A message request this device holds for its person (spec §6.9, "Message requests"): a person of another org asks to
|
|
61
79
|
* write to them. `name`, `email` and `org` are the realm's verified profile of the sender, never the request's text.
|
|
62
80
|
*/
|
|
63
81
|
export const ContactRequestView = z.object({
|
|
64
|
-
/** The
|
|
82
|
+
/** The portable request id; old unsigned requests explicitly require a resend. */
|
|
65
83
|
id: Ulid,
|
|
66
84
|
kind: z.literal("contact"),
|
|
85
|
+
cid: ConvId.optional(),
|
|
86
|
+
state: z.enum(["pending", "answering", "accepted", "resend"]),
|
|
67
87
|
from: z.object({
|
|
68
88
|
person: PersonId,
|
|
69
89
|
device: MachineId,
|
|
70
|
-
label: MachineLabel,
|
|
90
|
+
label: MachineLabel.optional(),
|
|
71
91
|
name: z.string().max(200),
|
|
72
92
|
email: z.string().max(320).optional(),
|
|
73
93
|
org: z.enum(["same", "other", "none"]),
|
|
@@ -76,7 +96,9 @@ export const ContactRequestView = z.object({
|
|
|
76
96
|
text: z.string().max(LIMITS.requestChars),
|
|
77
97
|
at: Millis,
|
|
78
98
|
exp: Millis,
|
|
99
|
+
...Actions,
|
|
79
100
|
});
|
|
101
|
+
export const AgentInvitationRequest = z.strictObject({ kind: z.literal("agent-invitation"), id: Ulid, cid: ConvId, from: ParticipantId, to: ParticipantId, at: Millis, ...Actions });
|
|
80
102
|
/**
|
|
81
103
|
* A contact one of this person's agents asks them to make (spec §6.9, "Message requests"): `send {to: "me", request:
|
|
82
104
|
* {contact}}`. Only the person sends a message request, with `contact.request`; answering this one `decline`
|
|
@@ -89,17 +111,10 @@ export const ContactSuggestion = z.object({
|
|
|
89
111
|
agent: SessionName,
|
|
90
112
|
email: Email,
|
|
91
113
|
at: Millis,
|
|
114
|
+
...Actions,
|
|
92
115
|
});
|
|
93
116
|
/** 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]);
|
|
95
|
-
/**
|
|
96
|
-
* For an ask: `once` delivers it, guarded; `always` sets the sender's grant to `ask: "open"` (keeping its other
|
|
97
|
-
* levels), then delivers it, guarded. For a message request: `accept` sets the sender's grant to `chat: "open"`
|
|
98
|
-
* (keeping its other levels), files the request as their first line and sends them a card. For both: `decline`
|
|
99
|
-
* refuses it (`declined`); `block` sets the sender's grant to `chat: "blocked"` (keeping its other levels), then
|
|
100
|
-
* declines it. For a suggestion: `decline` dismisses it.
|
|
101
|
-
*/
|
|
102
|
-
export const RequestAnswer = z.enum(["once", "always", "accept", "decline", "block"]);
|
|
117
|
+
export const HeldRequest = z.union([AskRequest, ContactRequestView, ContactSuggestion, AgentInvitationRequest]);
|
|
103
118
|
/** A message request's text: 1 to `LIMITS.requestChars` UTF-16 code units. */
|
|
104
119
|
export const RequestText = z
|
|
105
120
|
.string()
|
|
@@ -115,10 +130,74 @@ export const RequestText = z
|
|
|
115
130
|
*/
|
|
116
131
|
export const PersonReach = z.object({ person: PersonId, can: CardCan, source: z.enum(["card", "default"]), at: Millis.optional(), devicesChangedAt: Millis.optional() });
|
|
117
132
|
/** `request.answer`'s params, which `request.answer.preview` takes too (spec §8.2, "Previews"). */
|
|
118
|
-
|
|
133
|
+
/**
|
|
134
|
+
* `request.answer`'s params. `token` is the request's action for this `answer` (`RequestAction`); it answers that one
|
|
135
|
+
* request, so it never comes with `all`. Under a Jelly host it is required.
|
|
136
|
+
*/
|
|
137
|
+
export const RequestAnswerParams = z
|
|
138
|
+
.strictObject({ id: Ulid, answer: RequestAnswer, all: z.boolean().optional(), token: ActionToken.optional() })
|
|
139
|
+
.refine((p) => p.token === undefined || p.all !== true, "an action token answers one request: drop all");
|
|
119
140
|
/** `contact.request`'s params; its preview takes them with `text` optional (it reads only `email`). */
|
|
120
141
|
export const ContactRequestParams = z.strictObject({ email: Email, text: RequestText, suggestion: Ulid.optional() });
|
|
142
|
+
/** A reach question (spec §6.9, "Reach choices"): `message` is "Who can message you", `ask` "How agents may ask yours". */
|
|
143
|
+
export const ReachGroupId = z.enum(["message", "ask"]);
|
|
144
|
+
/** An answer: Open, Ask me first (the default), Never. */
|
|
145
|
+
export const ReachValue = z.enum(["open", "ask", "never"]);
|
|
146
|
+
export const ReachOption = z.object({ value: ReachValue, label: z.string().max(40), description: z.string().max(300) });
|
|
147
|
+
/**
|
|
148
|
+
* One question with its options. `value` is the person's choice, or null while they have made none and the relation
|
|
149
|
+
* defaults apply. `lapsed` is a choice a removed device of the person made, which lapsed with it (§6.9, "Signers")
|
|
150
|
+
* and waits for the person to choose again.
|
|
151
|
+
*/
|
|
152
|
+
export const ReachGroup = z.object({
|
|
153
|
+
id: ReachGroupId,
|
|
154
|
+
label: z.string().max(80),
|
|
155
|
+
options: z.array(ReachOption).length(3),
|
|
156
|
+
value: ReachValue.nullable(),
|
|
157
|
+
lapsed: ReachValue.optional(),
|
|
158
|
+
});
|
|
159
|
+
/** `reach.get`'s result, and `reach.set`'s: both questions, and a `revision` that changes whenever either choice does. */
|
|
160
|
+
export const ReachSnapshot = z.object({ revision: z.string().min(1).max(64), available: z.boolean(), groups: z.array(ReachGroup).length(2) });
|
|
161
|
+
/** `reach.set`'s params, which `reach.set.preview` takes too: the `revision` read, and only the declared answers that change. */
|
|
162
|
+
export const ReachSetParams = z.strictObject({
|
|
163
|
+
revision: z.string().min(1).max(64),
|
|
164
|
+
changes: z.strictObject({ message: ReachValue.optional(), ask: ReachValue.optional() }),
|
|
165
|
+
});
|
|
166
|
+
/** The fixed words of the two questions, in `reach.get`'s order. */
|
|
167
|
+
export const REACH_QUESTIONS = [
|
|
168
|
+
{
|
|
169
|
+
id: "message",
|
|
170
|
+
label: "Who can message you",
|
|
171
|
+
options: [
|
|
172
|
+
{ value: "open", label: "Open", description: "Anyone outside your org may message you." },
|
|
173
|
+
{ value: "ask", label: "Ask me first", description: "People outside your org send a message request first, and you accept or decline it. People you have accepted may message you." },
|
|
174
|
+
{ value: "never", label: "Never", description: "No one outside your org may message you or send you a request. Your org, and people you have let message you, still can." },
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
id: "ask",
|
|
179
|
+
label: "How agents may ask yours",
|
|
180
|
+
options: [
|
|
181
|
+
{ value: "open", label: "Open", description: "Your teammates' agents may ask your agents without asking you. Their requests run guarded." },
|
|
182
|
+
{ value: "ask", label: "Ask me first", description: "Each request from a teammate's agent to yours waits for your answer." },
|
|
183
|
+
{ value: "never", label: "Never", description: "Your teammates' agents may not ask yours. Agents of people outside your org never may." },
|
|
184
|
+
],
|
|
185
|
+
},
|
|
186
|
+
];
|
|
121
187
|
export const CONSENT_METHODS = {
|
|
188
|
+
/**
|
|
189
|
+
* The person's two reach choices (spec §6.9, "Reach choices"), each with its options and the current value (null:
|
|
190
|
+
* none made, the defaults apply), and a revision. Read-only.
|
|
191
|
+
*/
|
|
192
|
+
"reach.get": { params: z.strictObject({}), result: ReachSnapshot },
|
|
193
|
+
/**
|
|
194
|
+
* Records the declared answers in `changes` as the person's relation grants, signed by this device and sent to
|
|
195
|
+
* their other devices, in one transaction; returns the new snapshot. `conflict` when `revision` is not the current
|
|
196
|
+
* one: nothing is changed, and the caller reads again.
|
|
197
|
+
*/
|
|
198
|
+
"reach.set": { params: ReachSetParams, result: ReachSnapshot },
|
|
199
|
+
/** What `reach.set` with these params would do, in one fixed sentence; checks them the same way (`conflict` too); changes nothing. */
|
|
200
|
+
"reach.set.preview": { params: ReachSetParams, result: z.object({ sentence: z.string().max(500) }) },
|
|
122
201
|
/** This person's grants, newest first, and the review list. With `subject`, only that subject's. */
|
|
123
202
|
"grant.list": {
|
|
124
203
|
params: z.strictObject({ subject: GrantTarget.optional() }),
|
|
@@ -152,7 +231,7 @@ export const CONSENT_METHODS = {
|
|
|
152
231
|
"request.answer.preview": {
|
|
153
232
|
params: RequestAnswerParams,
|
|
154
233
|
result: z.object({
|
|
155
|
-
kind: z.enum(["message", "task", "contact", "suggestion"]),
|
|
234
|
+
kind: z.enum(["message", "task", "contact", "suggestion", "agent-invitation"]),
|
|
156
235
|
from: z.object({ name: z.string().max(200), email: z.string().max(320).optional(), org: z.enum(["same", "other", "none"]) }),
|
|
157
236
|
agent: SessionName.optional(),
|
|
158
237
|
to: SessionName.optional(),
|
|
@@ -169,7 +248,7 @@ export const CONSENT_METHODS = {
|
|
|
169
248
|
*/
|
|
170
249
|
"contact.request": {
|
|
171
250
|
params: ContactRequestParams,
|
|
172
|
-
result: z.object({ id: Ulid,
|
|
251
|
+
result: z.object({ id: Ulid, cid: ConvId, person: PersonId }),
|
|
173
252
|
},
|
|
174
253
|
/** Whether `contact.request` would find a person for `email`; counts against the directory's lookup limits. Sends nothing. */
|
|
175
254
|
"contact.request.preview": { params: ContactRequestParams.partial({ text: true }), result: z.object({ found: z.boolean() }) },
|