@privos_ai/app-server 0.1.0 → 0.4.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.
- 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/platform-context.d.ts +43 -0
- package/dist/context/platform-context.d.ts.map +1 -0
- package/dist/context/platform-context.js +37 -0
- package/dist/context/platform-context.js.map +1 -0
- 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 +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -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
package/README.md
CHANGED
|
@@ -77,6 +77,195 @@ Your handler owns `tools/list`, `tools/call`, and custom resources.
|
|
|
77
77
|
import { verifyPrivosUser } from '@privos_ai/app-server/auth';
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
+
## Secretless workload identity
|
|
81
|
+
|
|
82
|
+
Production Cluster installs mount a per-installation Unix socket. The runtime creates an
|
|
83
|
+
ephemeral P-256 DPoP key in memory, attests through that socket, and refreshes a short-lived,
|
|
84
|
+
sender-constrained token without writing credentials to disk or environment variables.
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { getWorkloadIdentityClient } from '@privos_ai/app-server/workload';
|
|
88
|
+
|
|
89
|
+
const identity = getWorkloadIdentityClient();
|
|
90
|
+
const capabilities = await identity.getEffectiveCapabilities();
|
|
91
|
+
const stop = identity.onCapabilitiesChanged((next) => {
|
|
92
|
+
// Disable optional features when next.scopes no longer contains their scope.
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
identity.requireCapability('basic:information');
|
|
96
|
+
const response = await identity.authorizedRequest('/api/v1/mcp-apps.context', {
|
|
97
|
+
requiredScope: 'basic:information',
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`authorizedFetch` only sends authorization to the Hub origin supplied by the broker. It retries
|
|
102
|
+
one 401 automatically only for safe/idempotent methods; POST/PATCH require an explicit
|
|
103
|
+
`retryMode: 'idempotent'`. `authorizedRequest` maps 403 to `WorkloadPermissionDeniedError` so
|
|
104
|
+
optional-feature degradation is stable. Do not convert a workload token into a user identity;
|
|
105
|
+
user-delegated operations stay in the iframe host bridge.
|
|
106
|
+
|
|
107
|
+
Managed Cluster ingress uses the legacy workload boundary and should set
|
|
108
|
+
`workloadSecurity: 'required'`. Protocol-v3 `SELF_HOSTED_LOCAL` and
|
|
109
|
+
`PUBLISHER_HOSTED` runtimes must instead configure `runtimeDispatchV3`; the two
|
|
110
|
+
security modes are deliberately mutually exclusive.
|
|
111
|
+
|
|
112
|
+
## Protocol-v3 runtime dispatch
|
|
113
|
+
|
|
114
|
+
The v3 receiver verifies the final Hub-to-app boundary independently of the
|
|
115
|
+
managed workload broker. Direct HTTP accepts the assertion only in
|
|
116
|
+
`X-PrivOS-MCP-Dispatch-Assertion`. Relay accepts it only at
|
|
117
|
+
`params._meta.privosAuthorization`, reconstructs the exact signed logical RPC,
|
|
118
|
+
removes `privosAuthorization` and `privosUser`, and passes only the sanitized RPC
|
|
119
|
+
to the application handler.
|
|
120
|
+
|
|
121
|
+
Direct HTTP has no separate runtime-installation or room-binding wrapper
|
|
122
|
+
headers; both IDs are protected payload fields. Relay additionally carries
|
|
123
|
+
`runtimeInstallationId` and, for room work, `authorizationBindingId` inside the
|
|
124
|
+
reserved authorization wrapper, and the receiver cross-checks them before
|
|
125
|
+
consuming replay state.
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import {
|
|
129
|
+
BoundedRuntimeDispatchReplayConsumerV3,
|
|
130
|
+
createDirectRouter,
|
|
131
|
+
type RuntimeDispatchSecurityV3,
|
|
132
|
+
type RuntimeDispatchTrustHintV3,
|
|
133
|
+
type RuntimeDispatchTrustV3,
|
|
134
|
+
} from '@privos_ai/app-server';
|
|
135
|
+
|
|
136
|
+
// Load this from an authenticated provisioning/configuration channel. The
|
|
137
|
+
// assertion itself and its unverified resolver hint are never trust sources.
|
|
138
|
+
async function resolvePinnedTrust(
|
|
139
|
+
hint: RuntimeDispatchTrustHintV3,
|
|
140
|
+
): Promise<RuntimeDispatchTrustV3> {
|
|
141
|
+
return trustStore.loadExactActiveGeneration(hint);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const runtimeDispatchV3: RuntimeDispatchSecurityV3 = {
|
|
145
|
+
mode: 'required',
|
|
146
|
+
trust: resolvePinnedTrust,
|
|
147
|
+
replayConsumer: new BoundedRuntimeDispatchReplayConsumerV3(),
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
app.use(createDirectRouter({
|
|
151
|
+
descriptor,
|
|
152
|
+
handler,
|
|
153
|
+
runtimeDispatchV3,
|
|
154
|
+
}));
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The pinned trust record must contain the Hub P-256 public JWK and its RFC 7638
|
|
158
|
+
thumbprint `kid`, plus the exact workspace, deployment, app, execution mode,
|
|
159
|
+
generation, runtime installation, manifest digest, and resource-manifest hash.
|
|
160
|
+
Those stable fields exist before a local runtime starts. The signed assertion
|
|
161
|
+
always contains the runtime inventory hash, approval receipt hash, and current
|
|
162
|
+
authorization epoch; a dynamic publisher resolver may additionally supply any
|
|
163
|
+
of those three as exact expectations once its trusted control-plane state has
|
|
164
|
+
them. They are optional in static local startup trust because the runtime
|
|
165
|
+
inventory is finalized only after readiness evidence exists.
|
|
166
|
+
|
|
167
|
+
Use `parseRuntimeDispatchTrustV3Json` for strict JSON configuration parsing. It
|
|
168
|
+
rejects unknown keys, private or non-P-256 JWK material, a mismatched key
|
|
169
|
+
thumbprint, malformed affinity, and invalid optional expectations. A resolver
|
|
170
|
+
hint is attacker-controlled until signature verification completes; use it only
|
|
171
|
+
to locate a pre-provisioned record, then return that record for exact comparison.
|
|
172
|
+
|
|
173
|
+
`BoundedRuntimeDispatchReplayConsumerV3` is process-local and suitable only for
|
|
174
|
+
a single runtime process. A multi-process or multi-replica publisher must inject
|
|
175
|
+
a shared implementation whose `consume` operation atomically reserves both the
|
|
176
|
+
JTI and nonce until expiration. A full store fails closed; replay protection is
|
|
177
|
+
never silently disabled.
|
|
178
|
+
|
|
179
|
+
Local supervision or publisher connector provisioning must deliver the pinned
|
|
180
|
+
trust record out of band before executable traffic is admitted. The receiver
|
|
181
|
+
does not discover keys or affinity from a request and does not fall back to
|
|
182
|
+
legacy/unsigned authorization when v3 is configured. Treat a deployment as
|
|
183
|
+
blocked if its provisioning path does not supply this record (and, for multiple
|
|
184
|
+
replicas, a shared atomic replay consumer).
|
|
185
|
+
|
|
186
|
+
Publisher-hosted deployments can mount the production provisioning receiver:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
const store = new SingleProcessFilePublisherRuntimeTrustStoreV3({
|
|
190
|
+
filePath: '/var/lib/my-app/privos-runtime-trust.json',
|
|
191
|
+
deploymentMode: 'single-process',
|
|
192
|
+
});
|
|
193
|
+
const portalJwksResolver = createPinnedPortalJwksResolverV3({
|
|
194
|
+
issuer: 'portal:marketplace-broker',
|
|
195
|
+
jwksUrl: 'https://portal.privos.io/approval-jwks',
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Mount before any global JSON parser so canonical raw bytes and duplicate
|
|
199
|
+
// keys can be checked before proof verification.
|
|
200
|
+
app.use(createPublisherRuntimeTrustProvisioningRouterV3({
|
|
201
|
+
provisioningUrl: manifest.runtimeTrustProvisioningUrl,
|
|
202
|
+
mcpAppId: manifest.name,
|
|
203
|
+
portalJwksResolver,
|
|
204
|
+
store,
|
|
205
|
+
}));
|
|
206
|
+
|
|
207
|
+
const runtimeDispatchV3: RuntimeDispatchSecurityV3 = {
|
|
208
|
+
mode: 'required',
|
|
209
|
+
trust: (hint) => store.resolveDispatchTrust(hint),
|
|
210
|
+
replayConsumer: store,
|
|
211
|
+
};
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The receiver independently verifies canonical Portal Ed25519 approval,
|
|
215
|
+
execution-grant, and deployment-descriptor bytes, the exact artifact chain,
|
|
216
|
+
the approval-pinned Hub P-256 key, a current single-use Hub possession proof,
|
|
217
|
+
and the canonical request body before an atomic state transition. `PREPARE`
|
|
218
|
+
exposes stable trust only to the three exact signed readiness RPCs. `ACTIVATE`
|
|
219
|
+
pins the final inventory hash, approval hash, and authorization epoch; only then
|
|
220
|
+
does the normal dynamic resolver authorize application traffic. An exact
|
|
221
|
+
already-PREPARED generation may complete `ACTIVATE` after the Portal envelopes
|
|
222
|
+
expire, but a fresh expired `PREPARE` is denied.
|
|
223
|
+
|
|
224
|
+
`SingleProcessFilePublisherRuntimeTrustStoreV3` uses atomic file replacement and
|
|
225
|
+
durable replay state but intentionally supports exactly one Node process. It is
|
|
226
|
+
not safe for PM2 cluster mode, Kubernetes replicas, or multiple containers.
|
|
227
|
+
Those deployments must provide a transactional shared implementation of
|
|
228
|
+
`PublisherRuntimeTrustDurableStoreV3`; do not share the reference file over NFS.
|
|
229
|
+
|
|
230
|
+
The default scaffolder maps the local production surface to
|
|
231
|
+
`PRIVOS_RUNTIME_SECURITY_MODE=runtime-v3`, strict trust JSON in
|
|
232
|
+
`PRIVOS_RUNTIME_DISPATCH_TRUST_V3`, and the optional Direct-only local activation
|
|
233
|
+
probe switch `PRIVOS_RUNTIME_ALLOW_UNSIGNED_PREACTIVATION_READINESS=true`.
|
|
234
|
+
Publisher mode instead uses `PRIVOS_PUBLISHER_RUNTIME_TRUST_STORE_PATH`,
|
|
235
|
+
`PRIVOS_PORTAL_JWKS_URL`, `PRIVOS_PORTAL_ISSUER`, and the explicit
|
|
236
|
+
`PRIVOS_PUBLISHER_SINGLE_PROCESS=true` guard. Publisher readiness is always
|
|
237
|
+
signed and never enables the unsigned exception.
|
|
238
|
+
|
|
239
|
+
Local Direct pre-activation probing has one explicit, non-authorizing exception:
|
|
240
|
+
`unsignedReadiness: 'initialize-and-tools-list'`. It permits only these exact Hub
|
|
241
|
+
messages:
|
|
242
|
+
|
|
243
|
+
1. `initialize`, numeric ID `1`, protocol `2025-03-26`, the exact MCP UI
|
|
244
|
+
capability, and client `privos-hub/1.0.0`;
|
|
245
|
+
2. bodyless `notifications/initialized` with no ID or params;
|
|
246
|
+
3. `tools/list`, numeric ID `2`, and exactly empty params.
|
|
247
|
+
|
|
248
|
+
Any extra key, metadata, different ID, or changed nested value is denied.
|
|
249
|
+
`tools/call`, `resources/read`, and custom methods always require a valid
|
|
250
|
+
assertion. Relay never permits unsigned discovery. After activation, Hub sends
|
|
251
|
+
signed discovery requests as well.
|
|
252
|
+
|
|
253
|
+
Verified runtime authorization is exposed as the frozen
|
|
254
|
+
`ToolCallContext.runtimeAuthorization`. Room assertions include both the parent
|
|
255
|
+
`runtimeInstallationId` and child `authorizationBindingId`; any separately
|
|
256
|
+
verified actor JWT with a missing or different room is denied.
|
|
257
|
+
|
|
258
|
+
## Manifest v2 preflight
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
npx privos-app-lint ./privos-app.json
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
The command rejects mixed legacy/v2 permission declarations and prints deterministic
|
|
265
|
+
`canonicalManifestHash` and `publisherPermissionDeclarationHash` values. The latter covers the
|
|
266
|
+
publisher declaration only; Hub/Portal compute the authoritative permission contract hash with
|
|
267
|
+
the versioned server-owned catalog and immutable image digest.
|
|
268
|
+
|
|
80
269
|
## Scripts
|
|
81
270
|
|
|
82
271
|
- `npm run build` — emit `dist/` (JS + `.d.ts`)
|
package/dist/app-descriptor.d.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
export declare const MCP_PROTOCOL_VERSION = "2025-03-26";
|
|
2
2
|
export declare const MCP_UI_MIME = "text/html;profile=mcp-app";
|
|
3
|
+
export type AppPermissionRequirement = 'required' | 'optional';
|
|
4
|
+
export type AppPermissionContext = 'workspace' | 'room';
|
|
5
|
+
export type AppPermissionExecutionContext = 'user' | 'background' | 'both';
|
|
6
|
+
export interface AppPermissionDescriptor {
|
|
7
|
+
scope: string;
|
|
8
|
+
requirement: AppPermissionRequirement;
|
|
9
|
+
context: AppPermissionContext;
|
|
10
|
+
executionContext: AppPermissionExecutionContext;
|
|
11
|
+
feature: string;
|
|
12
|
+
reason: string;
|
|
13
|
+
recommended?: boolean;
|
|
14
|
+
degradedBehavior?: string;
|
|
15
|
+
}
|
|
3
16
|
export interface AppDescriptor {
|
|
4
17
|
id: string;
|
|
5
18
|
name: string;
|
|
@@ -13,6 +26,8 @@ export interface AppDescriptor {
|
|
|
13
26
|
website?: string;
|
|
14
27
|
};
|
|
15
28
|
scopes?: readonly string[];
|
|
29
|
+
/** Manifest v2 permission contract. Do not combine with legacy scopes. */
|
|
30
|
+
permissions?: readonly AppPermissionDescriptor[];
|
|
16
31
|
/** Direct manifest icon path/URL (e.g. `/public/icon.svg`). */
|
|
17
32
|
manifestIcon?: string;
|
|
18
33
|
/** Pairing / initialize icon (often a data URI for relay). */
|
|
@@ -21,6 +36,7 @@ export interface AppDescriptor {
|
|
|
21
36
|
capabilities?: Record<string, unknown>;
|
|
22
37
|
}
|
|
23
38
|
export declare function validateDescriptorCapabilities(capabilities: Record<string, unknown> | undefined): void;
|
|
39
|
+
export declare function validateDescriptorPermissions(descriptor: Pick<AppDescriptor, 'scopes' | 'permissions'>): void;
|
|
24
40
|
export declare function buildInitializeResult(descriptor: AppDescriptor, options?: {
|
|
25
41
|
uiEnabled?: boolean;
|
|
26
42
|
}): {
|
|
@@ -35,5 +51,6 @@ export declare function buildPairingMetadata(descriptor: AppDescriptor): {
|
|
|
35
51
|
version: string;
|
|
36
52
|
icon?: string;
|
|
37
53
|
scopes?: string[];
|
|
54
|
+
permissions?: AppPermissionDescriptor[];
|
|
38
55
|
};
|
|
39
56
|
//# sourceMappingURL=app-descriptor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-descriptor.d.ts","sourceRoot":"","sources":["../src/app-descriptor.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,oBAAoB,eAAe,CAAC;AAEjD,eAAO,MAAM,WAAW,8BAA8B,CAAC;AAEvD,MAAM,WAAW,aAAa;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5D,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,+DAA+D;IAC/D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8FAA8F;IAC9F,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAID,wBAAgB,8BAA8B,CAC7C,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAC/C,IAAI,CASN;AAED,wBAAgB,qBAAqB,CACpC,UAAU,EAAE,aAAa,EACzB,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,GAC/B;IACF,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC,
|
|
1
|
+
{"version":3,"file":"app-descriptor.d.ts","sourceRoot":"","sources":["../src/app-descriptor.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,oBAAoB,eAAe,CAAC;AAEjD,eAAO,MAAM,WAAW,8BAA8B,CAAC;AAEvD,MAAM,MAAM,wBAAwB,GAAG,UAAU,GAAG,UAAU,CAAC;AAC/D,MAAM,MAAM,oBAAoB,GAAG,WAAW,GAAG,MAAM,CAAC;AACxD,MAAM,MAAM,6BAA6B,GAAG,MAAM,GAAG,YAAY,GAAG,MAAM,CAAC;AAE3E,MAAM,WAAW,uBAAuB;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,wBAAwB,CAAC;IACtC,OAAO,EAAE,oBAAoB,CAAC;IAC9B,gBAAgB,EAAE,6BAA6B,CAAC;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5D,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,0EAA0E;IAC1E,WAAW,CAAC,EAAE,SAAS,uBAAuB,EAAE,CAAC;IACjD,+DAA+D;IAC/D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8FAA8F;IAC9F,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAID,wBAAgB,8BAA8B,CAC7C,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAC/C,IAAI,CASN;AAED,wBAAgB,6BAA6B,CAAC,UAAU,EAAE,IAAI,CAAC,aAAa,EAAE,QAAQ,GAAG,aAAa,CAAC,GAAG,IAAI,CA4B7G;AAED,wBAAgB,qBAAqB,CACpC,UAAU,EAAE,aAAa,EACzB,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,GAC/B;IACF,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC,CA+BA;AAED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAsBpF;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,aAAa,GAAG;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,WAAW,CAAC,EAAE,uBAAuB,EAAE,CAAC;CACxC,CAUA"}
|
package/dist/app-descriptor.js
CHANGED
|
@@ -10,8 +10,39 @@ export function validateDescriptorCapabilities(capabilities) {
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
+
export function validateDescriptorPermissions(descriptor) {
|
|
14
|
+
if (descriptor.scopes?.length && descriptor.permissions?.length) {
|
|
15
|
+
throw new Error('descriptor must not combine legacy scopes with permissions v2');
|
|
16
|
+
}
|
|
17
|
+
if (!descriptor.permissions)
|
|
18
|
+
return;
|
|
19
|
+
if (descriptor.permissions.length === 0)
|
|
20
|
+
throw new Error('descriptor.permissions must not be empty');
|
|
21
|
+
const seen = new Set();
|
|
22
|
+
for (const permission of descriptor.permissions) {
|
|
23
|
+
if (!permission ||
|
|
24
|
+
!/^[-a-z0-9]+:[-a-z0-9:]+$/.test(permission.scope) ||
|
|
25
|
+
!['required', 'optional'].includes(permission.requirement) ||
|
|
26
|
+
!['workspace', 'room'].includes(permission.context) ||
|
|
27
|
+
!['user', 'background', 'both'].includes(permission.executionContext) ||
|
|
28
|
+
!/^[-a-z0-9.]+$/.test(permission.feature) ||
|
|
29
|
+
permission.reason.trim().length < 3) {
|
|
30
|
+
throw new Error(`invalid permission declaration: ${permission?.scope || 'unknown'}`);
|
|
31
|
+
}
|
|
32
|
+
if (seen.has(permission.scope))
|
|
33
|
+
throw new Error(`duplicate permission scope: ${permission.scope}`);
|
|
34
|
+
if (permission.requirement === 'optional' && (!permission.degradedBehavior || permission.degradedBehavior.trim().length < 10)) {
|
|
35
|
+
throw new Error(`optional permission must declare safe degraded behavior: ${permission.scope}`);
|
|
36
|
+
}
|
|
37
|
+
if (permission.requirement === 'required' && permission.degradedBehavior) {
|
|
38
|
+
throw new Error(`required permission cannot declare degraded behavior: ${permission.scope}`);
|
|
39
|
+
}
|
|
40
|
+
seen.add(permission.scope);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
13
43
|
export function buildInitializeResult(descriptor, options) {
|
|
14
44
|
validateDescriptorCapabilities(descriptor.capabilities);
|
|
45
|
+
validateDescriptorPermissions(descriptor);
|
|
15
46
|
const capabilities = {
|
|
16
47
|
tools: {},
|
|
17
48
|
...(options?.uiEnabled
|
|
@@ -33,6 +64,8 @@ export function buildInitializeResult(descriptor, options) {
|
|
|
33
64
|
serverInfo.icon = descriptor.relayIcon;
|
|
34
65
|
if (descriptor.scopes?.length)
|
|
35
66
|
serverInfo.scopes = [...descriptor.scopes];
|
|
67
|
+
if (descriptor.permissions?.length)
|
|
68
|
+
serverInfo.permissions = descriptor.permissions.map((permission) => ({ ...permission }));
|
|
36
69
|
return {
|
|
37
70
|
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
38
71
|
capabilities,
|
|
@@ -40,7 +73,9 @@ export function buildInitializeResult(descriptor, options) {
|
|
|
40
73
|
};
|
|
41
74
|
}
|
|
42
75
|
export function buildManifestJson(descriptor) {
|
|
76
|
+
validateDescriptorPermissions(descriptor);
|
|
43
77
|
return {
|
|
78
|
+
schemaVersion: descriptor.permissions ? 2 : 1,
|
|
44
79
|
name: descriptor.id,
|
|
45
80
|
version: descriptor.version,
|
|
46
81
|
title: descriptor.title || descriptor.name,
|
|
@@ -57,15 +92,18 @@ export function buildManifestJson(descriptor) {
|
|
|
57
92
|
: {}),
|
|
58
93
|
...(descriptor.homepage ? { homepage: descriptor.homepage } : {}),
|
|
59
94
|
...(descriptor.scopes ? { scopes: [...descriptor.scopes] } : {}),
|
|
95
|
+
...(descriptor.permissions ? { permissions: descriptor.permissions.map((permission) => ({ ...permission })) } : {}),
|
|
60
96
|
};
|
|
61
97
|
}
|
|
62
98
|
export function buildPairingMetadata(descriptor) {
|
|
99
|
+
validateDescriptorPermissions(descriptor);
|
|
63
100
|
return {
|
|
64
101
|
name: descriptor.title || descriptor.name || descriptor.id,
|
|
65
102
|
description: descriptor.description ?? '',
|
|
66
103
|
version: descriptor.version,
|
|
67
104
|
...(descriptor.relayIcon ? { icon: descriptor.relayIcon } : {}),
|
|
68
105
|
...(descriptor.scopes?.length ? { scopes: [...descriptor.scopes] } : {}),
|
|
106
|
+
...(descriptor.permissions?.length ? { permissions: descriptor.permissions.map((permission) => ({ ...permission })) } : {}),
|
|
69
107
|
};
|
|
70
108
|
}
|
|
71
109
|
//# sourceMappingURL=app-descriptor.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-descriptor.js","sourceRoot":"","sources":["../src/app-descriptor.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,oBAAoB,GAAG,YAAY,CAAC;AAEjD,MAAM,CAAC,MAAM,WAAW,GAAG,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"app-descriptor.js","sourceRoot":"","sources":["../src/app-descriptor.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,oBAAoB,GAAG,YAAY,CAAC;AAEjD,MAAM,CAAC,MAAM,WAAW,GAAG,2BAA2B,CAAC;AAoCvD,MAAM,wBAAwB,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC;AAElE,MAAM,UAAU,8BAA8B,CAC7C,YAAiD;IAEjD,IAAI,CAAC,YAAY;QAAE,OAAO;IAC1B,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC;QAC7C,IAAI,wBAAwB,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACvC,MAAM,IAAI,KAAK,CACd,kEAAkE,GAAG,GAAG,CACxE,CAAC;QACH,CAAC;IACF,CAAC;AACF,CAAC;AAED,MAAM,UAAU,6BAA6B,CAAC,UAAyD;IACtG,IAAI,UAAU,CAAC,MAAM,EAAE,MAAM,IAAI,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,CAAC;QACjE,MAAM,IAAI,KAAK,CAAC,+DAA+D,CAAC,CAAC;IAClF,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,WAAW;QAAE,OAAO;IACpC,IAAI,UAAU,CAAC,WAAW,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;IACrG,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,KAAK,MAAM,UAAU,IAAI,UAAU,CAAC,WAAW,EAAE,CAAC;QACjD,IACC,CAAC,UAAU;YACX,CAAC,0BAA0B,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;YAClD,CAAC,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,WAAW,CAAC;YAC1D,CAAC,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC;YACnD,CAAC,CAAC,MAAM,EAAE,YAAY,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,gBAAgB,CAAC;YACrE,CAAC,eAAe,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;YACzC,UAAU,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAClC,CAAC;YACF,MAAM,IAAI,KAAK,CAAC,mCAAmC,UAAU,EAAE,KAAK,IAAI,SAAS,EAAE,CAAC,CAAC;QACtF,CAAC;QACD,IAAI,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,+BAA+B,UAAU,CAAC,KAAK,EAAE,CAAC,CAAC;QACnG,IAAI,UAAU,CAAC,WAAW,KAAK,UAAU,IAAI,CAAC,CAAC,UAAU,CAAC,gBAAgB,IAAI,UAAU,CAAC,gBAAgB,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,EAAE,CAAC,EAAE,CAAC;YAC/H,MAAM,IAAI,KAAK,CAAC,4DAA4D,UAAU,CAAC,KAAK,EAAE,CAAC,CAAC;QACjG,CAAC;QACD,IAAI,UAAU,CAAC,WAAW,KAAK,UAAU,IAAI,UAAU,CAAC,gBAAgB,EAAE,CAAC;YAC1E,MAAM,IAAI,KAAK,CAAC,yDAAyD,UAAU,CAAC,KAAK,EAAE,CAAC,CAAC;QAC9F,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;IAC5B,CAAC;AACF,CAAC;AAED,MAAM,UAAU,qBAAqB,CACpC,UAAyB,EACzB,OAAiC;IAMjC,8BAA8B,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;IACxD,6BAA6B,CAAC,UAAU,CAAC,CAAC;IAE1C,MAAM,YAAY,GAA4B;QAC7C,KAAK,EAAE,EAAE;QACT,GAAG,CAAC,OAAO,EAAE,SAAS;YACrB,CAAC,CAAC;gBACA,UAAU,EAAE;oBACX,4BAA4B,EAAE;wBAC7B,SAAS,EAAE,CAAC,WAAW,CAAC;qBACxB;iBACD;aACD;YACF,CAAC,CAAC,EAAE,CAAC;QACN,GAAG,CAAC,UAAU,CAAC,YAAY,IAAI,EAAE,CAAC;KAClC,CAAC;IAEF,MAAM,UAAU,GAA4B;QAC3C,IAAI,EAAE,UAAU,CAAC,KAAK,IAAI,UAAU,CAAC,IAAI,IAAI,UAAU,CAAC,EAAE;QAC1D,OAAO,EAAE,UAAU,CAAC,OAAO;KAC3B,CAAC;IACF,IAAI,UAAU,CAAC,SAAS;QAAE,UAAU,CAAC,IAAI,GAAG,UAAU,CAAC,SAAS,CAAC;IACjE,IAAI,UAAU,CAAC,MAAM,EAAE,MAAM;QAAE,UAAU,CAAC,MAAM,GAAG,CAAC,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;IAC1E,IAAI,UAAU,CAAC,WAAW,EAAE,MAAM;QAAE,UAAU,CAAC,WAAW,GAAG,UAAU,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,UAAU,EAAE,CAAC,CAAC,CAAC;IAE7H,OAAO;QACN,eAAe,EAAE,oBAAoB;QACrC,YAAY;QACZ,UAAU;KACV,CAAC;AACH,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,UAAyB;IAC1D,6BAA6B,CAAC,UAAU,CAAC,CAAC;IAC1C,OAAO;QACN,aAAa,EAAE,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7C,IAAI,EAAE,UAAU,CAAC,EAAE;QACnB,OAAO,EAAE,UAAU,CAAC,OAAO;QAC3B,KAAK,EAAE,UAAU,CAAC,KAAK,IAAI,UAAU,CAAC,IAAI;QAC1C,WAAW,EAAE,UAAU,CAAC,WAAW,IAAI,EAAE;QACzC,GAAG,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACrE,GAAG,CAAC,UAAU,CAAC,MAAM;YACpB,CAAC,CAAC;gBACA,MAAM,EAAE;oBACP,IAAI,EAAE,UAAU,CAAC,MAAM,CAAC,IAAI;oBAC5B,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,UAAU,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;oBACtE,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,UAAU,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;iBAC5E;aACD;YACF,CAAC,CAAC,EAAE,CAAC;QACN,GAAG,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,UAAU,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjE,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,GAAG,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChE,GAAG,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,UAAU,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,UAAU,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACnH,CAAC;AACH,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,UAAyB;IAQ7D,6BAA6B,CAAC,UAAU,CAAC,CAAC;IAC1C,OAAO;QACN,IAAI,EAAE,UAAU,CAAC,KAAK,IAAI,UAAU,CAAC,IAAI,IAAI,UAAU,CAAC,EAAE;QAC1D,WAAW,EAAE,UAAU,CAAC,WAAW,IAAI,EAAE;QACzC,OAAO,EAAE,UAAU,CAAC,OAAO;QAC3B,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/D,GAAG,CAAC,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,GAAG,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACxE,GAAG,CAAC,UAAU,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,UAAU,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,UAAU,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC3H,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the platform tells an app about itself.
|
|
3
|
+
*
|
|
4
|
+
* A PrivOS-managed runtime receives two variables the platform owns. The
|
|
5
|
+
* `PRIVOS_` prefix is reserved end to end — the Portal refuses a manifest that
|
|
6
|
+
* declares it, the Hub refuses to send it, and the Cluster refuses a grant that
|
|
7
|
+
* carries it — so a value read here can only have come from the platform.
|
|
8
|
+
*/
|
|
9
|
+
export type AppAccessMode = 'managed-runtime' | 'publisher-hosted';
|
|
10
|
+
export interface PlatformContext {
|
|
11
|
+
/**
|
|
12
|
+
* This app's own public origin, e.g. `https://my-app.apps.privos.link`.
|
|
13
|
+
*
|
|
14
|
+
* This is NOT where tool calls and interface requests arrive: those ride the
|
|
15
|
+
* signed broker dispatch on `/mcp`, and reaching the app any other way is
|
|
16
|
+
* refused. Use the public origin for what genuinely has to be reachable from
|
|
17
|
+
* outside — public static media, webhook callbacks, OAuth redirect URIs.
|
|
18
|
+
*
|
|
19
|
+
* `undefined` when the app runs somewhere that does not allocate one, or on
|
|
20
|
+
* a platform release that predates the variable.
|
|
21
|
+
*/
|
|
22
|
+
publicUrl?: string;
|
|
23
|
+
/**
|
|
24
|
+
* `managed-runtime` when PrivOS operates this container behind the broker,
|
|
25
|
+
* `publisher-hosted` when the publisher runs the endpoint themselves.
|
|
26
|
+
* `undefined` on a platform release that predates the variable.
|
|
27
|
+
*/
|
|
28
|
+
accessMode?: AppAccessMode;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Read the platform-injected context. Undefined-safe by design: an app built
|
|
32
|
+
* against this SDK must still run on an older platform, and on a publisher's
|
|
33
|
+
* own infrastructure where nothing is injected at all.
|
|
34
|
+
*/
|
|
35
|
+
export declare function getPlatformContext(env?: NodeJS.ProcessEnv): PlatformContext;
|
|
36
|
+
/**
|
|
37
|
+
* Absolute URL for a path on this app's public origin, or `undefined` when no
|
|
38
|
+
* public origin was injected. Prefer this over string concatenation so a
|
|
39
|
+
* missing origin is a visible `undefined` rather than a relative URL that
|
|
40
|
+
* silently resolves against the wrong host.
|
|
41
|
+
*/
|
|
42
|
+
export declare function publicUrlFor(path: string, env?: NodeJS.ProcessEnv): string | undefined;
|
|
43
|
+
//# sourceMappingURL=platform-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"platform-context.d.ts","sourceRoot":"","sources":["../../src/context/platform-context.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,MAAM,aAAa,GAAG,iBAAiB,GAAG,kBAAkB,CAAC;AAEnE,MAAM,WAAW,eAAe;IAC/B;;;;;;;;;;OAUG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,UAAU,CAAC,EAAE,aAAa,CAAC;CAC3B;AAKD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,eAAe,CASxF;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,GAAG,SAAS,CAInG"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the platform tells an app about itself.
|
|
3
|
+
*
|
|
4
|
+
* A PrivOS-managed runtime receives two variables the platform owns. The
|
|
5
|
+
* `PRIVOS_` prefix is reserved end to end — the Portal refuses a manifest that
|
|
6
|
+
* declares it, the Hub refuses to send it, and the Cluster refuses a grant that
|
|
7
|
+
* carries it — so a value read here can only have come from the platform.
|
|
8
|
+
*/
|
|
9
|
+
const isAccessMode = (value) => value === 'managed-runtime' || value === 'publisher-hosted';
|
|
10
|
+
/**
|
|
11
|
+
* Read the platform-injected context. Undefined-safe by design: an app built
|
|
12
|
+
* against this SDK must still run on an older platform, and on a publisher's
|
|
13
|
+
* own infrastructure where nothing is injected at all.
|
|
14
|
+
*/
|
|
15
|
+
export function getPlatformContext(env = process.env) {
|
|
16
|
+
const publicUrl = env.PRIVOS_PUBLIC_URL;
|
|
17
|
+
const accessMode = env.PRIVOS_ACCESS_MODE;
|
|
18
|
+
return {
|
|
19
|
+
// A malformed value is treated as absent rather than passed on: an app
|
|
20
|
+
// that builds a callback URL from it would otherwise emit a broken one.
|
|
21
|
+
...(typeof publicUrl === 'string' && publicUrl.startsWith('https://') ? { publicUrl } : {}),
|
|
22
|
+
...(isAccessMode(accessMode) ? { accessMode } : {}),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Absolute URL for a path on this app's public origin, or `undefined` when no
|
|
27
|
+
* public origin was injected. Prefer this over string concatenation so a
|
|
28
|
+
* missing origin is a visible `undefined` rather than a relative URL that
|
|
29
|
+
* silently resolves against the wrong host.
|
|
30
|
+
*/
|
|
31
|
+
export function publicUrlFor(path, env = process.env) {
|
|
32
|
+
const { publicUrl } = getPlatformContext(env);
|
|
33
|
+
if (!publicUrl)
|
|
34
|
+
return undefined;
|
|
35
|
+
return new URL(path, publicUrl.endsWith('/') ? publicUrl : `${publicUrl}/`).toString();
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=platform-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"platform-context.js","sourceRoot":"","sources":["../../src/context/platform-context.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAyBH,MAAM,YAAY,GAAG,CAAC,KAAyB,EAA0B,EAAE,CAC1E,KAAK,KAAK,iBAAiB,IAAI,KAAK,KAAK,kBAAkB,CAAC;AAE7D;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAyB,OAAO,CAAC,GAAG;IACtE,MAAM,SAAS,GAAG,GAAG,CAAC,iBAAiB,CAAC;IACxC,MAAM,UAAU,GAAG,GAAG,CAAC,kBAAkB,CAAC;IAC1C,OAAO;QACN,uEAAuE;QACvE,wEAAwE;QACxE,GAAG,CAAC,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3F,GAAG,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACnD,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,MAAyB,OAAO,CAAC,GAAG;IAC9E,MAAM,EAAE,SAAS,EAAE,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC;IAC9C,IAAI,CAAC,SAAS;QAAE,OAAO,SAAS,CAAC;IACjC,OAAO,IAAI,GAAG,CAAC,IAAI,EAAE,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,SAAS,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC;AACxF,CAAC"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { VerifiedRuntimeDispatchAssertionV3 } from '../workload/dispatch-assertion.js';
|
|
1
2
|
export interface VerifiedActor {
|
|
2
3
|
/** JWT `sub` — stable opaque user id. */
|
|
3
4
|
userId: string;
|
|
@@ -17,6 +18,8 @@ export interface ToolCallContext {
|
|
|
17
18
|
appId?: string;
|
|
18
19
|
traceId?: string;
|
|
19
20
|
identityState: IdentityState;
|
|
21
|
+
/** Immutable Hub runtime authorization verified at the final HTTP/Relay boundary. */
|
|
22
|
+
readonly runtimeAuthorization?: VerifiedRuntimeDispatchAssertionV3;
|
|
20
23
|
/**
|
|
21
24
|
* Scope for duplicate in-flight JSON-RPC id detection.
|
|
22
25
|
* Direct: MCP-Session-Id or per-request ephemeral id.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-call-context.d.ts","sourceRoot":"","sources":["../../src/context/tool-call-context.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC7B,yCAAyC;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8BAA8B;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC1C;AAED,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;AAE/D,MAAM,WAAW,eAAe;IAC/B,SAAS,EAAE,QAAQ,GAAG,OAAO,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACnC,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,aAAa,CAAC;IAC7B;;;;OAIG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB,2FAA2F;IAC3F,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB"}
|
|
1
|
+
{"version":3,"file":"tool-call-context.d.ts","sourceRoot":"","sources":["../../src/context/tool-call-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kCAAkC,EAAE,MAAM,mCAAmC,CAAC;AAE5F,MAAM,WAAW,aAAa;IAC7B,yCAAyC;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8BAA8B;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC1C;AAED,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;AAE/D,MAAM,WAAW,eAAe;IAC/B,SAAS,EAAE,QAAQ,GAAG,OAAO,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACnC,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,aAAa,CAAC;IAC7B,qFAAqF;IACrF,QAAQ,CAAC,oBAAoB,CAAC,EAAE,kCAAkC,CAAC;IACnE;;;;OAIG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB,2FAA2F;IAC3F,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB"}
|
|
@@ -2,6 +2,7 @@ import { Router, type Request } from 'express';
|
|
|
2
2
|
import { type AppDescriptor } from '../app-descriptor.js';
|
|
3
3
|
import type { AuthOptions } from '../auth/user-token.js';
|
|
4
4
|
import { AppServerRuntime, extractDirectCallerCredential, type AppErrorMapper, type AppMcpHandler, type AppServerRuntimeOptions, type CallerCredentialExtractor, type RuntimeLimits, type UiResourceProvider } from '../runtime.js';
|
|
5
|
+
import { type RuntimeDispatchSecurityV3 } from '../workload/dispatch-assertion.js';
|
|
5
6
|
export interface DirectRouterOptions {
|
|
6
7
|
descriptor: AppDescriptor | (() => AppDescriptor | Promise<AppDescriptor>);
|
|
7
8
|
handler: AppMcpHandler;
|
|
@@ -24,6 +25,10 @@ export interface DirectRouterOptions {
|
|
|
24
25
|
headers: Request['headers'];
|
|
25
26
|
}>;
|
|
26
27
|
runtime?: AppServerRuntime;
|
|
28
|
+
/** Require Hub dispatch assertions when the Cluster identity socket is mounted. */
|
|
29
|
+
workloadSecurity?: 'auto' | 'required' | 'disabled';
|
|
30
|
+
/** Explicit required final-boundary authorization for local or publisher protocol-v3 runtimes. */
|
|
31
|
+
runtimeDispatchV3?: RuntimeDispatchSecurityV3;
|
|
27
32
|
}
|
|
28
33
|
/**
|
|
29
34
|
* Composable Express Router for Direct MCP transport.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"express-router.d.ts","sourceRoot":"","sources":["../../src/direct/express-router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,OAAO,EAAoC,MAAM,SAAS,CAAC;AAGjF,OAAO,EAEN,KAAK,aAAa,EAClB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAOzD,OAAO,EACN,gBAAgB,EAGhB,6BAA6B,EAE7B,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,uBAAuB,EAC5B,KAAK,yBAAyB,EAC9B,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,MAAM,eAAe,CAAC;AAEvB,MAAM,WAAW,mBAAmB;IACnC,UAAU,EAAE,aAAa,GAAG,CAAC,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC;IAC3E,OAAO,EAAE,aAAa,CAAC;IACvB,EAAE,CAAC,EAAE,kBAAkB,CAAC;IACxB,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,WAAW,CAAC,EAAE,cAAc,CAAC;IAC7B,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,MAAM,CAAC,EAAE,uBAAuB,CAAC,QAAQ,CAAC,CAAC;IAC3C,sBAAsB;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gDAAgD;IAChD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,oEAAoE;IACpE,uBAAuB,CAAC,EAAE,yBAAyB,CAAC;QACnD,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;KAC5B,CAAC,CAAC;IACH,OAAO,CAAC,EAAE,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"express-router.d.ts","sourceRoot":"","sources":["../../src/direct/express-router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,OAAO,EAAoC,MAAM,SAAS,CAAC;AAGjF,OAAO,EAEN,KAAK,aAAa,EAClB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAOzD,OAAO,EACN,gBAAgB,EAGhB,6BAA6B,EAE7B,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,uBAAuB,EAC5B,KAAK,yBAAyB,EAC9B,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,MAAM,eAAe,CAAC;AAEvB,OAAO,EAIN,KAAK,yBAAyB,EAE9B,MAAM,mCAAmC,CAAC;AAE3C,MAAM,WAAW,mBAAmB;IACnC,UAAU,EAAE,aAAa,GAAG,CAAC,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC;IAC3E,OAAO,EAAE,aAAa,CAAC;IACvB,EAAE,CAAC,EAAE,kBAAkB,CAAC;IACxB,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,WAAW,CAAC,EAAE,cAAc,CAAC;IAC7B,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,MAAM,CAAC,EAAE,uBAAuB,CAAC,QAAQ,CAAC,CAAC;IAC3C,sBAAsB;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gDAAgD;IAChD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,oEAAoE;IACpE,uBAAuB,CAAC,EAAE,yBAAyB,CAAC;QACnD,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;KAC5B,CAAC,CAAC;IACH,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,mFAAmF;IACnF,gBAAgB,CAAC,EAAE,MAAM,GAAG,UAAU,GAAG,UAAU,CAAC;IACpD,kGAAkG;IAClG,iBAAiB,CAAC,EAAE,yBAAyB,CAAC;CAC9C;AAYD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,CAsFvE;AAuKD,OAAO,EAAE,gBAAgB,EAAE,6BAA6B,EAAE,CAAC"}
|
|
@@ -3,12 +3,16 @@ import express from 'express';
|
|
|
3
3
|
import { buildManifestJson, } from '../app-descriptor.js';
|
|
4
4
|
import { INVALID_REQUEST, PARSE_ERROR, errorResponse, jsonRpcError, } from '../protocol/errors.js';
|
|
5
5
|
import { AppServerRuntime, DEFAULT_MAX_MESSAGE_BYTES, ephemeralSessionScope, extractDirectCallerCredential, resolveCallerCredential, } from '../runtime.js';
|
|
6
|
+
import { getWorkloadIdentityClient } from '../workload/workload-identity.js';
|
|
7
|
+
import { isUnsignedRuntimeReadinessRpcV3, verifyDispatchAssertion, verifyRuntimeDispatchAssertionV3, } from '../workload/dispatch-assertion.js';
|
|
6
8
|
const REQUIRED_CORS_HEADERS = [
|
|
7
9
|
'Content-Type',
|
|
8
10
|
'Authorization',
|
|
9
11
|
'X-MCP-User-Id',
|
|
10
12
|
'Mcp-Session-Id',
|
|
11
13
|
'MCP-Protocol-Version',
|
|
14
|
+
'X-PrivOS-Dispatch-Assertion',
|
|
15
|
+
'X-PrivOS-MCP-Dispatch-Assertion',
|
|
12
16
|
];
|
|
13
17
|
/**
|
|
14
18
|
* Composable Express Router for Direct MCP transport.
|
|
@@ -27,6 +31,12 @@ export function createDirectRouter(options) {
|
|
|
27
31
|
logger: options.logger,
|
|
28
32
|
});
|
|
29
33
|
const mcpPath = options.mcpPath ?? '/mcp';
|
|
34
|
+
if (options.runtimeDispatchV3 && mcpPath !== '/mcp') {
|
|
35
|
+
throw new Error('Protocol-v3 runtime dispatch requires the canonical /mcp path.');
|
|
36
|
+
}
|
|
37
|
+
if (options.runtimeDispatchV3 && options.workloadSecurity === 'required') {
|
|
38
|
+
throw new Error('Legacy managed workload security and runtime-v3 security cannot both be required.');
|
|
39
|
+
}
|
|
30
40
|
const manifestPath = options.manifestPath ?? '/.well-known/mcp/manifest.json';
|
|
31
41
|
const allowHeaders = mergeCorsHeaders(options.corsAllowHeaders);
|
|
32
42
|
const maxBytes = runtime.getLimits().maxMessageBytes || DEFAULT_MAX_MESSAGE_BYTES;
|
|
@@ -82,25 +92,94 @@ export function createDirectRouter(options) {
|
|
|
82
92
|
return router;
|
|
83
93
|
}
|
|
84
94
|
async function handleMcpPost(req, res, runtime, options) {
|
|
85
|
-
const extract = options.extractCallerCredential ??
|
|
86
|
-
((ingress) => extractDirectCallerCredential(ingress.headers));
|
|
87
|
-
const credentialResolution = await resolveCallerCredential(extract, {
|
|
88
|
-
headers: req.headers,
|
|
89
|
-
});
|
|
90
95
|
const body = req.body;
|
|
91
|
-
const
|
|
96
|
+
const candidateRequestId = body && typeof body === 'object' && Object.prototype.hasOwnProperty.call(body, 'id')
|
|
92
97
|
? body.id
|
|
93
98
|
: undefined;
|
|
99
|
+
const requestId = candidateRequestId === null ||
|
|
100
|
+
typeof candidateRequestId === 'string' ||
|
|
101
|
+
typeof candidateRequestId === 'number'
|
|
102
|
+
? candidateRequestId
|
|
103
|
+
: undefined;
|
|
104
|
+
let runtimeAuthorization;
|
|
105
|
+
const legacyAssertionHeaders = rawHeaderValues(req, 'x-privos-dispatch-assertion');
|
|
106
|
+
const runtimeAssertionHeaders = rawHeaderValues(req, 'x-privos-mcp-dispatch-assertion');
|
|
107
|
+
const unsupportedRuntimeAffinityHeaders = [
|
|
108
|
+
...rawHeaderValues(req, 'x-privos-mcp-runtime-installation-id'),
|
|
109
|
+
...rawHeaderValues(req, 'x-privos-mcp-authorization-binding-id'),
|
|
110
|
+
];
|
|
111
|
+
if (unsupportedRuntimeAffinityHeaders.length > 0) {
|
|
112
|
+
denyDispatch(res, requestId);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (options.runtimeDispatchV3) {
|
|
116
|
+
try {
|
|
117
|
+
if (legacyAssertionHeaders.length > 0 || runtimeAssertionHeaders.length > 1) {
|
|
118
|
+
throw new Error('dispatch_assertion_ambiguous');
|
|
119
|
+
}
|
|
120
|
+
if (runtimeAssertionHeaders.length === 0) {
|
|
121
|
+
if (!isUnsignedRuntimeReadinessRpcV3(body, options.runtimeDispatchV3)) {
|
|
122
|
+
throw new Error('dispatch_assertion_missing');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
runtimeAuthorization = await verifyRuntimeDispatchAssertionV3({
|
|
127
|
+
compact: runtimeAssertionHeaders[0],
|
|
128
|
+
body,
|
|
129
|
+
security: options.runtimeDispatchV3,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
denyDispatch(res, requestId);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
if (runtimeAssertionHeaders.length > 0) {
|
|
140
|
+
denyDispatch(res, requestId);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const workloadClient = getWorkloadIdentityClient();
|
|
144
|
+
const workloadSecurity = options.workloadSecurity ?? 'auto';
|
|
145
|
+
const requireDispatchAssertion = workloadSecurity === 'required' || (workloadSecurity === 'auto' && workloadClient.isAvailable());
|
|
146
|
+
if (requireDispatchAssertion) {
|
|
147
|
+
try {
|
|
148
|
+
const assertion = headerValue(req.headers['x-privos-dispatch-assertion']);
|
|
149
|
+
if (!assertion)
|
|
150
|
+
throw new Error('dispatch_assertion_missing');
|
|
151
|
+
verifyDispatchAssertion({ compact: assertion, body, context: await workloadClient.brokerContext() });
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
denyDispatch(res, requestId);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const extract = options.extractCallerCredential ??
|
|
160
|
+
((ingress) => extractDirectCallerCredential(ingress.headers));
|
|
161
|
+
const credentialResolution = await resolveCallerCredential(extract, { headers: req.headers });
|
|
94
162
|
const sessionHeader = headerValue(req.headers['mcp-session-id']);
|
|
95
163
|
const sessionScope = sessionHeader?.trim()
|
|
96
164
|
? `mcp-session:${sessionHeader.trim()}`
|
|
97
165
|
: ephemeralSessionScope('direct');
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
166
|
+
let context;
|
|
167
|
+
try {
|
|
168
|
+
context = await runtime.buildContext({
|
|
169
|
+
transport: 'direct',
|
|
170
|
+
requestId,
|
|
171
|
+
sessionScope,
|
|
172
|
+
credentialResolution,
|
|
173
|
+
...(runtimeAuthorization ? { runtimeAuthorization } : {}),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
if (runtimeAuthorization && error instanceof Error && error.message === 'dispatch_assertion_binding_mismatch') {
|
|
178
|
+
denyDispatch(res, requestId);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
104
183
|
const outcome = await runtime.dispatchObject(body, context);
|
|
105
184
|
if (outcome.type === 'no_response' || outcome.type === 'protocol_warning') {
|
|
106
185
|
res.status(202).end();
|
|
@@ -133,6 +212,19 @@ function headerValue(value) {
|
|
|
133
212
|
return value[0];
|
|
134
213
|
return value;
|
|
135
214
|
}
|
|
215
|
+
function rawHeaderValues(req, name) {
|
|
216
|
+
const values = [];
|
|
217
|
+
for (let index = 0; index < req.rawHeaders.length; index += 2) {
|
|
218
|
+
if (req.rawHeaders[index]?.toLowerCase() === name)
|
|
219
|
+
values.push(req.rawHeaders[index + 1] ?? '');
|
|
220
|
+
}
|
|
221
|
+
return values;
|
|
222
|
+
}
|
|
223
|
+
function denyDispatch(res, requestId) {
|
|
224
|
+
res.status(403).json(errorResponse(requestId ?? null, jsonRpcError(INVALID_REQUEST, 'Authenticated private dispatch required', {
|
|
225
|
+
code: 'DISPATCH_ASSERTION_INVALID',
|
|
226
|
+
})));
|
|
227
|
+
}
|
|
136
228
|
function mergeCorsHeaders(extra) {
|
|
137
229
|
const set = new Set(REQUIRED_CORS_HEADERS.map((h) => h.toLowerCase()));
|
|
138
230
|
const ordered = [...REQUIRED_CORS_HEADERS];
|