@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,918 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import * as fsSync from 'node:fs';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import express, { Router } from 'express';
|
|
6
|
+
import { assertRuntimeDispatchTrustConfigurationV3 } from './dispatch-assertion.js';
|
|
7
|
+
const HASH = /^[A-Za-z0-9_-]{43}$/;
|
|
8
|
+
const DIGEST = /^sha256:[a-f0-9]{64}$/;
|
|
9
|
+
const COMPACT = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
|
10
|
+
const MAX_COMPACT_BYTES = 20_000;
|
|
11
|
+
const MAX_PROVISIONING_BODY_BYTES = 80_000;
|
|
12
|
+
/** Builds a resolver pinned to one operator-configured Portal issuer and JWKS URL. */
|
|
13
|
+
export function createPinnedPortalJwksResolverV3(options) {
|
|
14
|
+
if (!id(options.issuer))
|
|
15
|
+
throw new Error('publisher_portal_jwks_configuration_invalid');
|
|
16
|
+
let target;
|
|
17
|
+
try {
|
|
18
|
+
target = new URL(options.jwksUrl);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
throw new Error('publisher_portal_jwks_configuration_invalid');
|
|
22
|
+
}
|
|
23
|
+
if (target.protocol !== 'https:' || !target.hostname || target.username || target.password || target.search || target.hash ||
|
|
24
|
+
target.toString() !== options.jwksUrl)
|
|
25
|
+
throw new Error('publisher_portal_jwks_configuration_invalid');
|
|
26
|
+
const cacheSeconds = options.cacheSeconds ?? 300;
|
|
27
|
+
if (!Number.isSafeInteger(cacheSeconds) || cacheSeconds < 1 || cacheSeconds > 3600) {
|
|
28
|
+
throw new Error('publisher_portal_jwks_configuration_invalid');
|
|
29
|
+
}
|
|
30
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
31
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 30_000) {
|
|
32
|
+
throw new Error('publisher_portal_jwks_configuration_invalid');
|
|
33
|
+
}
|
|
34
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
35
|
+
let cached;
|
|
36
|
+
return async ({ issuer, kid }) => {
|
|
37
|
+
if (issuer !== options.issuer || !id(kid))
|
|
38
|
+
throw new Error('publisher_portal_jwks_issuer_invalid');
|
|
39
|
+
const now = Math.floor(Date.now() / 1000);
|
|
40
|
+
if (!cached || cached.expiresAt <= now) {
|
|
41
|
+
const response = await fetchImpl(options.jwksUrl, {
|
|
42
|
+
method: 'GET',
|
|
43
|
+
headers: { Accept: 'application/json' },
|
|
44
|
+
redirect: 'error',
|
|
45
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
46
|
+
});
|
|
47
|
+
if (!response.ok || response.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase() !== 'application/json') {
|
|
48
|
+
throw new Error('publisher_portal_jwks_fetch_failed');
|
|
49
|
+
}
|
|
50
|
+
const declaredLength = response.headers.get('content-length');
|
|
51
|
+
if (declaredLength !== null && (!/^(0|[1-9][0-9]*)$/.test(declaredLength) || Number(declaredLength) > 256_000)) {
|
|
52
|
+
throw new Error('publisher_portal_jwks_fetch_failed');
|
|
53
|
+
}
|
|
54
|
+
if (!response.body)
|
|
55
|
+
throw new Error('publisher_portal_jwks_fetch_failed');
|
|
56
|
+
const reader = response.body.getReader();
|
|
57
|
+
const chunks = [];
|
|
58
|
+
let byteLength = 0;
|
|
59
|
+
try {
|
|
60
|
+
for (;;) {
|
|
61
|
+
const chunk = await reader.read();
|
|
62
|
+
if (chunk.done)
|
|
63
|
+
break;
|
|
64
|
+
byteLength += chunk.value.byteLength;
|
|
65
|
+
if (byteLength > 256_000) {
|
|
66
|
+
await reader.cancel('publisher Portal JWKS response exceeded byte limit');
|
|
67
|
+
throw new Error('publisher_portal_jwks_fetch_failed');
|
|
68
|
+
}
|
|
69
|
+
chunks.push(chunk.value);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
reader.releaseLock();
|
|
74
|
+
}
|
|
75
|
+
const raw = Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)), byteLength).toString('utf8');
|
|
76
|
+
let document;
|
|
77
|
+
try {
|
|
78
|
+
document = JSON.parse(raw);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
throw new Error('publisher_portal_jwks_fetch_failed');
|
|
82
|
+
}
|
|
83
|
+
if (!isRecord(document) || !exactKeys(document, ['keys']) || !Array.isArray(document.keys) || document.keys.length > 100) {
|
|
84
|
+
throw new Error('publisher_portal_jwks_fetch_failed');
|
|
85
|
+
}
|
|
86
|
+
cached = { expiresAt: now + cacheSeconds, keys: document.keys };
|
|
87
|
+
}
|
|
88
|
+
const matches = cached.keys.filter((key) => key && key.kid === kid && key.kty === 'OKP' && key.crv === 'Ed25519' &&
|
|
89
|
+
key.use !== 'enc' && (!key.alg || key.alg === 'EdDSA') && !key.d && typeof key.x === 'string');
|
|
90
|
+
if (matches.length !== 1)
|
|
91
|
+
throw new Error('publisher_portal_jwks_key_invalid');
|
|
92
|
+
return { publicJwk: matches[0] };
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function isRecord(value) {
|
|
96
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
97
|
+
}
|
|
98
|
+
function exactKeys(value, keys) {
|
|
99
|
+
return Object.keys(value).sort().join('\0') === [...keys].sort().join('\0');
|
|
100
|
+
}
|
|
101
|
+
function canonical(value) {
|
|
102
|
+
if (Array.isArray(value))
|
|
103
|
+
return `[${value.map(canonical).join(',')}]`;
|
|
104
|
+
if (isRecord(value)) {
|
|
105
|
+
return `{${Object.keys(value)
|
|
106
|
+
.sort()
|
|
107
|
+
.map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`)
|
|
108
|
+
.join(',')}}`;
|
|
109
|
+
}
|
|
110
|
+
return JSON.stringify(value);
|
|
111
|
+
}
|
|
112
|
+
function canonicalHash(value) {
|
|
113
|
+
return crypto.createHash('sha256').update(canonical(value), 'utf8').digest('base64url');
|
|
114
|
+
}
|
|
115
|
+
function id(value) {
|
|
116
|
+
return typeof value === 'string' && value.length > 0 && value.length <= 256;
|
|
117
|
+
}
|
|
118
|
+
function positiveInteger(value) {
|
|
119
|
+
return Number.isSafeInteger(value) && Number(value) >= 1;
|
|
120
|
+
}
|
|
121
|
+
function assertAbsoluteProvisioningUrl(value) {
|
|
122
|
+
let parsed;
|
|
123
|
+
try {
|
|
124
|
+
parsed = new URL(value);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
throw new Error('publisher_runtime_trust_configuration_invalid');
|
|
128
|
+
}
|
|
129
|
+
if (parsed.protocol !== 'https:' ||
|
|
130
|
+
!parsed.hostname ||
|
|
131
|
+
parsed.username ||
|
|
132
|
+
parsed.password ||
|
|
133
|
+
parsed.search ||
|
|
134
|
+
parsed.hash ||
|
|
135
|
+
parsed.toString() !== value ||
|
|
136
|
+
parsed.pathname === '/' ||
|
|
137
|
+
parsed.pathname.endsWith('/') ||
|
|
138
|
+
parsed.pathname.includes('%') ||
|
|
139
|
+
parsed.pathname.includes('//') ||
|
|
140
|
+
parsed.pathname.split('/').slice(1).some((segment) => !segment || segment === '.' || segment === '..' || !/^[A-Za-z0-9._~-]+$/.test(segment))) {
|
|
141
|
+
throw new Error('publisher_runtime_trust_configuration_invalid');
|
|
142
|
+
}
|
|
143
|
+
return parsed;
|
|
144
|
+
}
|
|
145
|
+
function decodeCanonicalSegment(segment) {
|
|
146
|
+
let bytes;
|
|
147
|
+
let value;
|
|
148
|
+
try {
|
|
149
|
+
bytes = Buffer.from(segment, 'base64url');
|
|
150
|
+
if (bytes.toString('base64url') !== segment)
|
|
151
|
+
throw new Error('non-canonical base64url');
|
|
152
|
+
value = JSON.parse(bytes.toString('utf8'));
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
throw new Error('publisher_runtime_trust_artifact_invalid');
|
|
156
|
+
}
|
|
157
|
+
if (!isRecord(value) || canonical(value) !== bytes.toString('utf8')) {
|
|
158
|
+
throw new Error('publisher_runtime_trust_artifact_invalid');
|
|
159
|
+
}
|
|
160
|
+
return { value, bytes };
|
|
161
|
+
}
|
|
162
|
+
function compactParts(compact) {
|
|
163
|
+
if (compact.length < 32 || compact.length > MAX_COMPACT_BYTES || !COMPACT.test(compact)) {
|
|
164
|
+
throw new Error('publisher_runtime_trust_artifact_invalid');
|
|
165
|
+
}
|
|
166
|
+
const [encodedHeader, encodedPayload, encodedSignature] = compact.split('.');
|
|
167
|
+
const header = decodeCanonicalSegment(encodedHeader);
|
|
168
|
+
const payload = decodeCanonicalSegment(encodedPayload);
|
|
169
|
+
const signature = Buffer.from(encodedSignature, 'base64url');
|
|
170
|
+
if (signature.toString('base64url') !== encodedSignature)
|
|
171
|
+
throw new Error('publisher_runtime_trust_artifact_invalid');
|
|
172
|
+
return { encodedHeader: encodedHeader, encodedPayload: encodedPayload, header: header.value, payload: payload.value, signature };
|
|
173
|
+
}
|
|
174
|
+
const ENVELOPE_KEYS = ['audience', 'exp', 'iat', 'issuer', 'jti', 'kid', 'payload', 'protocolVersion', 'type'];
|
|
175
|
+
async function verifyPortalArtifact(compact, type, resolver, now, clockSkewSeconds, allowExpired) {
|
|
176
|
+
const parsed = compactParts(compact);
|
|
177
|
+
const expectedTyp = `privos-${type}+jws`;
|
|
178
|
+
if (!exactKeys(parsed.header, ['alg', 'kid', 'privos_protocol', 'typ']) ||
|
|
179
|
+
parsed.header.alg !== 'EdDSA' ||
|
|
180
|
+
parsed.header.typ !== expectedTyp ||
|
|
181
|
+
parsed.header.privos_protocol !== 3 ||
|
|
182
|
+
!id(parsed.header.kid) ||
|
|
183
|
+
!exactKeys(parsed.payload, ENVELOPE_KEYS) ||
|
|
184
|
+
parsed.payload.protocolVersion !== 3 ||
|
|
185
|
+
parsed.payload.type !== type ||
|
|
186
|
+
!id(parsed.payload.issuer) ||
|
|
187
|
+
!id(parsed.payload.audience) ||
|
|
188
|
+
parsed.payload.kid !== parsed.header.kid ||
|
|
189
|
+
!id(parsed.payload.jti) ||
|
|
190
|
+
!positiveInteger(parsed.payload.iat) ||
|
|
191
|
+
!positiveInteger(parsed.payload.exp) ||
|
|
192
|
+
parsed.payload.exp <= parsed.payload.iat ||
|
|
193
|
+
parsed.payload.iat > now + clockSkewSeconds ||
|
|
194
|
+
(!allowExpired && parsed.payload.exp <= now) ||
|
|
195
|
+
!isRecord(parsed.payload.payload)) {
|
|
196
|
+
throw new Error('publisher_runtime_trust_artifact_invalid');
|
|
197
|
+
}
|
|
198
|
+
const resolved = await resolver({
|
|
199
|
+
issuer: parsed.payload.issuer,
|
|
200
|
+
kid: parsed.header.kid,
|
|
201
|
+
type,
|
|
202
|
+
});
|
|
203
|
+
try {
|
|
204
|
+
if (!resolved ||
|
|
205
|
+
!resolved.publicJwk ||
|
|
206
|
+
resolved.publicJwk.kty !== 'OKP' ||
|
|
207
|
+
resolved.publicJwk.crv !== 'Ed25519' ||
|
|
208
|
+
resolved.publicJwk.d ||
|
|
209
|
+
typeof resolved.publicJwk.x !== 'string' ||
|
|
210
|
+
!crypto.verify(null, Buffer.from(`${parsed.encodedHeader}.${parsed.encodedPayload}`, 'utf8'), crypto.createPublicKey({ key: resolved.publicJwk, format: 'jwk' }), parsed.signature)) {
|
|
211
|
+
throw new Error('invalid');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
throw new Error('publisher_runtime_trust_artifact_signature_invalid');
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
header: parsed.header,
|
|
219
|
+
envelope: parsed.payload,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const ACQUISITION_AFFINITY_KEYS = [
|
|
223
|
+
'approvedPermissionCeiling',
|
|
224
|
+
'availabilityTier',
|
|
225
|
+
'dataPolicyHash',
|
|
226
|
+
'deploymentId',
|
|
227
|
+
'executionMode',
|
|
228
|
+
'imageDigest',
|
|
229
|
+
'instanceThumbprint',
|
|
230
|
+
'listingId',
|
|
231
|
+
'localArtifactDigest',
|
|
232
|
+
'machineClientId',
|
|
233
|
+
'manifestDigest',
|
|
234
|
+
'offerId',
|
|
235
|
+
'permissionCeilingHash',
|
|
236
|
+
'permissionContractHash',
|
|
237
|
+
'price',
|
|
238
|
+
'versionId',
|
|
239
|
+
'workspaceId',
|
|
240
|
+
];
|
|
241
|
+
function assertAcquisitionAffinity(value) {
|
|
242
|
+
const approvedPermissionCeiling = isRecord(value) && Array.isArray(value.approvedPermissionCeiling)
|
|
243
|
+
? value.approvedPermissionCeiling
|
|
244
|
+
: undefined;
|
|
245
|
+
const normalizedScopes = approvedPermissionCeiling
|
|
246
|
+
? [...approvedPermissionCeiling].sort((left, right) => String(left) < String(right) ? -1 : String(left) > String(right) ? 1 : 0)
|
|
247
|
+
: [];
|
|
248
|
+
if (!isRecord(value) ||
|
|
249
|
+
!exactKeys(value, ACQUISITION_AFFINITY_KEYS) ||
|
|
250
|
+
!id(value.workspaceId) ||
|
|
251
|
+
!id(value.deploymentId) ||
|
|
252
|
+
!id(value.machineClientId) ||
|
|
253
|
+
!HASH.test(String(value.instanceThumbprint)) ||
|
|
254
|
+
!id(value.listingId) ||
|
|
255
|
+
!id(value.versionId) ||
|
|
256
|
+
!id(value.offerId) ||
|
|
257
|
+
value.executionMode !== 'PUBLISHER_HOSTED' ||
|
|
258
|
+
!['single', 'ha'].includes(String(value.availabilityTier)) ||
|
|
259
|
+
!HASH.test(String(value.dataPolicyHash)) ||
|
|
260
|
+
!DIGEST.test(String(value.manifestDigest)) ||
|
|
261
|
+
value.imageDigest !== null ||
|
|
262
|
+
value.localArtifactDigest !== null ||
|
|
263
|
+
!HASH.test(String(value.permissionContractHash)) ||
|
|
264
|
+
!approvedPermissionCeiling ||
|
|
265
|
+
approvedPermissionCeiling.some((scope) => typeof scope !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9:._/-]{0,159}$/.test(scope)) ||
|
|
266
|
+
new Set(approvedPermissionCeiling).size !== approvedPermissionCeiling.length ||
|
|
267
|
+
normalizedScopes.some((scope, index) => scope !== approvedPermissionCeiling[index]) ||
|
|
268
|
+
!HASH.test(String(value.permissionCeilingHash)) ||
|
|
269
|
+
canonicalHash(approvedPermissionCeiling) !== value.permissionCeilingHash ||
|
|
270
|
+
!isRecord(value.price) ||
|
|
271
|
+
!exactKeys(value.price, ['creatorAmountCents', 'currency', 'runtimeCostApplies', 'runtimeCostCapCents']) ||
|
|
272
|
+
!Number.isSafeInteger(value.price.creatorAmountCents) || Number(value.price.creatorAmountCents) < 0 ||
|
|
273
|
+
!/^\p{Lu}{3}$/u.test(String(value.price.currency)) ||
|
|
274
|
+
typeof value.price.runtimeCostApplies !== 'boolean' ||
|
|
275
|
+
(value.price.runtimeCostCapCents !== null &&
|
|
276
|
+
(!Number.isSafeInteger(value.price.runtimeCostCapCents) || Number(value.price.runtimeCostCapCents) < 0))) {
|
|
277
|
+
throw new Error('publisher_runtime_trust_portal_chain_invalid');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const EXECUTION_KEYS = [
|
|
281
|
+
'approvalJti',
|
|
282
|
+
'authorizationEpoch',
|
|
283
|
+
'deploymentAppId',
|
|
284
|
+
'entitlementId',
|
|
285
|
+
'entitlementSeatId',
|
|
286
|
+
'generationId',
|
|
287
|
+
'generationNumber',
|
|
288
|
+
'installationId',
|
|
289
|
+
'listingId',
|
|
290
|
+
'manifestDigest',
|
|
291
|
+
'permissionCeilingHash',
|
|
292
|
+
'provisioningNonce',
|
|
293
|
+
'runtimeCredentialId',
|
|
294
|
+
'versionId',
|
|
295
|
+
'workspaceId',
|
|
296
|
+
'deploymentId',
|
|
297
|
+
];
|
|
298
|
+
const DESCRIPTOR_KEYS = [
|
|
299
|
+
'availabilityTier',
|
|
300
|
+
'deploymentAppId',
|
|
301
|
+
'executionGrantJti',
|
|
302
|
+
'executionMode',
|
|
303
|
+
'generationId',
|
|
304
|
+
'generationNumber',
|
|
305
|
+
'imageDigest',
|
|
306
|
+
'imageRepository',
|
|
307
|
+
'installationId',
|
|
308
|
+
'listingId',
|
|
309
|
+
'localArtifactDigest',
|
|
310
|
+
'manifest',
|
|
311
|
+
'manifestDigest',
|
|
312
|
+
'releaseAttestationHash',
|
|
313
|
+
'releaseAttestationJws',
|
|
314
|
+
'resourceManifest',
|
|
315
|
+
'resourceManifestHash',
|
|
316
|
+
'resourceManifestTemplate',
|
|
317
|
+
'resourceManifestTemplateHash',
|
|
318
|
+
'versionDigest',
|
|
319
|
+
'versionId',
|
|
320
|
+
'workspaceId',
|
|
321
|
+
'deploymentId',
|
|
322
|
+
];
|
|
323
|
+
function assertResourceManifest(value, template) {
|
|
324
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > 1024) {
|
|
325
|
+
throw new Error('publisher_runtime_trust_portal_chain_invalid');
|
|
326
|
+
}
|
|
327
|
+
for (const item of value) {
|
|
328
|
+
if (!isRecord(item))
|
|
329
|
+
throw new Error('publisher_runtime_trust_portal_chain_invalid');
|
|
330
|
+
const optionalReference = !template && item.referenceCount !== undefined ? ['referenceCount'] : [];
|
|
331
|
+
const keys = template
|
|
332
|
+
? ['absenceAdapter', 'dataClass', 'expectedCount', 'ownershipScope', 'purgeAdapter', 'resourceClass', 'resourceKey']
|
|
333
|
+
: ['absenceAdapter', 'dataClass', 'expectedCount', 'ownershipScope', 'purgeAdapter', 'resourceClass', 'resourceId', ...optionalReference];
|
|
334
|
+
const identity = template ? item.resourceKey : item.resourceId;
|
|
335
|
+
if (!exactKeys(item, keys) ||
|
|
336
|
+
!id(item.resourceClass) ||
|
|
337
|
+
!['APP_PRIVATE', 'HUB_NATIVE_USER_OWNED', 'PUBLISHER_EXTERNAL'].includes(String(item.dataClass)) ||
|
|
338
|
+
!['ROOM_BINDING', 'INSTALLATION_GENERATION', 'DEPLOYMENT_SHARED', 'PLATFORM_SHARED'].includes(String(item.ownershipScope)) ||
|
|
339
|
+
!id(identity) ||
|
|
340
|
+
!Number.isSafeInteger(item.expectedCount) || Number(item.expectedCount) < 0 ||
|
|
341
|
+
!id(item.purgeAdapter) || !id(item.absenceAdapter) ||
|
|
342
|
+
(item.referenceCount !== undefined && (!Number.isSafeInteger(item.referenceCount) || Number(item.referenceCount) < 0)))
|
|
343
|
+
throw new Error('publisher_runtime_trust_portal_chain_invalid');
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function assertPortalChain(input) {
|
|
347
|
+
const approval = input.approval.envelope;
|
|
348
|
+
const execution = input.execution.envelope;
|
|
349
|
+
const descriptor = input.descriptor.envelope;
|
|
350
|
+
const ap = approval.payload;
|
|
351
|
+
const ep = execution.payload;
|
|
352
|
+
const dp = descriptor.payload;
|
|
353
|
+
if (!exactKeys(ap, ['approvedBy', 'affinity', 'commercial', 'portalPolicyVersion', 'proposalHash']) ||
|
|
354
|
+
!HASH.test(String(ap.proposalHash)) ||
|
|
355
|
+
!isRecord(ap.approvedBy) ||
|
|
356
|
+
!exactKeys(ap.approvedBy, ['approvedAt', 'opaqueUserRef', 'role']) ||
|
|
357
|
+
!id(ap.approvedBy.opaqueUserRef) ||
|
|
358
|
+
ap.approvedBy.role !== 'OWNER' ||
|
|
359
|
+
!positiveInteger(ap.approvedBy.approvedAt) ||
|
|
360
|
+
!isRecord(ap.commercial) ||
|
|
361
|
+
!exactKeys(ap.commercial, ['entitlementId', 'entitlementState', 'seatAllowance', 'validUntil']) ||
|
|
362
|
+
!id(ap.commercial.entitlementId) ||
|
|
363
|
+
!['ACTIVE', 'PENDING_PAYMENT'].includes(String(ap.commercial.entitlementState)) ||
|
|
364
|
+
!positiveInteger(ap.commercial.seatAllowance) ||
|
|
365
|
+
(ap.commercial.validUntil !== null && !positiveInteger(ap.commercial.validUntil)) ||
|
|
366
|
+
!positiveInteger(ap.portalPolicyVersion))
|
|
367
|
+
throw new Error('publisher_runtime_trust_portal_chain_invalid');
|
|
368
|
+
assertAcquisitionAffinity(ap.affinity);
|
|
369
|
+
const approvalAffinity = ap.affinity;
|
|
370
|
+
assertResourceManifest(dp.resourceManifest, false);
|
|
371
|
+
assertResourceManifest(dp.resourceManifestTemplate, true);
|
|
372
|
+
if (!exactKeys(ep, EXECUTION_KEYS) ||
|
|
373
|
+
!exactKeys(dp, DESCRIPTOR_KEYS) ||
|
|
374
|
+
approval.issuer !== execution.issuer ||
|
|
375
|
+
approval.issuer !== descriptor.issuer ||
|
|
376
|
+
approval.audience !== `hub:${String(ep.deploymentId)}` ||
|
|
377
|
+
execution.audience !== `installation:${String(ep.installationId)}` ||
|
|
378
|
+
descriptor.audience !== execution.audience ||
|
|
379
|
+
ep.approvalJti !== approval.jti ||
|
|
380
|
+
dp.executionGrantJti !== execution.jti ||
|
|
381
|
+
!positiveInteger(ep.authorizationEpoch) ||
|
|
382
|
+
!positiveInteger(ep.generationNumber) ||
|
|
383
|
+
!positiveInteger(dp.generationNumber) ||
|
|
384
|
+
!isRecord(dp.manifest) ||
|
|
385
|
+
!Array.isArray(dp.resourceManifest) ||
|
|
386
|
+
!Array.isArray(dp.resourceManifestTemplate) ||
|
|
387
|
+
canonicalHash(dp.resourceManifest) !== dp.resourceManifestHash ||
|
|
388
|
+
canonicalHash(dp.resourceManifestTemplate) !== dp.resourceManifestTemplateHash ||
|
|
389
|
+
dp.executionMode !== 'PUBLISHER_HOSTED' ||
|
|
390
|
+
dp.imageDigest !== null ||
|
|
391
|
+
dp.localArtifactDigest !== null ||
|
|
392
|
+
dp.imageRepository !== null ||
|
|
393
|
+
dp.releaseAttestationJws !== null ||
|
|
394
|
+
dp.releaseAttestationHash !== null ||
|
|
395
|
+
dp.availabilityTier !== approvalAffinity.availabilityTier ||
|
|
396
|
+
ep.entitlementId !== ap.commercial.entitlementId ||
|
|
397
|
+
ep.permissionCeilingHash !== ap.affinity.permissionCeilingHash ||
|
|
398
|
+
!id(ep.deploymentAppId) || !id(ep.generationId) || !id(ep.installationId) ||
|
|
399
|
+
!id(ep.workspaceId) || !id(ep.deploymentId) || !id(ep.listingId) || !id(ep.versionId) ||
|
|
400
|
+
!DIGEST.test(String(ep.manifestDigest)) || !HASH.test(String(ep.permissionCeilingHash)) ||
|
|
401
|
+
!id(ep.entitlementSeatId) || !id(ep.runtimeCredentialId) || !id(ep.provisioningNonce) ||
|
|
402
|
+
!DIGEST.test(String(dp.versionDigest)) || !DIGEST.test(String(dp.manifestDigest)) ||
|
|
403
|
+
!HASH.test(String(dp.resourceManifestHash)) || !HASH.test(String(dp.resourceManifestTemplateHash)))
|
|
404
|
+
throw new Error('publisher_runtime_trust_portal_chain_invalid');
|
|
405
|
+
const commonKeys = ['workspaceId', 'deploymentId', 'listingId', 'versionId', 'manifestDigest'];
|
|
406
|
+
if (commonKeys.some((key) => ep[key] !== approvalAffinity[key] || dp[key] !== ep[key]) ||
|
|
407
|
+
dp.deploymentAppId !== ep.deploymentAppId ||
|
|
408
|
+
dp.generationId !== ep.generationId ||
|
|
409
|
+
dp.generationNumber !== ep.generationNumber ||
|
|
410
|
+
dp.installationId !== ep.installationId ||
|
|
411
|
+
dp.manifest.name !== input.mcpAppId ||
|
|
412
|
+
dp.manifest.runtimeTrustProvisioningUrl !== input.provisioningUrl ||
|
|
413
|
+
approvalAffinity.instanceThumbprint !== input.trust.hubKid)
|
|
414
|
+
throw new Error('publisher_runtime_trust_portal_chain_invalid');
|
|
415
|
+
const affinity = input.trust.affinity;
|
|
416
|
+
if (affinity.executionMode !== 'PUBLISHER_HOSTED' ||
|
|
417
|
+
affinity.workspaceId !== ep.workspaceId ||
|
|
418
|
+
affinity.deploymentId !== ep.deploymentId ||
|
|
419
|
+
affinity.mcpAppId !== input.mcpAppId ||
|
|
420
|
+
affinity.generationId !== ep.generationId ||
|
|
421
|
+
affinity.generationNumber !== ep.generationNumber ||
|
|
422
|
+
affinity.runtimeInstallationId !== ep.installationId ||
|
|
423
|
+
affinity.manifestDigest !== ep.manifestDigest ||
|
|
424
|
+
affinity.resourceManifestHash !== dp.resourceManifestHash)
|
|
425
|
+
throw new Error('publisher_runtime_trust_portal_chain_invalid');
|
|
426
|
+
}
|
|
427
|
+
function verifyHubProof(input) {
|
|
428
|
+
const parsed = compactParts(input.compact);
|
|
429
|
+
const payload = parsed.payload;
|
|
430
|
+
const bodyDigest = canonicalHash(input.bodyWithoutProof);
|
|
431
|
+
if (!exactKeys(parsed.header, ['alg', 'kid', 'privos_protocol', 'typ']) ||
|
|
432
|
+
parsed.header.alg !== 'ES256' ||
|
|
433
|
+
parsed.header.kid !== input.trust.hubKid ||
|
|
434
|
+
parsed.header.privos_protocol !== 3 ||
|
|
435
|
+
parsed.header.typ !== 'privos-publisher-runtime-trust-provision+jws' ||
|
|
436
|
+
!exactKeys(payload, ['aud', 'bodyDigest', 'exp', 'htm', 'htu', 'iat', 'iss', 'jti', 'nonce', 'protocolVersion', 'type']) ||
|
|
437
|
+
payload.protocolVersion !== 3 ||
|
|
438
|
+
payload.type !== 'publisher-runtime-trust-provisioning-proof' ||
|
|
439
|
+
payload.iss !== `hub:${input.trust.affinity.deploymentId}` ||
|
|
440
|
+
payload.aud !== `mcp-runtime:${input.mcpAppId}` ||
|
|
441
|
+
!id(payload.jti) || !id(payload.nonce) ||
|
|
442
|
+
!positiveInteger(payload.iat) || !positiveInteger(payload.exp) ||
|
|
443
|
+
payload.exp <= payload.iat || Number(payload.exp) - Number(payload.iat) > 30 ||
|
|
444
|
+
Number(payload.iat) > input.now + input.clockSkewSeconds || Number(payload.exp) <= input.now ||
|
|
445
|
+
payload.htm !== 'PUT' || payload.htu !== input.provisioningUrl || payload.bodyDigest !== bodyDigest)
|
|
446
|
+
throw new Error('publisher_runtime_trust_hub_proof_invalid');
|
|
447
|
+
try {
|
|
448
|
+
if (!crypto.verify('sha256', Buffer.from(`${parsed.encodedHeader}.${parsed.encodedPayload}`, 'utf8'), { key: crypto.createPublicKey({ key: input.trust.hubPublicJwk, format: 'jwk' }), dsaEncoding: 'ieee-p1363' }, parsed.signature))
|
|
449
|
+
throw new Error('invalid');
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
throw new Error('publisher_runtime_trust_hub_proof_invalid');
|
|
453
|
+
}
|
|
454
|
+
return {
|
|
455
|
+
issuer: String(payload.iss),
|
|
456
|
+
jti: String(payload.jti),
|
|
457
|
+
nonce: String(payload.nonce),
|
|
458
|
+
expiresAt: Number(payload.exp),
|
|
459
|
+
bodyDigest,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
function recordKey(trust) {
|
|
463
|
+
return JSON.stringify([
|
|
464
|
+
trust.affinity.workspaceId,
|
|
465
|
+
trust.affinity.deploymentId,
|
|
466
|
+
trust.affinity.mcpAppId,
|
|
467
|
+
trust.affinity.generationId,
|
|
468
|
+
trust.affinity.generationNumber,
|
|
469
|
+
trust.affinity.runtimeInstallationId,
|
|
470
|
+
]);
|
|
471
|
+
}
|
|
472
|
+
function preparedEvidence(trust, now) {
|
|
473
|
+
const withoutHash = {
|
|
474
|
+
protocol_version: 3,
|
|
475
|
+
runtime_installation_id: trust.affinity.runtimeInstallationId,
|
|
476
|
+
generation_id: trust.affinity.generationId,
|
|
477
|
+
generation_number: trust.affinity.generationNumber,
|
|
478
|
+
state: 'PREPARED',
|
|
479
|
+
runtime_dispatch_trust_hash: canonicalHash(trust),
|
|
480
|
+
prepared_at: now,
|
|
481
|
+
};
|
|
482
|
+
return Object.freeze({ ...withoutHash, provisioning_evidence_hash: canonicalHash(withoutHash) });
|
|
483
|
+
}
|
|
484
|
+
function activeEvidence(trust, prepared, now) {
|
|
485
|
+
const withoutHash = {
|
|
486
|
+
protocol_version: 3,
|
|
487
|
+
runtime_installation_id: trust.affinity.runtimeInstallationId,
|
|
488
|
+
generation_id: trust.affinity.generationId,
|
|
489
|
+
generation_number: trust.affinity.generationNumber,
|
|
490
|
+
state: 'ACTIVE',
|
|
491
|
+
runtime_dispatch_trust_hash: canonicalHash(trust),
|
|
492
|
+
prepared_at: prepared.prepared_at,
|
|
493
|
+
activated_at: now,
|
|
494
|
+
};
|
|
495
|
+
return Object.freeze({ ...withoutHash, provisioning_evidence_hash: canonicalHash(withoutHash) });
|
|
496
|
+
}
|
|
497
|
+
function emptyState() {
|
|
498
|
+
return {
|
|
499
|
+
protocolVersion: 3,
|
|
500
|
+
records: {},
|
|
501
|
+
provisioningJtis: {},
|
|
502
|
+
provisioningNonces: {},
|
|
503
|
+
dispatchJtis: {},
|
|
504
|
+
dispatchNonces: {},
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Crash-safe atomic file store for exactly one Node process. It deliberately
|
|
509
|
+
* rejects every other deployment mode: multiple replicas require an external
|
|
510
|
+
* transactional store implementing PublisherRuntimeTrustDurableStoreV3.
|
|
511
|
+
*/
|
|
512
|
+
export class SingleProcessFilePublisherRuntimeTrustStoreV3 {
|
|
513
|
+
options;
|
|
514
|
+
queue = Promise.resolve();
|
|
515
|
+
lockPath;
|
|
516
|
+
lockNonce = crypto.randomUUID();
|
|
517
|
+
lockFd;
|
|
518
|
+
exitCleanup;
|
|
519
|
+
constructor(options) {
|
|
520
|
+
this.options = options;
|
|
521
|
+
if (options.deploymentMode !== 'single-process' || !path.isAbsolute(options.filePath)) {
|
|
522
|
+
throw new Error('publisher_runtime_trust_store_configuration_invalid');
|
|
523
|
+
}
|
|
524
|
+
const directory = path.dirname(options.filePath);
|
|
525
|
+
fsSync.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
526
|
+
this.assertSafeDirectorySync(directory);
|
|
527
|
+
this.lockPath = `${options.filePath}.lock`;
|
|
528
|
+
this.lockFd = this.acquireProcessLockSync();
|
|
529
|
+
this.exitCleanup = () => this.releaseProcessLockSync();
|
|
530
|
+
process.once('exit', this.exitCleanup);
|
|
531
|
+
}
|
|
532
|
+
assertSafeDirectorySync(directory) {
|
|
533
|
+
const stat = fsSync.lstatSync(directory);
|
|
534
|
+
const uid = typeof process.getuid === 'function' ? process.getuid() : stat.uid;
|
|
535
|
+
const gid = typeof process.getgid === 'function' ? process.getgid() : stat.gid;
|
|
536
|
+
if (!stat.isDirectory() || stat.uid !== uid || stat.gid !== gid || (stat.mode & 0o777) !== 0o700 ||
|
|
537
|
+
fsSync.realpathSync(directory) !== directory) {
|
|
538
|
+
throw new Error('publisher_runtime_trust_store_permissions_invalid');
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
acquireProcessLockSync() {
|
|
542
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
543
|
+
try {
|
|
544
|
+
const fd = fsSync.openSync(this.lockPath, 'wx', 0o600);
|
|
545
|
+
fsSync.writeFileSync(fd, `${canonical({ nonce: this.lockNonce, pid: process.pid })}\n`, 'utf8');
|
|
546
|
+
fsSync.fsyncSync(fd);
|
|
547
|
+
return fd;
|
|
548
|
+
}
|
|
549
|
+
catch (error) {
|
|
550
|
+
if (error.code !== 'EEXIST')
|
|
551
|
+
throw error;
|
|
552
|
+
let ownerPid = -1;
|
|
553
|
+
try {
|
|
554
|
+
const lock = JSON.parse(fsSync.readFileSync(this.lockPath, 'utf8'));
|
|
555
|
+
if (Number.isSafeInteger(lock.pid) && Number(lock.pid) > 0)
|
|
556
|
+
ownerPid = Number(lock.pid);
|
|
557
|
+
}
|
|
558
|
+
catch {
|
|
559
|
+
throw new Error('publisher_runtime_trust_store_lock_invalid');
|
|
560
|
+
}
|
|
561
|
+
if (ownerPid < 1)
|
|
562
|
+
throw new Error('publisher_runtime_trust_store_lock_invalid');
|
|
563
|
+
try {
|
|
564
|
+
process.kill(ownerPid, 0);
|
|
565
|
+
throw new Error('publisher_runtime_trust_store_already_in_use');
|
|
566
|
+
}
|
|
567
|
+
catch (probe) {
|
|
568
|
+
if (probe.code !== 'ESRCH')
|
|
569
|
+
throw probe;
|
|
570
|
+
}
|
|
571
|
+
try {
|
|
572
|
+
const stalePath = `${this.lockPath}.stale.${crypto.randomUUID()}`;
|
|
573
|
+
fsSync.renameSync(this.lockPath, stalePath);
|
|
574
|
+
fsSync.unlinkSync(stalePath);
|
|
575
|
+
}
|
|
576
|
+
catch (renameError) {
|
|
577
|
+
if (renameError.code !== 'ENOENT')
|
|
578
|
+
throw renameError;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
throw new Error('publisher_runtime_trust_store_lock_invalid');
|
|
583
|
+
}
|
|
584
|
+
releaseProcessLockSync() {
|
|
585
|
+
if (this.lockFd < 0)
|
|
586
|
+
return;
|
|
587
|
+
try {
|
|
588
|
+
fsSync.closeSync(this.lockFd);
|
|
589
|
+
}
|
|
590
|
+
catch { /* fail closed on the next open */ }
|
|
591
|
+
this.lockFd = -1;
|
|
592
|
+
try {
|
|
593
|
+
const current = JSON.parse(fsSync.readFileSync(this.lockPath, 'utf8'));
|
|
594
|
+
if (current.nonce === this.lockNonce && current.pid === process.pid)
|
|
595
|
+
fsSync.unlinkSync(this.lockPath);
|
|
596
|
+
}
|
|
597
|
+
catch { /* a missing or changed lock must never be removed */ }
|
|
598
|
+
}
|
|
599
|
+
async close() {
|
|
600
|
+
await this.queue;
|
|
601
|
+
process.removeListener('exit', this.exitCleanup);
|
|
602
|
+
this.releaseProcessLockSync();
|
|
603
|
+
}
|
|
604
|
+
async exclusive(callback) {
|
|
605
|
+
let release;
|
|
606
|
+
const previous = this.queue;
|
|
607
|
+
this.queue = new Promise((resolve) => { release = resolve; });
|
|
608
|
+
await previous;
|
|
609
|
+
try {
|
|
610
|
+
let state = emptyState();
|
|
611
|
+
try {
|
|
612
|
+
const stat = await fs.lstat(this.options.filePath);
|
|
613
|
+
const uid = typeof process.getuid === 'function' ? process.getuid() : stat.uid;
|
|
614
|
+
const gid = typeof process.getgid === 'function' ? process.getgid() : stat.gid;
|
|
615
|
+
if (!stat.isFile() || stat.uid !== uid || stat.gid !== gid || (stat.mode & 0o777) !== 0o600 ||
|
|
616
|
+
(await fs.realpath(this.options.filePath)) !== this.options.filePath) {
|
|
617
|
+
throw new Error('publisher_runtime_trust_store_permissions_invalid');
|
|
618
|
+
}
|
|
619
|
+
const parsed = JSON.parse(await fs.readFile(this.options.filePath, 'utf8'));
|
|
620
|
+
if (!isRecord(parsed) || parsed.protocolVersion !== 3 || !isRecord(parsed.records) ||
|
|
621
|
+
!isRecord(parsed.provisioningJtis) || !isRecord(parsed.provisioningNonces) ||
|
|
622
|
+
!isRecord(parsed.dispatchJtis) || !isRecord(parsed.dispatchNonces)) {
|
|
623
|
+
throw new Error('publisher_runtime_trust_store_corrupt');
|
|
624
|
+
}
|
|
625
|
+
state = parsed;
|
|
626
|
+
}
|
|
627
|
+
catch (error) {
|
|
628
|
+
if (error?.code !== 'ENOENT')
|
|
629
|
+
throw error;
|
|
630
|
+
}
|
|
631
|
+
const outcome = await callback(state);
|
|
632
|
+
if (outcome.dirty)
|
|
633
|
+
await this.persist(state);
|
|
634
|
+
return outcome.result;
|
|
635
|
+
}
|
|
636
|
+
finally {
|
|
637
|
+
release();
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
async persist(state) {
|
|
641
|
+
const directory = path.dirname(this.options.filePath);
|
|
642
|
+
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
643
|
+
this.assertSafeDirectorySync(directory);
|
|
644
|
+
const temporary = `${this.options.filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
645
|
+
const handle = await fs.open(temporary, 'wx', 0o600);
|
|
646
|
+
try {
|
|
647
|
+
await handle.writeFile(`${canonical(state)}\n`, 'utf8');
|
|
648
|
+
await handle.sync();
|
|
649
|
+
}
|
|
650
|
+
finally {
|
|
651
|
+
await handle.close();
|
|
652
|
+
}
|
|
653
|
+
try {
|
|
654
|
+
await fs.rename(temporary, this.options.filePath);
|
|
655
|
+
await fs.chmod(this.options.filePath, 0o600);
|
|
656
|
+
const directoryHandle = await fs.open(directory, 'r');
|
|
657
|
+
try {
|
|
658
|
+
await directoryHandle.sync();
|
|
659
|
+
}
|
|
660
|
+
finally {
|
|
661
|
+
await directoryHandle.close();
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
catch (error) {
|
|
665
|
+
await fs.rm(temporary, { force: true }).catch(() => undefined);
|
|
666
|
+
throw error;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
async apply(input) {
|
|
670
|
+
return this.exclusive((state) => {
|
|
671
|
+
for (const [key, value] of Object.entries(state.provisioningJtis))
|
|
672
|
+
if (value.expiresAt <= input.now)
|
|
673
|
+
delete state.provisioningJtis[key];
|
|
674
|
+
for (const [key, value] of Object.entries(state.provisioningNonces))
|
|
675
|
+
if (value.expiresAt <= input.now)
|
|
676
|
+
delete state.provisioningNonces[key];
|
|
677
|
+
const jtiKey = JSON.stringify([input.proof.issuer, input.proof.jti]);
|
|
678
|
+
const nonceKey = JSON.stringify([input.proof.issuer, input.proof.nonce]);
|
|
679
|
+
const priorJti = state.provisioningJtis[jtiKey];
|
|
680
|
+
const priorNonce = state.provisioningNonces[nonceKey];
|
|
681
|
+
if ((priorJti && priorJti.bodyDigest !== input.bodyDigest) || (priorNonce && priorNonce.bodyDigest !== input.bodyDigest)) {
|
|
682
|
+
throw new Error('publisher_runtime_trust_replay_conflict');
|
|
683
|
+
}
|
|
684
|
+
const key = recordKey(input.trust);
|
|
685
|
+
const existing = state.records[key];
|
|
686
|
+
let evidence;
|
|
687
|
+
if (input.operation === 'PREPARE') {
|
|
688
|
+
if (input.trust.affinity.runtimeResourceInventoryHash !== undefined ||
|
|
689
|
+
input.trust.affinity.runtimeApprovalReceiptHash !== undefined ||
|
|
690
|
+
input.trust.affinity.runtimeAuthorizationEpoch !== undefined) {
|
|
691
|
+
throw new Error('publisher_runtime_trust_transition_conflict');
|
|
692
|
+
}
|
|
693
|
+
if (input.portalArtifactsExpired && !existing) {
|
|
694
|
+
throw new Error('publisher_runtime_trust_artifact_invalid');
|
|
695
|
+
}
|
|
696
|
+
if (existing) {
|
|
697
|
+
if (existing.prepareBodyDigest !== input.bodyDigest || existing.portalArtifactsHash !== input.portalArtifactsHash ||
|
|
698
|
+
existing.provisioningUrl !== input.provisioningUrl || canonical(existing.preparedTrust) !== canonical(input.trust)) {
|
|
699
|
+
throw new Error('publisher_runtime_trust_transition_conflict');
|
|
700
|
+
}
|
|
701
|
+
evidence = existing.preparedEvidence;
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
const prepared = preparedEvidence(input.trust, input.now);
|
|
705
|
+
state.records[key] = {
|
|
706
|
+
key,
|
|
707
|
+
provisioningUrl: input.provisioningUrl,
|
|
708
|
+
mcpAppId: input.mcpAppId,
|
|
709
|
+
portalArtifactsHash: input.portalArtifactsHash,
|
|
710
|
+
prepareBodyDigest: input.bodyDigest,
|
|
711
|
+
preparedTrust: input.trust,
|
|
712
|
+
preparedEvidence: prepared,
|
|
713
|
+
};
|
|
714
|
+
evidence = prepared;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
if (!existing || canonical(existing.preparedTrust.hubPublicJwk) !== canonical(input.trust.hubPublicJwk) ||
|
|
719
|
+
existing.preparedTrust.hubKid !== input.trust.hubKid || existing.portalArtifactsHash !== input.portalArtifactsHash ||
|
|
720
|
+
existing.provisioningUrl !== input.provisioningUrl || !HASH.test(String(input.trust.affinity.runtimeResourceInventoryHash)) ||
|
|
721
|
+
!HASH.test(String(input.trust.affinity.runtimeApprovalReceiptHash)) ||
|
|
722
|
+
!positiveInteger(input.trust.affinity.runtimeAuthorizationEpoch)) {
|
|
723
|
+
throw new Error('publisher_runtime_trust_transition_conflict');
|
|
724
|
+
}
|
|
725
|
+
const stableTrust = {
|
|
726
|
+
...input.trust,
|
|
727
|
+
affinity: Object.fromEntries(Object.entries(input.trust.affinity).filter(([key]) => !['runtimeResourceInventoryHash', 'runtimeApprovalReceiptHash', 'runtimeAuthorizationEpoch'].includes(key))),
|
|
728
|
+
};
|
|
729
|
+
if (canonical(stableTrust) !== canonical(existing.preparedTrust))
|
|
730
|
+
throw new Error('publisher_runtime_trust_transition_conflict');
|
|
731
|
+
if (existing.activeEvidence) {
|
|
732
|
+
if (existing.activateBodyDigest !== input.bodyDigest || canonical(existing.activeTrust) !== canonical(input.trust)) {
|
|
733
|
+
throw new Error('publisher_runtime_trust_transition_conflict');
|
|
734
|
+
}
|
|
735
|
+
evidence = existing.activeEvidence;
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
const active = activeEvidence(input.trust, existing.preparedEvidence, input.now);
|
|
739
|
+
existing.activateBodyDigest = input.bodyDigest;
|
|
740
|
+
existing.activeTrust = input.trust;
|
|
741
|
+
existing.activeEvidence = active;
|
|
742
|
+
evidence = active;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
state.provisioningJtis[jtiKey] = { bodyDigest: input.bodyDigest, expiresAt: input.proof.expiresAt };
|
|
746
|
+
state.provisioningNonces[nonceKey] = { bodyDigest: input.bodyDigest, expiresAt: input.proof.expiresAt };
|
|
747
|
+
return { result: evidence, dirty: true };
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
async loadActive(hint) {
|
|
751
|
+
return this.exclusive((state) => {
|
|
752
|
+
const key = JSON.stringify([
|
|
753
|
+
hint.workspaceId, hint.deploymentId, hint.mcpAppId, hint.generationId,
|
|
754
|
+
hint.generationNumber, hint.runtimeInstallationId,
|
|
755
|
+
]);
|
|
756
|
+
const record = state.records[key];
|
|
757
|
+
if (!record?.activeTrust || record.activeTrust.hubKid !== hint.kid || record.activeTrust.affinity.executionMode !== hint.executionMode) {
|
|
758
|
+
throw new Error('publisher_runtime_trust_not_active');
|
|
759
|
+
}
|
|
760
|
+
return { result: Object.freeze(record.activeTrust), dirty: false };
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
async resolveDispatchTrust(hint) {
|
|
764
|
+
return this.exclusive((state) => {
|
|
765
|
+
const key = JSON.stringify([
|
|
766
|
+
hint.workspaceId, hint.deploymentId, hint.mcpAppId, hint.generationId,
|
|
767
|
+
hint.generationNumber, hint.runtimeInstallationId,
|
|
768
|
+
]);
|
|
769
|
+
const record = state.records[key];
|
|
770
|
+
if (!record || record.preparedTrust.hubKid !== hint.kid || record.preparedTrust.affinity.executionMode !== hint.executionMode) {
|
|
771
|
+
throw new Error('publisher_runtime_trust_not_found');
|
|
772
|
+
}
|
|
773
|
+
if (record.activeTrust)
|
|
774
|
+
return { result: Object.freeze(record.activeTrust), dirty: false };
|
|
775
|
+
if (hint.preactivationReadiness === true)
|
|
776
|
+
return { result: Object.freeze(record.preparedTrust), dirty: false };
|
|
777
|
+
throw new Error('publisher_runtime_trust_not_active');
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
async consume(input) {
|
|
781
|
+
return this.exclusive((state) => {
|
|
782
|
+
for (const [key, expiresAt] of Object.entries(state.dispatchJtis))
|
|
783
|
+
if (expiresAt <= input.now)
|
|
784
|
+
delete state.dispatchJtis[key];
|
|
785
|
+
for (const [key, expiresAt] of Object.entries(state.dispatchNonces))
|
|
786
|
+
if (expiresAt <= input.now)
|
|
787
|
+
delete state.dispatchNonces[key];
|
|
788
|
+
const jtiKey = JSON.stringify([input.issuer, input.jti]);
|
|
789
|
+
const nonceKey = JSON.stringify([input.issuer, input.nonce]);
|
|
790
|
+
if (state.dispatchJtis[jtiKey] || state.dispatchNonces[nonceKey])
|
|
791
|
+
return { result: false, dirty: false };
|
|
792
|
+
state.dispatchJtis[jtiKey] = input.expiresAt;
|
|
793
|
+
state.dispatchNonces[nonceKey] = input.expiresAt;
|
|
794
|
+
return { result: true, dirty: true };
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
function parseProvisioningRequest(value) {
|
|
799
|
+
if (!isRecord(value) || !exactKeys(value, [
|
|
800
|
+
'cloud_approval_jws', 'deployment_descriptor_jws', 'execution_grant_jws', 'hub_proof_jws',
|
|
801
|
+
'operation', 'protocol_version', 'runtime_dispatch_trust',
|
|
802
|
+
]) || value.protocol_version !== 3 || !['PREPARE', 'ACTIVATE'].includes(String(value.operation)) ||
|
|
803
|
+
!COMPACT.test(String(value.cloud_approval_jws)) || !COMPACT.test(String(value.execution_grant_jws)) ||
|
|
804
|
+
!COMPACT.test(String(value.deployment_descriptor_jws)) || !COMPACT.test(String(value.hub_proof_jws))) {
|
|
805
|
+
throw new Error('publisher_runtime_trust_request_invalid');
|
|
806
|
+
}
|
|
807
|
+
for (const key of ['cloud_approval_jws', 'execution_grant_jws', 'deployment_descriptor_jws', 'hub_proof_jws']) {
|
|
808
|
+
if (String(value[key]).length > MAX_COMPACT_BYTES)
|
|
809
|
+
throw new Error('publisher_runtime_trust_request_invalid');
|
|
810
|
+
}
|
|
811
|
+
assertRuntimeDispatchTrustConfigurationV3(value.runtime_dispatch_trust);
|
|
812
|
+
const trust = value.runtime_dispatch_trust;
|
|
813
|
+
if (trust.affinity.executionMode !== 'PUBLISHER_HOSTED')
|
|
814
|
+
throw new Error('publisher_runtime_trust_request_invalid');
|
|
815
|
+
const dynamic = [trust.affinity.runtimeResourceInventoryHash, trust.affinity.runtimeApprovalReceiptHash, trust.affinity.runtimeAuthorizationEpoch];
|
|
816
|
+
if ((value.operation === 'PREPARE' && dynamic.some((item) => item !== undefined)) ||
|
|
817
|
+
(value.operation === 'ACTIVATE' && dynamic.some((item) => item === undefined))) {
|
|
818
|
+
throw new Error('publisher_runtime_trust_request_invalid');
|
|
819
|
+
}
|
|
820
|
+
return value;
|
|
821
|
+
}
|
|
822
|
+
export function createPublisherRuntimeTrustProvisioningRouterV3(options) {
|
|
823
|
+
const target = assertAbsoluteProvisioningUrl(options.provisioningUrl);
|
|
824
|
+
if (!id(options.mcpAppId) || !options.portalJwksResolver || !options.store) {
|
|
825
|
+
throw new Error('publisher_runtime_trust_configuration_invalid');
|
|
826
|
+
}
|
|
827
|
+
const clockSkewSeconds = options.clockSkewSeconds ?? 5;
|
|
828
|
+
if (!Number.isSafeInteger(clockSkewSeconds) || clockSkewSeconds < 0 || clockSkewSeconds > 30) {
|
|
829
|
+
throw new Error('publisher_runtime_trust_configuration_invalid');
|
|
830
|
+
}
|
|
831
|
+
const router = Router();
|
|
832
|
+
router.put(target.pathname, express.json({
|
|
833
|
+
limit: MAX_PROVISIONING_BODY_BYTES,
|
|
834
|
+
strict: true,
|
|
835
|
+
type: 'application/json',
|
|
836
|
+
verify: (request, _response, bytes) => {
|
|
837
|
+
request.publisherRuntimeTrustRawBody = Buffer.from(bytes);
|
|
838
|
+
},
|
|
839
|
+
}), async (request, response) => {
|
|
840
|
+
try {
|
|
841
|
+
if (request.originalUrl.includes('?') || request.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase() !== 'application/json') {
|
|
842
|
+
throw new Error('publisher_runtime_trust_request_invalid');
|
|
843
|
+
}
|
|
844
|
+
const body = parseProvisioningRequest(request.body);
|
|
845
|
+
const rawBody = request.publisherRuntimeTrustRawBody;
|
|
846
|
+
if (!rawBody || rawBody.toString('utf8') !== canonical(body)) {
|
|
847
|
+
throw new Error('publisher_runtime_trust_request_encoding_invalid');
|
|
848
|
+
}
|
|
849
|
+
const now = options.now?.() ?? Math.floor(Date.now() / 1000);
|
|
850
|
+
if (!positiveInteger(now))
|
|
851
|
+
throw new Error('publisher_runtime_trust_configuration_invalid');
|
|
852
|
+
// Signature and affinity verification remains mandatory after expiry. The
|
|
853
|
+
// durable store makes the freshness decision atomically with generation
|
|
854
|
+
// state so a crash after remote PREPARE cannot strand provisioning.
|
|
855
|
+
const [approval, execution, descriptor] = await Promise.all([
|
|
856
|
+
verifyPortalArtifact(body.cloud_approval_jws, 'cloud-approval', options.portalJwksResolver, now, clockSkewSeconds, true),
|
|
857
|
+
verifyPortalArtifact(body.execution_grant_jws, 'execution-grant', options.portalJwksResolver, now, clockSkewSeconds, true),
|
|
858
|
+
verifyPortalArtifact(body.deployment_descriptor_jws, 'runtime-deployment-descriptor', options.portalJwksResolver, now, clockSkewSeconds, true),
|
|
859
|
+
]);
|
|
860
|
+
const portalArtifactsExpired = [approval, execution, descriptor].some((artifact) => artifact.envelope.exp <= now);
|
|
861
|
+
assertPortalChain({
|
|
862
|
+
approval,
|
|
863
|
+
execution,
|
|
864
|
+
descriptor,
|
|
865
|
+
trust: body.runtime_dispatch_trust,
|
|
866
|
+
mcpAppId: options.mcpAppId,
|
|
867
|
+
provisioningUrl: options.provisioningUrl,
|
|
868
|
+
});
|
|
869
|
+
const approvalReceiptHash = canonicalHash(approval.envelope);
|
|
870
|
+
if (body.operation === 'ACTIVATE' &&
|
|
871
|
+
(body.runtime_dispatch_trust.affinity.runtimeApprovalReceiptHash !== approvalReceiptHash ||
|
|
872
|
+
body.runtime_dispatch_trust.affinity.runtimeAuthorizationEpoch !== execution.envelope.payload.authorizationEpoch)) {
|
|
873
|
+
throw new Error('publisher_runtime_trust_portal_chain_invalid');
|
|
874
|
+
}
|
|
875
|
+
const bodyWithoutProof = {
|
|
876
|
+
protocol_version: body.protocol_version,
|
|
877
|
+
operation: body.operation,
|
|
878
|
+
runtime_dispatch_trust: body.runtime_dispatch_trust,
|
|
879
|
+
cloud_approval_jws: body.cloud_approval_jws,
|
|
880
|
+
execution_grant_jws: body.execution_grant_jws,
|
|
881
|
+
deployment_descriptor_jws: body.deployment_descriptor_jws,
|
|
882
|
+
};
|
|
883
|
+
const proof = verifyHubProof({
|
|
884
|
+
compact: body.hub_proof_jws,
|
|
885
|
+
bodyWithoutProof,
|
|
886
|
+
trust: body.runtime_dispatch_trust,
|
|
887
|
+
provisioningUrl: options.provisioningUrl,
|
|
888
|
+
mcpAppId: options.mcpAppId,
|
|
889
|
+
now,
|
|
890
|
+
clockSkewSeconds,
|
|
891
|
+
});
|
|
892
|
+
const evidence = await options.store.apply({
|
|
893
|
+
operation: body.operation,
|
|
894
|
+
provisioningUrl: options.provisioningUrl,
|
|
895
|
+
mcpAppId: options.mcpAppId,
|
|
896
|
+
trust: body.runtime_dispatch_trust,
|
|
897
|
+
portalArtifactsHash: canonicalHash({
|
|
898
|
+
cloud_approval_jws: body.cloud_approval_jws,
|
|
899
|
+
execution_grant_jws: body.execution_grant_jws,
|
|
900
|
+
deployment_descriptor_jws: body.deployment_descriptor_jws,
|
|
901
|
+
}),
|
|
902
|
+
portalArtifactsExpired,
|
|
903
|
+
bodyDigest: proof.bodyDigest,
|
|
904
|
+
proof,
|
|
905
|
+
now,
|
|
906
|
+
});
|
|
907
|
+
response.status(200).type('application/json').send(canonical(evidence));
|
|
908
|
+
}
|
|
909
|
+
catch (error) {
|
|
910
|
+
const code = error instanceof Error ? error.message : 'publisher_runtime_trust_request_invalid';
|
|
911
|
+
const status = code.includes('signature') || code.includes('hub_proof') ? 401 :
|
|
912
|
+
code.includes('conflict') || code.includes('replay') ? 409 : 400;
|
|
913
|
+
response.status(status).json({ error: code });
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
return router;
|
|
917
|
+
}
|
|
918
|
+
//# sourceMappingURL=publisher-runtime-trust.js.map
|