@tribe-nest/media-protocol 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/build/_fixtures/joinTokenVectors.json +103 -0
  2. package/build/claims.d.ts +327 -0
  3. package/build/claims.d.ts.map +1 -0
  4. package/build/claims.js +91 -0
  5. package/build/claims.js.map +1 -0
  6. package/build/codecs.d.ts +102 -0
  7. package/build/codecs.d.ts.map +1 -0
  8. package/build/codecs.js +138 -0
  9. package/build/codecs.js.map +1 -0
  10. package/build/errorSchema.d.ts +8 -0
  11. package/build/errorSchema.d.ts.map +1 -0
  12. package/build/errorSchema.js +12 -0
  13. package/build/errorSchema.js.map +1 -0
  14. package/build/errors.d.ts +18 -0
  15. package/build/errors.d.ts.map +1 -0
  16. package/build/errors.js +47 -0
  17. package/build/errors.js.map +1 -0
  18. package/build/frames.d.ts +1291 -0
  19. package/build/frames.d.ts.map +1 -0
  20. package/build/frames.js +222 -0
  21. package/build/frames.js.map +1 -0
  22. package/build/grants.d.ts +307 -0
  23. package/build/grants.d.ts.map +1 -0
  24. package/build/grants.js +228 -0
  25. package/build/grants.js.map +1 -0
  26. package/build/index.d.ts +24 -0
  27. package/build/index.d.ts.map +1 -0
  28. package/build/index.js +40 -0
  29. package/build/index.js.map +1 -0
  30. package/build/mp-build/_fixtures/joinTokenVectors.json +103 -0
  31. package/build/mp-build/claims.d.ts +299 -0
  32. package/build/mp-build/claims.d.ts.map +1 -0
  33. package/build/mp-build/claims.js +60 -0
  34. package/build/mp-build/claims.js.map +1 -0
  35. package/build/mp-build/errorSchema.d.ts +8 -0
  36. package/build/mp-build/errorSchema.d.ts.map +1 -0
  37. package/build/mp-build/errorSchema.js +12 -0
  38. package/build/mp-build/errorSchema.js.map +1 -0
  39. package/build/mp-build/errors.d.ts +18 -0
  40. package/build/mp-build/errors.d.ts.map +1 -0
  41. package/build/mp-build/errors.js +47 -0
  42. package/build/mp-build/errors.js.map +1 -0
  43. package/build/mp-build/frames.d.ts +786 -0
  44. package/build/mp-build/frames.d.ts.map +1 -0
  45. package/build/mp-build/frames.js +153 -0
  46. package/build/mp-build/frames.js.map +1 -0
  47. package/build/mp-build/grants.d.ts +245 -0
  48. package/build/mp-build/grants.d.ts.map +1 -0
  49. package/build/mp-build/grants.js +135 -0
  50. package/build/mp-build/grants.js.map +1 -0
  51. package/build/mp-build/index.d.ts +23 -0
  52. package/build/mp-build/index.d.ts.map +1 -0
  53. package/build/mp-build/index.js +39 -0
  54. package/build/mp-build/index.js.map +1 -0
  55. package/build/mp-build/token.d.ts +66 -0
  56. package/build/mp-build/token.d.ts.map +1 -0
  57. package/build/mp-build/token.js +208 -0
  58. package/build/mp-build/token.js.map +1 -0
  59. package/build/mp-build/version.d.ts +9 -0
  60. package/build/mp-build/version.d.ts.map +1 -0
  61. package/build/mp-build/version.js +12 -0
  62. package/build/mp-build/version.js.map +1 -0
  63. package/build/token.d.ts +66 -0
  64. package/build/token.d.ts.map +1 -0
  65. package/build/token.js +208 -0
  66. package/build/token.js.map +1 -0
  67. package/build/version.d.ts +9 -0
  68. package/build/version.d.ts.map +1 -0
  69. package/build/version.js +12 -0
  70. package/build/version.js.map +1 -0
  71. package/package.json +53 -0
  72. package/src/_fixtures/joinTokenVectors.json +103 -0
  73. package/src/_tests/codecs.spec.ts +66 -0
  74. package/src/_tests/publishGrants.spec.ts +109 -0
  75. package/src/_tests/token.spec.ts +272 -0
  76. package/src/claims.ts +116 -0
  77. package/src/codecs.ts +165 -0
  78. package/src/errorSchema.ts +9 -0
  79. package/src/errors.ts +44 -0
  80. package/src/frames.ts +234 -0
  81. package/src/grants.ts +248 -0
  82. package/src/index.ts +23 -0
  83. package/src/token.ts +237 -0
  84. package/src/version.ts +8 -0
@@ -0,0 +1,66 @@
1
+ import { type MediaControlClaims, type MediaJoinClaims, type VerifyResult } from "./claims";
2
+ /**
3
+ * Ed25519-signed join and control tokens.
4
+ *
5
+ * ## Why asymmetric, when LiveKit uses a shared secret
6
+ *
7
+ * LiveKit signs with an API key + secret, which is HMAC: every party that can
8
+ * verify can also mint. Media nodes are public-facing, accept connections from
9
+ * anyone, and there are many of them. Under a shared secret, compromising ONE
10
+ * node yields the ability to mint a token for any room in the system -
11
+ * including a Mandantengespräch.
12
+ *
13
+ * So the backend holds a PRIVATE key and signs; nodes hold only PUBLIC keys and
14
+ * verify. A compromised node can verify and nothing else. The cost is a keypair
15
+ * instead of a string.
16
+ *
17
+ * ## Why this file is Node-only, and the package root is not
18
+ *
19
+ * Signing and verifying need `node:crypto`. The client SDK imports the wire
20
+ * frames and grant TYPES and never verifies anything - it only carries a token
21
+ * it was handed - so those live at the package root and stay isomorphic. This
22
+ * module is a separate entry point (`@tribe-nest/media-protocol/token`) so
23
+ * importing a frame type cannot drag `crypto` into a browser bundle.
24
+ *
25
+ * ## Why verification is PURE
26
+ *
27
+ * No I/O, no clock unless you pass one, no Redis. Replay defence needs state
28
+ * and therefore lives in the media server (`auth/replayGuard`), composed
29
+ * around this. Keeping the crypto and claim checks free of state is what lets
30
+ * the golden vectors pin exactly one code path on both sides of the wire.
31
+ */
32
+ export type PublicKeyring = Record<string, string>;
33
+ export type PrivateKeyring = Record<string, string>;
34
+ /** Default ceiling on `exp - iat`. A join ticket, not a session. */
35
+ export declare const MAX_TOKEN_TTL_SECONDS: number;
36
+ export declare function signJoinToken(privateKeyPem: string, kid: string, claims: MediaJoinClaims): string;
37
+ export declare function signControlToken(privateKeyPem: string, kid: string, claims: MediaControlClaims): string;
38
+ type VerifyArgs = {
39
+ token: string;
40
+ keys: PublicKeyring;
41
+ issuer: string;
42
+ maxTtlSeconds?: number;
43
+ /** Seconds since epoch. Injected so vectors are deterministic. */
44
+ nowSeconds?: number;
45
+ skewSeconds?: number;
46
+ /** When given, the token's room must match. */
47
+ expectedRoom?: string;
48
+ };
49
+ export declare function verifyJoinToken(args: VerifyArgs): VerifyResult<MediaJoinClaims>;
50
+ export declare function verifyControlToken(args: VerifyArgs): VerifyResult<MediaControlClaims>;
51
+ /**
52
+ * Boot guard for a media node.
53
+ *
54
+ * A node must hold verification keys ONLY. If a private key reaches a node's
55
+ * keyring - a copy-paste, a shared secrets bundle - that node can mint, and the
56
+ * whole reason for the asymmetric split is gone silently. This makes it a
57
+ * refusal to start rather than a latent capability.
58
+ */
59
+ export declare function assertPublicKeyringIsPublicOnly(keys: PublicKeyring): void;
60
+ /** Convenience for tooling and tests; not used at runtime. */
61
+ export declare function generateKeypair(): {
62
+ publicKey: string;
63
+ privateKey: string;
64
+ };
65
+ export {};
66
+ //# sourceMappingURL=token.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../src/token.ts"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,KAAK,YAAY,EAClB,MAAM,UAAU,CAAC;AAGlB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AACnD,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAMpD,oEAAoE;AACpE,eAAO,MAAM,qBAAqB,QAAU,CAAC;AAe7C,wBAAgB,aAAa,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,MAAM,CAgBjG;AAED,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAIvG;AAED,KAAK,UAAU,GAAG;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,aAAa,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+CAA+C;IAC/C,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAuEF,wBAAgB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,YAAY,CAAC,eAAe,CAAC,CAqB/E;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,UAAU,GAAG,YAAY,CAAC,kBAAkB,CAAC,CAWrF;AAED;;;;;;;GAOG;AACH,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CAazE;AAED,8DAA8D;AAC9D,wBAAgB,eAAe,IAAI;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAM3E"}
@@ -0,0 +1,208 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.MAX_TOKEN_TTL_SECONDS = void 0;
37
+ exports.signJoinToken = signJoinToken;
38
+ exports.signControlToken = signControlToken;
39
+ exports.verifyJoinToken = verifyJoinToken;
40
+ exports.verifyControlToken = verifyControlToken;
41
+ exports.assertPublicKeyringIsPublicOnly = assertPublicKeyringIsPublicOnly;
42
+ exports.generateKeypair = generateKeypair;
43
+ const crypto = __importStar(require("crypto"));
44
+ const claims_1 = require("./claims");
45
+ const grants_1 = require("./grants");
46
+ const ALG = "EdDSA";
47
+ const b64u = (b) => b.toString("base64url");
48
+ const fromB64u = (s) => Buffer.from(s, "base64url");
49
+ /** Default ceiling on `exp - iat`. A join ticket, not a session. */
50
+ exports.MAX_TOKEN_TTL_SECONDS = 15 * 60;
51
+ function encodeSegment(value) {
52
+ return b64u(Buffer.from(JSON.stringify(value), "utf8"));
53
+ }
54
+ function signCompact(privateKeyPem, kid, payload) {
55
+ const header = encodeSegment({ alg: ALG, typ: "JWT", kid });
56
+ const body = encodeSegment(payload);
57
+ const signingInput = `${header}.${body}`;
58
+ // For Ed25519 the digest algorithm is null: the curve prescribes it.
59
+ const signature = crypto.sign(null, Buffer.from(signingInput, "utf8"), privateKeyPem);
60
+ return `${signingInput}.${b64u(signature)}`;
61
+ }
62
+ function signJoinToken(privateKeyPem, kid, claims) {
63
+ const parsed = claims_1.mediaJoinClaimsSchema.parse(claims);
64
+ // Mint-time refusals. A token that should never exist is better stopped here
65
+ // than relied upon to be refused by every future verifier.
66
+ const grantCheck = (0, grants_1.grantsAreValidForKind)(parsed.grants, parsed.kind);
67
+ if (grantCheck !== true)
68
+ throw new Error(`refusing to mint: ${grantCheck}`);
69
+ if (parsed.exp <= parsed.iat)
70
+ throw new Error("refusing to mint: exp is not after iat");
71
+ if (parsed.exp - parsed.iat > exports.MAX_TOKEN_TTL_SECONDS) {
72
+ throw new Error(`refusing to mint: ttl exceeds ${exports.MAX_TOKEN_TTL_SECONDS}s`);
73
+ }
74
+ if (parsed.transport !== "webrtc" && parsed.kind === "human") {
75
+ throw new Error("refusing to mint: a human participant may only use a webrtc transport");
76
+ }
77
+ return signCompact(privateKeyPem, kid, parsed);
78
+ }
79
+ function signControlToken(privateKeyPem, kid, claims) {
80
+ const parsed = claims_1.mediaControlClaimsSchema.parse(claims);
81
+ if (parsed.exp <= parsed.iat)
82
+ throw new Error("refusing to mint: exp is not after iat");
83
+ return signCompact(privateKeyPem, kid, parsed);
84
+ }
85
+ function verifyCompact(args, audience) {
86
+ const parts = args.token.split(".");
87
+ if (parts.length !== 3)
88
+ return { ok: false, reason: "malformed" };
89
+ const [headerSeg, bodySeg, sigSeg] = parts;
90
+ let header;
91
+ try {
92
+ header = JSON.parse(fromB64u(headerSeg).toString("utf8"));
93
+ }
94
+ catch {
95
+ return { ok: false, reason: "malformed" };
96
+ }
97
+ // Only ever EdDSA. Accepting whatever the token names is the classic JWT
98
+ // algorithm-confusion hole, and `none` is the worst case of it.
99
+ if (header.alg !== ALG)
100
+ return { ok: false, reason: "malformed" };
101
+ if (typeof header.kid !== "string" || !header.kid)
102
+ return { ok: false, reason: "malformed" };
103
+ const publicKeyPem = args.keys[header.kid];
104
+ if (!publicKeyPem)
105
+ return { ok: false, reason: "unknown_kid" };
106
+ // Signature BEFORE claims: never parse a payload that has not been shown to
107
+ // come from us.
108
+ let signatureValid = false;
109
+ try {
110
+ signatureValid = crypto.verify(null, Buffer.from(`${headerSeg}.${bodySeg}`, "utf8"), publicKeyPem, fromB64u(sigSeg));
111
+ }
112
+ catch {
113
+ return { ok: false, reason: "bad_signature" };
114
+ }
115
+ if (!signatureValid)
116
+ return { ok: false, reason: "bad_signature" };
117
+ let payload;
118
+ try {
119
+ payload = JSON.parse(fromB64u(bodySeg).toString("utf8"));
120
+ }
121
+ catch {
122
+ return { ok: false, reason: "malformed" };
123
+ }
124
+ const asRecord = payload;
125
+ if (asRecord?.aud !== audience)
126
+ return { ok: false, reason: "wrong_audience" };
127
+ if (asRecord?.iss !== args.issuer)
128
+ return { ok: false, reason: "wrong_issuer" };
129
+ return { ok: true, payload, kid: header.kid };
130
+ }
131
+ function checkTiming(claims, args) {
132
+ const now = args.nowSeconds ?? Math.floor(Date.now() / 1000);
133
+ const skew = args.skewSeconds ?? 30;
134
+ const maxTtl = args.maxTtlSeconds ?? exports.MAX_TOKEN_TTL_SECONDS;
135
+ if (claims.exp + skew <= now)
136
+ return "expired";
137
+ if (claims.nbf !== undefined && claims.nbf - skew > now)
138
+ return "not_yet_valid";
139
+ // A long-lived token is a stolen credential with a long life, so the ceiling
140
+ // is enforced at VERIFY too - not only at mint, which a compromised minter
141
+ // would simply skip.
142
+ if (claims.exp - claims.iat > maxTtl)
143
+ return "ttl_too_long";
144
+ return null;
145
+ }
146
+ function verifyJoinToken(args) {
147
+ const base = verifyCompact(args, "media");
148
+ if (!base.ok)
149
+ return base;
150
+ const parsed = claims_1.mediaJoinClaimsSchema.safeParse(base.payload);
151
+ // `.strict()` fires here. A token carrying `subscribeOnly` instead of
152
+ // `subscribe` is rejected outright rather than read as "no rule, so
153
+ // everyone" - which is how an information barrier fails open.
154
+ if (!parsed.success)
155
+ return { ok: false, reason: "bad_claims" };
156
+ const timing = checkTiming(parsed.data, args);
157
+ if (timing)
158
+ return { ok: false, reason: timing };
159
+ if (args.expectedRoom !== undefined && parsed.data.room !== args.expectedRoom) {
160
+ return { ok: false, reason: "wrong_room" };
161
+ }
162
+ if ((0, grants_1.grantsAreValidForKind)(parsed.data.grants, parsed.data.kind) !== true) {
163
+ return { ok: false, reason: "bad_claims" };
164
+ }
165
+ return { ok: true, claims: parsed.data, kid: base.kid };
166
+ }
167
+ function verifyControlToken(args) {
168
+ const base = verifyCompact(args, "control");
169
+ if (!base.ok)
170
+ return base;
171
+ const parsed = claims_1.mediaControlClaimsSchema.safeParse(base.payload);
172
+ if (!parsed.success)
173
+ return { ok: false, reason: "bad_claims" };
174
+ const timing = checkTiming(parsed.data, args);
175
+ if (timing)
176
+ return { ok: false, reason: timing };
177
+ return { ok: true, claims: parsed.data, kid: base.kid };
178
+ }
179
+ /**
180
+ * Boot guard for a media node.
181
+ *
182
+ * A node must hold verification keys ONLY. If a private key reaches a node's
183
+ * keyring - a copy-paste, a shared secrets bundle - that node can mint, and the
184
+ * whole reason for the asymmetric split is gone silently. This makes it a
185
+ * refusal to start rather than a latent capability.
186
+ */
187
+ function assertPublicKeyringIsPublicOnly(keys) {
188
+ for (const [kid, pem] of Object.entries(keys)) {
189
+ if (/PRIVATE KEY/.test(pem)) {
190
+ throw new Error(`media token keyring "${kid}" contains a PRIVATE key. Nodes verify and must never be able to mint.`);
191
+ }
192
+ try {
193
+ crypto.createPublicKey(pem);
194
+ }
195
+ catch {
196
+ throw new Error(`media token keyring "${kid}" is not a readable public key`);
197
+ }
198
+ }
199
+ }
200
+ /** Convenience for tooling and tests; not used at runtime. */
201
+ function generateKeypair() {
202
+ const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
203
+ return {
204
+ publicKey: publicKey.export({ type: "spki", format: "pem" }).toString(),
205
+ privateKey: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
206
+ };
207
+ }
208
+ //# sourceMappingURL=token.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token.js","sourceRoot":"","sources":["../src/token.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Bumped when a frame shape changes in a way an older peer cannot parse.
3
+ *
4
+ * The client sends it on connect and a node refuses a mismatch outright, rather
5
+ * than negotiating: a half-understood signalling session fails later, somewhere
6
+ * else, and looks like a media bug.
7
+ */
8
+ export declare const MEDIA_PROTOCOL_VERSION: 1;
9
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,EAAG,CAAU,CAAC"}
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MEDIA_PROTOCOL_VERSION = void 0;
4
+ /**
5
+ * Bumped when a frame shape changes in a way an older peer cannot parse.
6
+ *
7
+ * The client sends it on connect and a node refuses a mismatch outright, rather
8
+ * than negotiating: a half-understood signalling session fails later, somewhere
9
+ * else, and looks like a media bug.
10
+ */
11
+ exports.MEDIA_PROTOCOL_VERSION = 1;
12
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":";;;AAAA;;;;;;GAMG;AACU,QAAA,sBAAsB,GAAG,CAAU,CAAC"}
@@ -0,0 +1,66 @@
1
+ import { type MediaControlClaims, type MediaJoinClaims, type VerifyResult } from "./claims";
2
+ /**
3
+ * Ed25519-signed join and control tokens.
4
+ *
5
+ * ## Why asymmetric, when LiveKit uses a shared secret
6
+ *
7
+ * LiveKit signs with an API key + secret, which is HMAC: every party that can
8
+ * verify can also mint. Media nodes are public-facing, accept connections from
9
+ * anyone, and there are many of them. Under a shared secret, compromising ONE
10
+ * node yields the ability to mint a token for any room in the system -
11
+ * including a Mandantengespräch.
12
+ *
13
+ * So the backend holds a PRIVATE key and signs; nodes hold only PUBLIC keys and
14
+ * verify. A compromised node can verify and nothing else. The cost is a keypair
15
+ * instead of a string.
16
+ *
17
+ * ## Why this file is Node-only, and the package root is not
18
+ *
19
+ * Signing and verifying need `node:crypto`. The client SDK imports the wire
20
+ * frames and grant TYPES and never verifies anything - it only carries a token
21
+ * it was handed - so those live at the package root and stay isomorphic. This
22
+ * module is a separate entry point (`@tribe-nest/media-protocol/token`) so
23
+ * importing a frame type cannot drag `crypto` into a browser bundle.
24
+ *
25
+ * ## Why verification is PURE
26
+ *
27
+ * No I/O, no clock unless you pass one, no Redis. Replay defence needs state
28
+ * and therefore lives in the media server (`auth/replayGuard`), composed
29
+ * around this. Keeping the crypto and claim checks free of state is what lets
30
+ * the golden vectors pin exactly one code path on both sides of the wire.
31
+ */
32
+ export type PublicKeyring = Record<string, string>;
33
+ export type PrivateKeyring = Record<string, string>;
34
+ /** Default ceiling on `exp - iat`. A join ticket, not a session. */
35
+ export declare const MAX_TOKEN_TTL_SECONDS: number;
36
+ export declare function signJoinToken(privateKeyPem: string, kid: string, claims: MediaJoinClaims): string;
37
+ export declare function signControlToken(privateKeyPem: string, kid: string, claims: MediaControlClaims): string;
38
+ type VerifyArgs = {
39
+ token: string;
40
+ keys: PublicKeyring;
41
+ issuer: string;
42
+ maxTtlSeconds?: number;
43
+ /** Seconds since epoch. Injected so vectors are deterministic. */
44
+ nowSeconds?: number;
45
+ skewSeconds?: number;
46
+ /** When given, the token's room must match. */
47
+ expectedRoom?: string;
48
+ };
49
+ export declare function verifyJoinToken(args: VerifyArgs): VerifyResult<MediaJoinClaims>;
50
+ export declare function verifyControlToken(args: VerifyArgs): VerifyResult<MediaControlClaims>;
51
+ /**
52
+ * Boot guard for a media node.
53
+ *
54
+ * A node must hold verification keys ONLY. If a private key reaches a node's
55
+ * keyring - a copy-paste, a shared secrets bundle - that node can mint, and the
56
+ * whole reason for the asymmetric split is gone silently. This makes it a
57
+ * refusal to start rather than a latent capability.
58
+ */
59
+ export declare function assertPublicKeyringIsPublicOnly(keys: PublicKeyring): void;
60
+ /** Convenience for tooling and tests; not used at runtime. */
61
+ export declare function generateKeypair(): {
62
+ publicKey: string;
63
+ privateKey: string;
64
+ };
65
+ export {};
66
+ //# sourceMappingURL=token.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../src/token.ts"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,KAAK,YAAY,EAClB,MAAM,UAAU,CAAC;AAGlB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AACnD,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAMpD,oEAAoE;AACpE,eAAO,MAAM,qBAAqB,QAAU,CAAC;AAe7C,wBAAgB,aAAa,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,MAAM,CAgBjG;AAED,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAIvG;AAED,KAAK,UAAU,GAAG;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,aAAa,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+CAA+C;IAC/C,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAuEF,wBAAgB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,YAAY,CAAC,eAAe,CAAC,CAqB/E;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,UAAU,GAAG,YAAY,CAAC,kBAAkB,CAAC,CAWrF;AAED;;;;;;;GAOG;AACH,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CAazE;AAED,8DAA8D;AAC9D,wBAAgB,eAAe,IAAI;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAM3E"}
package/build/token.js ADDED
@@ -0,0 +1,208 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.MAX_TOKEN_TTL_SECONDS = void 0;
37
+ exports.signJoinToken = signJoinToken;
38
+ exports.signControlToken = signControlToken;
39
+ exports.verifyJoinToken = verifyJoinToken;
40
+ exports.verifyControlToken = verifyControlToken;
41
+ exports.assertPublicKeyringIsPublicOnly = assertPublicKeyringIsPublicOnly;
42
+ exports.generateKeypair = generateKeypair;
43
+ const crypto = __importStar(require("crypto"));
44
+ const claims_1 = require("./claims");
45
+ const grants_1 = require("./grants");
46
+ const ALG = "EdDSA";
47
+ const b64u = (b) => b.toString("base64url");
48
+ const fromB64u = (s) => Buffer.from(s, "base64url");
49
+ /** Default ceiling on `exp - iat`. A join ticket, not a session. */
50
+ exports.MAX_TOKEN_TTL_SECONDS = 15 * 60;
51
+ function encodeSegment(value) {
52
+ return b64u(Buffer.from(JSON.stringify(value), "utf8"));
53
+ }
54
+ function signCompact(privateKeyPem, kid, payload) {
55
+ const header = encodeSegment({ alg: ALG, typ: "JWT", kid });
56
+ const body = encodeSegment(payload);
57
+ const signingInput = `${header}.${body}`;
58
+ // For Ed25519 the digest algorithm is null: the curve prescribes it.
59
+ const signature = crypto.sign(null, Buffer.from(signingInput, "utf8"), privateKeyPem);
60
+ return `${signingInput}.${b64u(signature)}`;
61
+ }
62
+ function signJoinToken(privateKeyPem, kid, claims) {
63
+ const parsed = claims_1.mediaJoinClaimsSchema.parse(claims);
64
+ // Mint-time refusals. A token that should never exist is better stopped here
65
+ // than relied upon to be refused by every future verifier.
66
+ const grantCheck = (0, grants_1.grantsAreValidForKind)(parsed.grants, parsed.kind);
67
+ if (grantCheck !== true)
68
+ throw new Error(`refusing to mint: ${grantCheck}`);
69
+ if (parsed.exp <= parsed.iat)
70
+ throw new Error("refusing to mint: exp is not after iat");
71
+ if (parsed.exp - parsed.iat > exports.MAX_TOKEN_TTL_SECONDS) {
72
+ throw new Error(`refusing to mint: ttl exceeds ${exports.MAX_TOKEN_TTL_SECONDS}s`);
73
+ }
74
+ if (parsed.transport !== "webrtc" && parsed.kind === "human") {
75
+ throw new Error("refusing to mint: a human participant may only use a webrtc transport");
76
+ }
77
+ return signCompact(privateKeyPem, kid, parsed);
78
+ }
79
+ function signControlToken(privateKeyPem, kid, claims) {
80
+ const parsed = claims_1.mediaControlClaimsSchema.parse(claims);
81
+ if (parsed.exp <= parsed.iat)
82
+ throw new Error("refusing to mint: exp is not after iat");
83
+ return signCompact(privateKeyPem, kid, parsed);
84
+ }
85
+ function verifyCompact(args, audience) {
86
+ const parts = args.token.split(".");
87
+ if (parts.length !== 3)
88
+ return { ok: false, reason: "malformed" };
89
+ const [headerSeg, bodySeg, sigSeg] = parts;
90
+ let header;
91
+ try {
92
+ header = JSON.parse(fromB64u(headerSeg).toString("utf8"));
93
+ }
94
+ catch {
95
+ return { ok: false, reason: "malformed" };
96
+ }
97
+ // Only ever EdDSA. Accepting whatever the token names is the classic JWT
98
+ // algorithm-confusion hole, and `none` is the worst case of it.
99
+ if (header.alg !== ALG)
100
+ return { ok: false, reason: "malformed" };
101
+ if (typeof header.kid !== "string" || !header.kid)
102
+ return { ok: false, reason: "malformed" };
103
+ const publicKeyPem = args.keys[header.kid];
104
+ if (!publicKeyPem)
105
+ return { ok: false, reason: "unknown_kid" };
106
+ // Signature BEFORE claims: never parse a payload that has not been shown to
107
+ // come from us.
108
+ let signatureValid = false;
109
+ try {
110
+ signatureValid = crypto.verify(null, Buffer.from(`${headerSeg}.${bodySeg}`, "utf8"), publicKeyPem, fromB64u(sigSeg));
111
+ }
112
+ catch {
113
+ return { ok: false, reason: "bad_signature" };
114
+ }
115
+ if (!signatureValid)
116
+ return { ok: false, reason: "bad_signature" };
117
+ let payload;
118
+ try {
119
+ payload = JSON.parse(fromB64u(bodySeg).toString("utf8"));
120
+ }
121
+ catch {
122
+ return { ok: false, reason: "malformed" };
123
+ }
124
+ const asRecord = payload;
125
+ if (asRecord?.aud !== audience)
126
+ return { ok: false, reason: "wrong_audience" };
127
+ if (asRecord?.iss !== args.issuer)
128
+ return { ok: false, reason: "wrong_issuer" };
129
+ return { ok: true, payload, kid: header.kid };
130
+ }
131
+ function checkTiming(claims, args) {
132
+ const now = args.nowSeconds ?? Math.floor(Date.now() / 1000);
133
+ const skew = args.skewSeconds ?? 30;
134
+ const maxTtl = args.maxTtlSeconds ?? exports.MAX_TOKEN_TTL_SECONDS;
135
+ if (claims.exp + skew <= now)
136
+ return "expired";
137
+ if (claims.nbf !== undefined && claims.nbf - skew > now)
138
+ return "not_yet_valid";
139
+ // A long-lived token is a stolen credential with a long life, so the ceiling
140
+ // is enforced at VERIFY too - not only at mint, which a compromised minter
141
+ // would simply skip.
142
+ if (claims.exp - claims.iat > maxTtl)
143
+ return "ttl_too_long";
144
+ return null;
145
+ }
146
+ function verifyJoinToken(args) {
147
+ const base = verifyCompact(args, "media");
148
+ if (!base.ok)
149
+ return base;
150
+ const parsed = claims_1.mediaJoinClaimsSchema.safeParse(base.payload);
151
+ // `.strict()` fires here. A token carrying `subscribeOnly` instead of
152
+ // `subscribe` is rejected outright rather than read as "no rule, so
153
+ // everyone" - which is how an information barrier fails open.
154
+ if (!parsed.success)
155
+ return { ok: false, reason: "bad_claims" };
156
+ const timing = checkTiming(parsed.data, args);
157
+ if (timing)
158
+ return { ok: false, reason: timing };
159
+ if (args.expectedRoom !== undefined && parsed.data.room !== args.expectedRoom) {
160
+ return { ok: false, reason: "wrong_room" };
161
+ }
162
+ if ((0, grants_1.grantsAreValidForKind)(parsed.data.grants, parsed.data.kind) !== true) {
163
+ return { ok: false, reason: "bad_claims" };
164
+ }
165
+ return { ok: true, claims: parsed.data, kid: base.kid };
166
+ }
167
+ function verifyControlToken(args) {
168
+ const base = verifyCompact(args, "control");
169
+ if (!base.ok)
170
+ return base;
171
+ const parsed = claims_1.mediaControlClaimsSchema.safeParse(base.payload);
172
+ if (!parsed.success)
173
+ return { ok: false, reason: "bad_claims" };
174
+ const timing = checkTiming(parsed.data, args);
175
+ if (timing)
176
+ return { ok: false, reason: timing };
177
+ return { ok: true, claims: parsed.data, kid: base.kid };
178
+ }
179
+ /**
180
+ * Boot guard for a media node.
181
+ *
182
+ * A node must hold verification keys ONLY. If a private key reaches a node's
183
+ * keyring - a copy-paste, a shared secrets bundle - that node can mint, and the
184
+ * whole reason for the asymmetric split is gone silently. This makes it a
185
+ * refusal to start rather than a latent capability.
186
+ */
187
+ function assertPublicKeyringIsPublicOnly(keys) {
188
+ for (const [kid, pem] of Object.entries(keys)) {
189
+ if (/PRIVATE KEY/.test(pem)) {
190
+ throw new Error(`media token keyring "${kid}" contains a PRIVATE key. Nodes verify and must never be able to mint.`);
191
+ }
192
+ try {
193
+ crypto.createPublicKey(pem);
194
+ }
195
+ catch {
196
+ throw new Error(`media token keyring "${kid}" is not a readable public key`);
197
+ }
198
+ }
199
+ }
200
+ /** Convenience for tooling and tests; not used at runtime. */
201
+ function generateKeypair() {
202
+ const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
203
+ return {
204
+ publicKey: publicKey.export({ type: "spki", format: "pem" }).toString(),
205
+ privateKey: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
206
+ };
207
+ }
208
+ //# sourceMappingURL=token.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token.js","sourceRoot":"","sources":["../src/token.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Bumped when a frame shape changes in a way an older peer cannot parse.
3
+ *
4
+ * The client sends it on connect and a node refuses a mismatch outright, rather
5
+ * than negotiating: a half-understood signalling session fails later, somewhere
6
+ * else, and looks like a media bug.
7
+ */
8
+ export declare const MEDIA_PROTOCOL_VERSION: 1;
9
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,EAAG,CAAU,CAAC"}
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MEDIA_PROTOCOL_VERSION = void 0;
4
+ /**
5
+ * Bumped when a frame shape changes in a way an older peer cannot parse.
6
+ *
7
+ * The client sends it on connect and a node refuses a mismatch outright, rather
8
+ * than negotiating: a half-understood signalling session fails later, somewhere
9
+ * else, and looks like a media bug.
10
+ */
11
+ exports.MEDIA_PROTOCOL_VERSION = 1;
12
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":";;;AAAA;;;;;;GAMG;AACU,QAAA,sBAAsB,GAAG,CAAU,CAAC"}