@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,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relay binding (urn:messaging:binding:relay:v1): JSON text frames over one WebSocket per machine.
|
|
3
|
+
* Each frame is one JSON object of at most LIMITS.frameBytes, discriminated by `t`.
|
|
4
|
+
* Receivers ignore frames with an unknown `t` (forward compatibility); they never drop the link for it.
|
|
5
|
+
*
|
|
6
|
+
* Connection:
|
|
7
|
+
* relay → challenge {nonce}
|
|
8
|
+
* client → [pre-auth, optional] join {team, inviteId, proof} → relay: roster {entries} (only for an open invite)
|
|
9
|
+
* proof = Ed25519(invite seed, DOMAIN.inviteJoin + JCS({relay, team, inviteId, nonce})), verified under invitePub
|
|
10
|
+
* client → [pre-auth, optional] roster.append {entry} (genesis to create a team, or invite.redeem to join)
|
|
11
|
+
* client → hello {team, machine, ts, sig} sig = Ed25519(DOMAIN.relayAuth + JCS({proto, relay, team, machine, nonce, ts}))
|
|
12
|
+
* relay → welcome {head, online}, then the latest presence envelope of every member, then queued deliveries
|
|
13
|
+
*
|
|
14
|
+
* After welcome:
|
|
15
|
+
* send {env} relay checks header + signature + membership, stores, forwards; replies ack|nack by env.id
|
|
16
|
+
* deliver {env} relay → recipient. Recipient replies recv.ack {id} after storing durably; the relay then drops it
|
|
17
|
+
* presence {env} relay keeps the latest per machine and fans it out to online team members as deliver
|
|
18
|
+
* roster.append admin entries; relay validates against its head, appends, replies roster.ok, broadcasts roster
|
|
19
|
+
* roster.sync {after} relay replies roster {entries with seq > after}
|
|
20
|
+
* peer {machine, online} relay → members on connect/disconnect
|
|
21
|
+
* When a member is removed, the relay closes its connection with code 4003.
|
|
22
|
+
*/
|
|
23
|
+
import { z } from "../../vendor/zod/index.js";
|
|
24
|
+
import { Base64Url, MachineId, Millis, RELAY_PROTOCOL, TeamId, Ulid } from "./common.js";
|
|
25
|
+
import { Envelope } from "./envelope.js";
|
|
26
|
+
import { ErrorCode } from "./errors.js";
|
|
27
|
+
import { RosterEntry, RosterHead } from "./roster.js";
|
|
28
|
+
const Req = z.string().min(1).max(64);
|
|
29
|
+
// ---- relay → client ----
|
|
30
|
+
export const ChallengeFrame = z.object({
|
|
31
|
+
t: z.literal("challenge"),
|
|
32
|
+
proto: z.literal(RELAY_PROTOCOL),
|
|
33
|
+
relay: z.string().min(1).describe("relay id the client signs, e.g. its public origin"),
|
|
34
|
+
nonce: Base64Url,
|
|
35
|
+
});
|
|
36
|
+
export const WelcomeFrame = z.object({ t: z.literal("welcome"), head: RosterHead, online: z.array(MachineId) });
|
|
37
|
+
/**
|
|
38
|
+
* Deliveries are parsed loosely so a v1 client can still ack (and answer with a rejected receipt) an
|
|
39
|
+
* envelope type added later and forwarded by a newer relay. The daemon verifies the raw envelope itself.
|
|
40
|
+
*/
|
|
41
|
+
export const DeliverFrame = z.object({
|
|
42
|
+
t: z.literal("deliver"),
|
|
43
|
+
env: z.looseObject({ v: z.string(), id: Ulid, type: z.string() }),
|
|
44
|
+
});
|
|
45
|
+
export const AckFrame = z.object({ t: z.literal("ack"), id: Ulid, stage: z.literal("relayed") });
|
|
46
|
+
/** `retryAfterMs`: with `rate_limited` from a directory frame (spec §7, "Directory frames"), when to ask again. */
|
|
47
|
+
export const NackFrame = z.object({
|
|
48
|
+
t: z.literal("nack"),
|
|
49
|
+
id: z.string(),
|
|
50
|
+
code: ErrorCode,
|
|
51
|
+
message: z.string().max(500),
|
|
52
|
+
retryAfterMs: z.number().int().positive().optional(),
|
|
53
|
+
});
|
|
54
|
+
/** `more`: the reply was paged; ask again with `after` = the last entry's seq. */
|
|
55
|
+
export const RosterFrame = z.object({
|
|
56
|
+
t: z.literal("roster"),
|
|
57
|
+
req: Req.optional(),
|
|
58
|
+
entries: z.array(RosterEntry),
|
|
59
|
+
more: z.boolean().optional(),
|
|
60
|
+
});
|
|
61
|
+
export const RosterOkFrame = z.object({ t: z.literal("roster.ok"), req: Req, head: RosterHead });
|
|
62
|
+
export const PeerFrame = z.object({ t: z.literal("peer"), machine: MachineId, online: z.boolean() });
|
|
63
|
+
export const ErrorFrame = z.object({ t: z.literal("error"), code: ErrorCode, message: z.string().max(500) });
|
|
64
|
+
export const PongFrame = z.object({ t: z.literal("pong") });
|
|
65
|
+
export const RelayToClient = z.discriminatedUnion("t", [
|
|
66
|
+
ChallengeFrame,
|
|
67
|
+
WelcomeFrame,
|
|
68
|
+
DeliverFrame,
|
|
69
|
+
AckFrame,
|
|
70
|
+
NackFrame,
|
|
71
|
+
RosterFrame,
|
|
72
|
+
RosterOkFrame,
|
|
73
|
+
PeerFrame,
|
|
74
|
+
ErrorFrame,
|
|
75
|
+
PongFrame,
|
|
76
|
+
]);
|
|
77
|
+
// ---- client → relay ----
|
|
78
|
+
export const JoinFrame = z.object({
|
|
79
|
+
t: z.literal("join"),
|
|
80
|
+
req: Req,
|
|
81
|
+
team: TeamId,
|
|
82
|
+
inviteId: Ulid,
|
|
83
|
+
/** Ed25519(invite seed, DOMAIN.inviteJoin + JCS({relay, team, inviteId, nonce})) for this connection's challenge; see `inviteJoinProof`. */
|
|
84
|
+
proof: Base64Url,
|
|
85
|
+
after: z.number().int().min(-1).optional().describe("page: entries with seq > after"),
|
|
86
|
+
});
|
|
87
|
+
export const RosterAppendFrame = z.object({ t: z.literal("roster.append"), req: Req, entry: RosterEntry });
|
|
88
|
+
export const HelloFrame = z.object({ t: z.literal("hello"), team: TeamId, machine: MachineId, ts: Millis, sig: Base64Url });
|
|
89
|
+
export const SendFrame = z.object({ t: z.literal("send"), env: Envelope });
|
|
90
|
+
export const RecvAckFrame = z.object({ t: z.literal("recv.ack"), id: Ulid });
|
|
91
|
+
export const PresenceFrame = z.object({ t: z.literal("presence"), env: Envelope });
|
|
92
|
+
export const RosterSyncFrame = z.object({ t: z.literal("roster.sync"), req: Req, after: z.number().int().min(-1) });
|
|
93
|
+
export const PingFrame = z.object({ t: z.literal("ping") });
|
|
94
|
+
export const ClientToRelay = z.discriminatedUnion("t", [
|
|
95
|
+
JoinFrame,
|
|
96
|
+
RosterAppendFrame,
|
|
97
|
+
HelloFrame,
|
|
98
|
+
SendFrame,
|
|
99
|
+
RecvAckFrame,
|
|
100
|
+
PresenceFrame,
|
|
101
|
+
RosterSyncFrame,
|
|
102
|
+
PingFrame,
|
|
103
|
+
]);
|
|
104
|
+
/** WebSocket close codes used by the relay. */
|
|
105
|
+
export const RELAY_CLOSE = {
|
|
106
|
+
authFailed: 4001,
|
|
107
|
+
protocolError: 4002,
|
|
108
|
+
removed: 4003,
|
|
109
|
+
replaced: 4004, // the same machine connected again; the older socket is closed
|
|
110
|
+
full: 1013, // RFC 6455 "try again later": the relay holds as many authenticated connections as it accepts
|
|
111
|
+
tooLarge: 1009, // RFC 6455 "message too big", sent by the transport library itself
|
|
112
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The team roster: an append-only, hash-chained log of signed entries.
|
|
3
|
+
* The relay sequences appends; every machine verifies the whole chain itself.
|
|
4
|
+
*
|
|
5
|
+
* hash(entry) = SHA-256(UTF-8(JCS(entry))), base64url, over the full entry including sig
|
|
6
|
+
* sig = Ed25519(by, DOMAIN.roster + JCS(entry without sig))
|
|
7
|
+
* team id = hash(genesis)
|
|
8
|
+
* proof = Ed25519(invite seed, DOMAIN.invite + JCS({team, inviteId, member})) (invite.redeem)
|
|
9
|
+
*
|
|
10
|
+
* Authority rules (enforced by `Roster.apply`):
|
|
11
|
+
* genesis seq 0, prev null, by = founder.machine, founder.role = admin
|
|
12
|
+
* member.add by a current admin; machine not already a member; (person, label) not used by a current member
|
|
13
|
+
* member.remove by a current admin, or by the member itself (leave); never the last admin
|
|
14
|
+
* member.role by a current admin; never demotes the last admin
|
|
15
|
+
* member.rekey by the member itself or a current admin; the machine is a current member (never a
|
|
16
|
+
* removed one); replaces only that member's `enc`
|
|
17
|
+
* invite.create by a current admin; expires − ts ≤ LIMITS.ttlMs
|
|
18
|
+
* invite.redeem by the joining machine (by = member.machine); invite open and unexpired at entry ts;
|
|
19
|
+
* proof verifies under the invite's invitePub; member.person and role match the invite;
|
|
20
|
+
* (person, label) not used by a current member. Single use.
|
|
21
|
+
* invite.revoke by a current admin
|
|
22
|
+
* Every admitted member key (genesis founder, member.add, redeem member) and every invitePub must be a
|
|
23
|
+
* canonical Ed25519 encoding of a point that is not of small order.
|
|
24
|
+
* A removed machine can never rejoin with the same key; it must generate a new identity.
|
|
25
|
+
* Inviting or adding an existing person adds another machine for that person, under another label.
|
|
26
|
+
*/
|
|
27
|
+
import { z } from "../../vendor/zod/index.js";
|
|
28
|
+
import { Base64Url, EncKey, Hash, Key32, MachineId, MachineLabel, MemberRole, Millis, PersonName, PROTOCOL, RelayUrl, TeamId, TeamName, Ulid, } from "./common.js";
|
|
29
|
+
export const Member = z.object({
|
|
30
|
+
machine: MachineId,
|
|
31
|
+
enc: EncKey,
|
|
32
|
+
person: PersonName,
|
|
33
|
+
role: MemberRole,
|
|
34
|
+
label: MachineLabel,
|
|
35
|
+
});
|
|
36
|
+
const base = {
|
|
37
|
+
v: z.literal(PROTOCOL),
|
|
38
|
+
seq: z.number().int().nonnegative(),
|
|
39
|
+
ts: Millis,
|
|
40
|
+
by: MachineId,
|
|
41
|
+
sig: Base64Url,
|
|
42
|
+
};
|
|
43
|
+
const chained = { ...base, team: TeamId, prev: Hash };
|
|
44
|
+
export const GenesisEntry = z.object({
|
|
45
|
+
...base,
|
|
46
|
+
prev: z.null(),
|
|
47
|
+
kind: z.literal("genesis"),
|
|
48
|
+
body: z.object({ name: TeamName, founder: Member }),
|
|
49
|
+
});
|
|
50
|
+
export const MemberAddEntry = z.object({ ...chained, kind: z.literal("member.add"), body: Member });
|
|
51
|
+
export const MemberRemoveEntry = z.object({
|
|
52
|
+
...chained,
|
|
53
|
+
kind: z.literal("member.remove"),
|
|
54
|
+
body: z.object({ machine: MachineId, reason: z.string().max(200).optional() }),
|
|
55
|
+
});
|
|
56
|
+
export const MemberRoleEntry = z.object({
|
|
57
|
+
...chained,
|
|
58
|
+
kind: z.literal("member.role"),
|
|
59
|
+
body: z.object({ machine: MachineId, role: MemberRole }),
|
|
60
|
+
});
|
|
61
|
+
/**
|
|
62
|
+
* Replaces a member's X25519 encryption key. Senders seal to the new `enc` once they have
|
|
63
|
+
* applied this entry; the member keeps its previous key long enough to open what was sealed before (spec §4).
|
|
64
|
+
*/
|
|
65
|
+
export const MemberRekeyEntry = z.object({
|
|
66
|
+
...chained,
|
|
67
|
+
kind: z.literal("member.rekey"),
|
|
68
|
+
body: z.object({ machine: MachineId, enc: EncKey }),
|
|
69
|
+
});
|
|
70
|
+
export const InviteCreateEntry = z.object({
|
|
71
|
+
...chained,
|
|
72
|
+
kind: z.literal("invite.create"),
|
|
73
|
+
body: z.object({
|
|
74
|
+
inviteId: Ulid,
|
|
75
|
+
/** Ed25519 public key of the invite seed (the ticket `secret`). Only the seed can make a redeem proof. */
|
|
76
|
+
invitePub: Key32,
|
|
77
|
+
person: PersonName,
|
|
78
|
+
expires: Millis,
|
|
79
|
+
}),
|
|
80
|
+
});
|
|
81
|
+
export const InviteRedeemEntry = z.object({
|
|
82
|
+
...chained,
|
|
83
|
+
kind: z.literal("invite.redeem"),
|
|
84
|
+
body: z.object({
|
|
85
|
+
inviteId: Ulid,
|
|
86
|
+
/**
|
|
87
|
+
* Ed25519(invite seed, DOMAIN.invite + JCS({team, inviteId, member})), verified under the invite's
|
|
88
|
+
* `invitePub`. It binds the redeem to this member's keys, person, role and label; the seed itself
|
|
89
|
+
* never appears in the roster or on the wire.
|
|
90
|
+
*/
|
|
91
|
+
proof: Base64Url,
|
|
92
|
+
member: Member,
|
|
93
|
+
}),
|
|
94
|
+
});
|
|
95
|
+
export const InviteRevokeEntry = z.object({
|
|
96
|
+
...chained,
|
|
97
|
+
kind: z.literal("invite.revoke"),
|
|
98
|
+
body: z.object({ inviteId: Ulid }),
|
|
99
|
+
});
|
|
100
|
+
export const RosterEntry = z.discriminatedUnion("kind", [
|
|
101
|
+
GenesisEntry,
|
|
102
|
+
MemberAddEntry,
|
|
103
|
+
MemberRemoveEntry,
|
|
104
|
+
MemberRoleEntry,
|
|
105
|
+
MemberRekeyEntry,
|
|
106
|
+
InviteCreateEntry,
|
|
107
|
+
InviteRedeemEntry,
|
|
108
|
+
InviteRevokeEntry,
|
|
109
|
+
]);
|
|
110
|
+
export const RosterHead = z.object({ seq: z.number().int().nonnegative(), hash: Hash });
|
|
111
|
+
/**
|
|
112
|
+
* Invite ticket, shared out of band. Encoded as `messaging1.` + base64url(UTF-8(JCS(ticket))).
|
|
113
|
+
* `secret` is the invite seed: a 32-byte Ed25519 private key (RFC 8032 seed) whose public key is the
|
|
114
|
+
* invite's `invitePub`. The joiner signs its redeem proof with it; the seed never leaves the ticket.
|
|
115
|
+
*/
|
|
116
|
+
export const InviteTicket = z.object({
|
|
117
|
+
v: z.literal(PROTOCOL),
|
|
118
|
+
team: TeamId,
|
|
119
|
+
name: TeamName,
|
|
120
|
+
relay: RelayUrl,
|
|
121
|
+
inviteId: Ulid,
|
|
122
|
+
secret: Key32,
|
|
123
|
+
person: PersonName,
|
|
124
|
+
expires: Millis,
|
|
125
|
+
});
|
|
126
|
+
export const TICKET_PREFIX = "messaging1.";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { sign as ed25519Sign, verify as ed25519Verify } from "node:crypto";
|
|
2
|
+
import { b64u, utf8 } from "./b64u.js";
|
|
3
|
+
import { asMessagingError } from "./check.js";
|
|
4
|
+
import { canonicalize } from "./jcs.js";
|
|
5
|
+
import { publicKeyFromRaw } from "./keys.js";
|
|
6
|
+
/** The exact bytes signed: UTF-8(domain) || UTF-8(JCS(value)). Throws `invalid_request` for non-I-JSON. */
|
|
7
|
+
export function signingInput(domain, value) {
|
|
8
|
+
return Buffer.concat([utf8(domain), utf8(canonicalize(value))]);
|
|
9
|
+
}
|
|
10
|
+
/** Ed25519 signature (base64url) over `domain` + JCS(value). Use a `DOMAIN` prefix. Throws only MessagingError. */
|
|
11
|
+
export function sign(identity, domain, value) {
|
|
12
|
+
return signWith(identity.signingKey, domain, value);
|
|
13
|
+
}
|
|
14
|
+
/** `sign` with a bare Ed25519 private key, such as an invite seed. */
|
|
15
|
+
export function signWith(key, domain, value) {
|
|
16
|
+
try {
|
|
17
|
+
return b64u.encode(ed25519Sign(null, signingInput(domain, value), key));
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
throw asMessagingError(error, "signing input");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** True only for a valid signature by `machine`. Never throws: malformed keys, values or signatures are false. */
|
|
24
|
+
export function verify(machine, domain, value, sig) {
|
|
25
|
+
try {
|
|
26
|
+
return verifyInput(machine, signingInput(domain, value), sig);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** `verify` over bytes from `signingInput`. Never throws. */
|
|
33
|
+
export function verifyInput(machine, input, sig) {
|
|
34
|
+
try {
|
|
35
|
+
const signature = b64u.decode(sig);
|
|
36
|
+
if (signature.length !== 64)
|
|
37
|
+
return false;
|
|
38
|
+
return ed25519Verify(null, input, publicKeyFromRaw("ed25519", machine), signature);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { INTERRUPTED_STATES, TERMINAL_STATES } from "./schema/a2a.js";
|
|
2
|
+
const NEXT = {
|
|
3
|
+
TASK_STATE_SUBMITTED: [
|
|
4
|
+
"TASK_STATE_WORKING",
|
|
5
|
+
"TASK_STATE_INPUT_REQUIRED",
|
|
6
|
+
"TASK_STATE_AUTH_REQUIRED",
|
|
7
|
+
"TASK_STATE_COMPLETED",
|
|
8
|
+
"TASK_STATE_FAILED",
|
|
9
|
+
"TASK_STATE_REJECTED",
|
|
10
|
+
"TASK_STATE_CANCELED",
|
|
11
|
+
],
|
|
12
|
+
TASK_STATE_WORKING: [
|
|
13
|
+
"TASK_STATE_WORKING",
|
|
14
|
+
"TASK_STATE_INPUT_REQUIRED",
|
|
15
|
+
"TASK_STATE_AUTH_REQUIRED",
|
|
16
|
+
"TASK_STATE_COMPLETED",
|
|
17
|
+
"TASK_STATE_FAILED",
|
|
18
|
+
"TASK_STATE_CANCELED",
|
|
19
|
+
],
|
|
20
|
+
TASK_STATE_INPUT_REQUIRED: ["TASK_STATE_WORKING", "TASK_STATE_COMPLETED", "TASK_STATE_FAILED", "TASK_STATE_CANCELED"],
|
|
21
|
+
TASK_STATE_AUTH_REQUIRED: ["TASK_STATE_WORKING", "TASK_STATE_COMPLETED", "TASK_STATE_FAILED", "TASK_STATE_CANCELED"],
|
|
22
|
+
TASK_STATE_COMPLETED: [],
|
|
23
|
+
TASK_STATE_FAILED: [],
|
|
24
|
+
TASK_STATE_CANCELED: [],
|
|
25
|
+
TASK_STATE_REJECTED: [],
|
|
26
|
+
};
|
|
27
|
+
/** The task state machine in spec §6.3. Unknown states never transition. */
|
|
28
|
+
export function canTransition(from, to) {
|
|
29
|
+
return Object.hasOwn(NEXT, from) && NEXT[from].includes(to);
|
|
30
|
+
}
|
|
31
|
+
export function isTerminal(state) {
|
|
32
|
+
return TERMINAL_STATES.includes(state);
|
|
33
|
+
}
|
|
34
|
+
/** INPUT_REQUIRED or AUTH_REQUIRED: waiting on the requester. */
|
|
35
|
+
export function isInterrupted(state) {
|
|
36
|
+
return INTERRUPTED_STATES.includes(state);
|
|
37
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@messaging/core/testing`: helpers for test vectors and tests that need fixed randomness or envelopes a
|
|
3
|
+
* conforming sender never makes. Never import this from production code.
|
|
4
|
+
*/
|
|
5
|
+
export { sealEnvelopeWith, sealPlaintext } from "./envelope.js";
|
|
6
|
+
export { encodeTicket } from "./roster.js";
|
|
7
|
+
export { grantHash } from "./delegation.js";
|
|
8
|
+
export { signJellyToken } from "./directory/jelly-issuer.js";
|
|
9
|
+
export { jellyHostDouble } from "./jelly-host-double.js";
|
|
10
|
+
export { sealConvMetaWith, sealConvMsgWith, sealKeySharesWith } from "./conversation/seal.js";
|
|
11
|
+
/**
|
|
12
|
+
* Narrows a result whose `kind` tells its shape (`item.read`, `conv.read`) to the expected kind, and
|
|
13
|
+
* throws with both kinds named otherwise: `client.call("item.read", p).then(asKind("received"))`.
|
|
14
|
+
*/
|
|
15
|
+
export function asKind(kind) {
|
|
16
|
+
return (result) => {
|
|
17
|
+
if (result.kind !== kind)
|
|
18
|
+
throw new Error(`expected a ${kind} result, got ${result.kind}`);
|
|
19
|
+
return result;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { MessagingError } from "./schema/errors.js";
|
|
3
|
+
const ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
4
|
+
const MAX_TIME = 2 ** 48 - 1;
|
|
5
|
+
/**
|
|
6
|
+
* A ULID for `now` (default `Date.now()`), with 80 fresh random bits per id. Ids sort by millisecond,
|
|
7
|
+
* but ids from one millisecond are in random order, so no id predicts another (spec §2).
|
|
8
|
+
*/
|
|
9
|
+
export function ulid(now = Date.now()) {
|
|
10
|
+
if (!Number.isInteger(now) || now < 0 || now > MAX_TIME)
|
|
11
|
+
throw new MessagingError("invalid_request", "ulid time out of range");
|
|
12
|
+
return encodeTime(now) + encodeRandom(randomBytes(10));
|
|
13
|
+
}
|
|
14
|
+
function encodeTime(time) {
|
|
15
|
+
let out = "";
|
|
16
|
+
for (let i = 0; i < 10; i++) {
|
|
17
|
+
out = ALPHABET[time % 32] + out;
|
|
18
|
+
time = Math.floor(time / 32);
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
function encodeRandom(bytes) {
|
|
23
|
+
let out = "";
|
|
24
|
+
let buffer = 0;
|
|
25
|
+
let bits = 0;
|
|
26
|
+
for (const byte of bytes) {
|
|
27
|
+
buffer = (buffer << 8) | byte;
|
|
28
|
+
bits += 8;
|
|
29
|
+
while (bits >= 5) {
|
|
30
|
+
bits -= 5;
|
|
31
|
+
out += ALPHABET[(buffer >> bits) & 31];
|
|
32
|
+
}
|
|
33
|
+
buffer &= (1 << bits) - 1;
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { LIMITS } from "./schema/common.js";
|
|
2
|
+
import { MessagingError } from "./schema/errors.js";
|
|
3
|
+
// What the scanner expects next.
|
|
4
|
+
const VALUE = 0; // a value
|
|
5
|
+
const FIRST_VALUE = 1; // a value or `]`, right after `[`
|
|
6
|
+
const NAME = 2; // a member name
|
|
7
|
+
const FIRST_NAME = 3; // a member name or `}`, right after `{`
|
|
8
|
+
const AFTER = 4; // `,`, a closing bracket, or the end of the text
|
|
9
|
+
const ESCAPED = new Map([
|
|
10
|
+
[0x22, 0x22], // \"
|
|
11
|
+
[0x5c, 0x5c], // \\
|
|
12
|
+
[0x2f, 0x2f], // \/
|
|
13
|
+
[0x62, 0x08], // \b
|
|
14
|
+
[0x66, 0x0c], // \f
|
|
15
|
+
[0x6e, 0x0a], // \n
|
|
16
|
+
[0x72, 0x0d], // \r
|
|
17
|
+
[0x74, 0x09], // \t
|
|
18
|
+
]);
|
|
19
|
+
/**
|
|
20
|
+
* Parses JSON text that came from outside the process: a relay frame, an IPC line, a decrypted body,
|
|
21
|
+
* an invite ticket. It returns exactly what `JSON.parse` returns, after a single linear, iterative
|
|
22
|
+
* (never recursive) scan that throws `invalid_request` for:
|
|
23
|
+
* - text that is not RFC 8259 JSON;
|
|
24
|
+
* - an object with two members of the same name, compared after unescaping;
|
|
25
|
+
* - arrays and objects nested deeper than `LIMITS.jsonDepth`, before anything is built;
|
|
26
|
+
* - a string or member name with a lone surrogate, raw or escaped;
|
|
27
|
+
* - more than `maxBytes` UTF-8 bytes, when `maxBytes` is given.
|
|
28
|
+
* Error messages give the reason and an offset, never the text, and chain no `cause`.
|
|
29
|
+
*/
|
|
30
|
+
export function parseWire(text, options = {}) {
|
|
31
|
+
if (typeof text !== "string")
|
|
32
|
+
throw invalid("wire input must be text");
|
|
33
|
+
const { maxBytes } = options;
|
|
34
|
+
if (maxBytes !== undefined) {
|
|
35
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0)
|
|
36
|
+
throw invalid("maxBytes must be a non-negative integer");
|
|
37
|
+
// A UTF-16 code unit is at least one UTF-8 byte, so the length is a cheap first bound.
|
|
38
|
+
if (text.length > maxBytes || Buffer.byteLength(text, "utf8") > maxBytes)
|
|
39
|
+
throw invalid(`input exceeds ${maxBytes} bytes`);
|
|
40
|
+
}
|
|
41
|
+
scan(text);
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(text);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Not reached: scan accepts only RFC 8259 JSON. A SyntaxError message would quote the text.
|
|
47
|
+
throw invalid("not JSON");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function scan(text) {
|
|
51
|
+
// One entry per open container: the member names seen so far for an object, null for an array.
|
|
52
|
+
const open = [];
|
|
53
|
+
let state = VALUE;
|
|
54
|
+
let i = 0;
|
|
55
|
+
for (;;) {
|
|
56
|
+
let c = text.charCodeAt(i);
|
|
57
|
+
while (c === 0x20 || c === 0x0a || c === 0x0d || c === 0x09)
|
|
58
|
+
c = text.charCodeAt(++i);
|
|
59
|
+
switch (state) {
|
|
60
|
+
case VALUE:
|
|
61
|
+
case FIRST_VALUE:
|
|
62
|
+
if (c === 0x5d && state === FIRST_VALUE) {
|
|
63
|
+
open.pop();
|
|
64
|
+
i++;
|
|
65
|
+
state = AFTER;
|
|
66
|
+
}
|
|
67
|
+
else if (c === 0x5b || c === 0x7b) {
|
|
68
|
+
if (open.length >= LIMITS.jsonDepth)
|
|
69
|
+
throw invalid(`nested deeper than ${LIMITS.jsonDepth}`, i);
|
|
70
|
+
open.push(c === 0x7b ? new Set() : null);
|
|
71
|
+
i++;
|
|
72
|
+
state = c === 0x7b ? FIRST_NAME : FIRST_VALUE;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
i = scalar(text, i, c);
|
|
76
|
+
state = AFTER;
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
case NAME:
|
|
80
|
+
case FIRST_NAME: {
|
|
81
|
+
if (c === 0x7d && state === FIRST_NAME) {
|
|
82
|
+
open.pop();
|
|
83
|
+
i++;
|
|
84
|
+
state = AFTER;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
if (c !== 0x22)
|
|
88
|
+
throw invalid("expected a member name", i);
|
|
89
|
+
const name = memberName(text, i);
|
|
90
|
+
const names = open[open.length - 1];
|
|
91
|
+
if (names.has(name.value))
|
|
92
|
+
throw invalid("duplicate member name", i);
|
|
93
|
+
names.add(name.value);
|
|
94
|
+
i = name.end;
|
|
95
|
+
c = text.charCodeAt(i);
|
|
96
|
+
while (c === 0x20 || c === 0x0a || c === 0x0d || c === 0x09)
|
|
97
|
+
c = text.charCodeAt(++i);
|
|
98
|
+
if (c !== 0x3a)
|
|
99
|
+
throw invalid("expected a colon", i);
|
|
100
|
+
i++;
|
|
101
|
+
state = VALUE;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
default: {
|
|
105
|
+
if (open.length === 0) {
|
|
106
|
+
if (i !== text.length)
|
|
107
|
+
throw invalid("unexpected text after the value", i);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const names = open[open.length - 1];
|
|
111
|
+
if (c === 0x2c) {
|
|
112
|
+
i++;
|
|
113
|
+
state = names ? NAME : VALUE;
|
|
114
|
+
}
|
|
115
|
+
else if (c === (names ? 0x7d : 0x5d)) {
|
|
116
|
+
open.pop();
|
|
117
|
+
i++;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
throw invalid("expected a comma or a closing bracket", i);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** Skips a string, number or literal starting at `i` (whose code unit is `c`) and returns the offset after it. */
|
|
127
|
+
function scalar(text, i, c) {
|
|
128
|
+
if (c === 0x22)
|
|
129
|
+
return stringEnd(text, i);
|
|
130
|
+
if (c === 0x74 && text.startsWith("true", i))
|
|
131
|
+
return i + 4;
|
|
132
|
+
if (c === 0x66 && text.startsWith("false", i))
|
|
133
|
+
return i + 5;
|
|
134
|
+
if (c === 0x6e && text.startsWith("null", i))
|
|
135
|
+
return i + 4;
|
|
136
|
+
const start = i;
|
|
137
|
+
if (c === 0x2d)
|
|
138
|
+
c = text.charCodeAt(++i);
|
|
139
|
+
if (c === 0x30) {
|
|
140
|
+
c = text.charCodeAt(++i);
|
|
141
|
+
}
|
|
142
|
+
else if (c >= 0x31 && c <= 0x39) {
|
|
143
|
+
do
|
|
144
|
+
c = text.charCodeAt(++i);
|
|
145
|
+
while (c >= 0x30 && c <= 0x39);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
throw invalid("expected a value", start);
|
|
149
|
+
}
|
|
150
|
+
if (c === 0x2e)
|
|
151
|
+
i = digits(text, i + 1, start);
|
|
152
|
+
c = text.charCodeAt(i);
|
|
153
|
+
if (c === 0x65 || c === 0x45) {
|
|
154
|
+
c = text.charCodeAt(++i);
|
|
155
|
+
if (c === 0x2b || c === 0x2d)
|
|
156
|
+
i++;
|
|
157
|
+
i = digits(text, i, start);
|
|
158
|
+
}
|
|
159
|
+
return i;
|
|
160
|
+
}
|
|
161
|
+
/** One or more digits from `i`; returns the offset after them. */
|
|
162
|
+
function digits(text, i, start) {
|
|
163
|
+
let c = text.charCodeAt(i);
|
|
164
|
+
if (!(c >= 0x30 && c <= 0x39))
|
|
165
|
+
throw invalid("bad number", start);
|
|
166
|
+
do
|
|
167
|
+
c = text.charCodeAt(++i);
|
|
168
|
+
while (c >= 0x30 && c <= 0x39);
|
|
169
|
+
return i;
|
|
170
|
+
}
|
|
171
|
+
/** Checks a string value from its opening quote and returns the offset after its closing quote, without building it. */
|
|
172
|
+
function stringEnd(text, start) {
|
|
173
|
+
let i = start + 1;
|
|
174
|
+
let high = false; // the last code unit was a high surrogate still waiting for its low half
|
|
175
|
+
for (;;) {
|
|
176
|
+
let c = text.charCodeAt(i);
|
|
177
|
+
if (c >= 0x20 && c !== 0x22 && c !== 0x5c && (c < 0xd800 || c > 0xdfff)) {
|
|
178
|
+
if (high)
|
|
179
|
+
throw invalid("lone surrogate", start);
|
|
180
|
+
i++;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (c === 0x22) {
|
|
184
|
+
if (high)
|
|
185
|
+
throw invalid("lone surrogate", start);
|
|
186
|
+
return i + 1;
|
|
187
|
+
}
|
|
188
|
+
if (c === 0x5c) {
|
|
189
|
+
const escape = escapeAt(text, i);
|
|
190
|
+
c = escape.unit;
|
|
191
|
+
i = escape.end;
|
|
192
|
+
}
|
|
193
|
+
else if (c < 0x20 || Number.isNaN(c)) {
|
|
194
|
+
throw invalid(Number.isNaN(c) ? "unterminated string" : "control character in a string", i);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
i++;
|
|
198
|
+
}
|
|
199
|
+
if (c >= 0xd800 && c <= 0xdbff) {
|
|
200
|
+
if (high)
|
|
201
|
+
throw invalid("lone surrogate", start);
|
|
202
|
+
high = true;
|
|
203
|
+
}
|
|
204
|
+
else if (c >= 0xdc00 && c <= 0xdfff) {
|
|
205
|
+
if (!high)
|
|
206
|
+
throw invalid("lone surrogate", start);
|
|
207
|
+
high = false;
|
|
208
|
+
}
|
|
209
|
+
else if (high) {
|
|
210
|
+
throw invalid("lone surrogate", start);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/** A member name, decoded (names are compared after unescaping), and the offset after it. */
|
|
215
|
+
function memberName(text, start) {
|
|
216
|
+
const end = stringEnd(text, start);
|
|
217
|
+
const raw = text.slice(start + 1, end - 1);
|
|
218
|
+
if (!raw.includes("\\"))
|
|
219
|
+
return { value: raw, end };
|
|
220
|
+
let value = "";
|
|
221
|
+
for (let i = start + 1; i < end - 1;) {
|
|
222
|
+
if (text.charCodeAt(i) === 0x5c) {
|
|
223
|
+
const escape = escapeAt(text, i);
|
|
224
|
+
value += String.fromCharCode(escape.unit);
|
|
225
|
+
i = escape.end;
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
value += text[i];
|
|
229
|
+
i++;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return { value, end };
|
|
233
|
+
}
|
|
234
|
+
/** The code unit of the escape at `i` (a backslash) and the offset after it. */
|
|
235
|
+
function escapeAt(text, i) {
|
|
236
|
+
const e = text.charCodeAt(i + 1);
|
|
237
|
+
if (e === 0x75) {
|
|
238
|
+
let unit = 0;
|
|
239
|
+
for (let k = i + 2; k < i + 6; k++) {
|
|
240
|
+
const h = text.charCodeAt(k);
|
|
241
|
+
const nibble = h >= 0x30 && h <= 0x39 ? h - 0x30 : h >= 0x61 && h <= 0x66 ? h - 0x57 : h >= 0x41 && h <= 0x46 ? h - 0x37 : -1;
|
|
242
|
+
if (nibble < 0)
|
|
243
|
+
throw invalid("bad \\u escape", i);
|
|
244
|
+
unit = unit * 16 + nibble;
|
|
245
|
+
}
|
|
246
|
+
return { unit, end: i + 6 };
|
|
247
|
+
}
|
|
248
|
+
const unit = ESCAPED.get(e);
|
|
249
|
+
if (unit === undefined)
|
|
250
|
+
throw invalid("bad escape", i);
|
|
251
|
+
return { unit, end: i + 2 };
|
|
252
|
+
}
|
|
253
|
+
function invalid(reason, offset) {
|
|
254
|
+
return new MessagingError("invalid_request", `invalid JSON${offset === undefined ? "" : ` at offset ${offset}`}: ${reason}`);
|
|
255
|
+
}
|