@joe-broadhead/messaging 0.0.0-stage → 0.7.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 +175 -0
- package/LICENSE +21 -0
- package/NOTICE +12 -0
- package/README.md +36 -2
- package/dist/bin/messaging-mcp.js +25887 -0
- package/dist/bin/messaging-relay.js +19014 -0
- package/dist/bin/messaging.js +14732 -0
- package/dist/bin/messagingd.js +28682 -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 +458 -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/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 +44 -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 +254 -0
- package/dist/lib/modules/core/schema/consent.js +120 -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 +65 -0
- package/dist/lib/modules/core/schema/index.js +15 -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 +20 -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 +3426 -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 +948 -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/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 +38 -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 +272 -0
- package/dist/types/core/schema/consent.d.ts +140 -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 +750 -0
- package/dist/types/core/schema/errors.d.ts +104 -0
- package/dist/types/core/schema/index.d.ts +15 -0
- package/dist/types/core/schema/ipc/consent.d.ts +555 -0
- package/dist/types/core/schema/ipc/device.d.ts +729 -0
- package/dist/types/core/schema/ipc/jelly.d.ts +501 -0
- package/dist/types/core/schema/ipc/shapes.d.ts +411 -0
- package/dist/types/core/schema/ipc/surfaces.d.ts +674 -0
- package/dist/types/core/schema/ipc.d.ts +3131 -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 +1122 -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 +18 -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,320 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, diffieHellman, generateKeyPairSync, hkdfSync, randomBytes } from "node:crypto";
|
|
2
|
+
import { b64u, utf8 } from "./b64u.js";
|
|
3
|
+
import { asMessagingError, parseWith } from "./check.js";
|
|
4
|
+
import { canonicalize } from "./jcs.js";
|
|
5
|
+
import { privateKeyFromRaw, publicKeyFromRaw, rawPublicKey } from "./keys.js";
|
|
6
|
+
import { DOMAIN, LIMITS, PROTOCOL, Ulid } from "./schema/common.js";
|
|
7
|
+
import { Envelope, EnvelopeType, PAYLOAD_BY_TYPE, SEAL_ALG, } from "./schema/envelope.js";
|
|
8
|
+
import { MessagingError } from "./schema/errors.js";
|
|
9
|
+
import { signingInput, sign, verifyInput } from "./sign.js";
|
|
10
|
+
import { ulid } from "./ulid.js";
|
|
11
|
+
import { parseWire } from "./wire.js";
|
|
12
|
+
const Header = Envelope.omit({ body: true, sig: true });
|
|
13
|
+
const NONCE_BYTES = 12;
|
|
14
|
+
const TAG_BYTES = 16;
|
|
15
|
+
/** Builds, seals (spec §5) and signs an envelope. Presence is signed but not sealed. */
|
|
16
|
+
export function sealEnvelope(args) {
|
|
17
|
+
return seal(args, fresh);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* `sealEnvelope` with caller-chosen ephemeral key and nonce, for test vectors and tests only; exported from
|
|
21
|
+
* `@messaging/core/testing`. Reusing an ephemeral key and nonce for real traffic breaks confidentiality.
|
|
22
|
+
*/
|
|
23
|
+
export function sealEnvelopeWith(args, randomness) {
|
|
24
|
+
return seal(args, fixed(randomness));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Seals arbitrary `plaintext` bytes as the body of a sealed type, with a valid header and signature but no
|
|
28
|
+
* payload validation or canonicalization, so tests can build envelopes a conforming sender never makes.
|
|
29
|
+
* Tests only; exported from `@messaging/core/testing`.
|
|
30
|
+
*/
|
|
31
|
+
export function sealPlaintext(args, plaintext, randomness) {
|
|
32
|
+
const type = parseWith(EnvelopeType, args.type, "envelope type");
|
|
33
|
+
if (type === "presence")
|
|
34
|
+
throw new MessagingError("invalid_request", "presence is not sealed");
|
|
35
|
+
return sealBytes(args, type, args.class ?? classOf(args.from, args.to), plaintext, randomness === undefined ? fresh : fixed(randomness));
|
|
36
|
+
}
|
|
37
|
+
function fresh() {
|
|
38
|
+
return { ephemeral: generateKeyPairSync("x25519").privateKey, nonce: randomBytes(NONCE_BYTES) };
|
|
39
|
+
}
|
|
40
|
+
function fixed(randomness) {
|
|
41
|
+
return () => {
|
|
42
|
+
if (randomness.nonce.length !== NONCE_BYTES)
|
|
43
|
+
throw new MessagingError("invalid_request", "nonce must be 12 bytes");
|
|
44
|
+
return { ephemeral: privateKeyFromRaw("x25519", randomness.ephemeral), nonce: randomness.nonce };
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function seal(args, randomness) {
|
|
48
|
+
const type = parseWith(EnvelopeType, args.type, "envelope type");
|
|
49
|
+
const payload = parseWith(PAYLOAD_BY_TYPE[type], args.payload, `${type} payload`);
|
|
50
|
+
const plaintext = utf8(canonicalize(payload));
|
|
51
|
+
if (plaintext.length > LIMITS.plaintextBytes)
|
|
52
|
+
throw new MessagingError("too_large", `payload exceeds ${LIMITS.plaintextBytes} bytes`);
|
|
53
|
+
// Spec §5, "Class": a message request is the one class its payload chooses.
|
|
54
|
+
const klass = type === "message" && payload.conv?.kind === "request" ? "request" : classOf(args.from, args.to);
|
|
55
|
+
assertMeaning(type, args.from, args.to, klass, payload);
|
|
56
|
+
if (type !== "presence")
|
|
57
|
+
return sealBytes(args, type, klass, plaintext, randomness);
|
|
58
|
+
if (args.recipientEnc !== undefined)
|
|
59
|
+
throw new MessagingError("invalid_request", "presence is not sealed");
|
|
60
|
+
const header = parseWith(Header, { ...base(args, type, undefined), enc: "none" }, "envelope header");
|
|
61
|
+
assertShape(header);
|
|
62
|
+
return signed(args.identity, { ...header, body: payload });
|
|
63
|
+
}
|
|
64
|
+
function base(args, type, klass) {
|
|
65
|
+
if (args.from?.device !== args.identity.machine)
|
|
66
|
+
throw new MessagingError("invalid_request", "from.device must be the signing identity");
|
|
67
|
+
const ttl = args.ttlMs ?? LIMITS.ttlMs;
|
|
68
|
+
if (!Number.isSafeInteger(ttl) || ttl <= 0 || ttl > LIMITS.ttlMs)
|
|
69
|
+
throw new MessagingError("invalid_request", "ttlMs out of range");
|
|
70
|
+
const now = args.now ?? Date.now();
|
|
71
|
+
return {
|
|
72
|
+
v: PROTOCOL,
|
|
73
|
+
id: args.id ?? ulid(now),
|
|
74
|
+
realm: args.realm,
|
|
75
|
+
type,
|
|
76
|
+
from: args.from,
|
|
77
|
+
to: args.to,
|
|
78
|
+
ts: now,
|
|
79
|
+
exp: now + ttl,
|
|
80
|
+
anchor: args.anchor?.kind === "epoch" ? { kind: "epoch", size: args.anchor.size, root: args.anchor.root } : { kind: args.anchor?.kind, seq: args.anchor?.seq, hash: args.anchor?.hash },
|
|
81
|
+
...(klass === undefined ? {} : { class: klass }),
|
|
82
|
+
...(args.replyTo === undefined ? {} : { replyTo: args.replyTo }),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Spec §5, "Class": `self` between two devices of one person, `contact` otherwise (a message request chooses `request`). */
|
|
86
|
+
function classOf(from, to) {
|
|
87
|
+
return to !== undefined && "person" in to && to.person === from?.person ? "self" : "contact";
|
|
88
|
+
}
|
|
89
|
+
function sealBytes(args, type, klass, plaintext, randomness) {
|
|
90
|
+
const head = base(args, type, klass);
|
|
91
|
+
const recipient = recipientKey(args.recipientEnc);
|
|
92
|
+
const { ephemeral, nonce } = randomness();
|
|
93
|
+
const epk = rawPublicKey(ephemeral);
|
|
94
|
+
const header = parseWith(Header, { ...head, enc: SEAL_ALG, epk, nonce: b64u.encode(nonce) }, "envelope header");
|
|
95
|
+
assertShape(header);
|
|
96
|
+
let shared;
|
|
97
|
+
try {
|
|
98
|
+
shared = diffieHellman({ privateKey: ephemeral, publicKey: recipient.key });
|
|
99
|
+
}
|
|
100
|
+
catch (cause) {
|
|
101
|
+
throw new MessagingError("invalid_request", "recipientEnc is not a usable X25519 key", { cause });
|
|
102
|
+
}
|
|
103
|
+
const cipher = createCipheriv("aes-256-gcm", sealKey(shared, b64u.decode(epk), recipient.raw), nonce, { authTagLength: TAG_BYTES });
|
|
104
|
+
cipher.setAAD(utf8(canonicalize(header)));
|
|
105
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final(), cipher.getAuthTag()]);
|
|
106
|
+
return signed(args.identity, { ...header, body: b64u.encode(ciphertext) });
|
|
107
|
+
}
|
|
108
|
+
function recipientKey(enc) {
|
|
109
|
+
if (enc === undefined)
|
|
110
|
+
throw new MessagingError("invalid_request", "recipientEnc is required to seal");
|
|
111
|
+
try {
|
|
112
|
+
return { key: publicKeyFromRaw("x25519", enc), raw: b64u.decode(enc) };
|
|
113
|
+
}
|
|
114
|
+
catch (cause) {
|
|
115
|
+
throw new MessagingError("invalid_request", "recipientEnc is not an X25519 public key", { cause });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function signed(identity, unsigned) {
|
|
119
|
+
return { ...unsigned, sig: sign(identity, DOMAIN.envelope, unsigned) };
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Receiver checks, spec §5 steps 1–6. Membership is checked against the locally verified realm and the
|
|
123
|
+
* signature over the raw JSON value, so unknown fields from newer peers stay covered. Returns the raw
|
|
124
|
+
* envelope; deduplication and recipient policy are the caller's. Throws only MessagingError.
|
|
125
|
+
*/
|
|
126
|
+
export function verifyEnvelope(raw, ctx) {
|
|
127
|
+
try {
|
|
128
|
+
return check(raw, ctx);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
throw asMessagingError(error, "envelope");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function check(raw, ctx) {
|
|
135
|
+
if (!Number.isSafeInteger(ctx.now))
|
|
136
|
+
throw new MessagingError("invalid_request", "now must be an integer");
|
|
137
|
+
parseWith(Envelope, raw, "envelope");
|
|
138
|
+
const env = raw;
|
|
139
|
+
assertShape(env);
|
|
140
|
+
if (env.type === "presence" ? !isPlainObject(env.body) : typeof env.body !== "string") {
|
|
141
|
+
throw new MessagingError("invalid_request", "envelope body does not match its encoding");
|
|
142
|
+
}
|
|
143
|
+
const { sig, ...unsigned } = env;
|
|
144
|
+
// The signed bytes. Computing them here also requires I-JSON within LIMITS.jsonDepth (step 1).
|
|
145
|
+
const input = signingInput(DOMAIN.envelope, unsigned);
|
|
146
|
+
if (env.realm !== ctx.realm.realm)
|
|
147
|
+
throw new MessagingError("not_member", "envelope is for another realm");
|
|
148
|
+
if (env.anchor.kind !== ctx.realm.anchor().kind)
|
|
149
|
+
throw new MessagingError("invalid_request", "envelope anchor is of another realm kind");
|
|
150
|
+
if (env.ts > env.exp)
|
|
151
|
+
throw new MessagingError("expired", "envelope ts is after its exp");
|
|
152
|
+
if (env.ts > ctx.now + LIMITS.clockSkewMs)
|
|
153
|
+
throw new MessagingError("expired", "envelope timestamp is in the future");
|
|
154
|
+
if (env.exp <= ctx.now)
|
|
155
|
+
throw new MessagingError("expired", "envelope has expired");
|
|
156
|
+
if (env.exp - env.ts > LIMITS.ttlMs)
|
|
157
|
+
throw new MessagingError("expired", "envelope TTL exceeds the maximum");
|
|
158
|
+
// Step 4: a roster realm calls the sender not a member; a directory realm, an unknown device (spec §5).
|
|
159
|
+
const unknown = ctx.realm.anchor().kind === "epoch" ? "unknown_device" : "not_member";
|
|
160
|
+
const sender = ctx.realm.device(env.from.device);
|
|
161
|
+
if (!sender)
|
|
162
|
+
throw new MessagingError(unknown, "sender is not a current device of the realm");
|
|
163
|
+
if (sender.person !== env.from.person)
|
|
164
|
+
throw new MessagingError(unknown, "sender person does not match the realm");
|
|
165
|
+
if (!verifyInput(env.from.device, input, sig))
|
|
166
|
+
throw new MessagingError("bad_signature", "envelope signature is invalid");
|
|
167
|
+
if (ctx.self !== undefined && "device" in env.to) {
|
|
168
|
+
if (env.to.device !== ctx.self)
|
|
169
|
+
throw new MessagingError("invalid_request", "envelope is addressed to another device");
|
|
170
|
+
if (env.to.person !== ctx.realm.device(ctx.self)?.person)
|
|
171
|
+
throw new MessagingError("invalid_request", "envelope is addressed to another person");
|
|
172
|
+
}
|
|
173
|
+
return { env, sender };
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Decrypts (or, for presence, reads) the body and validates it against `PAYLOAD_BY_TYPE[type]`.
|
|
177
|
+
* A seal failure is `bad_signature`; an oversized body is `too_large`; a body that decrypts but is not
|
|
178
|
+
* UTF-8 I-JSON (`parseWire`, then `canonicalize`) or not a valid payload is `invalid_request`, so every
|
|
179
|
+
* payload returned canonicalizes. Throws only MessagingError, and never chains the plaintext as a cause.
|
|
180
|
+
*/
|
|
181
|
+
export function openEnvelope(env, identity) {
|
|
182
|
+
try {
|
|
183
|
+
return open(env, identity);
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
throw asMessagingError(error, "envelope");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function open(env, identity) {
|
|
190
|
+
const type = parseWith(EnvelopeType, env.type, "envelope type");
|
|
191
|
+
assertShape(env);
|
|
192
|
+
const schema = PAYLOAD_BY_TYPE[type];
|
|
193
|
+
if (type === "presence") {
|
|
194
|
+
if (utf8(canonicalize(env.body)).length > LIMITS.presenceBytes)
|
|
195
|
+
throw new MessagingError("too_large", "presence body is too large");
|
|
196
|
+
return parseWith(schema, env.body, "presence payload");
|
|
197
|
+
}
|
|
198
|
+
const plaintext = decrypt(env, identity);
|
|
199
|
+
let text;
|
|
200
|
+
try {
|
|
201
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(plaintext);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
throw new MessagingError("invalid_request", "envelope body is not UTF-8");
|
|
205
|
+
}
|
|
206
|
+
const value = parseWire(text, { maxBytes: LIMITS.plaintextBytes });
|
|
207
|
+
// parseWire rejects duplicate names, lone surrogates and deep nesting; this adds the I-JSON number rules.
|
|
208
|
+
canonicalize(value);
|
|
209
|
+
const payload = parseWith(schema, value, `${type} payload`);
|
|
210
|
+
assertMeaning(type, env.from, env.to, env.class, payload);
|
|
211
|
+
return payload;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Spec §5, "Meaning": a `message` to an agent is mail or a task request, in no other conversation; one to a device
|
|
215
|
+
* with no agent is a chat line (a DM with no `conv`, or a channel change or post), a sync between one person's own
|
|
216
|
+
* devices, a message request or a contact card: kind `message`, outside any task, with a ULID
|
|
217
|
+
* `messageId` and, when set, a ULID `contextId`. A `self` sync goes only between two devices of one person; a card
|
|
218
|
+
* only between two persons, from the person (no agent); a message request is exactly the envelope of class
|
|
219
|
+
* `request`.
|
|
220
|
+
*/
|
|
221
|
+
function assertMeaning(type, from, to, klass, payload) {
|
|
222
|
+
if (type !== "message" || to === undefined || !("device" in to))
|
|
223
|
+
return;
|
|
224
|
+
const p = payload;
|
|
225
|
+
if ((klass === "request") !== (p.conv?.kind === "request"))
|
|
226
|
+
throw new MessagingError("invalid_request", "a message request is exactly an envelope of class request");
|
|
227
|
+
if (to.agent !== undefined) {
|
|
228
|
+
if (p.conv !== undefined)
|
|
229
|
+
throw new MessagingError("invalid_request", "mail to an agent has no conv");
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (p.conv?.kind === "self" && from?.person !== to.person)
|
|
233
|
+
throw new MessagingError("invalid_request", "a self sync goes between devices of one person");
|
|
234
|
+
if (p.conv?.kind === "card" && (from?.person === to.person || from?.agent !== undefined))
|
|
235
|
+
throw new MessagingError("invalid_request", "a card goes from a person to another person");
|
|
236
|
+
const line = p.kind === "message" &&
|
|
237
|
+
p.message.taskId === undefined &&
|
|
238
|
+
Ulid.safeParse(p.message.messageId).success &&
|
|
239
|
+
(p.message.contextId === undefined || Ulid.safeParse(p.message.contextId).success);
|
|
240
|
+
if (!line)
|
|
241
|
+
throw new MessagingError("invalid_request", "a chat line is kind message, outside any task, with ULID messageId and contextId");
|
|
242
|
+
}
|
|
243
|
+
function decrypt(env, identity) {
|
|
244
|
+
let data;
|
|
245
|
+
try {
|
|
246
|
+
data = b64u.decode(env.body);
|
|
247
|
+
}
|
|
248
|
+
catch (cause) {
|
|
249
|
+
throw new MessagingError("bad_signature", "envelope seal failed", { cause });
|
|
250
|
+
}
|
|
251
|
+
if (data.length - TAG_BYTES > LIMITS.plaintextBytes)
|
|
252
|
+
throw new MessagingError("too_large", "envelope body is too large");
|
|
253
|
+
try {
|
|
254
|
+
const epk = b64u.decode(env.epk ?? "");
|
|
255
|
+
const nonce = b64u.decode(env.nonce ?? "");
|
|
256
|
+
if (nonce.length !== NONCE_BYTES || data.length < TAG_BYTES)
|
|
257
|
+
throw new Error("bad seal lengths");
|
|
258
|
+
const shared = diffieHellman({ privateKey: identity.encKey, publicKey: publicKeyFromRaw("x25519", env.epk ?? "") });
|
|
259
|
+
const { body: _body, sig: _sig, ...header } = env;
|
|
260
|
+
const decipher = createDecipheriv("aes-256-gcm", sealKey(shared, epk, b64u.decode(identity.enc)), nonce, { authTagLength: TAG_BYTES });
|
|
261
|
+
decipher.setAAD(utf8(canonicalize(header)));
|
|
262
|
+
decipher.setAuthTag(data.subarray(data.length - TAG_BYTES));
|
|
263
|
+
return Buffer.concat([decipher.update(data.subarray(0, data.length - TAG_BYTES)), decipher.final()]);
|
|
264
|
+
}
|
|
265
|
+
catch (cause) {
|
|
266
|
+
throw new MessagingError("bad_signature", "envelope seal failed", { cause });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function sealKey(shared, epk, recipientEnc) {
|
|
270
|
+
return Buffer.from(hkdfSync("sha256", shared, Buffer.concat([epk, recipientEnc]), DOMAIN.seal, 32));
|
|
271
|
+
}
|
|
272
|
+
/** Type, encoding and addressing must agree: presence is plain and team-wide; every other type is sealed to a device. */
|
|
273
|
+
function assertShape(env) {
|
|
274
|
+
const problem = shapeProblem(env);
|
|
275
|
+
if (problem)
|
|
276
|
+
throw new MessagingError("invalid_request", problem);
|
|
277
|
+
}
|
|
278
|
+
function shapeProblem(env) {
|
|
279
|
+
if (env.type === "presence") {
|
|
280
|
+
if (env.enc !== "none")
|
|
281
|
+
return "presence must not be sealed";
|
|
282
|
+
if (!("team" in env.to) || "device" in env.to)
|
|
283
|
+
return "presence must be addressed to the team";
|
|
284
|
+
if (env.epk !== undefined || env.nonce !== undefined)
|
|
285
|
+
return "a plain envelope has no epk or nonce";
|
|
286
|
+
if (env.class !== undefined)
|
|
287
|
+
return "presence has no class";
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
if (env.enc !== SEAL_ALG)
|
|
291
|
+
return `${env.type} must be sealed`;
|
|
292
|
+
// A task update names the task's request (spec §6.2).
|
|
293
|
+
if ((env.type === "task.status" || env.type === "task.artifact") && env.replyTo === undefined)
|
|
294
|
+
return `${env.type} needs replyTo`;
|
|
295
|
+
if (!("device" in env.to) || "team" in env.to)
|
|
296
|
+
return `${env.type} must be addressed to a device`;
|
|
297
|
+
if (env.epk === undefined || env.nonce === undefined)
|
|
298
|
+
return "a sealed envelope needs epk and nonce";
|
|
299
|
+
// Spec §5, "Class": the header says whether the two ends are one person.
|
|
300
|
+
if (env.class === undefined)
|
|
301
|
+
return "a sealed envelope needs class";
|
|
302
|
+
// Spec §6.9, "Message requests": a person's line to another person, with no agent at either end and no reply.
|
|
303
|
+
if (env.class === "request") {
|
|
304
|
+
if (env.type !== "message")
|
|
305
|
+
return "only a message is of class request";
|
|
306
|
+
if (classOf(env.from, env.to) !== "contact")
|
|
307
|
+
return "a message request goes to another person";
|
|
308
|
+
if (env.from.agent !== undefined || ("agent" in env.to && env.to.agent !== undefined))
|
|
309
|
+
return "a message request goes from a person to a person";
|
|
310
|
+
if (env.replyTo !== undefined)
|
|
311
|
+
return "a message request answers nothing";
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
if (env.class !== classOf(env.from, env.to))
|
|
315
|
+
return `class ${env.class} does not match the ends`;
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
function isPlainObject(value) {
|
|
319
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
320
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { DOMAIN } from "./schema/common.js";
|
|
2
|
+
import { Envelope } from "./schema/envelope.js";
|
|
3
|
+
import { verify } from "./sign.js";
|
|
4
|
+
export function headRelation(roster, head) {
|
|
5
|
+
return roster.relate({ kind: "roster", seq: head.seq, hash: head.hash });
|
|
6
|
+
}
|
|
7
|
+
/** The anchor a roster realm's envelopes carry: the roster's head (spec §5). */
|
|
8
|
+
export function rosterAnchor(roster) {
|
|
9
|
+
return roster.anchor();
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The roster head `raw` commits to, if `raw` is an envelope of `realm` (schema) whose signature over the
|
|
13
|
+
* raw JSON verifies under `from.device`. Membership and the time window are not checked: a receiver
|
|
14
|
+
* uses this to bring its roster up to the head before it checks membership (a new joiner is not in a
|
|
15
|
+
* stale roster), and to check stored evidence. Never throws; anything else is undefined.
|
|
16
|
+
*/
|
|
17
|
+
export function signedHead(raw, realm) {
|
|
18
|
+
if (!Envelope.safeParse(raw).success)
|
|
19
|
+
return undefined;
|
|
20
|
+
const env = raw;
|
|
21
|
+
if (env.realm !== realm || env.anchor.kind !== "roster")
|
|
22
|
+
return undefined;
|
|
23
|
+
const { sig, ...unsigned } = env;
|
|
24
|
+
if (!verify(env.from.device, DOMAIN.envelope, unsigned, sig))
|
|
25
|
+
return undefined;
|
|
26
|
+
return { head: { seq: env.anchor.seq, hash: env.anchor.hash }, machine: env.from.device, person: env.from.person, id: env.id };
|
|
27
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { generateKeyPairSync } from "node:crypto";
|
|
2
|
+
import { z } from "../vendor/zod/index.js";
|
|
3
|
+
import { b64u } from "./b64u.js";
|
|
4
|
+
import { parseWith } from "./check.js";
|
|
5
|
+
import { isPrivateKey, privateKeyFromRaw, rawPrivateKey, rawPublicKey } from "./keys.js";
|
|
6
|
+
import { EncKey, MachineId, PROTOCOL } from "./schema/common.js";
|
|
7
|
+
import { MessagingError } from "./schema/errors.js";
|
|
8
|
+
const PrivateKey32 = z.string().regex(/^[A-Za-z0-9_-]{43}$/, "32 bytes, base64url without padding");
|
|
9
|
+
/** `$MESSAGING_HOME/identity.json`. `signingKey` and `encKey` are the private JWK `d` values. */
|
|
10
|
+
export const IdentityFile = z.object({
|
|
11
|
+
v: z.literal(PROTOCOL),
|
|
12
|
+
machine: MachineId,
|
|
13
|
+
enc: EncKey,
|
|
14
|
+
signingKey: PrivateKey32,
|
|
15
|
+
encKey: PrivateKey32,
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* A machine identity: an Ed25519 signing key and an X25519 encryption key. The public ids are
|
|
19
|
+
* derived from the private keys, so they always match. The private keys are held in private
|
|
20
|
+
* fields and never appear in `JSON.stringify`, `String()` or `util.inspect` output.
|
|
21
|
+
*/
|
|
22
|
+
export class Identity {
|
|
23
|
+
machine;
|
|
24
|
+
enc;
|
|
25
|
+
#signingKey;
|
|
26
|
+
#encKey;
|
|
27
|
+
constructor(signingKey, encKey) {
|
|
28
|
+
if (!isPrivateKey(signingKey, "ed25519"))
|
|
29
|
+
throw new MessagingError("invalid_request", "signing key must be an Ed25519 private key");
|
|
30
|
+
if (!isPrivateKey(encKey, "x25519"))
|
|
31
|
+
throw new MessagingError("invalid_request", "encryption key must be an X25519 private key");
|
|
32
|
+
this.#signingKey = signingKey;
|
|
33
|
+
this.#encKey = encKey;
|
|
34
|
+
this.machine = rawPublicKey(signingKey);
|
|
35
|
+
this.enc = rawPublicKey(encKey);
|
|
36
|
+
}
|
|
37
|
+
get signingKey() {
|
|
38
|
+
return this.#signingKey;
|
|
39
|
+
}
|
|
40
|
+
get encKey() {
|
|
41
|
+
return this.#encKey;
|
|
42
|
+
}
|
|
43
|
+
toJSON() {
|
|
44
|
+
return { machine: this.machine, enc: this.enc };
|
|
45
|
+
}
|
|
46
|
+
toString() {
|
|
47
|
+
return `Identity(${this.machine})`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function generateIdentity() {
|
|
51
|
+
return new Identity(generateKeyPairSync("ed25519").privateKey, generateKeyPairSync("x25519").privateKey);
|
|
52
|
+
}
|
|
53
|
+
/** The identity file contents. This is the only API that returns private key material. */
|
|
54
|
+
export function exportIdentity(identity) {
|
|
55
|
+
return {
|
|
56
|
+
v: PROTOCOL,
|
|
57
|
+
machine: identity.machine,
|
|
58
|
+
enc: identity.enc,
|
|
59
|
+
signingKey: rawPrivateKey(identity.signingKey),
|
|
60
|
+
encKey: rawPrivateKey(identity.encKey),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Validates an identity file and checks that its public keys belong to its private keys. */
|
|
64
|
+
export function importIdentity(file) {
|
|
65
|
+
const parsed = parseWith(IdentityFile, file, "identity file");
|
|
66
|
+
const identity = new Identity(privateKeyFromRaw("ed25519", b64u.decode(parsed.signingKey)), privateKeyFromRaw("x25519", b64u.decode(parsed.encKey)));
|
|
67
|
+
if (identity.machine !== parsed.machine || identity.enc !== parsed.enc) {
|
|
68
|
+
throw new MessagingError("invalid_request", "identity public keys do not match its private keys");
|
|
69
|
+
}
|
|
70
|
+
return identity;
|
|
71
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Wire contracts (frozen; change only with a spec update and new test vectors).
|
|
2
|
+
export * from "./schema/index.js";
|
|
3
|
+
// Protocol logic; see docs/spec/messaging-v1.md §9.
|
|
4
|
+
export { b64u } from "./b64u.js";
|
|
5
|
+
export { VERSION } from "./version.js";
|
|
6
|
+
export { canonicalize } from "./jcs.js";
|
|
7
|
+
export { parseWire } from "./wire.js";
|
|
8
|
+
export { sha256 } from "./hash.js";
|
|
9
|
+
export { ulid } from "./ulid.js";
|
|
10
|
+
export { Identity, IdentityFile, generateIdentity, exportIdentity, importIdentity } from "./identity.js";
|
|
11
|
+
export { sign, verify } from "./sign.js";
|
|
12
|
+
// Fixed-randomness and raw sealing for tests live in `@messaging/core/testing`.
|
|
13
|
+
export { sealEnvelope, verifyEnvelope, openEnvelope } from "./envelope.js";
|
|
14
|
+
export { headRelation, rosterAnchor, signedHead } from "./head.js";
|
|
15
|
+
export { Roster, entryHash, createGenesis, createEntry, createInvite, decodeTicket, createRedeem, relayAuthPayload, inviteJoinPayload, inviteJoinProof, } from "./roster.js";
|
|
16
|
+
// A daemon under a Jelly host (spec §8.11): action grants and session tickets.
|
|
17
|
+
export { ACT_GRANT_MAX_S, ACTION_CLASSES, HOST_GRANT_MAX_S, JELLY_HOST_SKEW_S, SESSION_TICKET_MAX_S, actionClass, grantInput, parseActKey, readWakeTicket, verifyActGrant, verifySessionTicket, } from "./jelly-host.js";
|
|
18
|
+
// Hosted realms: the key directory's data structures and verifiers (spec §15).
|
|
19
|
+
export { personId } from "./directory/person.js";
|
|
20
|
+
export { DirectoryRealmConfig, directoryRealmId } from "./directory/realm.js";
|
|
21
|
+
export { emailOrg, hostedHandle } from "./directory/handle.js";
|
|
22
|
+
export { JELLY_HISTORY_TTL_MS, JELLY_IAT_LEEWAY_S, JELLY_JWKS_TTL_MS, JELLY_MAX_LIFETIME_S, JELLY_REFETCH_MIN_MS, JELLY_REVOCATION_CHECK_MS, JellyKeyring, cnfJwk, decodeJellyToken, isBindableKey, jellyKeyCovers, jellyMintRefusal, verifyJellyToken, } from "./directory/jelly-token.js";
|
|
23
|
+
export { DeviceLog, logEntryHash, signLogEntry, } from "./directory/device-log.js";
|
|
24
|
+
export { DEVICE_GRANT_TYPE, JELLY_ANSWER_BYTES, LOGIN_INTERVAL_MAX_S, LOGIN_SLOW_DOWN_S, deviceCodeForm, devicePollForm, jellyKeyFingerprint, jellyKeyThumbprint, readDeviceCodeAnswer, readDevicePoll, readDeviceSignIn, } from "./directory/jelly-device.js";
|
|
25
|
+
export { LINK_PREFIX, createLinkRequest, decodeLink, encodeLink, linkSas, verifyLinkRequest, verifyLinkSignature } from "./directory/link.js";
|
|
26
|
+
export { createKeysStatement, keysStatementHash, verifyKeysStatement } from "./directory/keys-statement.js";
|
|
27
|
+
export { adoptSth, epochAnchor, epochLeafHash, signKeyRotation, signSth, verifyEpochProof, verifySthSignature, } from "./directory/epoch.js";
|
|
28
|
+
export { checkPersonSuccession, DirectoryInconsistency, verifyPersonHead, verifyPersonView, } from "./directory/person-view.js";
|
|
29
|
+
export { MerkleLog, merkleLeafHash, verifyConsistency, verifyInclusion } from "./directory/merkle-log.js";
|
|
30
|
+
export { MAP_DEPTH, SparseMerkleTree, TOMBSTONE, emptyMapHash, mapKey, mapRootFromProof, mapValue, verifyMapProof } from "./directory/smt.js";
|
|
31
|
+
export { parseAddress, formatAddress } from "./address.js";
|
|
32
|
+
export { decodeAdmitCard, decodeTeamCard, encodeAdmitCard, encodeTeamCard, } from "./cards.js";
|
|
33
|
+
export { decide, effectivePolicy, grantSubjectKey, levelsOf, preferGrant, resignGrant, restrictingLevels, signConsentGrant, verifyConsentGrant, wideningLevels, } from "./consent.js";
|
|
34
|
+
export { cardCan, signContactCard, verifyContactCard } from "./contact-card.js";
|
|
35
|
+
export { createGrant, verifyChain } from "./delegation.js";
|
|
36
|
+
export { canTransition, isTerminal, isInterrupted } from "./task.js";
|
|
37
|
+
export { TERMINAL_STATES } from "./schema/a2a.js";
|
|
38
|
+
export { cleanPeerText, createFence, terminalSafe, displayWidth, HOST_TAGS, PEER_FENCE_TAG, PEER_DATA_NOTICE, provenanceOf } from "./peer-text.js";
|
|
39
|
+
// The agent tool contract shared by the OpenCode plugin and messaging-mcp (spec §11, "Agent tools").
|
|
40
|
+
export { AgentTool, TOOL_VERBS, TOOL_DESCRIPTIONS, ToolExpect, UNTRUSTED_SENTENCE, TOOL_REMEDIES, DONE_STATES, TaskStateName, ToolErrorBody, ToolFiles, a2aTaskState, conversationOf, decodeToolCursor, encodeToolCursor, formatToolAddress, parseToolAddress, taskStateName, toolErrorBody, toolName, } from "./agent-tools.js";
|
|
41
|
+
// The agent tools' operations and presenters, shared by the plugin and messaging-mcp (spec §11).
|
|
42
|
+
export { Operations, } from "./agent/operations.js";
|
|
43
|
+
export { agentCan, consentOf, conversationOfPeer, followUpRead, isoOf, presentAgent, presentConvEntry, presentInboxItem, presentLine, presentMail, presentSent, presentTask, presentTaskItem, presentTaskView, } from "./agent/present.js";
|
|
44
|
+
export { chainPath, compact, delegated, delegationPrompt, EXPECT_TEXT, isSafeId, messageAttributes, validatedAddress, } from "./agent/text.js";
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { LIMITS } from "./schema/common.js";
|
|
2
|
+
import { MessagingError } from "./schema/errors.js";
|
|
3
|
+
const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
|
4
|
+
/** True when `text` has a UTF-16 surrogate without its partner, which has no UTF-8 form. */
|
|
5
|
+
function hasLoneSurrogate(text) {
|
|
6
|
+
return LONE_SURROGATE.test(text);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* RFC 8785 JSON Canonicalization Scheme over the I-JSON subset Messaging signs.
|
|
10
|
+
* Object members whose value is `undefined` are dropped. Throws `invalid_request` for anything
|
|
11
|
+
* that is not I-JSON: NaN, ±Infinity, integers beyond ±(2^53 − 1), bigint, functions, symbols,
|
|
12
|
+
* `undefined` array elements or top-level values, lone surrogates, cycles, and non-plain objects.
|
|
13
|
+
* It also throws `invalid_request` for arrays and objects nested deeper than `LIMITS.jsonDepth`.
|
|
14
|
+
*
|
|
15
|
+
* The cost is linear in the size of the value, and the result never depends on the call stack: each
|
|
16
|
+
* node is visited once with O(1) bookkeeping, recursion stops at `LIMITS.jsonDepth`, and a cycle is
|
|
17
|
+
* caught by that same limit (sorting member names is the only superlinear step).
|
|
18
|
+
*/
|
|
19
|
+
export function canonicalize(value) {
|
|
20
|
+
return write(value, []);
|
|
21
|
+
}
|
|
22
|
+
function write(value, ancestors) {
|
|
23
|
+
if (value === null)
|
|
24
|
+
return "null";
|
|
25
|
+
switch (typeof value) {
|
|
26
|
+
case "string":
|
|
27
|
+
return string(value);
|
|
28
|
+
case "number":
|
|
29
|
+
return number(value);
|
|
30
|
+
case "boolean":
|
|
31
|
+
return value ? "true" : "false";
|
|
32
|
+
case "object":
|
|
33
|
+
break;
|
|
34
|
+
default:
|
|
35
|
+
throw invalid(`${typeof value} is not JSON`);
|
|
36
|
+
}
|
|
37
|
+
const object = value;
|
|
38
|
+
// A cycle always runs into the depth limit, so the ancestors are searched only once that is hit.
|
|
39
|
+
if (ancestors.length >= LIMITS.jsonDepth) {
|
|
40
|
+
throw invalid(ancestors.includes(object) ? "cyclic value" : `nested deeper than ${LIMITS.jsonDepth}`);
|
|
41
|
+
}
|
|
42
|
+
ancestors.push(object);
|
|
43
|
+
let out;
|
|
44
|
+
if (Array.isArray(object)) {
|
|
45
|
+
out = "[";
|
|
46
|
+
for (let i = 0; i < object.length; i++) {
|
|
47
|
+
const item = object[i];
|
|
48
|
+
if (item === undefined)
|
|
49
|
+
throw invalid("undefined array element");
|
|
50
|
+
if (i > 0)
|
|
51
|
+
out += ",";
|
|
52
|
+
out += write(item, ancestors);
|
|
53
|
+
}
|
|
54
|
+
out += "]";
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const proto = Object.getPrototypeOf(object);
|
|
58
|
+
if (proto !== Object.prototype && proto !== null && Object.getPrototypeOf(proto) !== null)
|
|
59
|
+
throw invalid("only plain objects are JSON");
|
|
60
|
+
const record = object;
|
|
61
|
+
const keys = Object.keys(record);
|
|
62
|
+
if (keys.length > 1)
|
|
63
|
+
keys.sort();
|
|
64
|
+
out = "{";
|
|
65
|
+
for (let i = 0; i < keys.length; i++) {
|
|
66
|
+
const key = keys[i];
|
|
67
|
+
const member = record[key];
|
|
68
|
+
if (member === undefined)
|
|
69
|
+
continue;
|
|
70
|
+
out += `${out.length > 1 ? "," : ""}${string(key)}:${write(member, ancestors)}`;
|
|
71
|
+
}
|
|
72
|
+
out += "}";
|
|
73
|
+
}
|
|
74
|
+
ancestors.pop();
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
function number(value) {
|
|
78
|
+
if (Number.isSafeInteger(value))
|
|
79
|
+
return String(value);
|
|
80
|
+
if (!Number.isFinite(value))
|
|
81
|
+
throw invalid("non-finite number");
|
|
82
|
+
if (Number.isInteger(value))
|
|
83
|
+
throw invalid("integer outside ±(2^53 − 1)");
|
|
84
|
+
// ECMAScript Number::toString, as RFC 8785 §3.2.2.3 requires.
|
|
85
|
+
return String(value);
|
|
86
|
+
}
|
|
87
|
+
function string(value) {
|
|
88
|
+
const json = JSON.stringify(value);
|
|
89
|
+
// JSON.stringify escapes a lone surrogate as \udXXX, so only such output needs the exact check.
|
|
90
|
+
if (json.includes("\\ud") && hasLoneSurrogate(value))
|
|
91
|
+
throw invalid("lone surrogate");
|
|
92
|
+
return json;
|
|
93
|
+
}
|
|
94
|
+
function invalid(reason) {
|
|
95
|
+
return new MessagingError("invalid_request", `not I-JSON: ${reason}`);
|
|
96
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Jelly host double, for tests only (exported from `@messaging/core/testing`): it holds an action key and mints
|
|
3
|
+
* action grants (`jelly-act+jwt`) and session tickets (`jelly-session+jwt`) as Jelly's host does (spec §8.11). Every
|
|
4
|
+
* claim can be overridden, so tests can mint what a conforming host never would.
|
|
5
|
+
*/
|
|
6
|
+
import { generateKeyPairSync } from "node:crypto";
|
|
7
|
+
import { signJellyToken } from "./directory/jelly-issuer.js";
|
|
8
|
+
import { actionClass, grantInput } from "./jelly-host.js";
|
|
9
|
+
import { JELLY_SERVICE_AUDIENCE } from "./schema/jelly.js";
|
|
10
|
+
let counter = 0;
|
|
11
|
+
/** A host double with a fresh action key, signing at `now()` (milliseconds; default the wall clock). */
|
|
12
|
+
export function jellyHostDouble(options = {}) {
|
|
13
|
+
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
14
|
+
const kid = options.kid ?? "act-1";
|
|
15
|
+
const x = publicKey.export({ format: "jwk" }).x;
|
|
16
|
+
const key = { kty: "OKP", crv: "Ed25519", x, kid };
|
|
17
|
+
const now = () => Math.floor((options.now ?? Date.now)() / 1000);
|
|
18
|
+
const sign = (typ, claims, header = {}) => signJellyToken(privateKey, { alg: "EdDSA", typ, kid, ...header }, claims);
|
|
19
|
+
return {
|
|
20
|
+
key,
|
|
21
|
+
keyText: `${JSON.stringify(key)}\n`,
|
|
22
|
+
act(method, params = {}, claims = {}, header = {}) {
|
|
23
|
+
const iat = now();
|
|
24
|
+
return sign("jelly-act+jwt", { aud: JELLY_SERVICE_AUDIENCE, act: method, in: grantInput(params), iat, exp: iat + 60, jti: `j${++counter}-${iat}`, dev: "dev_test", cls: actionClass(method), ...claims }, header);
|
|
25
|
+
},
|
|
26
|
+
host(claims = {}) {
|
|
27
|
+
const iat = now();
|
|
28
|
+
return sign("jelly-act+jwt", { aud: JELLY_SERVICE_AUDIENCE, act: "host", iat, exp: iat + 600, jti: `h${++counter}-${iat}`, ...claims });
|
|
29
|
+
},
|
|
30
|
+
ticket(agt, name, claims = {}, header = {}) {
|
|
31
|
+
const iat = now();
|
|
32
|
+
return sign("jelly-session+jwt", { aud: JELLY_SERVICE_AUDIENCE, agt, name, iat, exp: iat + 7 * 86_400, ...claims }, header);
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|