@privos_ai/app-server 0.1.0 → 0.3.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/README.md +189 -0
- package/dist/app-descriptor.d.ts +17 -0
- package/dist/app-descriptor.d.ts.map +1 -1
- package/dist/app-descriptor.js +38 -0
- package/dist/app-descriptor.js.map +1 -1
- package/dist/context/tool-call-context.d.ts +3 -0
- package/dist/context/tool-call-context.d.ts.map +1 -1
- package/dist/direct/express-router.d.ts +5 -0
- package/dist/direct/express-router.d.ts.map +1 -1
- package/dist/direct/express-router.js +104 -12
- package/dist/direct/express-router.js.map +1 -1
- package/dist/direct/http-ingress.d.ts.map +1 -1
- package/dist/direct/http-ingress.js +10 -0
- package/dist/direct/http-ingress.js.map +1 -1
- package/dist/index.d.ts +10 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/manifest-lint-cli.d.ts +3 -0
- package/dist/manifest-lint-cli.d.ts.map +1 -0
- package/dist/manifest-lint-cli.js +18 -0
- package/dist/manifest-lint-cli.js.map +1 -0
- package/dist/manifest-tools.d.ts +15 -0
- package/dist/manifest-tools.d.ts.map +1 -0
- package/dist/manifest-tools.js +0 -0
- package/dist/manifest-tools.js.map +1 -0
- package/dist/relay/relay-client.d.ts +5 -1
- package/dist/relay/relay-client.d.ts.map +1 -1
- package/dist/relay/relay-client.js +81 -10
- package/dist/relay/relay-client.js.map +1 -1
- package/dist/runtime.d.ts +3 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +28 -1
- package/dist/runtime.js.map +1 -1
- package/dist/workload/dispatch-assertion.d.ts +167 -0
- package/dist/workload/dispatch-assertion.d.ts.map +1 -0
- package/dist/workload/dispatch-assertion.js +653 -0
- package/dist/workload/dispatch-assertion.js.map +1 -0
- package/dist/workload/index.d.ts +7 -0
- package/dist/workload/index.d.ts.map +1 -0
- package/dist/workload/index.js +4 -0
- package/dist/workload/index.js.map +1 -0
- package/dist/workload/publisher-runtime-trust.d.ts +118 -0
- package/dist/workload/publisher-runtime-trust.d.ts.map +1 -0
- package/dist/workload/publisher-runtime-trust.js +918 -0
- package/dist/workload/publisher-runtime-trust.js.map +1 -0
- package/dist/workload/workload-identity.d.ts +118 -0
- package/dist/workload/workload-identity.d.ts.map +1 -0
- package/dist/workload/workload-identity.js +451 -0
- package/dist/workload/workload-identity.js.map +1 -0
- package/package.json +13 -1
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
const replay = new Map();
|
|
3
|
+
const RUNTIME_DISPATCH_V3_HEADER_KEYS = ['alg', 'kid', 'privos_protocol', 'typ'];
|
|
4
|
+
const RUNTIME_DISPATCH_V3_COMMON_KEYS = [
|
|
5
|
+
'aud',
|
|
6
|
+
'authorizationContext',
|
|
7
|
+
'bodyDigest',
|
|
8
|
+
'deploymentId',
|
|
9
|
+
'executionMode',
|
|
10
|
+
'exp',
|
|
11
|
+
'generationId',
|
|
12
|
+
'generationNumber',
|
|
13
|
+
'htm',
|
|
14
|
+
'htu',
|
|
15
|
+
'iat',
|
|
16
|
+
'iss',
|
|
17
|
+
'jti',
|
|
18
|
+
'manifestDigest',
|
|
19
|
+
'mcpAppId',
|
|
20
|
+
'nonce',
|
|
21
|
+
'protocolVersion',
|
|
22
|
+
'resourceManifestHash',
|
|
23
|
+
'runtimeApprovalReceiptHash',
|
|
24
|
+
'runtimeAuthorizationEpoch',
|
|
25
|
+
'runtimeInstallationId',
|
|
26
|
+
'runtimeResourceInventoryHash',
|
|
27
|
+
'type',
|
|
28
|
+
'workspaceId',
|
|
29
|
+
];
|
|
30
|
+
const RUNTIME_DISPATCH_V3_ROOM_KEYS = [
|
|
31
|
+
'authorizationBindingId',
|
|
32
|
+
'bindingEpoch',
|
|
33
|
+
'bindingGrantHash',
|
|
34
|
+
'bindingReceiptHash',
|
|
35
|
+
'bindingTokenVersion',
|
|
36
|
+
'roomId',
|
|
37
|
+
];
|
|
38
|
+
const RUNTIME_DISPATCH_V3_AFFINITY_REQUIRED_KEYS = [
|
|
39
|
+
'workspaceId',
|
|
40
|
+
'deploymentId',
|
|
41
|
+
'mcpAppId',
|
|
42
|
+
'executionMode',
|
|
43
|
+
'generationId',
|
|
44
|
+
'generationNumber',
|
|
45
|
+
'runtimeInstallationId',
|
|
46
|
+
'manifestDigest',
|
|
47
|
+
'resourceManifestHash',
|
|
48
|
+
];
|
|
49
|
+
const RUNTIME_DISPATCH_V3_AFFINITY_OPTIONAL_KEYS = [
|
|
50
|
+
'runtimeResourceInventoryHash',
|
|
51
|
+
'runtimeApprovalReceiptHash',
|
|
52
|
+
'runtimeAuthorizationEpoch',
|
|
53
|
+
];
|
|
54
|
+
const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
55
|
+
const RUNTIME_HASH_PATTERN = /^[A-Za-z0-9_-]{43}$/;
|
|
56
|
+
const RUNTIME_MANIFEST_DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
57
|
+
const RUNTIME_NONCE_PATTERN = /^[A-Za-z0-9_-]{32,128}$/;
|
|
58
|
+
const DEFAULT_RUNTIME_REPLAY_CAPACITY = 10_000;
|
|
59
|
+
const MAX_RUNTIME_COMPACT_BYTES = 32_768;
|
|
60
|
+
const UNSIGNED_RUNTIME_INITIALIZE_PARAMS_V3 = Object.freeze({
|
|
61
|
+
protocolVersion: '2025-03-26',
|
|
62
|
+
capabilities: Object.freeze({
|
|
63
|
+
extensions: Object.freeze({
|
|
64
|
+
'io.modelcontextprotocol/ui': Object.freeze({
|
|
65
|
+
mimeTypes: Object.freeze(['text/html;profile=mcp-app']),
|
|
66
|
+
}),
|
|
67
|
+
}),
|
|
68
|
+
}),
|
|
69
|
+
clientInfo: Object.freeze({ name: 'privos-hub', version: '1.0.0' }),
|
|
70
|
+
});
|
|
71
|
+
/** Process-local bounded replay protection. Publishers with multiple replicas must inject a shared atomic consumer. */
|
|
72
|
+
export class BoundedRuntimeDispatchReplayConsumerV3 {
|
|
73
|
+
capacity;
|
|
74
|
+
jtis = new Map();
|
|
75
|
+
nonces = new Map();
|
|
76
|
+
constructor(capacity = DEFAULT_RUNTIME_REPLAY_CAPACITY) {
|
|
77
|
+
this.capacity = capacity;
|
|
78
|
+
if (!Number.isSafeInteger(capacity) || capacity < 1)
|
|
79
|
+
throw new RangeError('Runtime dispatch replay capacity must be positive.');
|
|
80
|
+
}
|
|
81
|
+
consume(input) {
|
|
82
|
+
for (const [key, expiresAt] of this.jtis) {
|
|
83
|
+
if (expiresAt <= input.now)
|
|
84
|
+
this.jtis.delete(key);
|
|
85
|
+
}
|
|
86
|
+
for (const [key, expiresAt] of this.nonces) {
|
|
87
|
+
if (expiresAt <= input.now)
|
|
88
|
+
this.nonces.delete(key);
|
|
89
|
+
}
|
|
90
|
+
const jtiKey = JSON.stringify([input.issuer, input.jti]);
|
|
91
|
+
const nonceKey = JSON.stringify([input.issuer, input.nonce]);
|
|
92
|
+
if (this.jtis.has(jtiKey) || this.nonces.has(nonceKey))
|
|
93
|
+
return false;
|
|
94
|
+
if (this.jtis.size >= this.capacity)
|
|
95
|
+
throw new Error('dispatch_assertion_replay_store_full');
|
|
96
|
+
this.jtis.set(jtiKey, input.expiresAt);
|
|
97
|
+
this.nonces.set(nonceKey, input.expiresAt);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const defaultRuntimeReplayConsumer = new BoundedRuntimeDispatchReplayConsumerV3();
|
|
102
|
+
function canonicalize(value) {
|
|
103
|
+
if (Array.isArray(value))
|
|
104
|
+
return value.map(canonicalize);
|
|
105
|
+
if (value && typeof value === 'object') {
|
|
106
|
+
return Object.fromEntries(Object.entries(value)
|
|
107
|
+
.filter(([, child]) => child !== undefined)
|
|
108
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
109
|
+
.map(([key, child]) => [key, canonicalize(child)]));
|
|
110
|
+
}
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
function isRecord(value) {
|
|
114
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
115
|
+
}
|
|
116
|
+
function exactKeys(value, expected) {
|
|
117
|
+
const actual = Object.keys(value).sort();
|
|
118
|
+
const sortedExpected = [...expected].sort();
|
|
119
|
+
return actual.length === sortedExpected.length && actual.every((key, index) => key === sortedExpected[index]);
|
|
120
|
+
}
|
|
121
|
+
function runtimeId(value) {
|
|
122
|
+
return typeof value === 'string' && value.length >= 1 && value.length <= 256;
|
|
123
|
+
}
|
|
124
|
+
function positiveSafeInteger(value) {
|
|
125
|
+
return Number.isSafeInteger(value) && Number(value) >= 1;
|
|
126
|
+
}
|
|
127
|
+
function runtimeHash(value) {
|
|
128
|
+
return typeof value === 'string' && RUNTIME_HASH_PATTERN.test(value);
|
|
129
|
+
}
|
|
130
|
+
function strictBase64urlDecode(value) {
|
|
131
|
+
if (!value || !BASE64URL_PATTERN.test(value))
|
|
132
|
+
throw new Error('dispatch_assertion_invalid');
|
|
133
|
+
const decoded = Buffer.from(value, 'base64url');
|
|
134
|
+
if (decoded.toString('base64url') !== value)
|
|
135
|
+
throw new Error('dispatch_assertion_invalid');
|
|
136
|
+
return decoded;
|
|
137
|
+
}
|
|
138
|
+
function parseRuntimeDispatchCompactV3(compact) {
|
|
139
|
+
if (typeof compact !== 'string' || Buffer.byteLength(compact, 'utf8') > MAX_RUNTIME_COMPACT_BYTES) {
|
|
140
|
+
throw new Error('dispatch_assertion_invalid');
|
|
141
|
+
}
|
|
142
|
+
const parts = compact.split('.');
|
|
143
|
+
if (parts.length !== 3 || parts.some((part) => !part))
|
|
144
|
+
throw new Error('dispatch_assertion_invalid');
|
|
145
|
+
let header;
|
|
146
|
+
let payload;
|
|
147
|
+
const headerBytes = strictBase64urlDecode(parts[0]);
|
|
148
|
+
const payloadBytes = strictBase64urlDecode(parts[1]);
|
|
149
|
+
const signature = strictBase64urlDecode(parts[2]);
|
|
150
|
+
try {
|
|
151
|
+
header = JSON.parse(headerBytes.toString('utf8'));
|
|
152
|
+
payload = JSON.parse(payloadBytes.toString('utf8'));
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
throw new Error('dispatch_assertion_invalid');
|
|
156
|
+
}
|
|
157
|
+
if (!isRecord(header) || !isRecord(payload))
|
|
158
|
+
throw new Error('dispatch_assertion_invalid');
|
|
159
|
+
if (parts[0] !== Buffer.from(canonical(header), 'utf8').toString('base64url') ||
|
|
160
|
+
parts[1] !== Buffer.from(canonical(payload), 'utf8').toString('base64url')) {
|
|
161
|
+
throw new Error('dispatch_assertion_invalid');
|
|
162
|
+
}
|
|
163
|
+
return { encodedHeader: parts[0], encodedPayload: parts[1], signature, header, payload };
|
|
164
|
+
}
|
|
165
|
+
function assertRuntimeDispatchHeaderV3(header) {
|
|
166
|
+
if (!exactKeys(header, RUNTIME_DISPATCH_V3_HEADER_KEYS) ||
|
|
167
|
+
header.alg !== 'ES256' ||
|
|
168
|
+
header.typ !== 'privos-hub-runtime-dispatch+jws' ||
|
|
169
|
+
header.privos_protocol !== 3 ||
|
|
170
|
+
!runtimeId(header.kid)) {
|
|
171
|
+
throw new Error('dispatch_assertion_invalid');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function assertRuntimeDispatchPayloadV3(payload) {
|
|
175
|
+
const context = payload.authorizationContext;
|
|
176
|
+
const expectedKeys = context === 'room'
|
|
177
|
+
? [...RUNTIME_DISPATCH_V3_COMMON_KEYS, ...RUNTIME_DISPATCH_V3_ROOM_KEYS]
|
|
178
|
+
: RUNTIME_DISPATCH_V3_COMMON_KEYS;
|
|
179
|
+
if (!exactKeys(payload, expectedKeys))
|
|
180
|
+
throw new Error('dispatch_assertion_invalid');
|
|
181
|
+
if (payload.protocolVersion !== 3 ||
|
|
182
|
+
payload.type !== 'hub-runtime-dispatch-assertion' ||
|
|
183
|
+
!runtimeId(payload.iss) ||
|
|
184
|
+
!runtimeId(payload.aud) ||
|
|
185
|
+
!runtimeId(payload.jti) ||
|
|
186
|
+
typeof payload.nonce !== 'string' ||
|
|
187
|
+
!RUNTIME_NONCE_PATTERN.test(payload.nonce) ||
|
|
188
|
+
!positiveSafeInteger(payload.iat) ||
|
|
189
|
+
!positiveSafeInteger(payload.exp) ||
|
|
190
|
+
!runtimeId(payload.workspaceId) ||
|
|
191
|
+
!runtimeId(payload.deploymentId) ||
|
|
192
|
+
!runtimeId(payload.mcpAppId) ||
|
|
193
|
+
!['SELF_HOSTED_LOCAL', 'PUBLISHER_HOSTED'].includes(String(payload.executionMode)) ||
|
|
194
|
+
!runtimeId(payload.generationId) ||
|
|
195
|
+
!positiveSafeInteger(payload.generationNumber) ||
|
|
196
|
+
!runtimeId(payload.runtimeInstallationId) ||
|
|
197
|
+
typeof payload.manifestDigest !== 'string' ||
|
|
198
|
+
!RUNTIME_MANIFEST_DIGEST_PATTERN.test(payload.manifestDigest) ||
|
|
199
|
+
!runtimeHash(payload.resourceManifestHash) ||
|
|
200
|
+
!runtimeHash(payload.runtimeResourceInventoryHash) ||
|
|
201
|
+
!runtimeHash(payload.runtimeApprovalReceiptHash) ||
|
|
202
|
+
!positiveSafeInteger(payload.runtimeAuthorizationEpoch) ||
|
|
203
|
+
(context !== 'workspace' && context !== 'room') ||
|
|
204
|
+
payload.htm !== 'POST' ||
|
|
205
|
+
payload.htu !== '/mcp' ||
|
|
206
|
+
!runtimeHash(payload.bodyDigest)) {
|
|
207
|
+
throw new Error('dispatch_assertion_invalid');
|
|
208
|
+
}
|
|
209
|
+
if (context === 'room') {
|
|
210
|
+
if (!runtimeId(payload.roomId) ||
|
|
211
|
+
!runtimeId(payload.authorizationBindingId) ||
|
|
212
|
+
!runtimeHash(payload.bindingReceiptHash) ||
|
|
213
|
+
!runtimeHash(payload.bindingGrantHash) ||
|
|
214
|
+
!positiveSafeInteger(payload.bindingEpoch) ||
|
|
215
|
+
!positiveSafeInteger(payload.bindingTokenVersion)) {
|
|
216
|
+
throw new Error('dispatch_assertion_invalid');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function runtimeDispatchTrustHintV3(header, payload, body) {
|
|
221
|
+
return Object.freeze({
|
|
222
|
+
kid: header.kid,
|
|
223
|
+
workspaceId: payload.workspaceId,
|
|
224
|
+
deploymentId: payload.deploymentId,
|
|
225
|
+
mcpAppId: payload.mcpAppId,
|
|
226
|
+
executionMode: payload.executionMode,
|
|
227
|
+
generationId: payload.generationId,
|
|
228
|
+
generationNumber: payload.generationNumber,
|
|
229
|
+
runtimeInstallationId: payload.runtimeInstallationId,
|
|
230
|
+
authorizationContext: payload.authorizationContext,
|
|
231
|
+
...(isExactRuntimeReadinessRpcV3(body) ? { preactivationReadiness: true } : {}),
|
|
232
|
+
...(payload.authorizationContext === 'room'
|
|
233
|
+
? { roomId: String(payload.roomId), authorizationBindingId: String(payload.authorizationBindingId) }
|
|
234
|
+
: {}),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
export function assertRuntimeDispatchTrustConfigurationV3(value) {
|
|
238
|
+
if (!isRecord(value) || !exactKeys(value, ['affinity', 'hubKid', 'hubPublicJwk'])) {
|
|
239
|
+
throw new Error('runtime_dispatch_trust_invalid');
|
|
240
|
+
}
|
|
241
|
+
const jwk = value.hubPublicJwk;
|
|
242
|
+
const affinity = value.affinity;
|
|
243
|
+
if (typeof value.hubKid !== 'string' ||
|
|
244
|
+
!RUNTIME_HASH_PATTERN.test(value.hubKid) ||
|
|
245
|
+
!isRecord(jwk) ||
|
|
246
|
+
!exactKeys(jwk, ['crv', 'kty', 'x', 'y']) ||
|
|
247
|
+
jwk.kty !== 'EC' ||
|
|
248
|
+
jwk.crv !== 'P-256' ||
|
|
249
|
+
typeof jwk.x !== 'string' ||
|
|
250
|
+
typeof jwk.y !== 'string' ||
|
|
251
|
+
!RUNTIME_HASH_PATTERN.test(jwk.x) ||
|
|
252
|
+
!RUNTIME_HASH_PATTERN.test(jwk.y) ||
|
|
253
|
+
!isRecord(affinity)) {
|
|
254
|
+
throw new Error('runtime_dispatch_trust_invalid');
|
|
255
|
+
}
|
|
256
|
+
const optionalKeys = RUNTIME_DISPATCH_V3_AFFINITY_OPTIONAL_KEYS.filter((key) => affinity[key] !== undefined);
|
|
257
|
+
let canonicalPublicKey = false;
|
|
258
|
+
try {
|
|
259
|
+
const x = Buffer.from(String(jwk.x), 'base64url');
|
|
260
|
+
const y = Buffer.from(String(jwk.y), 'base64url');
|
|
261
|
+
const key = crypto.createPublicKey({ key: jwk, format: 'jwk' });
|
|
262
|
+
const exported = key.export({ format: 'jwk' });
|
|
263
|
+
canonicalPublicKey =
|
|
264
|
+
x.byteLength === 32 &&
|
|
265
|
+
y.byteLength === 32 &&
|
|
266
|
+
x.toString('base64url') === jwk.x &&
|
|
267
|
+
y.toString('base64url') === jwk.y &&
|
|
268
|
+
exported.kty === 'EC' &&
|
|
269
|
+
exported.crv === 'P-256' &&
|
|
270
|
+
exported.x === jwk.x &&
|
|
271
|
+
exported.y === jwk.y &&
|
|
272
|
+
!exported.d;
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
canonicalPublicKey = false;
|
|
276
|
+
}
|
|
277
|
+
if (!exactKeys(affinity, [...RUNTIME_DISPATCH_V3_AFFINITY_REQUIRED_KEYS, ...optionalKeys]) ||
|
|
278
|
+
!runtimeId(affinity.workspaceId) ||
|
|
279
|
+
!runtimeId(affinity.deploymentId) ||
|
|
280
|
+
!runtimeId(affinity.mcpAppId) ||
|
|
281
|
+
!['SELF_HOSTED_LOCAL', 'PUBLISHER_HOSTED'].includes(String(affinity.executionMode)) ||
|
|
282
|
+
!runtimeId(affinity.generationId) ||
|
|
283
|
+
!positiveSafeInteger(affinity.generationNumber) ||
|
|
284
|
+
!runtimeId(affinity.runtimeInstallationId) ||
|
|
285
|
+
typeof affinity.manifestDigest !== 'string' ||
|
|
286
|
+
!RUNTIME_MANIFEST_DIGEST_PATTERN.test(affinity.manifestDigest) ||
|
|
287
|
+
!runtimeHash(affinity.resourceManifestHash) ||
|
|
288
|
+
(affinity.runtimeResourceInventoryHash !== undefined && !runtimeHash(affinity.runtimeResourceInventoryHash)) ||
|
|
289
|
+
(affinity.runtimeApprovalReceiptHash !== undefined && !runtimeHash(affinity.runtimeApprovalReceiptHash)) ||
|
|
290
|
+
(affinity.runtimeAuthorizationEpoch !== undefined && !positiveSafeInteger(affinity.runtimeAuthorizationEpoch)) ||
|
|
291
|
+
!canonicalPublicKey ||
|
|
292
|
+
crypto.createHash('sha256').update(canonical({
|
|
293
|
+
crv: jwk.crv,
|
|
294
|
+
kty: jwk.kty,
|
|
295
|
+
x: jwk.x,
|
|
296
|
+
y: jwk.y,
|
|
297
|
+
})).digest('base64url') !== value.hubKid) {
|
|
298
|
+
throw new Error('runtime_dispatch_trust_invalid');
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
export function parseRuntimeDispatchTrustV3Json(raw) {
|
|
302
|
+
let parsed;
|
|
303
|
+
try {
|
|
304
|
+
parsed = JSON.parse(raw);
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
throw new Error('runtime_dispatch_trust_invalid');
|
|
308
|
+
}
|
|
309
|
+
assertRuntimeDispatchTrustConfigurationV3(parsed);
|
|
310
|
+
return Object.freeze({
|
|
311
|
+
hubKid: parsed.hubKid,
|
|
312
|
+
hubPublicJwk: Object.freeze({ ...parsed.hubPublicJwk }),
|
|
313
|
+
affinity: Object.freeze({ ...parsed.affinity }),
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
function assertRuntimeDispatchTrustMatchesV3(trust, header, payload) {
|
|
317
|
+
try {
|
|
318
|
+
assertRuntimeDispatchTrustConfigurationV3(trust);
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
throw new Error('dispatch_assertion_invalid');
|
|
322
|
+
}
|
|
323
|
+
if (trust.hubKid !== header.kid)
|
|
324
|
+
throw new Error('dispatch_assertion_invalid');
|
|
325
|
+
if (RUNTIME_DISPATCH_V3_AFFINITY_REQUIRED_KEYS.some((key) => trust.affinity[key] !== payload[key]) ||
|
|
326
|
+
RUNTIME_DISPATCH_V3_AFFINITY_OPTIONAL_KEYS.some((key) => trust.affinity[key] !== undefined && trust.affinity[key] !== payload[key])) {
|
|
327
|
+
throw new Error('dispatch_assertion_binding_mismatch');
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
export function sha256RuntimeDispatchBodyV3(body) {
|
|
331
|
+
return crypto.createHash('sha256').update(canonical(body), 'utf8').digest('base64url');
|
|
332
|
+
}
|
|
333
|
+
export function isUnsignedRuntimeReadinessRpcV3(body, security) {
|
|
334
|
+
if (security.mode !== 'required' ||
|
|
335
|
+
security.unsignedReadiness !== 'initialize-and-tools-list' ||
|
|
336
|
+
!isExactRuntimeReadinessRpcV3(body))
|
|
337
|
+
return false;
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
/** Exact readiness classification; it grants no authority without a verified signed assertion. */
|
|
341
|
+
export function isExactRuntimeReadinessRpcV3(body) {
|
|
342
|
+
if (!isRecord(body))
|
|
343
|
+
return false;
|
|
344
|
+
if (body.method === 'initialize') {
|
|
345
|
+
return exactKeys(body, ['id', 'jsonrpc', 'method', 'params']) &&
|
|
346
|
+
body.jsonrpc === '2.0' &&
|
|
347
|
+
body.id === 1 &&
|
|
348
|
+
canonical(body.params) === canonical(UNSIGNED_RUNTIME_INITIALIZE_PARAMS_V3);
|
|
349
|
+
}
|
|
350
|
+
if (body.method === 'notifications/initialized') {
|
|
351
|
+
return exactKeys(body, ['jsonrpc', 'method']) && body.jsonrpc === '2.0';
|
|
352
|
+
}
|
|
353
|
+
if (body.method === 'tools/list') {
|
|
354
|
+
return exactKeys(body, ['id', 'jsonrpc', 'method', 'params']) &&
|
|
355
|
+
body.jsonrpc === '2.0' &&
|
|
356
|
+
body.id === 2 &&
|
|
357
|
+
isRecord(body.params) &&
|
|
358
|
+
exactKeys(body.params, []);
|
|
359
|
+
}
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
export async function verifyRuntimeDispatchAssertionV3(input) {
|
|
363
|
+
if (!input.security || input.security.mode !== 'required')
|
|
364
|
+
throw new Error('dispatch_assertion_invalid');
|
|
365
|
+
const parsed = parseRuntimeDispatchCompactV3(input.compact);
|
|
366
|
+
assertRuntimeDispatchHeaderV3(parsed.header);
|
|
367
|
+
assertRuntimeDispatchPayloadV3(parsed.payload);
|
|
368
|
+
const trust = typeof input.security.trust === 'function'
|
|
369
|
+
? await input.security.trust(runtimeDispatchTrustHintV3(parsed.header, parsed.payload, input.body))
|
|
370
|
+
: input.security.trust;
|
|
371
|
+
assertRuntimeDispatchTrustMatchesV3(trust, parsed.header, parsed.payload);
|
|
372
|
+
let key;
|
|
373
|
+
try {
|
|
374
|
+
key = crypto.createPublicKey({ key: trust.hubPublicJwk, format: 'jwk' });
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
throw new Error('dispatch_assertion_invalid');
|
|
378
|
+
}
|
|
379
|
+
if (parsed.signature.byteLength !== 64 ||
|
|
380
|
+
!crypto.verify('sha256', Buffer.from(`${parsed.encodedHeader}.${parsed.encodedPayload}`, 'ascii'), { key, dsaEncoding: 'ieee-p1363' }, parsed.signature)) {
|
|
381
|
+
throw new Error('dispatch_assertion_invalid');
|
|
382
|
+
}
|
|
383
|
+
if (parsed.payload.iss !== `hub:${parsed.payload.deploymentId}` ||
|
|
384
|
+
parsed.payload.aud !== `mcp-runtime:${parsed.payload.mcpAppId}`) {
|
|
385
|
+
throw new Error('dispatch_assertion_binding_mismatch');
|
|
386
|
+
}
|
|
387
|
+
const now = input.now ?? input.security.now?.() ?? Math.floor(Date.now() / 1_000);
|
|
388
|
+
const skew = input.security.clockSkewSeconds ?? 5;
|
|
389
|
+
if (!Number.isSafeInteger(now) || now < 1 || !Number.isSafeInteger(skew) || skew < 0 || skew > 30) {
|
|
390
|
+
throw new Error('dispatch_assertion_invalid');
|
|
391
|
+
}
|
|
392
|
+
if (parsed.payload.iat > now + skew ||
|
|
393
|
+
parsed.payload.exp <= now ||
|
|
394
|
+
parsed.payload.exp <= parsed.payload.iat ||
|
|
395
|
+
parsed.payload.exp - parsed.payload.iat > 30) {
|
|
396
|
+
throw new Error('dispatch_assertion_time_invalid');
|
|
397
|
+
}
|
|
398
|
+
if (parsed.payload.bodyDigest !== sha256RuntimeDispatchBodyV3(input.body)) {
|
|
399
|
+
throw new Error('dispatch_assertion_body_mismatch');
|
|
400
|
+
}
|
|
401
|
+
if (input.relayAuthorization) {
|
|
402
|
+
assertRuntimeDispatchRelayAffinityV3(input.relayAuthorization, parsed.payload);
|
|
403
|
+
}
|
|
404
|
+
const consumed = await (input.security.replayConsumer ?? defaultRuntimeReplayConsumer).consume({
|
|
405
|
+
issuer: parsed.payload.iss,
|
|
406
|
+
jti: parsed.payload.jti,
|
|
407
|
+
nonce: parsed.payload.nonce,
|
|
408
|
+
expiresAt: parsed.payload.exp,
|
|
409
|
+
now,
|
|
410
|
+
});
|
|
411
|
+
if (!consumed)
|
|
412
|
+
throw new Error('dispatch_assertion_replayed');
|
|
413
|
+
return Object.freeze({ ...parsed.payload });
|
|
414
|
+
}
|
|
415
|
+
export function extractRuntimeDispatchRelayEnvelopeV3(value) {
|
|
416
|
+
if (!isRecord(value) || !isRecord(value.params) || !isRecord(value.params._meta)) {
|
|
417
|
+
throw new Error('dispatch_assertion_missing');
|
|
418
|
+
}
|
|
419
|
+
const meta = value.params._meta;
|
|
420
|
+
const authorization = meta.privosAuthorization;
|
|
421
|
+
if (!isRecord(authorization))
|
|
422
|
+
throw new Error('dispatch_assertion_missing');
|
|
423
|
+
const allowedAuthorizationKeys = authorization.authorizationBindingId === undefined
|
|
424
|
+
? ['assertion', 'runtimeInstallationId']
|
|
425
|
+
: ['assertion', 'authorizationBindingId', 'runtimeInstallationId'];
|
|
426
|
+
if (!exactKeys(authorization, allowedAuthorizationKeys) ||
|
|
427
|
+
typeof authorization.assertion !== 'string' ||
|
|
428
|
+
!authorization.assertion ||
|
|
429
|
+
!runtimeId(authorization.runtimeInstallationId) ||
|
|
430
|
+
(authorization.authorizationBindingId !== undefined && !runtimeId(authorization.authorizationBindingId))) {
|
|
431
|
+
throw new Error('dispatch_assertion_invalid');
|
|
432
|
+
}
|
|
433
|
+
const logicalMeta = { ...meta };
|
|
434
|
+
delete logicalMeta.privosAuthorization;
|
|
435
|
+
delete logicalMeta.privosUser;
|
|
436
|
+
const logicalParams = { ...value.params };
|
|
437
|
+
if (Object.keys(logicalMeta).length === 0)
|
|
438
|
+
delete logicalParams._meta;
|
|
439
|
+
else
|
|
440
|
+
logicalParams._meta = logicalMeta;
|
|
441
|
+
const logicalRpc = { ...value, params: logicalParams };
|
|
442
|
+
return Object.freeze({
|
|
443
|
+
logicalRpc,
|
|
444
|
+
authorization: Object.freeze({
|
|
445
|
+
assertion: authorization.assertion,
|
|
446
|
+
runtimeInstallationId: authorization.runtimeInstallationId,
|
|
447
|
+
...(authorization.authorizationBindingId !== undefined
|
|
448
|
+
? { authorizationBindingId: authorization.authorizationBindingId }
|
|
449
|
+
: {}),
|
|
450
|
+
}),
|
|
451
|
+
...(Object.prototype.hasOwnProperty.call(meta, 'privosUser') ? { userMetadata: meta.privosUser } : {}),
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
export function assertRuntimeDispatchRelayAffinityV3(authorization, verified) {
|
|
455
|
+
if (authorization.runtimeInstallationId !== verified.runtimeInstallationId ||
|
|
456
|
+
(verified.authorizationContext === 'workspace' && authorization.authorizationBindingId !== undefined) ||
|
|
457
|
+
(verified.authorizationContext === 'room' && authorization.authorizationBindingId !== verified.authorizationBindingId)) {
|
|
458
|
+
throw new Error('dispatch_assertion_binding_mismatch');
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
function canonical(value) {
|
|
462
|
+
return JSON.stringify(canonicalize(value));
|
|
463
|
+
}
|
|
464
|
+
const CLUSTER_DISPATCH_V3_COMMON_KEYS = [
|
|
465
|
+
'aud',
|
|
466
|
+
'authorizationContext',
|
|
467
|
+
'bodyDigest',
|
|
468
|
+
'clusterAppId',
|
|
469
|
+
'clusterId',
|
|
470
|
+
'deploymentId',
|
|
471
|
+
'exp',
|
|
472
|
+
'generationId',
|
|
473
|
+
'generationNumber',
|
|
474
|
+
'htm',
|
|
475
|
+
'htu',
|
|
476
|
+
'iat',
|
|
477
|
+
'iss',
|
|
478
|
+
'jti',
|
|
479
|
+
'manifestDigest',
|
|
480
|
+
'mcpAppId',
|
|
481
|
+
'nonce',
|
|
482
|
+
'protocolVersion',
|
|
483
|
+
'resourceManifestHash',
|
|
484
|
+
'runtimeApprovalReceiptHash',
|
|
485
|
+
'runtimeGrantEpoch',
|
|
486
|
+
'runtimeInstallationId',
|
|
487
|
+
'runtimeResourceInventoryHash',
|
|
488
|
+
'type',
|
|
489
|
+
'workspaceId',
|
|
490
|
+
];
|
|
491
|
+
const CLUSTER_DISPATCH_V3_ROOM_KEYS = [
|
|
492
|
+
...CLUSTER_DISPATCH_V3_COMMON_KEYS,
|
|
493
|
+
'authorizationBindingId',
|
|
494
|
+
'bindingEpoch',
|
|
495
|
+
'bindingReceiptHash',
|
|
496
|
+
'roomId',
|
|
497
|
+
];
|
|
498
|
+
/**
|
|
499
|
+
* Verify the dispatch a Hub routes to an App Library runtime through its
|
|
500
|
+
* cluster. The assertion reuses the replica `typ` but carries generation
|
|
501
|
+
* affinity instead of replica affinity, so every field is checked against the
|
|
502
|
+
* generation the node attested — the runtime trusts no claim the broker did
|
|
503
|
+
* not already bind it to.
|
|
504
|
+
*/
|
|
505
|
+
export function verifyClusterDispatchAssertionV3(input) {
|
|
506
|
+
const generation = input.context.binding.generation;
|
|
507
|
+
if (!generation)
|
|
508
|
+
throw new Error('dispatch_assertion_invalid');
|
|
509
|
+
const parts = input.compact.split('.');
|
|
510
|
+
if (parts.length !== 3)
|
|
511
|
+
throw new Error('dispatch_assertion_invalid');
|
|
512
|
+
let header;
|
|
513
|
+
let payload;
|
|
514
|
+
try {
|
|
515
|
+
header = JSON.parse(Buffer.from(parts[0], 'base64url').toString('utf8'));
|
|
516
|
+
payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
517
|
+
}
|
|
518
|
+
catch {
|
|
519
|
+
throw new Error('dispatch_assertion_invalid');
|
|
520
|
+
}
|
|
521
|
+
if (!isRecord(header) ||
|
|
522
|
+
!isRecord(payload) ||
|
|
523
|
+
!exactKeys(header, RUNTIME_DISPATCH_V3_HEADER_KEYS) ||
|
|
524
|
+
header.alg !== 'ES256' ||
|
|
525
|
+
header.typ !== 'privos-hub-dispatch+jws' ||
|
|
526
|
+
header.privos_protocol !== 3 ||
|
|
527
|
+
header.kid !== input.context.hubKid) {
|
|
528
|
+
throw new Error('dispatch_assertion_invalid');
|
|
529
|
+
}
|
|
530
|
+
const key = crypto.createPublicKey({ key: input.context.hubPublicJwk, format: 'jwk' });
|
|
531
|
+
if (!crypto.verify('sha256', Buffer.from(`${parts[0]}.${parts[1]}`, 'utf8'), { key, dsaEncoding: 'ieee-p1363' }, Buffer.from(parts[2], 'base64url')))
|
|
532
|
+
throw new Error('dispatch_assertion_invalid');
|
|
533
|
+
const now = input.now ?? Math.floor(Date.now() / 1000);
|
|
534
|
+
const room = payload.authorizationContext === 'room';
|
|
535
|
+
if (!exactKeys(payload, room ? CLUSTER_DISPATCH_V3_ROOM_KEYS : CLUSTER_DISPATCH_V3_COMMON_KEYS) ||
|
|
536
|
+
payload.protocolVersion !== 3 ||
|
|
537
|
+
payload.type !== 'hub-dispatch-assertion' ||
|
|
538
|
+
payload.aud !== 'privos-mcp-app' ||
|
|
539
|
+
payload.htm !== 'POST' ||
|
|
540
|
+
payload.htu !== '/mcp' ||
|
|
541
|
+
typeof payload.jti !== 'string' ||
|
|
542
|
+
typeof payload.iss !== 'string' ||
|
|
543
|
+
typeof payload.nonce !== 'string' ||
|
|
544
|
+
!Number.isInteger(payload.iat) ||
|
|
545
|
+
!Number.isInteger(payload.exp) ||
|
|
546
|
+
Number(payload.iat) > now + 5 ||
|
|
547
|
+
Number(payload.exp) < now ||
|
|
548
|
+
Number(payload.exp) - Number(payload.iat) > 30) {
|
|
549
|
+
throw new Error('dispatch_assertion_invalid');
|
|
550
|
+
}
|
|
551
|
+
if (payload.workspaceId !== input.context.binding.workspaceId ||
|
|
552
|
+
payload.runtimeInstallationId !== input.context.binding.installationId ||
|
|
553
|
+
payload.mcpAppId !== input.context.binding.mcpAppId ||
|
|
554
|
+
payload.runtimeApprovalReceiptHash !== input.context.binding.receiptHash ||
|
|
555
|
+
payload.runtimeGrantEpoch !== input.context.binding.grantEpoch ||
|
|
556
|
+
payload.clusterId !== generation.clusterId ||
|
|
557
|
+
payload.deploymentId !== generation.deploymentId ||
|
|
558
|
+
payload.generationId !== generation.generationId ||
|
|
559
|
+
payload.generationNumber !== generation.generationNumber ||
|
|
560
|
+
payload.manifestDigest !== generation.manifestDigest ||
|
|
561
|
+
payload.resourceManifestHash !== generation.resourceManifestHash ||
|
|
562
|
+
payload.runtimeResourceInventoryHash !== generation.runtimeResourceInventoryHash ||
|
|
563
|
+
typeof payload.clusterAppId !== 'string' ||
|
|
564
|
+
payload.clusterAppId.length === 0 ||
|
|
565
|
+
payload.bodyDigest !== sha256RuntimeDispatchBodyV3(input.body)) {
|
|
566
|
+
throw new Error('dispatch_assertion_binding_mismatch');
|
|
567
|
+
}
|
|
568
|
+
if (room && (typeof payload.roomId !== 'string' ||
|
|
569
|
+
payload.roomId.length === 0 ||
|
|
570
|
+
typeof payload.authorizationBindingId !== 'string' ||
|
|
571
|
+
payload.authorizationBindingId.length === 0 ||
|
|
572
|
+
typeof payload.bindingReceiptHash !== 'string' ||
|
|
573
|
+
!Number.isInteger(payload.bindingEpoch) ||
|
|
574
|
+
Number(payload.bindingEpoch) < 1)) {
|
|
575
|
+
throw new Error('dispatch_assertion_binding_mismatch');
|
|
576
|
+
}
|
|
577
|
+
if (!room && payload.authorizationContext !== 'workspace')
|
|
578
|
+
throw new Error('dispatch_assertion_invalid');
|
|
579
|
+
for (const [jti, exp] of replay)
|
|
580
|
+
if (exp < now)
|
|
581
|
+
replay.delete(jti);
|
|
582
|
+
if (replay.has(payload.jti))
|
|
583
|
+
throw new Error('dispatch_assertion_replayed');
|
|
584
|
+
replay.set(payload.jti, Number(payload.exp));
|
|
585
|
+
return Object.freeze({
|
|
586
|
+
jti: payload.jti,
|
|
587
|
+
issuedAt: Number(payload.iat),
|
|
588
|
+
expiresAt: Number(payload.exp),
|
|
589
|
+
authorizationContext: room ? 'room' : 'workspace',
|
|
590
|
+
...(room ? { roomId: String(payload.roomId), authorizationBindingId: String(payload.authorizationBindingId) } : {}),
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
export function verifyDispatchAssertion(input) {
|
|
594
|
+
const parts = input.compact.split('.');
|
|
595
|
+
if (parts.length !== 3)
|
|
596
|
+
throw new Error('dispatch_assertion_invalid');
|
|
597
|
+
const header = JSON.parse(Buffer.from(parts[0], 'base64url').toString('utf8'));
|
|
598
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
599
|
+
if (header.alg !== 'ES256' || header.typ !== 'privos-hub-dispatch+jws' || header.kid !== input.context.hubKid) {
|
|
600
|
+
throw new Error('dispatch_assertion_invalid');
|
|
601
|
+
}
|
|
602
|
+
const key = crypto.createPublicKey({ key: input.context.hubPublicJwk, format: 'jwk' });
|
|
603
|
+
if (!crypto.verify('sha256', Buffer.from(`${parts[0]}.${parts[1]}`, 'utf8'), { key, dsaEncoding: 'ieee-p1363' }, Buffer.from(parts[2], 'base64url')))
|
|
604
|
+
throw new Error('dispatch_assertion_invalid');
|
|
605
|
+
const now = input.now ?? Math.floor(Date.now() / 1000);
|
|
606
|
+
if (payload.type !== 'hub-dispatch-assertion' ||
|
|
607
|
+
payload.aud !== 'privos-mcp-app' ||
|
|
608
|
+
payload.htm !== 'POST' ||
|
|
609
|
+
payload.htu !== '/mcp' ||
|
|
610
|
+
!Number.isInteger(payload.iat) ||
|
|
611
|
+
!Number.isInteger(payload.exp) ||
|
|
612
|
+
Number(payload.iat) > now + 5 ||
|
|
613
|
+
Number(payload.exp) < now ||
|
|
614
|
+
Number(payload.exp) - Number(payload.iat) > 30 ||
|
|
615
|
+
payload.workspaceId !== input.context.binding.workspaceId ||
|
|
616
|
+
payload.installationId !== input.context.binding.installationId ||
|
|
617
|
+
payload.mcpAppId !== input.context.binding.mcpAppId ||
|
|
618
|
+
payload.replicaId !== input.context.binding.replicaId ||
|
|
619
|
+
payload.receiptHash !== input.context.binding.receiptHash ||
|
|
620
|
+
payload.grantEpoch !== input.context.binding.grantEpoch ||
|
|
621
|
+
payload.bodyDigest !== `sha256:${crypto.createHash('sha256').update(canonical(input.body)).digest('hex')}` ||
|
|
622
|
+
typeof payload.jti !== 'string')
|
|
623
|
+
throw new Error('dispatch_assertion_binding_mismatch');
|
|
624
|
+
let actor;
|
|
625
|
+
if (payload.actor !== undefined) {
|
|
626
|
+
const candidate = payload.actor;
|
|
627
|
+
if (!candidate ||
|
|
628
|
+
typeof candidate !== 'object' ||
|
|
629
|
+
typeof candidate.subject !== 'string' ||
|
|
630
|
+
candidate.subject.length === 0 ||
|
|
631
|
+
(candidate.username !== undefined && typeof candidate.username !== 'string') ||
|
|
632
|
+
(candidate.roomId !== undefined && typeof candidate.roomId !== 'string'))
|
|
633
|
+
throw new Error('dispatch_assertion_actor_invalid');
|
|
634
|
+
actor = Object.freeze({
|
|
635
|
+
subject: candidate.subject,
|
|
636
|
+
...(candidate.username ? { username: candidate.username } : {}),
|
|
637
|
+
...(candidate.roomId ? { roomId: candidate.roomId } : {}),
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
for (const [jti, exp] of replay)
|
|
641
|
+
if (exp < now)
|
|
642
|
+
replay.delete(jti);
|
|
643
|
+
if (replay.has(payload.jti))
|
|
644
|
+
throw new Error('dispatch_assertion_replayed');
|
|
645
|
+
replay.set(payload.jti, Number(payload.exp));
|
|
646
|
+
return Object.freeze({
|
|
647
|
+
jti: payload.jti,
|
|
648
|
+
issuedAt: Number(payload.iat),
|
|
649
|
+
expiresAt: Number(payload.exp),
|
|
650
|
+
...(actor ? { actor } : {}),
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
//# sourceMappingURL=dispatch-assertion.js.map
|