@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,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a daemon in `deployment: "jelly"` verifies of its host (spec §8.11): action grants (`jelly-act+jwt`) on
|
|
3
|
+
* human- and admin-tier calls, and session tickets (`jelly-session+jwt`) at `session.register`. Both are compact
|
|
4
|
+
* JWS signed with EdDSA by the host's action key. Pure: the caller supplies the key, the clock and the record of
|
|
5
|
+
* spent `jti`s.
|
|
6
|
+
*/
|
|
7
|
+
import { verify as ed25519Verify } from "node:crypto";
|
|
8
|
+
import { b64u, utf8 } from "./b64u.js";
|
|
9
|
+
import { sha256 } from "./hash.js";
|
|
10
|
+
import { canonicalize } from "./jcs.js";
|
|
11
|
+
import { assertEd25519PublicKey, publicKeyFromRaw } from "./keys.js";
|
|
12
|
+
import { parseWire } from "./wire.js";
|
|
13
|
+
import { MessagingError } from "./schema/errors.js";
|
|
14
|
+
import { ActKeyJwk, JELLY_SERVICE_AUDIENCE, JellyActClaims, JellyActHeader, JellySessionClaims, JellySessionHeader, Jws, } from "./schema/jelly.js";
|
|
15
|
+
import { ADMIN_METHODS, HUMAN_METHODS } from "./schema/ipc.js";
|
|
16
|
+
/** A method grant lives at most 60 s (`exp - iat`). */
|
|
17
|
+
export const ACT_GRANT_MAX_S = 60;
|
|
18
|
+
/** A host-scope grant lives at most ten minutes. */
|
|
19
|
+
export const HOST_GRANT_MAX_S = 600;
|
|
20
|
+
/** A session ticket lives at most seven days: the engine's lifetime. */
|
|
21
|
+
export const SESSION_TICKET_MAX_S = 7 * 24 * 60 * 60;
|
|
22
|
+
/** The clock skew allowed on `iat` and `exp`. */
|
|
23
|
+
export const JELLY_HOST_SKEW_S = 5;
|
|
24
|
+
/** `chat`: the person's own lines and read state. */
|
|
25
|
+
const CHAT_METHODS = ["person.send", "conv.markRead", "notice.ack"];
|
|
26
|
+
/**
|
|
27
|
+
* `view`: the read-only methods a Jelly section reads with (`section.rpc`): the read-only human-tier methods, and
|
|
28
|
+
* `device.list`, the one admin-tier read a section shows. A preview is not one of them: it takes its action's class
|
|
29
|
+
* (`local`), since it tells what a `local` action would do (`contact.request.preview` says whether an email has an
|
|
30
|
+
* account). Every method that writes is `chat` or `local`.
|
|
31
|
+
*/
|
|
32
|
+
const VIEW_METHODS = [
|
|
33
|
+
"conv.list",
|
|
34
|
+
"conv.read",
|
|
35
|
+
"conv.subscribe",
|
|
36
|
+
"people.search",
|
|
37
|
+
"people.get",
|
|
38
|
+
"grant.list",
|
|
39
|
+
"request.list",
|
|
40
|
+
"agents.list",
|
|
41
|
+
"agents.read",
|
|
42
|
+
"notice.subscribe",
|
|
43
|
+
"notice.list",
|
|
44
|
+
"device.list",
|
|
45
|
+
];
|
|
46
|
+
const classOf = (method) => CHAT_METHODS.includes(method) ? "chat" : VIEW_METHODS.includes(method) ? "view" : "local";
|
|
47
|
+
/**
|
|
48
|
+
* The class a method grant must name in `cls` for each human- and admin-tier method (spec §8.11): `chat` for
|
|
49
|
+
* `person.send`, `conv.markRead` and `notice.ack`, `view` for the read-only human-tier methods, `local` for every
|
|
50
|
+
* other. A plugin's `jelly` declaration gives each action and read the same class; a test there checks one against
|
|
51
|
+
* the other.
|
|
52
|
+
*/
|
|
53
|
+
export const ACTION_CLASSES = Object.freeze(Object.fromEntries([...HUMAN_METHODS, ...ADMIN_METHODS].map((method) => [method, classOf(method)])));
|
|
54
|
+
/** The class of `method` (`ACTION_CLASSES`); `local` for a method not in the table. */
|
|
55
|
+
export function actionClass(method) {
|
|
56
|
+
return Object.hasOwn(ACTION_CLASSES, method) ? ACTION_CLASSES[method] : "local";
|
|
57
|
+
}
|
|
58
|
+
/** `in`: base64url SHA-256 of the JCS of a call's params (absent params are `{}`). */
|
|
59
|
+
export function grantInput(params) {
|
|
60
|
+
return sha256(canonicalize(params ?? {}));
|
|
61
|
+
}
|
|
62
|
+
function decode(token, fail) {
|
|
63
|
+
if (!Jws.safeParse(token).success)
|
|
64
|
+
throw fail("is not a compact JWS");
|
|
65
|
+
const [header, payload, signature] = token.split(".");
|
|
66
|
+
const segment = (text, what) => {
|
|
67
|
+
try {
|
|
68
|
+
return parseWire(new TextDecoder("utf-8", { fatal: true }).decode(b64u.decode(text)), { maxBytes: 8192 });
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
throw fail(`${what} is not base64url JSON`);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
let sig;
|
|
75
|
+
try {
|
|
76
|
+
sig = b64u.decode(signature);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
throw fail("signature is not base64url");
|
|
80
|
+
}
|
|
81
|
+
if (sig.length !== 64)
|
|
82
|
+
throw fail("signature is not 64 bytes");
|
|
83
|
+
return { header: segment(header, "header"), claims: segment(payload, "claims"), input: utf8(`${header}.${payload}`), signature: sig };
|
|
84
|
+
}
|
|
85
|
+
function checkSignature(decoded, kid, key, fail) {
|
|
86
|
+
if (key.kid !== undefined && kid !== key.kid)
|
|
87
|
+
throw fail("names another key");
|
|
88
|
+
let ok = false;
|
|
89
|
+
try {
|
|
90
|
+
ok = ed25519Verify(null, decoded.input, publicKeyFromRaw("ed25519", key.x), decoded.signature);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
ok = false;
|
|
94
|
+
}
|
|
95
|
+
if (!ok)
|
|
96
|
+
throw fail("signature does not verify");
|
|
97
|
+
}
|
|
98
|
+
function checkTimes(iat, exp, max, nowMs, fail) {
|
|
99
|
+
const now = Math.floor(nowMs / 1000);
|
|
100
|
+
if (exp <= iat || exp - iat > max)
|
|
101
|
+
throw fail(`lifetime is not in (0, ${max}] seconds`);
|
|
102
|
+
if (iat > now + JELLY_HOST_SKEW_S)
|
|
103
|
+
throw fail("is issued in the future");
|
|
104
|
+
if (exp + JELLY_HOST_SKEW_S <= now)
|
|
105
|
+
throw fail("has expired");
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Verifies an action grant for one call (spec §8.11): the header, the signature by the action key, `aud`, then
|
|
109
|
+
* either `act` = the method with `in` = `grantInput(params)` and a lifetime of at most 60 s, or `act: "host"` for a
|
|
110
|
+
* method of the host-scope list with a lifetime of at most ten minutes. A method grant names the method's class in
|
|
111
|
+
* `cls` (`actionClass`); a host-scope grant names none. `mbr` is returned for the log and gives no authority. It does not spend the `jti`: the caller does, once per method grant. Throws `grant_invalid`; the message
|
|
112
|
+
* names the reason and never quotes the grant.
|
|
113
|
+
*/
|
|
114
|
+
export function verifyActGrant(token, check) {
|
|
115
|
+
const fail = (why) => new MessagingError("grant_invalid", `the action grant ${why}`);
|
|
116
|
+
const decoded = decode(token, fail);
|
|
117
|
+
const header = JellyActHeader.safeParse(decoded.header);
|
|
118
|
+
if (!header.success)
|
|
119
|
+
throw fail("header is not EdDSA jelly-act+jwt with a key id");
|
|
120
|
+
const parsed = JellyActClaims.safeParse(decoded.claims);
|
|
121
|
+
if (!parsed.success)
|
|
122
|
+
throw fail(`claims are malformed at ${parsed.error.issues[0]?.path.map(String).join(".") || "(root)"}`);
|
|
123
|
+
checkSignature(decoded, header.data.kid, check.key, fail);
|
|
124
|
+
const claims = parsed.data;
|
|
125
|
+
if (claims.aud !== JELLY_SERVICE_AUDIENCE)
|
|
126
|
+
throw fail("is for another audience");
|
|
127
|
+
if (claims.act === "host") {
|
|
128
|
+
if (!check.hostScope(check.method))
|
|
129
|
+
throw fail(`is host scope, which does not allow ${check.method}`);
|
|
130
|
+
checkTimes(claims.iat, claims.exp, HOST_GRANT_MAX_S, check.now, fail);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
if (claims.act !== check.method)
|
|
134
|
+
throw fail("is for another method");
|
|
135
|
+
if (claims.in === undefined || claims.in !== grantInput(check.params))
|
|
136
|
+
throw fail("is for other params");
|
|
137
|
+
checkTimes(claims.iat, claims.exp, ACT_GRANT_MAX_S, check.now, fail);
|
|
138
|
+
}
|
|
139
|
+
if (claims.act === "host") {
|
|
140
|
+
if (claims.cls !== undefined)
|
|
141
|
+
throw fail("is host scope, which names no action class");
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
if (claims.cls === undefined)
|
|
145
|
+
throw fail("names no action class");
|
|
146
|
+
if (claims.cls !== actionClass(check.method))
|
|
147
|
+
throw fail(`names action class ${claims.cls}, not ${actionClass(check.method)}`);
|
|
148
|
+
}
|
|
149
|
+
return { scope: claims.act === "host" ? "host" : "action", jti: claims.jti, exp: claims.exp, dev: claims.dev, mbr: claims.mbr };
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Verifies a session ticket (spec §8.11): the header, the signature by the action key, `aud`, and a lifetime of at
|
|
153
|
+
* most seven days. Throws `ticket_invalid`; the message never quotes the ticket.
|
|
154
|
+
*/
|
|
155
|
+
export function verifySessionTicket(token, key, now) {
|
|
156
|
+
const fail = (why) => new MessagingError("ticket_invalid", `the session ticket ${why}`);
|
|
157
|
+
const decoded = decode(token, fail);
|
|
158
|
+
const header = JellySessionHeader.safeParse(decoded.header);
|
|
159
|
+
if (!header.success)
|
|
160
|
+
throw fail("header is not EdDSA jelly-session+jwt with a key id");
|
|
161
|
+
const parsed = JellySessionClaims.safeParse(decoded.claims);
|
|
162
|
+
if (!parsed.success)
|
|
163
|
+
throw fail(`claims are malformed at ${parsed.error.issues[0]?.path.map(String).join(".") || "(root)"}`);
|
|
164
|
+
checkSignature(decoded, header.data.kid, key, fail);
|
|
165
|
+
const claims = parsed.data;
|
|
166
|
+
if (claims.aud !== JELLY_SERVICE_AUDIENCE)
|
|
167
|
+
throw fail("is for another audience");
|
|
168
|
+
checkTimes(claims.iat, claims.exp, SESSION_TICKET_MAX_S, now, fail);
|
|
169
|
+
return { agt: claims.agt, name: claims.name, exp: claims.exp };
|
|
170
|
+
}
|
|
171
|
+
/** Reads `--act-key`'s text: exactly a public Ed25519 JWK (`ActKeyJwk`), a usable point. Throws `invalid_request`. */
|
|
172
|
+
export function parseActKey(text) {
|
|
173
|
+
let value;
|
|
174
|
+
try {
|
|
175
|
+
value = parseWire(text, { maxBytes: 4096 });
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
throw new MessagingError("invalid_request", "the action key is not JSON");
|
|
179
|
+
}
|
|
180
|
+
const parsed = ActKeyJwk.safeParse(value);
|
|
181
|
+
if (!parsed.success)
|
|
182
|
+
throw new MessagingError("invalid_request", "the action key is not exactly a public Ed25519 JWK");
|
|
183
|
+
try {
|
|
184
|
+
assertEd25519PublicKey(parsed.data.x, "action key");
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
throw new MessagingError("invalid_request", "the action key is not a usable Ed25519 key");
|
|
188
|
+
}
|
|
189
|
+
return parsed.data;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Reads a wake ticket's header and claims without verifying it: a compact JWS whose header is `jelly-wake+jwt`, with
|
|
193
|
+
* an integer `exp` and, when present, a string `aud` and `sub`. A relay holds the ticket only to present it to Jelly
|
|
194
|
+
* ID, which verifies the signature, the host and the single use; it reads `exp` to expire it and `aud` to check that the
|
|
195
|
+
* ticket is for the URL it is armed with. Throws `invalid_request`; the message never quotes the ticket.
|
|
196
|
+
*/
|
|
197
|
+
export function readWakeTicket(token) {
|
|
198
|
+
const fail = (why) => new MessagingError("invalid_request", `the wake ticket ${why}`);
|
|
199
|
+
const decoded = decode(token, fail);
|
|
200
|
+
const header = decoded.header;
|
|
201
|
+
if (typeof header !== "object" || header === null || header.typ !== "jelly-wake+jwt" || header.alg !== "EdDSA")
|
|
202
|
+
throw fail("header is not EdDSA jelly-wake+jwt");
|
|
203
|
+
const claims = decoded.claims;
|
|
204
|
+
if (typeof claims !== "object" || claims === null || typeof claims.exp !== "number" || !Number.isSafeInteger(claims.exp))
|
|
205
|
+
throw fail("has no exp");
|
|
206
|
+
if (claims.aud !== undefined && typeof claims.aud !== "string")
|
|
207
|
+
throw fail("aud is not one string");
|
|
208
|
+
if (claims.sub !== undefined && typeof claims.sub !== "string")
|
|
209
|
+
throw fail("sub is not a string");
|
|
210
|
+
return { exp: claims.exp, aud: claims.aud, sub: claims.sub };
|
|
211
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createPrivateKey, createPublicKey, KeyObject } from "node:crypto";
|
|
2
|
+
import { b64u } from "./b64u.js";
|
|
3
|
+
import { MessagingError } from "./schema/errors.js";
|
|
4
|
+
const CURVE = { ed25519: "Ed25519", x25519: "X25519" };
|
|
5
|
+
// PKCS#8 DER header for a raw 32-byte private key (RFC 8410).
|
|
6
|
+
const PKCS8_HEADER = {
|
|
7
|
+
ed25519: Buffer.from("302e020100300506032b657004220420", "hex"),
|
|
8
|
+
x25519: Buffer.from("302e020100300506032b656e04220420", "hex"),
|
|
9
|
+
};
|
|
10
|
+
// libsodium's Ed25519 small-order blocklist, compared without the sign bit (bit 255): the encodings of
|
|
11
|
+
// the eight points of order 1, 2, 4 and 8, and the non-canonical encodings p and p + 1 of 0 and 1.
|
|
12
|
+
// With such a key one signature verifies for every message.
|
|
13
|
+
const SMALL_ORDER = [
|
|
14
|
+
"0000000000000000000000000000000000000000000000000000000000000000",
|
|
15
|
+
"0100000000000000000000000000000000000000000000000000000000000000",
|
|
16
|
+
"26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05",
|
|
17
|
+
"c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a",
|
|
18
|
+
"ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
|
|
19
|
+
"edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
|
|
20
|
+
"eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
|
|
21
|
+
].map((hex) => Buffer.from(hex, "hex"));
|
|
22
|
+
/** A private key from its raw 32 bytes (the JWK `d`). */
|
|
23
|
+
export function privateKeyFromRaw(type, raw) {
|
|
24
|
+
if (raw.length !== 32)
|
|
25
|
+
throw new MessagingError("invalid_request", `${type} private key must be 32 bytes`);
|
|
26
|
+
return createPrivateKey({ key: Buffer.concat([PKCS8_HEADER[type], raw]), format: "der", type: "pkcs8" });
|
|
27
|
+
}
|
|
28
|
+
/** A public key from its raw 32 bytes in base64url (the JWK `x`). Ed25519 keys must pass `assertEd25519PublicKey`. */
|
|
29
|
+
export function publicKeyFromRaw(type, x) {
|
|
30
|
+
if (type === "ed25519")
|
|
31
|
+
assertEd25519PublicKey(x, "Ed25519 public key");
|
|
32
|
+
else if (b64u.decode(x).length !== 32)
|
|
33
|
+
throw new MessagingError("invalid_request", `${type} public key must be 32 bytes`);
|
|
34
|
+
return createPublicKey({ key: { kty: "OKP", crv: CURVE[type], x }, format: "jwk" });
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Throws `invalid_request` unless `x` is 32 bytes of base64url encoding y < p (canonical) and not of
|
|
38
|
+
* small order, as libsodium checks before verifying (spec §3).
|
|
39
|
+
*/
|
|
40
|
+
export function assertEd25519PublicKey(x, what) {
|
|
41
|
+
const raw = b64u.decode(x);
|
|
42
|
+
if (raw.length !== 32)
|
|
43
|
+
throw new MessagingError("invalid_request", `${what} must be 32 bytes`);
|
|
44
|
+
if (!isCanonical(raw))
|
|
45
|
+
throw new MessagingError("invalid_request", `${what} is not a canonical encoding`);
|
|
46
|
+
if (SMALL_ORDER.some((weak) => sameIgnoringSign(raw, weak)))
|
|
47
|
+
throw new MessagingError("invalid_request", `${what} has small order`);
|
|
48
|
+
}
|
|
49
|
+
/** y (the low 255 bits, little-endian) is below p = 2^255 − 19, i.e. not in [p, 2^255 − 1]. */
|
|
50
|
+
function isCanonical(raw) {
|
|
51
|
+
if ((raw[31] & 0x7f) !== 0x7f)
|
|
52
|
+
return true;
|
|
53
|
+
for (let i = 30; i > 0; i--)
|
|
54
|
+
if (raw[i] !== 0xff)
|
|
55
|
+
return true;
|
|
56
|
+
return raw[0] < 0xed;
|
|
57
|
+
}
|
|
58
|
+
function sameIgnoringSign(raw, weak) {
|
|
59
|
+
for (let i = 0; i < 31; i++)
|
|
60
|
+
if (raw[i] !== weak[i])
|
|
61
|
+
return false;
|
|
62
|
+
return (raw[31] & 0x7f) === weak[31];
|
|
63
|
+
}
|
|
64
|
+
/** Raw public key (base64url) of a private or public key. */
|
|
65
|
+
export function rawPublicKey(key) {
|
|
66
|
+
const x = (key.type === "public" ? key : createPublicKey(key)).export({ format: "jwk" }).x;
|
|
67
|
+
if (typeof x !== "string")
|
|
68
|
+
throw new MessagingError("internal", "key has no raw public form");
|
|
69
|
+
return x;
|
|
70
|
+
}
|
|
71
|
+
export function rawPrivateKey(key) {
|
|
72
|
+
const d = key.export({ format: "jwk" }).d;
|
|
73
|
+
if (typeof d !== "string")
|
|
74
|
+
throw new MessagingError("internal", "key has no raw private form");
|
|
75
|
+
return d;
|
|
76
|
+
}
|
|
77
|
+
export function isPrivateKey(key, type) {
|
|
78
|
+
return key instanceof KeyObject && key.type === "private" && key.asymmetricKeyType === type;
|
|
79
|
+
}
|