@joe-broadhead/messaging 0.0.0-stage → 0.7.1
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 +220 -0
- package/LICENSE +21 -0
- package/NOTICE +12 -0
- package/README.md +36 -2
- package/dist/bin/messaging-mcp.js +26524 -0
- package/dist/bin/messaging-relay.js +19768 -0
- package/dist/bin/messaging.js +15465 -0
- package/dist/bin/messagingd.js +29449 -0
- package/dist/lib/core-testing.js +1 -0
- package/dist/lib/core.js +1 -0
- package/dist/lib/ipc-testing.js +1 -0
- package/dist/lib/ipc.js +1 -0
- package/dist/lib/modules/core/address.js +42 -0
- package/dist/lib/modules/core/agent/operations.js +595 -0
- package/dist/lib/modules/core/agent/present.js +244 -0
- package/dist/lib/modules/core/agent/text.js +79 -0
- package/dist/lib/modules/core/agent-tools.js +477 -0
- package/dist/lib/modules/core/b64u.js +20 -0
- package/dist/lib/modules/core/cards.js +55 -0
- package/dist/lib/modules/core/check.js +20 -0
- package/dist/lib/modules/core/consent.js +199 -0
- package/dist/lib/modules/core/contact-card.js +74 -0
- package/dist/lib/modules/core/conversation/context.js +80 -0
- package/dist/lib/modules/core/conversation/event.js +100 -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 +766 -0
- package/dist/lib/modules/core/conversation/ratchet.js +243 -0
- package/dist/lib/modules/core/conversation/seal.js +198 -0
- package/dist/lib/modules/core/delegation.js +174 -0
- package/dist/lib/modules/core/directory/device-log.js +258 -0
- package/dist/lib/modules/core/directory/epoch.js +103 -0
- package/dist/lib/modules/core/directory/handle.js +29 -0
- package/dist/lib/modules/core/directory/jelly-device.js +204 -0
- package/dist/lib/modules/core/directory/jelly-issuer.js +12 -0
- package/dist/lib/modules/core/directory/jelly-token.js +305 -0
- package/dist/lib/modules/core/directory/keys-statement.js +43 -0
- package/dist/lib/modules/core/directory/link.js +81 -0
- package/dist/lib/modules/core/directory/merkle-log.js +250 -0
- package/dist/lib/modules/core/directory/person-view.js +197 -0
- package/dist/lib/modules/core/directory/person.js +40 -0
- package/dist/lib/modules/core/directory/realm.js +59 -0
- package/dist/lib/modules/core/directory/smt.js +266 -0
- package/dist/lib/modules/core/envelope.js +320 -0
- package/dist/lib/modules/core/hash.js +5 -0
- package/dist/lib/modules/core/head.js +27 -0
- package/dist/lib/modules/core/identity.js +71 -0
- package/dist/lib/modules/core/index.js +53 -0
- package/dist/lib/modules/core/jcs.js +96 -0
- package/dist/lib/modules/core/jelly-host-double.js +35 -0
- package/dist/lib/modules/core/jelly-host.js +211 -0
- package/dist/lib/modules/core/keys.js +79 -0
- package/dist/lib/modules/core/peer-text.js +563 -0
- package/dist/lib/modules/core/realm.js +1 -0
- package/dist/lib/modules/core/roster.js +454 -0
- package/dist/lib/modules/core/schema/a2a.js +85 -0
- package/dist/lib/modules/core/schema/common.js +310 -0
- package/dist/lib/modules/core/schema/consent.js +120 -0
- package/dist/lib/modules/core/schema/conversation.js +292 -0
- package/dist/lib/modules/core/schema/delegation.js +58 -0
- package/dist/lib/modules/core/schema/directory-frames.js +270 -0
- package/dist/lib/modules/core/schema/directory.js +169 -0
- package/dist/lib/modules/core/schema/envelope.js +211 -0
- package/dist/lib/modules/core/schema/errors.js +104 -0
- package/dist/lib/modules/core/schema/index.js +16 -0
- package/dist/lib/modules/core/schema/ipc/consent.js +181 -0
- package/dist/lib/modules/core/schema/ipc/device.js +276 -0
- package/dist/lib/modules/core/schema/ipc/jelly.js +189 -0
- package/dist/lib/modules/core/schema/ipc/shapes.js +185 -0
- package/dist/lib/modules/core/schema/ipc/surfaces.js +223 -0
- package/dist/lib/modules/core/schema/ipc.js +451 -0
- package/dist/lib/modules/core/schema/jelly.js +122 -0
- package/dist/lib/modules/core/schema/realm.js +27 -0
- package/dist/lib/modules/core/schema/relay.js +112 -0
- package/dist/lib/modules/core/schema/roster.js +126 -0
- package/dist/lib/modules/core/sign.js +43 -0
- package/dist/lib/modules/core/task.js +37 -0
- package/dist/lib/modules/core/testing.js +21 -0
- package/dist/lib/modules/core/ulid.js +36 -0
- package/dist/lib/modules/core/version.js +6 -0
- package/dist/lib/modules/core/wire.js +255 -0
- package/dist/lib/modules/ipc/client.js +289 -0
- package/dist/lib/modules/ipc/framing.js +33 -0
- package/dist/lib/modules/ipc/index.js +6 -0
- package/dist/lib/modules/ipc/paths.js +102 -0
- package/dist/lib/modules/ipc/pipes.js +65 -0
- package/dist/lib/modules/ipc/rpc.js +8 -0
- package/dist/lib/modules/ipc/server.js +317 -0
- package/dist/lib/modules/ipc/testing.js +145 -0
- package/dist/lib/modules/ipc/trust.js +166 -0
- package/dist/lib/modules/ipc/windows-acl.js +275 -0
- package/dist/lib/modules/vendor/zod/index.js +7 -0
- package/dist/lib/modules/vendor/zod/v4/classic/checks.js +1 -0
- package/dist/lib/modules/vendor/zod/v4/classic/coerce.js +17 -0
- package/dist/lib/modules/vendor/zod/v4/classic/compat.js +31 -0
- package/dist/lib/modules/vendor/zod/v4/classic/deep-partial.js +13 -0
- package/dist/lib/modules/vendor/zod/v4/classic/errors.js +57 -0
- package/dist/lib/modules/vendor/zod/v4/classic/external.js +17 -0
- package/dist/lib/modules/vendor/zod/v4/classic/from-json-schema.js +881 -0
- package/dist/lib/modules/vendor/zod/v4/classic/in-out.js +36 -0
- package/dist/lib/modules/vendor/zod/v4/classic/iso.js +15 -0
- package/dist/lib/modules/vendor/zod/v4/classic/parse.js +16 -0
- package/dist/lib/modules/vendor/zod/v4/classic/schemas.js +1535 -0
- package/dist/lib/modules/vendor/zod/v4/core/api.js +1107 -0
- package/dist/lib/modules/vendor/zod/v4/core/checks.js +516 -0
- package/dist/lib/modules/vendor/zod/v4/core/compile.js +1866 -0
- package/dist/lib/modules/vendor/zod/v4/core/core.js +141 -0
- package/dist/lib/modules/vendor/zod/v4/core/doc.js +38 -0
- package/dist/lib/modules/vendor/zod/v4/core/errors.js +267 -0
- package/dist/lib/modules/vendor/zod/v4/core/index.js +19 -0
- package/dist/lib/modules/vendor/zod/v4/core/json-schema-generator.js +99 -0
- package/dist/lib/modules/vendor/zod/v4/core/json-schema-processors.js +843 -0
- package/dist/lib/modules/vendor/zod/v4/core/json-schema.js +1 -0
- package/dist/lib/modules/vendor/zod/v4/core/memoizer.js +311 -0
- package/dist/lib/modules/vendor/zod/v4/core/parse.js +175 -0
- package/dist/lib/modules/vendor/zod/v4/core/regexes.js +156 -0
- package/dist/lib/modules/vendor/zod/v4/core/registries.js +51 -0
- package/dist/lib/modules/vendor/zod/v4/core/schemas.js +2636 -0
- package/dist/lib/modules/vendor/zod/v4/core/to-json-schema.js +651 -0
- package/dist/lib/modules/vendor/zod/v4/core/util.js +873 -0
- package/dist/lib/modules/vendor/zod/v4/core/versions.js +5 -0
- package/dist/lib/modules/vendor/zod/v4/core/visit.js +169 -0
- package/dist/lib/modules/vendor/zod/v4/locales/ar.js +111 -0
- package/dist/lib/modules/vendor/zod/v4/locales/az.js +110 -0
- package/dist/lib/modules/vendor/zod/v4/locales/be.js +168 -0
- package/dist/lib/modules/vendor/zod/v4/locales/bg.js +125 -0
- package/dist/lib/modules/vendor/zod/v4/locales/bn.js +113 -0
- package/dist/lib/modules/vendor/zod/v4/locales/ca.js +112 -0
- package/dist/lib/modules/vendor/zod/v4/locales/ckb.js +133 -0
- package/dist/lib/modules/vendor/zod/v4/locales/cs.js +116 -0
- package/dist/lib/modules/vendor/zod/v4/locales/da.js +120 -0
- package/dist/lib/modules/vendor/zod/v4/locales/de.js +113 -0
- package/dist/lib/modules/vendor/zod/v4/locales/el.js +112 -0
- package/dist/lib/modules/vendor/zod/v4/locales/en.js +125 -0
- package/dist/lib/modules/vendor/zod/v4/locales/eo.js +114 -0
- package/dist/lib/modules/vendor/zod/v4/locales/es.js +136 -0
- package/dist/lib/modules/vendor/zod/v4/locales/fa.js +119 -0
- package/dist/lib/modules/vendor/zod/v4/locales/fi.js +117 -0
- package/dist/lib/modules/vendor/zod/v4/locales/fr-CA.js +112 -0
- package/dist/lib/modules/vendor/zod/v4/locales/fr.js +129 -0
- package/dist/lib/modules/vendor/zod/v4/locales/gu.js +113 -0
- package/dist/lib/modules/vendor/zod/v4/locales/he.js +221 -0
- package/dist/lib/modules/vendor/zod/v4/locales/hi.js +111 -0
- package/dist/lib/modules/vendor/zod/v4/locales/hr.js +126 -0
- package/dist/lib/modules/vendor/zod/v4/locales/hu.js +113 -0
- package/dist/lib/modules/vendor/zod/v4/locales/hy.js +158 -0
- package/dist/lib/modules/vendor/zod/v4/locales/id.js +111 -0
- package/dist/lib/modules/vendor/zod/v4/locales/index.js +63 -0
- package/dist/lib/modules/vendor/zod/v4/locales/is.js +114 -0
- package/dist/lib/modules/vendor/zod/v4/locales/it.js +113 -0
- package/dist/lib/modules/vendor/zod/v4/locales/ja.js +112 -0
- package/dist/lib/modules/vendor/zod/v4/locales/ka.js +117 -0
- package/dist/lib/modules/vendor/zod/v4/locales/kh.js +5 -0
- package/dist/lib/modules/vendor/zod/v4/locales/km.js +115 -0
- package/dist/lib/modules/vendor/zod/v4/locales/kn.js +116 -0
- package/dist/lib/modules/vendor/zod/v4/locales/ko.js +116 -0
- package/dist/lib/modules/vendor/zod/v4/locales/lt.js +207 -0
- package/dist/lib/modules/vendor/zod/v4/locales/mk.js +114 -0
- package/dist/lib/modules/vendor/zod/v4/locales/ms.js +112 -0
- package/dist/lib/modules/vendor/zod/v4/locales/ne.js +111 -0
- package/dist/lib/modules/vendor/zod/v4/locales/nl.js +115 -0
- package/dist/lib/modules/vendor/zod/v4/locales/nn.js +113 -0
- package/dist/lib/modules/vendor/zod/v4/locales/no.js +113 -0
- package/dist/lib/modules/vendor/zod/v4/locales/ota.js +114 -0
- package/dist/lib/modules/vendor/zod/v4/locales/pl.js +114 -0
- package/dist/lib/modules/vendor/zod/v4/locales/ps.js +119 -0
- package/dist/lib/modules/vendor/zod/v4/locales/pt-BR.js +143 -0
- package/dist/lib/modules/vendor/zod/v4/locales/pt.js +142 -0
- package/dist/lib/modules/vendor/zod/v4/locales/ro.js +122 -0
- package/dist/lib/modules/vendor/zod/v4/locales/ru.js +168 -0
- package/dist/lib/modules/vendor/zod/v4/locales/sk.js +116 -0
- package/dist/lib/modules/vendor/zod/v4/locales/sl.js +114 -0
- package/dist/lib/modules/vendor/zod/v4/locales/sv.js +115 -0
- package/dist/lib/modules/vendor/zod/v4/locales/ta.js +115 -0
- package/dist/lib/modules/vendor/zod/v4/locales/tg.js +117 -0
- package/dist/lib/modules/vendor/zod/v4/locales/th.js +115 -0
- package/dist/lib/modules/vendor/zod/v4/locales/tk.js +107 -0
- package/dist/lib/modules/vendor/zod/v4/locales/tr.js +110 -0
- package/dist/lib/modules/vendor/zod/v4/locales/ua.js +5 -0
- package/dist/lib/modules/vendor/zod/v4/locales/uk.js +113 -0
- package/dist/lib/modules/vendor/zod/v4/locales/ur.js +115 -0
- package/dist/lib/modules/vendor/zod/v4/locales/uz.js +113 -0
- package/dist/lib/modules/vendor/zod/v4/locales/vi.js +113 -0
- package/dist/lib/modules/vendor/zod/v4/locales/yo.js +112 -0
- package/dist/lib/modules/vendor/zod/v4/locales/zh-CN.js +114 -0
- package/dist/lib/modules/vendor/zod/v4/locales/zh-TW.js +112 -0
- package/dist/lib/testing-jelly-id.js +1219 -0
- package/dist/lib/zod.js +1 -0
- package/dist/spec/messaging-v1.md +3744 -0
- package/dist/third-party-notices.md +239 -0
- package/dist/third-party.json +88 -0
- package/dist/types/core/address.d.ts +16 -0
- package/dist/types/core/agent/operations.d.ts +129 -0
- package/dist/types/core/agent/present.d.ts +57 -0
- package/dist/types/core/agent/text.d.ts +62 -0
- package/dist/types/core/agent-tools.d.ts +966 -0
- package/dist/types/core/b64u.d.ts +6 -0
- package/dist/types/core/cards.d.ts +21 -0
- package/dist/types/core/check.d.ts +10 -0
- package/dist/types/core/consent.d.ts +132 -0
- package/dist/types/core/contact-card.d.ts +34 -0
- 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/ids.d.ts +18 -0
- package/dist/types/core/conversation/link.d.ts +16 -0
- package/dist/types/core/conversation/log.d.ts +142 -0
- package/dist/types/core/conversation/ratchet.d.ts +95 -0
- package/dist/types/core/conversation/seal.d.ts +47 -0
- package/dist/types/core/delegation.d.ts +71 -0
- package/dist/types/core/directory/device-log.d.ts +119 -0
- package/dist/types/core/directory/epoch.d.ts +53 -0
- package/dist/types/core/directory/handle.d.ts +11 -0
- package/dist/types/core/directory/jelly-device.d.ts +88 -0
- package/dist/types/core/directory/jelly-issuer.d.ts +8 -0
- package/dist/types/core/directory/jelly-token.d.ts +116 -0
- package/dist/types/core/directory/keys-statement.d.ts +22 -0
- package/dist/types/core/directory/link.d.ts +36 -0
- package/dist/types/core/directory/merkle-log.d.ts +40 -0
- package/dist/types/core/directory/person-view.d.ts +90 -0
- package/dist/types/core/directory/person.d.ts +8 -0
- package/dist/types/core/directory/realm.d.ts +23 -0
- package/dist/types/core/directory/smt.d.ts +52 -0
- package/dist/types/core/envelope.d.ts +68 -0
- package/dist/types/core/hash.d.ts +2 -0
- package/dist/types/core/head.d.ts +34 -0
- package/dist/types/core/identity.d.ts +35 -0
- package/dist/types/core/index.d.ts +45 -0
- package/dist/types/core/jcs.d.ts +12 -0
- package/dist/types/core/jelly-host-double.d.ts +21 -0
- package/dist/types/core/jelly-host.d.ts +86 -0
- package/dist/types/core/keys.d.ts +16 -0
- package/dist/types/core/peer-text.d.ts +87 -0
- package/dist/types/core/realm.d.ts +36 -0
- package/dist/types/core/roster.d.ts +146 -0
- package/dist/types/core/schema/a2a.d.ts +251 -0
- package/dist/types/core/schema/common.d.ts +328 -0
- package/dist/types/core/schema/consent.d.ts +140 -0
- package/dist/types/core/schema/conversation.d.ts +1657 -0
- package/dist/types/core/schema/delegation.d.ts +58 -0
- package/dist/types/core/schema/directory-frames.d.ts +1301 -0
- package/dist/types/core/schema/directory.d.ts +375 -0
- package/dist/types/core/schema/envelope.d.ts +756 -0
- package/dist/types/core/schema/errors.d.ts +142 -0
- package/dist/types/core/schema/index.d.ts +16 -0
- package/dist/types/core/schema/ipc/consent.d.ts +555 -0
- package/dist/types/core/schema/ipc/device.d.ts +735 -0
- package/dist/types/core/schema/ipc/jelly.d.ts +501 -0
- package/dist/types/core/schema/ipc/shapes.d.ts +414 -0
- package/dist/types/core/schema/ipc/surfaces.d.ts +680 -0
- package/dist/types/core/schema/ipc.d.ts +3146 -0
- package/dist/types/core/schema/jelly.d.ts +160 -0
- package/dist/types/core/schema/realm.d.ts +38 -0
- package/dist/types/core/schema/relay.d.ts +1134 -0
- package/dist/types/core/schema/roster.d.ts +347 -0
- package/dist/types/core/sign.d.ts +13 -0
- package/dist/types/core/task.d.ts +6 -0
- package/dist/types/core/testing.d.ts +19 -0
- package/dist/types/core/ulid.d.ts +5 -0
- package/dist/types/core/version.d.ts +6 -0
- package/dist/types/core/wire.d.ts +14 -0
- package/dist/types/ipc/client.d.ts +85 -0
- package/dist/types/ipc/framing.d.ts +12 -0
- package/dist/types/ipc/index.d.ts +6 -0
- package/dist/types/ipc/paths.d.ts +39 -0
- package/dist/types/ipc/pipes.d.ts +16 -0
- package/dist/types/ipc/rpc.d.ts +13 -0
- package/dist/types/ipc/server.d.ts +94 -0
- package/dist/types/ipc/testing.d.ts +89 -0
- package/dist/types/ipc/trust.d.ts +49 -0
- package/dist/types/ipc/windows-acl.d.ts +56 -0
- package/dist/types/testing/jelly-id.d.ts +518 -0
- package/dist/types/zod.d.ts +1 -0
- package/package.json +77 -4
|
@@ -0,0 +1,292 @@
|
|
|
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
|
+
// ---- identifiers (spec §16.1) ----
|
|
20
|
+
/** A person, or one agent of a person (`person/agent`). Every agent belongs to one person. */
|
|
21
|
+
export const ParticipantId = z
|
|
22
|
+
.string()
|
|
23
|
+
.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");
|
|
24
|
+
/** `d_…` a dm (derived), `s_…` a person's `self` conversation (derived), `c_` + ULID a group or channel. */
|
|
25
|
+
export const ConvId = z.string().regex(/^(?:[ds]_[a-z2-7]{26}|c_[0-7][0-9A-HJKMNP-TV-Z]{25})$/, "conversation id");
|
|
26
|
+
export const ConvKind = z.enum(["dm", "group", "channel", "self"]);
|
|
27
|
+
/** Channels only; every other kind is private. */
|
|
28
|
+
export const Visibility = z.enum(["public", "private"]);
|
|
29
|
+
/** What a late joiner may read: `shared` gets earlier chain keys, `none` reads from its add onwards. */
|
|
30
|
+
export const HistoryPolicy = z.enum(["none", "shared"]);
|
|
31
|
+
export const ConvRole = z.enum(["owner", "admin", "member", "guest"]);
|
|
32
|
+
/** Who may do a thing in a group or channel. */
|
|
33
|
+
export const Permit = z.enum(["all", "admins"]);
|
|
34
|
+
/** Which mentions wake a seated agent: its own person's, any member's (under consent), or none. */
|
|
35
|
+
export const RespondTo = z.enum(["self", "members", "none"]);
|
|
36
|
+
/** A conversation's policy for members' agents mining it as context (spec §16.8). */
|
|
37
|
+
export const AgentContext = z.enum(["members", "seats", "none"]);
|
|
38
|
+
/** Why a sender epoch ends. */
|
|
39
|
+
export const RekeyCause = z.enum(["removed", "device", "added", "settings", "schedule"]);
|
|
40
|
+
const Seq = z.number().int().min(0);
|
|
41
|
+
const RetainDays = z.number().int().min(1).max(LIMITS.retainDaysMax);
|
|
42
|
+
/** A conversation event's signature: 64 bytes. */
|
|
43
|
+
const Sig = z.string().regex(/^[A-Za-z0-9_-]{86}$/, "Ed25519 signature, base64url");
|
|
44
|
+
/** An AES-GCM nonce: 12 bytes. */
|
|
45
|
+
const Nonce = z.string().regex(/^[A-Za-z0-9_-]{16}$/, "12-byte nonce, base64url");
|
|
46
|
+
const isAgent = (p) => p.includes("/");
|
|
47
|
+
// ---- ops (spec §16.2, "Ops") ----
|
|
48
|
+
/** A participant record as an op states it. `device` and `respondTo` are for agent participants only. */
|
|
49
|
+
export const Seat = z
|
|
50
|
+
.strictObject({ p: ParticipantId, role: ConvRole, device: MachineId.optional(), respondTo: RespondTo.optional() })
|
|
51
|
+
.refine((s) => isAgent(s.p) || (s.device === undefined && s.respondTo === undefined), { message: "device and respondTo are for agents only" });
|
|
52
|
+
export const ConvOp = z.discriminatedUnion("op", [
|
|
53
|
+
z.strictObject({
|
|
54
|
+
op: z.literal("create"),
|
|
55
|
+
kind: ConvKind,
|
|
56
|
+
visibility: Visibility.optional(),
|
|
57
|
+
history: HistoryPolicy,
|
|
58
|
+
retainDays: RetainDays,
|
|
59
|
+
/** A channel: the org it is open to when public. */
|
|
60
|
+
org: z.string().min(1).max(128).optional(),
|
|
61
|
+
participants: z.array(Seat).min(1).max(LIMITS.convParticipants),
|
|
62
|
+
/** Absent: `all`. */
|
|
63
|
+
post: Permit.optional(),
|
|
64
|
+
invite: Permit.optional(),
|
|
65
|
+
pins: Permit.optional(),
|
|
66
|
+
/** JCS(`ConvMeta`) sealed under the creator's epoch-0 message key 0 (spec §16.5). */
|
|
67
|
+
sealedMeta: Base64Url.max(2048).optional(),
|
|
68
|
+
}),
|
|
69
|
+
z.strictObject({ op: z.literal("add"), participants: z.array(Seat).min(1).max(LIMITS.convOpParticipants) }),
|
|
70
|
+
z.strictObject({ op: z.literal("accept") }),
|
|
71
|
+
z.strictObject({ op: z.literal("join") }),
|
|
72
|
+
z.strictObject({ op: z.literal("leave") }),
|
|
73
|
+
z.strictObject({ op: z.literal("remove"), participants: z.array(ParticipantId).min(1).max(LIMITS.convOpParticipants) }),
|
|
74
|
+
z.strictObject({ op: z.literal("role"), p: ParticipantId, role: ConvRole }),
|
|
75
|
+
z.strictObject({ op: z.literal("seat"), p: ParticipantId, device: MachineId.optional(), respondTo: RespondTo.optional() }),
|
|
76
|
+
z.strictObject({
|
|
77
|
+
op: z.literal("settings"),
|
|
78
|
+
visibility: Visibility.optional(),
|
|
79
|
+
history: HistoryPolicy.optional(),
|
|
80
|
+
retainDays: RetainDays.optional(),
|
|
81
|
+
post: Permit.optional(),
|
|
82
|
+
invite: Permit.optional(),
|
|
83
|
+
pins: Permit.optional(),
|
|
84
|
+
}),
|
|
85
|
+
z.strictObject({ op: z.literal("archive"), on: z.boolean() }),
|
|
86
|
+
z.strictObject({ op: z.literal("rekey"), epoch: z.number().int().min(1), cause: RekeyCause }),
|
|
87
|
+
]);
|
|
88
|
+
/** The plaintext of `create.sealedMeta` and of a `meta` body's fields. */
|
|
89
|
+
export const ConvMeta = z.strictObject({
|
|
90
|
+
name: ChannelName.optional(),
|
|
91
|
+
topic: z.string().max(250).optional(),
|
|
92
|
+
purpose: z.string().max(250).optional(),
|
|
93
|
+
});
|
|
94
|
+
// ---- message bodies (spec §16.3) ----
|
|
95
|
+
export const Mention = z.union([
|
|
96
|
+
z.strictObject({ p: ParticipantId }),
|
|
97
|
+
/** Every participant person. Never wakes an agent. */
|
|
98
|
+
z.strictObject({ all: z.literal("channel") }),
|
|
99
|
+
/** Every participant person online now. Never wakes an agent. */
|
|
100
|
+
z.strictObject({ all: z.literal("here") }),
|
|
101
|
+
]);
|
|
102
|
+
/** A file in the relay's blob store, sealed under its own AES-256-GCM key. The shape only; files land later. */
|
|
103
|
+
export const Attachment = z.strictObject({
|
|
104
|
+
blob: z.string().regex(/^b_[A-Za-z0-9_-]{43}$/, "blob id"),
|
|
105
|
+
key: Key32,
|
|
106
|
+
nonce: Nonce,
|
|
107
|
+
sha256: Hash,
|
|
108
|
+
bytes: z.number().int().positive().max(LIMITS.blobBytes),
|
|
109
|
+
mime: z.string().max(100),
|
|
110
|
+
name: z.string().max(255),
|
|
111
|
+
thumb: Base64Url.max(16_384).optional(),
|
|
112
|
+
});
|
|
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
|
+
/** A line's provenance when its person wrote it from a linked channel. Signed and sealed with the event. */
|
|
121
|
+
export const Via = z.strictObject({ channel: LinkChannel });
|
|
122
|
+
/**
|
|
123
|
+
* A person's linked channel, as a human-tier device records it in the person's `self` conversation (never the
|
|
124
|
+
* device log, which is public). `account` is the channel's own id for the person's account (Telegram `from.id`;
|
|
125
|
+
* Slack `team/user`); it exists only inside the sealed `self` conversation. The proof is the one-time code's digest
|
|
126
|
+
* and the six-digit code the person saw match on both sides. `revokedAt` ends it; the last writer wins by `seq`.
|
|
127
|
+
*/
|
|
128
|
+
export const LinkRecord = z.strictObject({
|
|
129
|
+
id: Ulid,
|
|
130
|
+
channel: LinkChannel,
|
|
131
|
+
account: z.string().regex(/^[A-Za-z0-9._:/-]{1,128}$/, "channel account id"),
|
|
132
|
+
/** The person's device that runs the bridge and wrote this record. */
|
|
133
|
+
host: MachineId,
|
|
134
|
+
proof: z.strictObject({ method: z.literal("code-sas"), codeDigest: Hash, sas: z.string().regex(/^[0-9]{6}$/, "six digits") }),
|
|
135
|
+
authority: z
|
|
136
|
+
.array(LinkAuthority)
|
|
137
|
+
.min(1)
|
|
138
|
+
.max(2)
|
|
139
|
+
.refine((a) => new Set(a).size === a.length, { message: "each authority once" }),
|
|
140
|
+
linkedAt: Millis,
|
|
141
|
+
revokedAt: Millis.optional(),
|
|
142
|
+
});
|
|
143
|
+
/**
|
|
144
|
+
* Who can read a piece of content, for the audience guard: a person, or a linked channel's provider as
|
|
145
|
+
* `ext:<channel>:<link id>`. Not on the wire; the guard's reader sets are typed with it.
|
|
146
|
+
*/
|
|
147
|
+
export const ReaderId = z.union([PersonId, z.string().regex(/^ext:(?:telegram|slack):[0-7][0-9A-HJKMNP-TV-Z]{25}$/, "ext:<channel>:<link id>")]);
|
|
148
|
+
export const ReaderSet = z.array(ReaderId).max(LIMITS.convParticipants + LIMITS.convParticipants);
|
|
149
|
+
export const ReceiptStage = z.enum(["delivered", "handed_to_agent", "read"]);
|
|
150
|
+
/**
|
|
151
|
+
* A task as a thread (spec §16.3): `request` is the thread root, from the requester to an agent participant;
|
|
152
|
+
* `status` and `artifact` come from that agent; `cancel` and `followUp` from the requester.
|
|
153
|
+
*/
|
|
154
|
+
export const TaskEventBody = z.discriminatedUnion("k", [
|
|
155
|
+
z.strictObject({ k: z.literal("request"), to: ParticipantId.refine(isAgent, "a task goes to an agent"), message: Message, delegation: DelegationChain }),
|
|
156
|
+
z.strictObject({ k: z.literal("status"), root: Ulid, update: TaskStatusUpdateEvent }),
|
|
157
|
+
z.strictObject({ k: z.literal("artifact"), root: Ulid, update: TaskArtifactUpdateEvent }),
|
|
158
|
+
z.strictObject({ k: z.literal("cancel"), root: Ulid, reason: z.string().max(1000).optional() }),
|
|
159
|
+
z.strictObject({ k: z.literal("followUp"), root: Ulid, message: Message }),
|
|
160
|
+
]);
|
|
161
|
+
export const ContextAccess = z.enum(["allow", "ask", "off"]);
|
|
162
|
+
export const ContextKind = z.enum(["own", "dm", "group", "channel.private", "channel.public"]);
|
|
163
|
+
export const ContextScope = z.union([z.strictObject({ cid: ConvId }), z.strictObject({ kind: ContextKind })]);
|
|
164
|
+
/** One of a person's rules for which of their agents may mine which conversations (spec §16.8). */
|
|
165
|
+
export const ContextRule = z.strictObject({
|
|
166
|
+
agent: z.union([SessionName, z.literal("*")]),
|
|
167
|
+
scope: ContextScope,
|
|
168
|
+
access: ContextAccess,
|
|
169
|
+
/** Absent: the conversation's retention. */
|
|
170
|
+
lookbackDays: RetainDays.optional(),
|
|
171
|
+
});
|
|
172
|
+
const scopeKey = (r) => `${r.agent} ${"cid" in r.scope ? `cid:${r.scope.cid}` : `kind:${r.scope.kind}`}`;
|
|
173
|
+
/** A person's whole rule set: at most one rule per (agent, scope). */
|
|
174
|
+
export const ContextRules = z
|
|
175
|
+
.array(ContextRule)
|
|
176
|
+
.max(LIMITS.contextRules)
|
|
177
|
+
.refine((rules) => new Set(rules.map(scopeKey)).size === rules.length, { message: "at most one rule per agent and scope" });
|
|
178
|
+
/** One sender chain at one index: `ck` is the chain key for message `idx` of `device`'s chain in `epoch`. */
|
|
179
|
+
export const KeyShare = z.strictObject({ epoch: Seq, device: MachineId, idx: Seq, ck: Key32 });
|
|
180
|
+
/** The plaintext of one recipient's sealed share in a `keys` event. */
|
|
181
|
+
export const KeySharePayload = z.strictObject({ shares: z.array(KeyShare).min(1).max(LIMITS.keyShares) });
|
|
182
|
+
/** The `self` conversation's bodies (spec §16.3, "The self conversation"). The last writer wins by `seq`. */
|
|
183
|
+
export const SyncBody = z.discriminatedUnion("s", [
|
|
184
|
+
z.strictObject({ s: z.literal("read"), cid: ConvId, upTo: Seq, thread: Ulid.optional() }),
|
|
185
|
+
z.strictObject({ s: z.literal("unread"), cid: ConvId, from: z.number().int().min(1) }),
|
|
186
|
+
z.strictObject({
|
|
187
|
+
s: z.literal("prefs"),
|
|
188
|
+
cid: ConvId,
|
|
189
|
+
notify: z.enum(["all", "mentions", "none"]),
|
|
190
|
+
muteUntil: Millis.optional(),
|
|
191
|
+
/** dms only; absent is off. */
|
|
192
|
+
receipts: z.boolean().optional(),
|
|
193
|
+
}),
|
|
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
|
+
/** A consent grant (spec §6.9), verified with `verifyConsentGrant` over this raw value. */
|
|
199
|
+
z.strictObject({ s: z.literal("grant"), grant: z.record(z.string(), z.unknown()) }),
|
|
200
|
+
z.strictObject({ s: z.literal("context"), rules: ContextRules }),
|
|
201
|
+
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
|
+
z.strictObject({ s: z.literal("link"), link: LinkRecord }),
|
|
204
|
+
]);
|
|
205
|
+
/** The plaintext of a `msg` event. */
|
|
206
|
+
export const MsgBody = z.discriminatedUnion("t", [
|
|
207
|
+
z.strictObject({
|
|
208
|
+
t: z.literal("line"),
|
|
209
|
+
text: z.string().max(LIMITS.lineChars),
|
|
210
|
+
format: z.enum(["plain", "md"]),
|
|
211
|
+
/** The thread root's event id. */
|
|
212
|
+
thread: Ulid.optional(),
|
|
213
|
+
/** A thread reply also shown in the main view. */
|
|
214
|
+
broadcast: z.boolean().optional(),
|
|
215
|
+
mentions: z.array(Mention).max(LIMITS.mentions).optional(),
|
|
216
|
+
files: z.array(Attachment).max(LIMITS.files).optional(),
|
|
217
|
+
/** A2A data parts, rendered as cards. */
|
|
218
|
+
parts: z.array(Part).max(8).optional(),
|
|
219
|
+
/** The mention this line answers. */
|
|
220
|
+
answers: Ulid.optional(),
|
|
221
|
+
grants: z.array(DelegationGrant).max(LIMITS.agentMentions).optional(),
|
|
222
|
+
/** Written by its person from a linked channel. Never on an agent's line. */
|
|
223
|
+
via: Via.optional(),
|
|
224
|
+
}),
|
|
225
|
+
z.strictObject({ t: z.literal("edit"), target: Ulid, text: z.string().max(LIMITS.lineChars), mentions: z.array(Mention).max(LIMITS.mentions).optional() }),
|
|
226
|
+
z.strictObject({ t: z.literal("delete"), target: Ulid }),
|
|
227
|
+
z.strictObject({ t: z.literal("react"), target: Ulid, key: z.string().min(1).max(64), on: z.boolean() }),
|
|
228
|
+
z.strictObject({ t: z.literal("pin"), target: Ulid, on: z.boolean() }),
|
|
229
|
+
z.strictObject({ t: z.literal("task"), task: TaskEventBody }),
|
|
230
|
+
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
|
+
z.strictObject({ t: z.literal("sync"), sync: SyncBody }),
|
|
241
|
+
]);
|
|
242
|
+
// ---- events and the log (spec §16.2) ----
|
|
243
|
+
const Position = z.strictObject({ seq: Seq, hash: Hash });
|
|
244
|
+
const header = {
|
|
245
|
+
v: z.literal(PROTOCOL),
|
|
246
|
+
realm: RealmId,
|
|
247
|
+
cid: ConvId,
|
|
248
|
+
/** Chosen by the author; unique in the conversation. */
|
|
249
|
+
id: Ulid,
|
|
250
|
+
by: z.strictObject({ person: PersonId, device: MachineId, agent: SessionName.optional() }),
|
|
251
|
+
ts: Millis,
|
|
252
|
+
/** The last op the author applied: its seq and h(seq), or {0, h(0)} before any. */
|
|
253
|
+
opHead: Position,
|
|
254
|
+
/** The head the author had, for gossip. */
|
|
255
|
+
seen: z.strictObject({ seq: z.number().int().min(1), hash: Hash }).optional(),
|
|
256
|
+
};
|
|
257
|
+
/** A control op, in plaintext: the relay routes and checks authority by it. */
|
|
258
|
+
export const ConvOpEvent = z.strictObject({ ...header, class: z.literal("op"), body: ConvOp, sig: Sig });
|
|
259
|
+
/** base64url(nonce ‖ AES-256-GCM ciphertext ‖ tag) of JCS(`MsgBody`), sealed under one message key. */
|
|
260
|
+
export const SealedBody = Base64Url.min(38).max(Math.ceil(((LIMITS.convPlaintextBytes + 28) * 4) / 3));
|
|
261
|
+
/** Everything else, sealed under the author device's sender key at (`epoch`, `idx`). */
|
|
262
|
+
export const ConvMsgEvent = z.strictObject({
|
|
263
|
+
...header,
|
|
264
|
+
class: z.literal("msg"),
|
|
265
|
+
epoch: Seq,
|
|
266
|
+
idx: Seq,
|
|
267
|
+
/** Own-person wake hint only: sessions of `by.person` that this event may wake. */
|
|
268
|
+
wake: z.array(SessionName).min(1).max(LIMITS.wakeHints).optional(),
|
|
269
|
+
body: SealedBody,
|
|
270
|
+
sig: Sig,
|
|
271
|
+
});
|
|
272
|
+
/** One recipient device's share, sealed to it under the envelope seal (spec §5, "Sealing"). */
|
|
273
|
+
export const SealedShare = z.strictObject({
|
|
274
|
+
to: z.strictObject({ person: PersonId, device: MachineId }),
|
|
275
|
+
epk: EncKey,
|
|
276
|
+
nonce: Nonce,
|
|
277
|
+
ct: Base64Url.max(Math.ceil(((LIMITS.keyShares * 160 + 16) * 4) / 3)),
|
|
278
|
+
});
|
|
279
|
+
export const KeysBody = z.strictObject({
|
|
280
|
+
shares: z
|
|
281
|
+
.array(SealedShare)
|
|
282
|
+
.min(1)
|
|
283
|
+
.max(LIMITS.keySharesPerEvent)
|
|
284
|
+
.refine((shares) => new Set(shares.map((s) => s.to.device)).size === shares.length, { message: "one share per device" }),
|
|
285
|
+
});
|
|
286
|
+
/** Sender-key and history shares, sealed pairwise; the relay serves each share only to its recipient. */
|
|
287
|
+
export const ConvKeysEvent = z.strictObject({ ...header, class: z.literal("keys"), body: KeysBody, sig: Sig });
|
|
288
|
+
export const ConvEvent = z.discriminatedUnion("class", [ConvOpEvent, ConvMsgEvent, ConvKeysEvent]);
|
|
289
|
+
/** An event as the relay stores and serves it. The relay does not sign it; members verify the chain. */
|
|
290
|
+
export const ConvLogEntry = z.strictObject({ seq: z.number().int().min(1), prev: Hash, event: ConvEvent });
|
|
291
|
+
/** A log position: a seq and h(seq). */
|
|
292
|
+
export const ConvHead = Position;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delegation chains (spec §6.7). A task request carries the chain of signed
|
|
3
|
+
* grants from its root requester to the session it addresses, so an executor (and whoever approves
|
|
4
|
+
* its work) sees who asked whom, for which task.
|
|
5
|
+
*
|
|
6
|
+
* sig = Ed25519(by, DOMAIN.delegation + JCS(grant without sig))
|
|
7
|
+
* grantHash = SHA-256(UTF-8(JCS(grant))), base64url, over the full grant including sig
|
|
8
|
+
* chain = [root grant, …, grant to this session]; each later grant has parent = grantHash(previous)
|
|
9
|
+
*
|
|
10
|
+
* Every hop is a new statement by the machine that delegates; nothing is forwarded as a credential.
|
|
11
|
+
* A hop can only narrow: scope ⊆ the previous scope (absent = unrestricted), maxDepth ≤ the previous
|
|
12
|
+
* maxDepth, exp ≤ the previous exp. `verifyChain` in core checks all of it.
|
|
13
|
+
*
|
|
14
|
+
* Grants are loose objects: a field a newer signer adds stays in the parsed grant, so the signature
|
|
15
|
+
* and the hash still cover it.
|
|
16
|
+
*/
|
|
17
|
+
import { z } from "../../vendor/zod/index.js";
|
|
18
|
+
import { Base64Url, Hash, LIMITS, MachineId, Millis, SessionAddress, Ulid } from "./common.js";
|
|
19
|
+
import { RealmId } from "./realm.js";
|
|
20
|
+
/** A scope label. Its meaning is left to clients; the protocol only checks that a hop never widens the set. */
|
|
21
|
+
export const DelegationScope = z.string().regex(/^[a-z0-9][a-z0-9._:/-]{0,63}$/, "scope label");
|
|
22
|
+
export const DelegationGrant = z.looseObject({
|
|
23
|
+
/** The realm of the signer and of every session in the chain (spec §5). */
|
|
24
|
+
realm: RealmId,
|
|
25
|
+
/** The task this grant asks `to` to run: the task id of this hop. */
|
|
26
|
+
task: Ulid,
|
|
27
|
+
/** The delegating session. Its person and label must be the roster's for `by`. */
|
|
28
|
+
from: SessionAddress,
|
|
29
|
+
/** The session asked to run `task`. */
|
|
30
|
+
to: SessionAddress,
|
|
31
|
+
/** The signing machine: a current member. */
|
|
32
|
+
by: MachineId,
|
|
33
|
+
ts: Millis,
|
|
34
|
+
/** `ts ≤ exp`, `exp − ts ≤ LIMITS.ttlMs`, and no later than the previous grant's exp. */
|
|
35
|
+
exp: Millis,
|
|
36
|
+
/** The most grants any chain through this grant may hold. */
|
|
37
|
+
maxDepth: z.number().int().min(1).max(LIMITS.delegationDepth),
|
|
38
|
+
/** Absent: unrestricted. `[]`: nothing named, the narrowest scope. */
|
|
39
|
+
scope: z.array(DelegationScope).max(LIMITS.delegationScopes).optional(),
|
|
40
|
+
/** grantHash of the previous grant; absent on the root. */
|
|
41
|
+
parent: Hash.optional(),
|
|
42
|
+
sig: Base64Url,
|
|
43
|
+
});
|
|
44
|
+
/** Root first. Carried as `MessagePayload.delegation` on `kind: "task"` only. */
|
|
45
|
+
export const DelegationChain = z.array(DelegationGrant).min(1).max(LIMITS.delegationDepth);
|
|
46
|
+
/** What a verified chain says, as the daemon shows it on `InboxItem.chain` and `TaskRecord.chain`. */
|
|
47
|
+
export const DelegationSummary = z.object({
|
|
48
|
+
/** Every session in the chain: the root requester first, the session the chain reaches last. */
|
|
49
|
+
addresses: z.array(SessionAddress).min(2).max(LIMITS.delegationDepth + 1),
|
|
50
|
+
/** Grants in the chain: `addresses.length − 1`. */
|
|
51
|
+
depth: z.number().int().min(1).max(LIMITS.delegationDepth),
|
|
52
|
+
/** The smallest maxDepth along the chain: the most grants any extension of it may hold. */
|
|
53
|
+
maxDepth: z.number().int().min(1).max(LIMITS.delegationDepth),
|
|
54
|
+
/** The last grant's scope; absent means unrestricted. Peer-chosen labels. */
|
|
55
|
+
scope: z.array(DelegationScope).max(LIMITS.delegationScopes).optional(),
|
|
56
|
+
/** The earliest grant expiry along the chain. */
|
|
57
|
+
exp: Millis,
|
|
58
|
+
});
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Directory frames (spec §7, "Directory frames"; design §3.2): the frames a hosted relay, one that runs the
|
|
3
|
+
* key directory, speaks on the same WebSocket as the relay binding. They follow the frame rules of §7: one
|
|
4
|
+
* JSON object per text frame, discriminated by `t`; requests carry `req`, replies echo it, and a refusal is
|
|
5
|
+
* the relay binding's `nack {id: req, code, message}` (with `retryAfterMs` for `rate_limited`).
|
|
6
|
+
*
|
|
7
|
+
* Connection, hosted:
|
|
8
|
+
* relay → challenge {proto, relay, nonce} (unchanged)
|
|
9
|
+
* client → [pre-auth, optional] dir.bind {req, entry} first binding of this device → dir.ok {req, person, seq, sth}
|
|
10
|
+
* client → [pre-auth, optional] dir.sth, dir.heads, dir.device what a device about to bind reads; heads only, no log
|
|
11
|
+
* client → [pre-auth, optional] dir.lookup {.., proof} a device reads its own person's log (its key proven)
|
|
12
|
+
* client → hello {realm, person, device, ts, sig} sig = Ed25519(device, DOMAIN.relayAuth + JCS(hostedRelayAuthPayload))
|
|
13
|
+
* relay → welcome {sth} or close 4001 (`unknown_device`) when the device is not current
|
|
14
|
+
*
|
|
15
|
+
* After welcome, besides send / deliver / presence / recv.ack / ping of §7:
|
|
16
|
+
* dir.append {req, entry} → dir.ok {req, seq, sth}
|
|
17
|
+
* dir.keys {req, stmt} → dir.ok {req, sth}
|
|
18
|
+
* dir.sth {req, size?, from?, epoch?} → dir.sthIs {req, sth, head, consistency?, epoch?, rotation?}
|
|
19
|
+
* dir.lookup {req, persons, size?} → dir.persons {req, sth, head, persons}
|
|
20
|
+
* dir.heads {req, persons, size?} → dir.headsAre {req, sth, head, persons}
|
|
21
|
+
* dir.resolve {req, handle | email} → dir.resolved {req, persons}
|
|
22
|
+
* dir.search {req, query, limit} → dir.resolved {req, persons}
|
|
23
|
+
* dir.watch {req, persons} → dir.ok {req}; then dir.changed {persons, sth} pushes
|
|
24
|
+
* presence.watch {req, persons} → dir.ok {req}
|
|
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")
|
|
27
|
+
* wake.disarm {req} → dir.ok {req}
|
|
28
|
+
* relay → dir.unbound {entry, sth} this device's own unbind, just before close 4003
|
|
29
|
+
*
|
|
30
|
+
* The directory's own structures (HostedPersonId, LogEntry, KeysStatement, STH, PersonView, ...) are in
|
|
31
|
+
* `schema/directory.ts` and `schema/common.ts`, with their verifiers in `packages/core/src/directory/` (spec §15).
|
|
32
|
+
* The frames take a hosted person (`HostedPersonId`) only.
|
|
33
|
+
*/
|
|
34
|
+
import { z } from "../../vendor/zod/index.js";
|
|
35
|
+
import { Base64Url, Hash, HostedPersonId, LIMITS, Millis, RELAY_PROTOCOL, SessionName, Ulid } from "./common.js";
|
|
36
|
+
import { EpochProof } from "./directory.js";
|
|
37
|
+
import { RealmId } from "./realm.js";
|
|
38
|
+
import { DeviceId, KeyRotation, KeysStatement, LogEntry, PersonHead, PersonView, STH } from "./directory.js";
|
|
39
|
+
import { Jws } from "./jelly.js";
|
|
40
|
+
const Req = z.string().min(1).max(64);
|
|
41
|
+
const Size = z.number().int().positive();
|
|
42
|
+
/** A hosted handle (design §6.1): the normalised local part of a verified email. */
|
|
43
|
+
export const Handle = z.string().regex(/^[a-z][a-z0-9-]{0,31}$/, "lowercase handle");
|
|
44
|
+
/** An exact email address, lower-cased (design §6.1): the domain is ASCII, as Jelly ID's verified emails are. */
|
|
45
|
+
export const Email = z
|
|
46
|
+
.string()
|
|
47
|
+
.max(254)
|
|
48
|
+
.regex(/^[^\s@A-Z]{1,64}@(?:[a-z0-9-]+\.)+[a-z0-9-]+$/, "lower-case email address");
|
|
49
|
+
/** An org (design §2.3): `domain:<d>` until Jelly contract v1.2, then the organisation id. Absent for an individual. */
|
|
50
|
+
export const OrgId = z.string().min(1).max(200);
|
|
51
|
+
/** The value a device signs (with `DOMAIN.relayAuth`) in the hosted `hello` frame. */
|
|
52
|
+
export function hostedRelayAuthPayload(args) {
|
|
53
|
+
return { proto: RELAY_PROTOCOL, relay: args.relay, realm: args.realm, person: args.person, device: args.device, nonce: args.nonce, ts: args.ts };
|
|
54
|
+
}
|
|
55
|
+
/** The value a device signs (with `DOMAIN.dirRead`) in a `dir.lookup` before `hello`: its key, on this connection. */
|
|
56
|
+
export function dirReadPayload(args) {
|
|
57
|
+
return { proto: RELAY_PROTOCOL, relay: args.relay, realm: args.realm, device: args.device, nonce: args.nonce };
|
|
58
|
+
}
|
|
59
|
+
/** A device's proof before `hello` (`dirReadPayload`): `sig` by `device` over this connection's challenge. */
|
|
60
|
+
export const DirReadProof = z.strictObject({ device: DeviceId, sig: Base64Url });
|
|
61
|
+
// ---- client → relay ----
|
|
62
|
+
/** Pre-auth: the first binding of this device, a `bind` entry with a `jelly` authority (design §2.2). */
|
|
63
|
+
export const DirBindFrame = z.object({ t: z.literal("dir.bind"), req: Req, entry: LogEntry });
|
|
64
|
+
/** The hosted `hello`: authenticates a device of a person in a directory realm. A roster relay refuses it. */
|
|
65
|
+
export const HostedHelloFrame = z.object({
|
|
66
|
+
t: z.literal("hello"),
|
|
67
|
+
realm: RealmId,
|
|
68
|
+
person: HostedPersonId,
|
|
69
|
+
device: DeviceId,
|
|
70
|
+
ts: Millis,
|
|
71
|
+
sig: Base64Url,
|
|
72
|
+
});
|
|
73
|
+
/** An `attest`, an `unbind`, or the `bind` of a QR approval (design §2.1, §2.5), appended to the sender's person's log. */
|
|
74
|
+
export const DirAppendFrame = z.object({ t: z.literal("dir.append"), req: Req, entry: LogEntry });
|
|
75
|
+
/** The sending device's own latest encryption-key statement (design §2.6). */
|
|
76
|
+
export const DirKeysFrame = z.object({ t: z.literal("dir.keys"), req: Req, stmt: KeysStatement });
|
|
77
|
+
/**
|
|
78
|
+
* The latest STH, or the STH at `size`. `from`: also a consistency proof from that size. `epoch`: also that
|
|
79
|
+
* epoch-log leaf with its inclusion proof (an entry's first epoch, design §2.2 step 7).
|
|
80
|
+
*/
|
|
81
|
+
export const DirSthFrame = z.object({
|
|
82
|
+
t: z.literal("dir.sth"),
|
|
83
|
+
req: Req,
|
|
84
|
+
size: Size.optional(),
|
|
85
|
+
from: Size.optional(),
|
|
86
|
+
epoch: z.number().int().nonnegative().optional(),
|
|
87
|
+
});
|
|
88
|
+
/**
|
|
89
|
+
* Persons' views at the directory's latest STH, which is at least `size`. `knownSeq`: send only the log after it.
|
|
90
|
+
* Before `hello`, only with `proof` (a device's key on this connection), and only for persons whose log names that
|
|
91
|
+
* device, current or not: a device reads its own person's log; anyone else reads heads (`dir.heads`).
|
|
92
|
+
*/
|
|
93
|
+
export const DirLookupFrame = z.object({
|
|
94
|
+
t: z.literal("dir.lookup"),
|
|
95
|
+
req: Req,
|
|
96
|
+
persons: z
|
|
97
|
+
.array(z.object({ person: HostedPersonId, knownSeq: z.number().int().nonnegative().optional() }))
|
|
98
|
+
.min(1)
|
|
99
|
+
.max(LIMITS.dirLookupPersons),
|
|
100
|
+
size: Size.optional(),
|
|
101
|
+
proof: DirReadProof.optional(),
|
|
102
|
+
});
|
|
103
|
+
/** Persons' heads (`PersonHead`) at the STH of `size`, or the latest; before or after `hello`. */
|
|
104
|
+
export const DirHeadsFrame = z.object({
|
|
105
|
+
t: z.literal("dir.heads"),
|
|
106
|
+
req: Req,
|
|
107
|
+
persons: z.array(HostedPersonId).min(1).max(LIMITS.dirLookupPersons),
|
|
108
|
+
size: Size.optional(),
|
|
109
|
+
});
|
|
110
|
+
/**
|
|
111
|
+
* Before `hello` only: the person whose log binds `device` as a current device, as `dir.headsAre` with that one head at
|
|
112
|
+
* the latest STH, or none. A device approved by link learns its person this way (design §2.5), then reads that
|
|
113
|
+
* person's log with `dir.lookup` and its proof.
|
|
114
|
+
*/
|
|
115
|
+
export const DirDeviceFrame = z.object({
|
|
116
|
+
t: z.literal("dir.device"),
|
|
117
|
+
req: Req,
|
|
118
|
+
device: DeviceId,
|
|
119
|
+
});
|
|
120
|
+
/** By handle (same org only) or by exact email: exactly one of the two. */
|
|
121
|
+
export const DirResolveFrame = z
|
|
122
|
+
.object({ t: z.literal("dir.resolve"), req: Req, handle: Handle.optional(), email: Email.optional() })
|
|
123
|
+
.refine((frame) => (frame.handle === undefined) !== (frame.email === undefined), { message: "exactly one of handle and email" });
|
|
124
|
+
/** A same-org prefix search on handle, name and email local part. */
|
|
125
|
+
export const DirSearchFrame = z.object({
|
|
126
|
+
t: z.literal("dir.search"),
|
|
127
|
+
req: Req,
|
|
128
|
+
query: z.string().min(1).max(64),
|
|
129
|
+
limit: z.number().int().min(1).max(LIMITS.dirSearchResults),
|
|
130
|
+
});
|
|
131
|
+
/** Sets the persons this connection watches for log and key changes; it replaces the previous set, and `[]` clears it. */
|
|
132
|
+
export const DirWatchFrame = z.object({
|
|
133
|
+
t: z.literal("dir.watch"),
|
|
134
|
+
req: Req,
|
|
135
|
+
persons: z.array(HostedPersonId).max(LIMITS.watchPersons),
|
|
136
|
+
});
|
|
137
|
+
/** Sets the persons whose presence this connection watches (design §8.2); it replaces the previous set. */
|
|
138
|
+
export const PresenceWatchFrame = z.object({
|
|
139
|
+
t: z.literal("presence.watch"),
|
|
140
|
+
req: Req,
|
|
141
|
+
persons: z.array(HostedPersonId).max(LIMITS.watchPersons),
|
|
142
|
+
});
|
|
143
|
+
/** The person erases their account from Messaging (design §3.7): an `unbind` of every current device, and a fresh `cnf` token for this device. */
|
|
144
|
+
export const DirEraseFrame = z.object({
|
|
145
|
+
t: z.literal("dir.erase"),
|
|
146
|
+
req: Req,
|
|
147
|
+
entries: z.array(LogEntry).min(1).max(LIMITS.devicesPerPerson),
|
|
148
|
+
token: Jws,
|
|
149
|
+
});
|
|
150
|
+
/** Most agents a wake ticket's filter names, and most awaited task requests (spec §7, "Wake tickets"). */
|
|
151
|
+
export const WAKE_AGENTS = 64;
|
|
152
|
+
export const WAKE_AWAITING = 500;
|
|
153
|
+
/** Most bytes of a wake ticket, and its longest life: Jelly's `jelly-wake+jwt` lives at most seven days. */
|
|
154
|
+
export const WAKE_TICKET_BYTES = 4096;
|
|
155
|
+
export const WAKE_TICKET_MAX_S = 7 * 24 * 60 * 60;
|
|
156
|
+
/** A wake URL: `https://<issuer host>/hosts/<hst>/wake` (or `http://` to a loopback issuer, for tests). */
|
|
157
|
+
export const WakeUrl = z.string().max(2048).regex(/^https?:\/\/[^/?#@\s]+\/hosts\/hst_[A-Za-z0-9._:-]{1,123}\/wake$/, "a Jelly ID wake URL");
|
|
158
|
+
/**
|
|
159
|
+
* A Jelly host's device arms a wake ticket before the host sleeps (spec §7, "Wake tickets"). `url` is Jelly ID's wake
|
|
160
|
+
* endpoint for this device's host; `ticket` the host's single-use `jelly-wake+jwt`, which the relay never verifies
|
|
161
|
+
* (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. A new
|
|
163
|
+
* arm replaces the device's previous one.
|
|
164
|
+
*/
|
|
165
|
+
export const WakeArmFrame = z.object({
|
|
166
|
+
t: z.literal("wake.arm"),
|
|
167
|
+
req: Req,
|
|
168
|
+
url: WakeUrl,
|
|
169
|
+
ticket: Jws.refine((ticket) => ticket.length <= WAKE_TICKET_BYTES, { message: `at most ${WAKE_TICKET_BYTES} bytes` }),
|
|
170
|
+
agents: z.array(SessionName).max(WAKE_AGENTS),
|
|
171
|
+
awaiting: z.array(Ulid).max(WAKE_AWAITING),
|
|
172
|
+
});
|
|
173
|
+
/** The relay forgets this device's wake ticket. */
|
|
174
|
+
export const WakeDisarmFrame = z.object({ t: z.literal("wake.disarm"), req: Req });
|
|
175
|
+
/** Every frame of a directory client that the relay binding (§7) does not already define. */
|
|
176
|
+
export const DirectoryClientFrame = z.discriminatedUnion("t", [
|
|
177
|
+
DirBindFrame,
|
|
178
|
+
HostedHelloFrame,
|
|
179
|
+
DirAppendFrame,
|
|
180
|
+
DirKeysFrame,
|
|
181
|
+
DirSthFrame,
|
|
182
|
+
DirLookupFrame,
|
|
183
|
+
DirDeviceFrame,
|
|
184
|
+
DirHeadsFrame,
|
|
185
|
+
DirResolveFrame,
|
|
186
|
+
DirSearchFrame,
|
|
187
|
+
DirWatchFrame,
|
|
188
|
+
PresenceWatchFrame,
|
|
189
|
+
DirEraseFrame,
|
|
190
|
+
WakeArmFrame,
|
|
191
|
+
WakeDisarmFrame,
|
|
192
|
+
]);
|
|
193
|
+
// ---- relay → client ----
|
|
194
|
+
/** The answer to a hosted `hello`: the directory's latest STH. */
|
|
195
|
+
export const HostedWelcomeFrame = z.object({ t: z.literal("welcome"), sth: STH });
|
|
196
|
+
/**
|
|
197
|
+
* Success. `dir.bind` gets `person`, `seq` and `sth`; `dir.append` gets `seq` and `sth`; `dir.keys` gets `sth`;
|
|
198
|
+
* `dir.watch`, `presence.watch` and `dir.erase` get none of them.
|
|
199
|
+
*/
|
|
200
|
+
export const DirOkFrame = z.object({
|
|
201
|
+
t: z.literal("dir.ok"),
|
|
202
|
+
req: Req,
|
|
203
|
+
person: HostedPersonId.optional(),
|
|
204
|
+
seq: z.number().int().nonnegative().optional(),
|
|
205
|
+
sth: STH.optional(),
|
|
206
|
+
});
|
|
207
|
+
/**
|
|
208
|
+
* An STH; `head` is its last epoch-log leaf (index `size − 1`, whose `mapRoot` lookups at this STH prove
|
|
209
|
+
* against) with its inclusion proof. `consistency` answers `from`, `epoch` answers `epoch`, and `rotation`
|
|
210
|
+
* comes with any STH signed by a new epoch key (design §3.8).
|
|
211
|
+
*/
|
|
212
|
+
export const DirSthIsFrame = z.object({
|
|
213
|
+
t: z.literal("dir.sthIs"),
|
|
214
|
+
req: Req,
|
|
215
|
+
sth: STH,
|
|
216
|
+
head: EpochProof,
|
|
217
|
+
consistency: z.array(Hash).max(64).optional(),
|
|
218
|
+
epoch: EpochProof.optional(),
|
|
219
|
+
rotation: KeyRotation.optional(),
|
|
220
|
+
});
|
|
221
|
+
/** One view per person asked, at `sth`. Fewer when the frame would pass `LIMITS.frameBytes`: ask again for the rest. */
|
|
222
|
+
export const DirPersonsFrame = z.object({
|
|
223
|
+
t: z.literal("dir.persons"),
|
|
224
|
+
req: Req,
|
|
225
|
+
sth: STH,
|
|
226
|
+
/** The STH's last epoch leaf (index `size − 1`) with its inclusion proof: what `verifyPersonView` checks the map against. */
|
|
227
|
+
head: EpochProof,
|
|
228
|
+
persons: z.array(PersonView).max(LIMITS.dirLookupPersons),
|
|
229
|
+
});
|
|
230
|
+
/** One head per person asked, at `sth`; `head` as in `dir.persons`. */
|
|
231
|
+
export const DirHeadsAreFrame = z.object({
|
|
232
|
+
t: z.literal("dir.headsAre"),
|
|
233
|
+
req: Req,
|
|
234
|
+
sth: STH,
|
|
235
|
+
head: EpochProof,
|
|
236
|
+
persons: z.array(PersonHead).max(LIMITS.dirLookupPersons),
|
|
237
|
+
});
|
|
238
|
+
/** A person a handle, an email or a search matched, as the directory reads them from the person's latest verified token. */
|
|
239
|
+
export const ResolvedPerson = z.object({
|
|
240
|
+
person: HostedPersonId,
|
|
241
|
+
handle: Handle,
|
|
242
|
+
name: z.string().max(200).optional(),
|
|
243
|
+
email: Email.optional(),
|
|
244
|
+
org: OrgId.optional(),
|
|
245
|
+
});
|
|
246
|
+
/** An unknown email and an undiscoverable one get the same empty answer. */
|
|
247
|
+
export const DirResolvedFrame = z.object({
|
|
248
|
+
t: z.literal("dir.resolved"),
|
|
249
|
+
req: Req,
|
|
250
|
+
persons: z.array(ResolvedPerson).max(LIMITS.dirSearchResults),
|
|
251
|
+
});
|
|
252
|
+
/** A push: the logs or keys of these watched persons changed as of `sth`. */
|
|
253
|
+
export const DirChangedFrame = z.object({
|
|
254
|
+
t: z.literal("dir.changed"),
|
|
255
|
+
persons: z.array(HostedPersonId).min(1).max(LIMITS.watchPersons),
|
|
256
|
+
sth: STH,
|
|
257
|
+
});
|
|
258
|
+
/** A push to a device just removed: its own signed `unbind` entry, before the relay closes with 4003. */
|
|
259
|
+
export const DirUnboundFrame = z.object({ t: z.literal("dir.unbound"), entry: LogEntry, sth: STH });
|
|
260
|
+
/** Every frame a directory relay sends that the relay binding (§7) does not already define. */
|
|
261
|
+
export const DirectoryRelayFrame = z.discriminatedUnion("t", [
|
|
262
|
+
HostedWelcomeFrame,
|
|
263
|
+
DirOkFrame,
|
|
264
|
+
DirSthIsFrame,
|
|
265
|
+
DirPersonsFrame,
|
|
266
|
+
DirHeadsAreFrame,
|
|
267
|
+
DirResolvedFrame,
|
|
268
|
+
DirChangedFrame,
|
|
269
|
+
DirUnboundFrame,
|
|
270
|
+
]);
|