@2kw/ai 7.3.0-dev.5 → 7.3.0-dev.9

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.
Files changed (40) hide show
  1. package/dist/bridge/hub.d.ts +11 -0
  2. package/dist/bridge/hub.js +44 -0
  3. package/dist/bridge/inventory.d.ts +27 -0
  4. package/dist/bridge/inventory.js +41 -0
  5. package/dist/bridge/pairing.d.ts +41 -0
  6. package/dist/bridge/pairing.js +158 -0
  7. package/dist/bridge/paths.d.ts +7 -0
  8. package/dist/bridge/paths.js +16 -0
  9. package/dist/bridge/run.d.ts +12 -0
  10. package/dist/bridge/run.js +134 -0
  11. package/dist/bridge/shell.d.ts +17 -0
  12. package/dist/bridge/shell.js +48 -0
  13. package/dist/bridge/store.d.ts +33 -0
  14. package/dist/bridge/store.js +110 -0
  15. package/dist/bridge/toml.d.ts +16 -0
  16. package/dist/bridge/toml.js +39 -0
  17. package/dist/commands/bridge.d.ts +7 -0
  18. package/dist/commands/bridge.js +228 -0
  19. package/dist/commands/extractions.js +24 -5
  20. package/dist/index.js +2 -0
  21. package/dist/lib/agent-run.js +1 -1
  22. package/dist/relay-protocol/b64.d.ts +13 -0
  23. package/dist/relay-protocol/b64.js +34 -0
  24. package/dist/relay-protocol/canonical-json.d.ts +22 -0
  25. package/dist/relay-protocol/canonical-json.js +101 -0
  26. package/dist/relay-protocol/frames.d.ts +75 -0
  27. package/dist/relay-protocol/frames.js +224 -0
  28. package/dist/relay-protocol/headers.d.ts +18 -0
  29. package/dist/relay-protocol/headers.js +88 -0
  30. package/dist/relay-protocol/jws.d.ts +28 -0
  31. package/dist/relay-protocol/jws.js +113 -0
  32. package/dist/relay-protocol/keys.d.ts +26 -0
  33. package/dist/relay-protocol/keys.js +80 -0
  34. package/dist/relay-protocol/pairing.d.ts +26 -0
  35. package/dist/relay-protocol/pairing.js +49 -0
  36. package/dist/relay-protocol/wire.d.ts +101 -0
  37. package/dist/relay-protocol/wire.js +63 -0
  38. package/dist/relay-protocol/wordlist.d.ts +30 -0
  39. package/dist/relay-protocol/wordlist.js +289 -0
  40. package/package.json +4 -1
@@ -0,0 +1,75 @@
1
+ import type { KeyObject } from "node:crypto";
2
+ import type { HeaderPair } from "./wire.js";
3
+ export type FrameType = "accepted" | "head" | "data" | "end" | "error";
4
+ export type UnsignedFrame = {
5
+ seq: number;
6
+ type: "accepted";
7
+ } | {
8
+ seq: number;
9
+ type: "head";
10
+ status: number;
11
+ headers: HeaderPair[];
12
+ } | {
13
+ seq: number;
14
+ type: "data";
15
+ bytes: string;
16
+ } | {
17
+ seq: number;
18
+ type: "end";
19
+ } | {
20
+ seq: number;
21
+ type: "error";
22
+ code: string;
23
+ message: string;
24
+ };
25
+ export type Frame = UnsignedFrame & {
26
+ sig: string;
27
+ };
28
+ type Without<T, K extends keyof never> = T extends unknown ? Omit<T, K> : never;
29
+ export type FrameFields = Without<UnsignedFrame, "seq">;
30
+ export declare class FrameError extends Error {
31
+ readonly code = "response_invalid";
32
+ constructor(message: string);
33
+ }
34
+ /** UTF-16 code units, so `String.length` says the same thing on both sides of the wire. */
35
+ export declare const MAX_FRAME_MESSAGE_LENGTH = 1024;
36
+ export declare function isErrorFrameCode(code: string): boolean;
37
+ export declare function isErrorFrameMessage(message: string): boolean;
38
+ /**
39
+ * Makes a message built from untrusted text signable: control characters become spaces and the
40
+ * result is truncated. Every caller that interpolates something the hub or an upstream chose —
41
+ * a `srv` claim, a method, an upstream error string — runs it through this, because
42
+ * `FrameSigner.next` refuses the frame otherwise and the relay would answer nothing at all.
43
+ */
44
+ export declare function sanitizeFrameMessage(message: string): string;
45
+ /** Contract C7. */
46
+ export declare function fieldsDigest(frame: UnsignedFrame): string;
47
+ export declare function frameSigningInput(requestId: string, frame: UnsignedFrame, prev: string): string;
48
+ export declare class FrameSigner {
49
+ private readonly key;
50
+ private readonly requestId;
51
+ private prev;
52
+ private seq;
53
+ constructor(key: KeyObject, requestId: string, jti: string);
54
+ get nextSeq(): number;
55
+ /**
56
+ * A `head`'s headers are signed as given: pass `filterHeaders(pairs, isReturnedResponseHeader)`
57
+ * output. An `error`'s code and message must already satisfy the injectivity rule — build the
58
+ * message with `sanitizeFrameMessage` when any part of it came from outside the relay — because
59
+ * the relay must never put a signature on a frame a hub could restate.
60
+ */
61
+ next(fields: FrameFields): Frame;
62
+ }
63
+ export declare function isFrameShape(raw: unknown): raw is Frame;
64
+ export declare class FrameChainVerifier {
65
+ private readonly publicKey;
66
+ private readonly requestId;
67
+ private expectedSeq;
68
+ private prev;
69
+ private last;
70
+ private done;
71
+ constructor(publicKey: KeyObject, requestId: string, jti: string);
72
+ verify(raw: unknown): Frame;
73
+ }
74
+ export {};
75
+ //# sourceMappingURL=frames.d.ts.map
@@ -0,0 +1,224 @@
1
+ import { b64url, fromB64url, sha256b64url } from "./b64.js";
2
+ import { HeaderError, canonicalHeaders, filterHeaders, isReturnedResponseHeader } from "./headers.js";
3
+ import { signBytes, verifyBytes } from "./keys.js";
4
+ export class FrameError extends Error {
5
+ code = "response_invalid";
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = "FrameError";
9
+ }
10
+ }
11
+ /**
12
+ * An `error` frame's digest is `code + "\n" + message`, so without a rule on the two fields the
13
+ * pair is not injective: `("upstream_incomplete", "a\nb")` and `("upstream_incomplete\na", "b")`
14
+ * hash the same and share one signature, which lets the untrusted hub (spec D14) restate either
15
+ * as the other. This is the rule C6 already applies to header values, in the one place C7 left
16
+ * open. The code is additionally constrained to a wire-code spelling because it ends up as a
17
+ * metric label and as a `McpRelayErrorCode` on the Java side; the message is bounded so a frame
18
+ * cannot carry a payload in a field nothing reads as one. `fieldsDigest` is unchanged: the rule
19
+ * lives in the shape gate, on the signer's side and the verifier's alike.
20
+ */
21
+ const ERROR_CODE_SHAPE = /^[a-z][a-z0-9_]{0,63}$/;
22
+ const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
23
+ /** UTF-16 code units, so `String.length` says the same thing on both sides of the wire. */
24
+ export const MAX_FRAME_MESSAGE_LENGTH = 1024;
25
+ export function isErrorFrameCode(code) {
26
+ return ERROR_CODE_SHAPE.test(code);
27
+ }
28
+ export function isErrorFrameMessage(message) {
29
+ return message.length <= MAX_FRAME_MESSAGE_LENGTH && !CONTROL_CHARACTERS.test(message);
30
+ }
31
+ /**
32
+ * Makes a message built from untrusted text signable: control characters become spaces and the
33
+ * result is truncated. Every caller that interpolates something the hub or an upstream chose —
34
+ * a `srv` claim, a method, an upstream error string — runs it through this, because
35
+ * `FrameSigner.next` refuses the frame otherwise and the relay would answer nothing at all.
36
+ */
37
+ export function sanitizeFrameMessage(message) {
38
+ return message.replace(/[\u0000-\u001f\u007f]/g, " ").slice(0, MAX_FRAME_MESSAGE_LENGTH);
39
+ }
40
+ /** Contract C7. */
41
+ export function fieldsDigest(frame) {
42
+ switch (frame.type) {
43
+ case "accepted":
44
+ case "end":
45
+ return sha256b64url(Buffer.alloc(0));
46
+ case "head":
47
+ return sha256b64url(`${frame.status}\n${canonicalHeaders(frame.headers, isReturnedResponseHeader)}`);
48
+ case "data":
49
+ return sha256b64url(fromB64url(frame.bytes));
50
+ case "error":
51
+ return sha256b64url(`${frame.code}\n${frame.message}`);
52
+ }
53
+ }
54
+ export function frameSigningInput(requestId, frame, prev) {
55
+ return `mcprelay-frame-v1\n${requestId}\n${frame.seq}\n${frame.type}\n${fieldsDigest(frame)}\n${prev}`;
56
+ }
57
+ export class FrameSigner {
58
+ key;
59
+ requestId;
60
+ prev;
61
+ seq = 0;
62
+ constructor(key, requestId, jti) {
63
+ this.key = key;
64
+ this.requestId = requestId;
65
+ this.prev = jti;
66
+ }
67
+ get nextSeq() {
68
+ return this.seq;
69
+ }
70
+ /**
71
+ * A `head`'s headers are signed as given: pass `filterHeaders(pairs, isReturnedResponseHeader)`
72
+ * output. An `error`'s code and message must already satisfy the injectivity rule — build the
73
+ * message with `sanitizeFrameMessage` when any part of it came from outside the relay — because
74
+ * the relay must never put a signature on a frame a hub could restate.
75
+ */
76
+ next(fields) {
77
+ const frame = { ...fields, seq: this.seq };
78
+ if (frame.type === "error") {
79
+ if (!isErrorFrameCode(frame.code)) {
80
+ throw new FrameError(`error frame code ${JSON.stringify(frame.code)} is not a wire code`);
81
+ }
82
+ if (!isErrorFrameMessage(frame.message)) {
83
+ throw new FrameError("error frame message carries a control character or exceeds 1024 characters");
84
+ }
85
+ }
86
+ const sig = b64url(signBytes(this.key, frameSigningInput(this.requestId, frame, this.prev)));
87
+ this.prev = sig;
88
+ this.seq += 1;
89
+ return { ...frame, sig };
90
+ }
91
+ }
92
+ function isRecord(value) {
93
+ return typeof value === "object" && value !== null && !Array.isArray(value);
94
+ }
95
+ function isHeaderPairs(value) {
96
+ return (Array.isArray(value) &&
97
+ value.every((pair) => Array.isArray(pair) && pair.length === 2 && typeof pair[0] === "string" && typeof pair[1] === "string"));
98
+ }
99
+ export function isFrameShape(raw) {
100
+ if (!isRecord(raw) || typeof raw.sig !== "string" || !Number.isInteger(raw.seq) || raw.seq < 0) {
101
+ return false;
102
+ }
103
+ switch (raw.type) {
104
+ case "accepted":
105
+ case "end":
106
+ return true;
107
+ case "head":
108
+ return Number.isInteger(raw.status) && isHeaderPairs(raw.headers);
109
+ case "data":
110
+ return typeof raw.bytes === "string";
111
+ case "error":
112
+ return (typeof raw.code === "string" &&
113
+ isErrorFrameCode(raw.code) &&
114
+ typeof raw.message === "string" &&
115
+ isErrorFrameMessage(raw.message));
116
+ default:
117
+ return false;
118
+ }
119
+ }
120
+ /**
121
+ * The signed fields of a frame and nothing else. `isFrameShape` tolerates extra properties — the
122
+ * hub is untrusted (spec D14) and may add any — and the digest covers only the enumerated ones,
123
+ * so a verified object must be rebuilt before it reaches a caller. Otherwise unsigned,
124
+ * attacker-chosen data rides inside an object the backend has just proved authentic. The header
125
+ * pairs are copied for the same reason: the arrays themselves are the hub's.
126
+ */
127
+ function signedFieldsOnly(frame) {
128
+ switch (frame.type) {
129
+ case "accepted":
130
+ return { seq: frame.seq, type: "accepted", sig: frame.sig };
131
+ case "end":
132
+ return { seq: frame.seq, type: "end", sig: frame.sig };
133
+ case "head":
134
+ return {
135
+ seq: frame.seq,
136
+ type: "head",
137
+ status: frame.status,
138
+ headers: frame.headers.map(([name, value]) => [name, value]),
139
+ sig: frame.sig,
140
+ };
141
+ case "data":
142
+ return { seq: frame.seq, type: "data", bytes: frame.bytes, sig: frame.sig };
143
+ case "error":
144
+ return { seq: frame.seq, type: "error", code: frame.code, message: frame.message, sig: frame.sig };
145
+ }
146
+ }
147
+ /**
148
+ * A `head` carries the canonical returned headers of contract C6 as sorted pairs. The digest
149
+ * covers that canonical form, so a frame whose pairs are spelled differently would let the hub
150
+ * restate a signed head; a HeaderError (duplicate name, control character in a value) says the
151
+ * same thing — these pairs are not the canonical form — and is therefore a rejection here, not
152
+ * an exception out of the verifier.
153
+ */
154
+ function isCanonicalHead(headers) {
155
+ try {
156
+ return JSON.stringify(filterHeaders(headers, isReturnedResponseHeader)) === JSON.stringify(headers);
157
+ }
158
+ catch (error) {
159
+ if (error instanceof HeaderError) {
160
+ return false;
161
+ }
162
+ throw error;
163
+ }
164
+ }
165
+ export class FrameChainVerifier {
166
+ publicKey;
167
+ requestId;
168
+ expectedSeq = 0;
169
+ prev;
170
+ last = null;
171
+ done = false;
172
+ constructor(publicKey, requestId, jti) {
173
+ this.publicKey = publicKey;
174
+ this.requestId = requestId;
175
+ this.prev = jti;
176
+ }
177
+ verify(raw) {
178
+ if (!isFrameShape(raw)) {
179
+ throw new FrameError("malformed frame");
180
+ }
181
+ if (raw.seq !== this.expectedSeq) {
182
+ throw new FrameError(`expected frame ${this.expectedSeq}, got ${raw.seq}`);
183
+ }
184
+ if (this.done) {
185
+ throw new FrameError("frame after a terminal frame");
186
+ }
187
+ const allowed = raw.seq === 0
188
+ ? raw.type === "accepted" || raw.type === "error"
189
+ : this.last === "accepted"
190
+ ? raw.type === "head" || raw.type === "error"
191
+ : raw.type === "data" || raw.type === "end" || raw.type === "error";
192
+ if (!allowed) {
193
+ throw new FrameError(`${raw.type} is not allowed at this point of the chain`);
194
+ }
195
+ if (raw.type === "head" && !isCanonicalHead(raw.headers)) {
196
+ throw new FrameError("head headers are not canonical");
197
+ }
198
+ let signature;
199
+ try {
200
+ signature = fromB64url(raw.sig);
201
+ }
202
+ catch {
203
+ throw new FrameError("malformed signature");
204
+ }
205
+ let signingInput;
206
+ try {
207
+ signingInput = frameSigningInput(this.requestId, raw, this.prev);
208
+ }
209
+ catch {
210
+ // A `data` payload that is not canonical base64url (contract C1) lands here: the hub is
211
+ // untrusted for integrity (spec D14), so its input is rejected, never thrown past us.
212
+ throw new FrameError("malformed frame fields");
213
+ }
214
+ if (!verifyBytes(this.publicKey, signingInput, signature)) {
215
+ throw new FrameError("frame signature does not verify");
216
+ }
217
+ this.prev = raw.sig;
218
+ this.expectedSeq += 1;
219
+ this.last = raw.type;
220
+ this.done = raw.type === "end" || raw.type === "error";
221
+ return signedFieldsOnly(raw);
222
+ }
223
+ }
224
+ //# sourceMappingURL=frames.js.map
@@ -0,0 +1,18 @@
1
+ import type { HeaderPair } from "./wire.js";
2
+ export type HeaderErrorCode = "duplicate_header" | "invalid_header";
3
+ export declare class HeaderError extends Error {
4
+ readonly code: HeaderErrorCode;
5
+ constructor(code: HeaderErrorCode, message: string);
6
+ }
7
+ export declare function isForwardedRequestHeader(name: string): boolean;
8
+ export declare function isReturnedResponseHeader(name: string): boolean;
9
+ /**
10
+ * Contract C6: lower-case, keep allowed, refuse control characters and non-token names,
11
+ * trim, refuse duplicates, sort by name. A header that is not on the allowlist is ignored
12
+ * without being validated — it never reaches the digest.
13
+ */
14
+ export declare function filterHeaders(pairs: Iterable<readonly [string, string]>, allowed: (name: string) => boolean): HeaderPair[];
15
+ export declare function canonicalHeaders(pairs: Iterable<readonly [string, string]>, allowed: (name: string) => boolean): string;
16
+ export declare function requestHeadersDigest(pairs: Iterable<readonly [string, string]>): string;
17
+ export declare function bodyDigest(body: Uint8Array): string;
18
+ //# sourceMappingURL=headers.d.ts.map
@@ -0,0 +1,88 @@
1
+ // GENERATED COPY of relay/src/protocol/headers.ts (#1377). Do not edit here: edit the relay's file,
2
+ // then run `npm run sync:relay-protocol` in cli/. `npm run check:relay-protocol` fails on any drift.
3
+ import { sha256b64url } from "./b64.js";
4
+ const FORWARDED_REQUEST_HEADERS = new Set([
5
+ "accept",
6
+ "authorization",
7
+ "content-type",
8
+ "idempotency-key",
9
+ "last-event-id",
10
+ "mcp-method",
11
+ "mcp-name",
12
+ "mcp-protocol-version",
13
+ "mcp-session-id",
14
+ "traceparent",
15
+ "tracestate",
16
+ ]);
17
+ const RETURNED_RESPONSE_HEADERS = new Set([
18
+ "cache-control",
19
+ "content-type",
20
+ "etag",
21
+ "location",
22
+ "mcp-protocol-version",
23
+ "mcp-session-id",
24
+ "retry-after",
25
+ "www-authenticate",
26
+ ]);
27
+ export class HeaderError extends Error {
28
+ code;
29
+ constructor(code, message) {
30
+ super(message);
31
+ this.name = "HeaderError";
32
+ this.code = code;
33
+ }
34
+ }
35
+ export function isForwardedRequestHeader(name) {
36
+ return FORWARDED_REQUEST_HEADERS.has(name) || name.startsWith("mcp-param-");
37
+ }
38
+ export function isReturnedResponseHeader(name) {
39
+ return RETURNED_RESPONSE_HEADERS.has(name);
40
+ }
41
+ const SURROUNDING_WHITESPACE = /^[ \t]+|[ \t]+$/g;
42
+ /** RFC 7230 token, already lower-cased. Only the `mcp-param-` prefix can reach it unvetted. */
43
+ const HEADER_NAME_TOKEN = /^[!#$%&'*+\-.^_`|~0-9a-z]+$/;
44
+ /**
45
+ * Any code unit below 0x20 except TAB, plus DEL. The canonical form joins pairs with ":"
46
+ * and "\n", so a value carrying either separator would let two different header sets
47
+ * produce one digest — and the hub, untrusted for integrity (spec D14), is the party that
48
+ * would exploit that. Refusing the whole control range keeps the rule trivial to restate
49
+ * in Java.
50
+ */
51
+ const FORBIDDEN_VALUE_CHARS = /[\u0000-\u0008\u000a-\u001f\u007f]/;
52
+ /**
53
+ * Contract C6: lower-case, keep allowed, refuse control characters and non-token names,
54
+ * trim, refuse duplicates, sort by name. A header that is not on the allowlist is ignored
55
+ * without being validated — it never reaches the digest.
56
+ */
57
+ export function filterHeaders(pairs, allowed) {
58
+ const kept = new Map();
59
+ for (const [rawName, rawValue] of pairs) {
60
+ const name = rawName.toLowerCase();
61
+ if (!allowed(name)) {
62
+ continue;
63
+ }
64
+ if (!HEADER_NAME_TOKEN.test(name)) {
65
+ throw new HeaderError("invalid_header", `header name ${JSON.stringify(name)} is not a token`);
66
+ }
67
+ if (FORBIDDEN_VALUE_CHARS.test(rawValue)) {
68
+ throw new HeaderError("invalid_header", `header ${name} carries a control character`);
69
+ }
70
+ if (kept.has(name)) {
71
+ throw new HeaderError("duplicate_header", `header ${name} appears more than once`);
72
+ }
73
+ kept.set(name, rawValue.replace(SURROUNDING_WHITESPACE, ""));
74
+ }
75
+ return [...kept.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
76
+ }
77
+ export function canonicalHeaders(pairs, allowed) {
78
+ return filterHeaders(pairs, allowed)
79
+ .map(([name, value]) => `${name}:${value}\n`)
80
+ .join("");
81
+ }
82
+ export function requestHeadersDigest(pairs) {
83
+ return sha256b64url(canonicalHeaders(pairs, isForwardedRequestHeader));
84
+ }
85
+ export function bodyDigest(body) {
86
+ return sha256b64url(body);
87
+ }
88
+ //# sourceMappingURL=headers.js.map
@@ -0,0 +1,28 @@
1
+ import type { KeyObject } from "node:crypto";
2
+ export declare class JwsError extends Error {
3
+ readonly code: "malformed" | "bad_alg" | "bad_typ" | "unknown_kid" | "bad_signature" | "bad_claims";
4
+ constructor(code: "malformed" | "bad_alg" | "bad_typ" | "unknown_kid" | "bad_signature" | "bad_claims", message: string);
5
+ }
6
+ export interface JwsHeader {
7
+ alg: "EdDSA";
8
+ typ: string;
9
+ kid: string;
10
+ }
11
+ export interface DecodedJws {
12
+ header: JwsHeader;
13
+ claims: Record<string, unknown>;
14
+ signingInput: string;
15
+ signature: Buffer;
16
+ }
17
+ /** Contract C3. */
18
+ export declare function signJws(typ: string, kid: string, claims: Record<string, unknown>, key: KeyObject): string;
19
+ export declare function decodeJws(token: string): DecodedJws;
20
+ export declare function verifyJws(token: string, expectedTyp: string, keyFor: (kid: string) => KeyObject | undefined): DecodedJws;
21
+ /** Contract C8. */
22
+ export declare function enrolProofInput(token: string, kid: string, publicKey: string): string;
23
+ export declare function signEnrolProof(key: KeyObject, token: string, kid: string, publicKey: string): string;
24
+ /** Contract C5. */
25
+ export declare function mintRelayJwt(key: KeyObject, kid: string, relayId: string, audience: string, nowSec: number): string;
26
+ /** Checks the claims of an already signature-verified relay JWT; returns the relay id. */
27
+ export declare function verifyRelayJwtClaims(claims: Record<string, unknown>, audience: string, nowSec: number): string;
28
+ //# sourceMappingURL=jws.d.ts.map
@@ -0,0 +1,113 @@
1
+ import { b64url, fromB64url, randomHex } from "./b64.js";
2
+ import { signBytes, verifyBytes } from "./keys.js";
3
+ import { CLOCK_SKEW_SECONDS, MAX_JWT_LIFETIME_SECONDS, RELAY_TYP } from "./wire.js";
4
+ export class JwsError extends Error {
5
+ code;
6
+ constructor(code, message) {
7
+ super(message);
8
+ this.code = code;
9
+ this.name = "JwsError";
10
+ }
11
+ }
12
+ function isRecord(value) {
13
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14
+ }
15
+ function part(json) {
16
+ return b64url(Buffer.from(JSON.stringify(json), "utf8"));
17
+ }
18
+ /** Contract C3. */
19
+ export function signJws(typ, kid, claims, key) {
20
+ const signingInput = `${part({ alg: "EdDSA", typ, kid })}.${part(claims)}`;
21
+ return `${signingInput}.${b64url(signBytes(key, signingInput))}`;
22
+ }
23
+ export function decodeJws(token) {
24
+ const parts = token.split(".");
25
+ if (parts.length !== 3) {
26
+ throw new JwsError("malformed", "a compact JWS has three parts");
27
+ }
28
+ const [headerPart, payloadPart, signaturePart] = parts;
29
+ let header;
30
+ let claims;
31
+ let signature;
32
+ try {
33
+ header = JSON.parse(fromB64url(headerPart).toString("utf8"));
34
+ claims = JSON.parse(fromB64url(payloadPart).toString("utf8"));
35
+ signature = fromB64url(signaturePart);
36
+ }
37
+ catch {
38
+ throw new JwsError("malformed", "unparseable header, payload or signature");
39
+ }
40
+ if (!isRecord(header) || !isRecord(claims)) {
41
+ throw new JwsError("malformed", "header and payload must be JSON objects");
42
+ }
43
+ if (header.alg !== "EdDSA") {
44
+ throw new JwsError("bad_alg", "alg must be EdDSA");
45
+ }
46
+ if (typeof header.typ !== "string") {
47
+ throw new JwsError("malformed", "typ must be a string");
48
+ }
49
+ if (typeof header.kid !== "string" || header.kid.length < 1 || header.kid.length > 64) {
50
+ throw new JwsError("malformed", "kid must be a string of 1 to 64 characters");
51
+ }
52
+ if (signature.length !== 64) {
53
+ throw new JwsError("malformed", "an Ed25519 signature has 64 bytes");
54
+ }
55
+ return {
56
+ header: { alg: "EdDSA", typ: header.typ, kid: header.kid },
57
+ claims,
58
+ signingInput: `${headerPart}.${payloadPart}`,
59
+ signature,
60
+ };
61
+ }
62
+ export function verifyJws(token, expectedTyp, keyFor) {
63
+ const decoded = decodeJws(token);
64
+ if (decoded.header.typ !== expectedTyp) {
65
+ throw new JwsError("bad_typ", `typ must be ${expectedTyp}`);
66
+ }
67
+ const key = keyFor(decoded.header.kid);
68
+ if (key === undefined) {
69
+ throw new JwsError("unknown_kid", "no key for this kid");
70
+ }
71
+ if (!verifyBytes(key, decoded.signingInput, decoded.signature)) {
72
+ throw new JwsError("bad_signature", "signature does not verify");
73
+ }
74
+ return decoded;
75
+ }
76
+ /** Contract C8. */
77
+ export function enrolProofInput(token, kid, publicKey) {
78
+ return `mcprelay-enrol-v1\n${token}\n${kid}\n${publicKey}`;
79
+ }
80
+ export function signEnrolProof(key, token, kid, publicKey) {
81
+ return b64url(signBytes(key, enrolProofInput(token, kid, publicKey)));
82
+ }
83
+ /** Contract C5. */
84
+ export function mintRelayJwt(key, kid, relayId, audience, nowSec) {
85
+ return signJws(RELAY_TYP, kid, { iss: relayId, aud: audience, iat: nowSec, exp: nowSec + MAX_JWT_LIFETIME_SECONDS, jti: randomHex(16) }, key);
86
+ }
87
+ /** Checks the claims of an already signature-verified relay JWT; returns the relay id. */
88
+ export function verifyRelayJwtClaims(claims, audience, nowSec) {
89
+ const { iss, aud, iat, exp } = claims;
90
+ if (typeof iss !== "string" || iss.length === 0) {
91
+ throw new JwsError("bad_claims", "relay JWT has no issuer");
92
+ }
93
+ if (aud !== audience) {
94
+ throw new JwsError("bad_claims", "relay JWT audience does not match");
95
+ }
96
+ if (typeof iat !== "number" || typeof exp !== "number" || !Number.isInteger(iat) || !Number.isInteger(exp)) {
97
+ throw new JwsError("bad_claims", "relay JWT times must be integers");
98
+ }
99
+ if (exp - iat > MAX_JWT_LIFETIME_SECONDS) {
100
+ throw new JwsError("bad_claims", "relay JWT lifetime exceeds 60 seconds");
101
+ }
102
+ if (exp < iat) {
103
+ throw new JwsError("bad_claims", "relay JWT expires before it was issued");
104
+ }
105
+ if (nowSec > exp + CLOCK_SKEW_SECONDS) {
106
+ throw new JwsError("bad_claims", "relay JWT expired");
107
+ }
108
+ if (iat > nowSec + CLOCK_SKEW_SECONDS) {
109
+ throw new JwsError("bad_claims", "relay JWT issued in the future");
110
+ }
111
+ return iss;
112
+ }
113
+ //# sourceMappingURL=jws.js.map
@@ -0,0 +1,26 @@
1
+ import { type KeyObject } from "node:crypto";
2
+ export interface Jwk {
3
+ kty: "OKP";
4
+ crv: "Ed25519";
5
+ x: string;
6
+ kid: string;
7
+ alg: "EdDSA";
8
+ use: "sig";
9
+ }
10
+ export declare function privateKeyFromSeed(seed: Uint8Array): KeyObject;
11
+ export declare function privateKeyFromPem(pem: string): KeyObject;
12
+ export declare function privateKeyToPem(key: KeyObject): string;
13
+ export declare function generatePrivateKey(): KeyObject;
14
+ export declare function rawPublicKey(key: KeyObject): Buffer;
15
+ export declare function publicKeyFromRaw(raw: Uint8Array): KeyObject;
16
+ export declare function publicKeyFromJwk(jwk: {
17
+ kty?: unknown;
18
+ crv?: unknown;
19
+ x?: unknown;
20
+ }): KeyObject;
21
+ /** Contract C2: first 16 characters of base64url(SHA-256(raw public key)). */
22
+ export declare function kidOf(raw: Uint8Array): string;
23
+ export declare function jwkOf(raw: Uint8Array): Jwk;
24
+ export declare function signBytes(key: KeyObject, data: Uint8Array | string): Buffer;
25
+ export declare function verifyBytes(key: KeyObject, data: Uint8Array | string, signature: Uint8Array): boolean;
26
+ //# sourceMappingURL=keys.d.ts.map
@@ -0,0 +1,80 @@
1
+ // GENERATED COPY of relay/src/protocol/keys.ts (#1377). Do not edit here: edit the relay's file,
2
+ // then run `npm run sync:relay-protocol` in cli/. `npm run check:relay-protocol` fails on any drift.
3
+ import { createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify, } from "node:crypto";
4
+ import { b64url, fromB64url, sha256b64url } from "./b64.js";
5
+ // DER prefixes for Ed25519 keys (RFC 8410): PKCS#8 private key and SPKI public key.
6
+ const PKCS8_ED25519_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
7
+ const SPKI_ED25519_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
8
+ export function privateKeyFromSeed(seed) {
9
+ if (seed.length !== 32) {
10
+ throw new Error("Ed25519 seed must be 32 bytes");
11
+ }
12
+ return createPrivateKey({
13
+ key: Buffer.concat([PKCS8_ED25519_PREFIX, Buffer.from(seed)]),
14
+ format: "der",
15
+ type: "pkcs8",
16
+ });
17
+ }
18
+ export function privateKeyFromPem(pem) {
19
+ const key = createPrivateKey(pem);
20
+ if (key.asymmetricKeyType !== "ed25519") {
21
+ throw new Error("not an Ed25519 private key");
22
+ }
23
+ return key;
24
+ }
25
+ export function privateKeyToPem(key) {
26
+ return key.export({ format: "pem", type: "pkcs8" }).toString();
27
+ }
28
+ export function generatePrivateKey() {
29
+ return generateKeyPairSync("ed25519").privateKey;
30
+ }
31
+ export function rawPublicKey(key) {
32
+ if (key.asymmetricKeyType !== "ed25519") {
33
+ throw new Error("not an Ed25519 key");
34
+ }
35
+ // createPublicKey derives the public key from a private one, but rejects a public KeyObject.
36
+ const publicKey = key.type === "public" ? key : createPublicKey(key);
37
+ const der = publicKey.export({ format: "der", type: "spki" });
38
+ return Buffer.from(der.subarray(der.length - 32));
39
+ }
40
+ export function publicKeyFromRaw(raw) {
41
+ if (raw.length !== 32) {
42
+ throw new Error("Ed25519 public key must be 32 bytes");
43
+ }
44
+ return createPublicKey({
45
+ key: Buffer.concat([SPKI_ED25519_PREFIX, Buffer.from(raw)]),
46
+ format: "der",
47
+ type: "spki",
48
+ });
49
+ }
50
+ export function publicKeyFromJwk(jwk) {
51
+ if (jwk.kty !== "OKP" || jwk.crv !== "Ed25519" || typeof jwk.x !== "string") {
52
+ throw new Error("not an Ed25519 JWK");
53
+ }
54
+ return publicKeyFromRaw(fromB64url(jwk.x));
55
+ }
56
+ /** Contract C2: first 16 characters of base64url(SHA-256(raw public key)). */
57
+ export function kidOf(raw) {
58
+ return sha256b64url(raw).slice(0, 16);
59
+ }
60
+ export function jwkOf(raw) {
61
+ return { kty: "OKP", crv: "Ed25519", x: b64url(raw), kid: kidOf(raw), alg: "EdDSA", use: "sig" };
62
+ }
63
+ function bytes(data) {
64
+ return typeof data === "string" ? Buffer.from(data, "utf8") : Buffer.from(data);
65
+ }
66
+ export function signBytes(key, data) {
67
+ return sign(null, bytes(data), key);
68
+ }
69
+ export function verifyBytes(key, data, signature) {
70
+ if (signature.length !== 64) {
71
+ return false;
72
+ }
73
+ try {
74
+ return verify(null, bytes(data), key, Buffer.from(signature));
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ }
80
+ //# sourceMappingURL=keys.js.map