@stigmer/server 3.18.0 → 3.18.1-dev.20260919070736
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 +1 -1
- package/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +42 -6
- package/dist/boot/compose.js.map +1 -1
- package/dist/boot/config.d.ts +6 -2
- package/dist/boot/config.d.ts.map +1 -1
- package/dist/boot/config.js.map +1 -1
- package/dist/boot/oauth-redirect-uri.d.ts +54 -0
- package/dist/boot/oauth-redirect-uri.d.ts.map +1 -0
- package/dist/boot/oauth-redirect-uri.js +37 -0
- package/dist/boot/oauth-redirect-uri.js.map +1 -0
- package/dist/domain/mcpserver/complete-endpoint-auth.d.ts +16 -0
- package/dist/domain/mcpserver/complete-endpoint-auth.d.ts.map +1 -0
- package/dist/domain/mcpserver/complete-endpoint-auth.js +260 -0
- package/dist/domain/mcpserver/complete-endpoint-auth.js.map +1 -0
- package/dist/domain/mcpserver/complete-oauth-connect.d.ts.map +1 -1
- package/dist/domain/mcpserver/complete-oauth-connect.js +1 -1
- package/dist/domain/mcpserver/complete-oauth-connect.js.map +1 -1
- package/dist/domain/mcpserver/connect.d.ts +25 -0
- package/dist/domain/mcpserver/connect.d.ts.map +1 -1
- package/dist/domain/mcpserver/connect.js +8 -23
- package/dist/domain/mcpserver/connect.js.map +1 -1
- package/dist/domain/mcpserver/controller.d.ts.map +1 -1
- package/dist/domain/mcpserver/controller.js +12 -0
- package/dist/domain/mcpserver/controller.js.map +1 -1
- package/dist/domain/mcpserver/initiate-oauth-connect.d.ts.map +1 -1
- package/dist/domain/mcpserver/initiate-oauth-connect.js +16 -13
- package/dist/domain/mcpserver/initiate-oauth-connect.js.map +1 -1
- package/dist/domain/mcpserver/oauth/dcr.d.ts +10 -1
- package/dist/domain/mcpserver/oauth/dcr.d.ts.map +1 -1
- package/dist/domain/mcpserver/oauth/dcr.js +1 -9
- package/dist/domain/mcpserver/oauth/dcr.js.map +1 -1
- package/dist/domain/mcpserver/oauth/discovery.d.ts +43 -20
- package/dist/domain/mcpserver/oauth/discovery.d.ts.map +1 -1
- package/dist/domain/mcpserver/oauth/discovery.js +69 -54
- package/dist/domain/mcpserver/oauth/discovery.js.map +1 -1
- package/dist/domain/mcpserver/oauth/preflight.d.ts +27 -1
- package/dist/domain/mcpserver/oauth/preflight.d.ts.map +1 -1
- package/dist/domain/mcpserver/oauth/preflight.js +1 -26
- package/dist/domain/mcpserver/oauth/preflight.js.map +1 -1
- package/dist/domain/mcpserver/oauth/refresh.d.ts +2 -1
- package/dist/domain/mcpserver/oauth/refresh.d.ts.map +1 -1
- package/dist/domain/mcpserver/oauth/refresh.js +1 -1
- package/dist/domain/mcpserver/oauth/refresh.js.map +1 -1
- package/dist/domain/mcpserver/oauth/token.d.ts +9 -2
- package/dist/domain/mcpserver/oauth/token.d.ts.map +1 -1
- package/dist/domain/mcpserver/oauth/token.js +2 -8
- package/dist/domain/mcpserver/oauth/token.js.map +1 -1
- package/dist/extensions/drivers.d.ts +17 -0
- package/dist/extensions/drivers.d.ts.map +1 -1
- package/dist/extensions/outbound-egress.d.ts +49 -0
- package/dist/extensions/outbound-egress.d.ts.map +1 -0
- package/dist/extensions/outbound-egress.js +51 -0
- package/dist/extensions/outbound-egress.js.map +1 -0
- package/dist/extensions/registry.d.ts +6 -0
- package/dist/extensions/registry.d.ts.map +1 -1
- package/dist/extensions/registry.js +10 -0
- package/dist/extensions/registry.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/pipeline/apiresource-labels.d.ts +14 -0
- package/dist/pipeline/apiresource-labels.d.ts.map +1 -1
- package/dist/pipeline/apiresource-labels.js +14 -0
- package/dist/pipeline/apiresource-labels.js.map +1 -1
- package/package.json +6 -5
- package/src/boot/__tests__/oauth-redirect-uri.test.ts +38 -0
- package/src/boot/compose.ts +47 -8
- package/src/boot/config.ts +6 -2
- package/src/boot/oauth-redirect-uri.ts +53 -0
- package/src/domain/mcpserver/__tests__/complete-endpoint-auth.test.ts +270 -0
- package/src/domain/mcpserver/__tests__/connect-authorization.test.ts +3 -0
- package/src/domain/mcpserver/__tests__/connect.test.ts +31 -0
- package/src/domain/mcpserver/complete-endpoint-auth.ts +277 -0
- package/src/domain/mcpserver/complete-oauth-connect.ts +1 -0
- package/src/domain/mcpserver/connect.ts +21 -15
- package/src/domain/mcpserver/controller.ts +16 -0
- package/src/domain/mcpserver/initiate-oauth-connect.ts +16 -12
- package/src/domain/mcpserver/oauth/__tests__/discovery.test.ts +120 -91
- package/src/domain/mcpserver/oauth/__tests__/token-refresh.test.ts +3 -1
- package/src/domain/mcpserver/oauth/dcr.ts +2 -1
- package/src/domain/mcpserver/oauth/discovery.ts +120 -89
- package/src/domain/mcpserver/oauth/preflight.ts +2 -1
- package/src/domain/mcpserver/oauth/refresh.ts +2 -1
- package/src/domain/mcpserver/oauth/token.ts +4 -3
- package/src/extensions/__tests__/outbound-egress-points.test.ts +58 -0
- package/src/extensions/drivers.ts +17 -0
- package/src/extensions/outbound-egress.ts +55 -0
- package/src/extensions/registry.ts +19 -0
- package/src/index.ts +10 -0
- package/src/pipeline/apiresource-labels.ts +16 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the outboundEgress driver point in the single-instance shape the
|
|
3
|
+
* registry already enforces for policyGrantScope and licenseStatus:
|
|
4
|
+
*
|
|
5
|
+
* - `drivers.outboundEgress` — the address policy the composition root
|
|
6
|
+
* builds the McpServer slice's one guarded fetch from; single instance;
|
|
7
|
+
* absent = open source's relaxed posture installs at the compose.ts
|
|
8
|
+
* consumption site.
|
|
9
|
+
*
|
|
10
|
+
* And the two exported postures: relaxed refuses only the link-local
|
|
11
|
+
* (cloud metadata) range, strict refuses loopback, private and unspecified
|
|
12
|
+
* too, and neither owns a copy of the ranges (both read the shared table).
|
|
13
|
+
*/
|
|
14
|
+
import { describe, expect, it } from "vitest";
|
|
15
|
+
|
|
16
|
+
import { relaxedEgressPolicy, strictEgressPolicy } from "../outbound-egress.js";
|
|
17
|
+
import { resolveExtensions } from "../registry.js";
|
|
18
|
+
|
|
19
|
+
describe("the outboundEgress driver point", () => {
|
|
20
|
+
it("is undefined with no extensions — the relaxed posture installs at the consumption site", () => {
|
|
21
|
+
expect(resolveExtensions([]).drivers.outboundEgress).toBeUndefined();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("carries the one registered instance through", () => {
|
|
25
|
+
const policy = strictEgressPolicy();
|
|
26
|
+
const resolved = resolveExtensions([{ name: "cloud-core", drivers: { outboundEgress: policy } }]);
|
|
27
|
+
expect(resolved.drivers.outboundEgress).toBe(policy);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("throws on a second registration, naming both units", () => {
|
|
31
|
+
expect(() =>
|
|
32
|
+
resolveExtensions([
|
|
33
|
+
{ name: "egress-a", drivers: { outboundEgress: strictEgressPolicy() } },
|
|
34
|
+
{ name: "egress-b", drivers: { outboundEgress: relaxedEgressPolicy() } },
|
|
35
|
+
]),
|
|
36
|
+
).toThrowError(/extension 'egress-b' registers an OutboundEgressPolicy, but 'egress-a' already did/);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("the two postures", () => {
|
|
41
|
+
it("relaxed refuses only the link-local range, so a local MCP server stays reachable", () => {
|
|
42
|
+
const relaxed = relaxedEgressPolicy();
|
|
43
|
+
expect(relaxed.name).toBe("relaxed");
|
|
44
|
+
expect(relaxed.blockedReason("127.0.0.1")).toBeNull();
|
|
45
|
+
expect(relaxed.blockedReason("10.0.0.1")).toBeNull();
|
|
46
|
+
expect(relaxed.blockedReason("169.254.169.254")).toBe("link-local (cloud metadata)");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("strict refuses loopback, private, link-local and unspecified addresses", () => {
|
|
50
|
+
const strict = strictEgressPolicy();
|
|
51
|
+
expect(strict.name).toBe("strict");
|
|
52
|
+
expect(strict.blockedReason("127.0.0.1")).toBe("loopback");
|
|
53
|
+
expect(strict.blockedReason("10.0.0.1")).toBe("private (RFC 1918)");
|
|
54
|
+
expect(strict.blockedReason("169.254.169.254")).toBe("link-local (cloud metadata)");
|
|
55
|
+
expect(strict.blockedReason("0.0.0.0")).toBe("unspecified");
|
|
56
|
+
expect(strict.blockedReason("104.16.0.1")).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -31,6 +31,10 @@
|
|
|
31
31
|
* port, which kinds an edition grants on, and the tuple-half query
|
|
32
32
|
* engine only an authorization backend can answer) — landed with
|
|
33
33
|
* 20260913.01, gate ruling Q-OR-10
|
|
34
|
+
* - outbound egress (which addresses the control plane may dial when
|
|
35
|
+
* it reaches a URL a user supplied: the MCP endpoint it probes at save
|
|
36
|
+
* time and the login server it reaches on Sign in) — landed with the
|
|
37
|
+
* save-time OAuth completion for URL-only MCP servers, 2026-09-19
|
|
34
38
|
*
|
|
35
39
|
* Merge rules (enforced by resolveExtensions, DD-006 §2b): the two
|
|
36
40
|
* provider kinds are single-instance points — a second declaring unit is
|
|
@@ -54,6 +58,7 @@ import type { SandboxProvisionerFactory } from "../sandbox/provisioner.js";
|
|
|
54
58
|
import type { AuthorizationQueryEngine } from "./authorization-queries.js";
|
|
55
59
|
import type { IdentityFederation } from "./identity-federation.js";
|
|
56
60
|
import type { LicenseStatusProvider } from "./license-status.js";
|
|
61
|
+
import type { OutboundEgressPolicy } from "./outbound-egress.js";
|
|
57
62
|
import type { ListReadScope } from "./list-read-scope.js";
|
|
58
63
|
import type { OrganizationDirectory } from "./organization-directory.js";
|
|
59
64
|
import type { PolicyGrantScope } from "./policy-grant-scope.js";
|
|
@@ -230,4 +235,16 @@ export interface ExtensionDrivers {
|
|
|
230
235
|
* way (extensions/license-status.ts carries the contract).
|
|
231
236
|
*/
|
|
232
237
|
readonly licenseStatus?: LicenseStatusProvider;
|
|
238
|
+
/**
|
|
239
|
+
* The outbound-egress policy (single-instance point): which addresses
|
|
240
|
+
* this edition's control plane may dial when it reaches a URL a user
|
|
241
|
+
* supplied. The composition root builds one guarded fetch from it and
|
|
242
|
+
* hands it to the McpServer connect slice, the only fetch its OAuth code
|
|
243
|
+
* and its save-time endpoint probe hold. When absent, open source's
|
|
244
|
+
* `relaxedEgressPolicy()` installs at the compose.ts consumption site:
|
|
245
|
+
* everything but the link-local (cloud metadata) range is allowed, so a
|
|
246
|
+
* server beside a self-hosted control plane keeps working
|
|
247
|
+
* (extensions/outbound-egress.ts carries the contract).
|
|
248
|
+
*/
|
|
249
|
+
readonly outboundEgress?: OutboundEgressPolicy;
|
|
233
250
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The outbound-egress driver point: WHICH addresses this edition's control
|
|
3
|
+
* plane may dial when it reaches a URL a user supplied. Single instance,
|
|
4
|
+
* registered as `drivers.outboundEgress` (the policyGrantScope shape: a
|
|
5
|
+
* driver point whose absence is open source's own behaviour). Absent, the
|
|
6
|
+
* composition root installs `relaxedEgressPolicy()`: everything but the
|
|
7
|
+
* link-local range is allowed, because a server beside a self-hosted
|
|
8
|
+
* control plane (`http://localhost:3000/mcp`, the proto's own example) is
|
|
9
|
+
* the normal case and the link-local range, which carries the cloud
|
|
10
|
+
* metadata endpoint, costs a local user nothing to refuse. The cloud
|
|
11
|
+
* registers `strictEgressPolicy()`: loopback, private, link-local and
|
|
12
|
+
* unspecified refused, because its control plane sits on infrastructure
|
|
13
|
+
* with reach to internal services.
|
|
14
|
+
*
|
|
15
|
+
* Who reads it: nobody in the domains directly. The composition root builds
|
|
16
|
+
* ONE guarded fetch from the policy (`@stigmer/outbound/egress`
|
|
17
|
+
* `guardedFetch`, which judges the first URL and every redirect hop) and
|
|
18
|
+
* hands it to the McpServer connect slice as `outboundFetch`, the only
|
|
19
|
+
* fetch the McpServer domain's OAuth code and its save-time endpoint probe
|
|
20
|
+
* hold. Before this point existed every OAuth fetch (discovery, dynamic
|
|
21
|
+
* client registration, the authorize preflight, token exchange and
|
|
22
|
+
* refresh) defaulted to the raw global `fetch` in every edition, which is
|
|
23
|
+
* the exposure the point closes.
|
|
24
|
+
*
|
|
25
|
+
* The contract every policy is held to:
|
|
26
|
+
*
|
|
27
|
+
* - It judges ADDRESSES, not names: the check resolves a hostname and
|
|
28
|
+
* asks about every address it has, so a name with one public and one
|
|
29
|
+
* private record is refused. A policy that wanted a hostname allowlist
|
|
30
|
+
* would be a different seam.
|
|
31
|
+
* - It is TOTAL: an unparseable address is refused ("unrecognized"),
|
|
32
|
+
* never a throw.
|
|
33
|
+
* - It is SYNCHRONOUS: an edition's egress posture is a fact about where
|
|
34
|
+
* it runs, never I/O.
|
|
35
|
+
*
|
|
36
|
+
* Both constructors are thin over the shared classification in
|
|
37
|
+
* `@stigmer/outbound/egress`, so a composition registers a posture and owns
|
|
38
|
+
* no copy of the ranges; the runner's `web_fetch` guard reads the same
|
|
39
|
+
* table, so the two processes refuse the same addresses under the same
|
|
40
|
+
* posture name.
|
|
41
|
+
*/
|
|
42
|
+
import { egressPolicyForPosture, type EgressPolicy } from "@stigmer/outbound/egress";
|
|
43
|
+
|
|
44
|
+
/** The egress contract (single-instance point, ExtensionDrivers.outboundEgress). */
|
|
45
|
+
export type OutboundEgressPolicy = EgressPolicy;
|
|
46
|
+
|
|
47
|
+
/** Open source's own posture: only the link-local (cloud metadata) range is refused. */
|
|
48
|
+
export function relaxedEgressPolicy(): OutboundEgressPolicy {
|
|
49
|
+
return egressPolicyForPosture("relaxed");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The managed-cloud posture: loopback, private, link-local and unspecified addresses refused. */
|
|
53
|
+
export function strictEgressPolicy(): OutboundEgressPolicy {
|
|
54
|
+
return egressPolicyForPosture("strict");
|
|
55
|
+
}
|
|
@@ -54,6 +54,7 @@ import { V1_VERSION } from "../encryption/v1-codec.js";
|
|
|
54
54
|
import type { AuthorizationQueryEngine } from "./authorization-queries.js";
|
|
55
55
|
import type { IdentityFederation } from "./identity-federation.js";
|
|
56
56
|
import type { LicenseStatusProvider } from "./license-status.js";
|
|
57
|
+
import type { OutboundEgressPolicy } from "./outbound-egress.js";
|
|
57
58
|
import type { ListReadScope } from "./list-read-scope.js";
|
|
58
59
|
import type { PolicyGrantScope } from "./policy-grant-scope.js";
|
|
59
60
|
import type { ModelCatalogProvider } from "../domain/workflow/registry/model-catalog-provider.js";
|
|
@@ -278,6 +279,11 @@ export interface ResolvedExtensionDrivers {
|
|
|
278
279
|
* built-in `absent` answer, so every edition serves getLicenseStatus.
|
|
279
280
|
*/
|
|
280
281
|
readonly licenseStatus: LicenseStatusProvider | undefined;
|
|
282
|
+
/**
|
|
283
|
+
* The outbound-egress policy — undefined = compose.ts installs open
|
|
284
|
+
* source's relaxed posture (only the link-local range refused).
|
|
285
|
+
*/
|
|
286
|
+
readonly outboundEgress: OutboundEgressPolicy | undefined;
|
|
281
287
|
}
|
|
282
288
|
|
|
283
289
|
/**
|
|
@@ -333,6 +339,8 @@ export function resolveExtensions(
|
|
|
333
339
|
let authorizationQueriesDeclaredBy: string | undefined;
|
|
334
340
|
let licenseStatus: LicenseStatusProvider | undefined;
|
|
335
341
|
let licenseStatusDeclaredBy: string | undefined;
|
|
342
|
+
let outboundEgress: OutboundEgressPolicy | undefined;
|
|
343
|
+
let outboundEgressDeclaredBy: string | undefined;
|
|
336
344
|
const artifactStorageDrivers = new Map<
|
|
337
345
|
string,
|
|
338
346
|
ArtifactStorageDriverFactory
|
|
@@ -534,6 +542,16 @@ export function resolveExtensions(
|
|
|
534
542
|
licenseStatusDeclaredBy = unit.name;
|
|
535
543
|
}
|
|
536
544
|
|
|
545
|
+
if (unit.drivers?.outboundEgress !== undefined) {
|
|
546
|
+
if (outboundEgressDeclaredBy !== undefined) {
|
|
547
|
+
throw new Error(
|
|
548
|
+
`extension '${unit.name}' registers an OutboundEgressPolicy, but '${outboundEgressDeclaredBy}' already did — exactly one may be composed`,
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
outboundEgress = unit.drivers.outboundEgress;
|
|
552
|
+
outboundEgressDeclaredBy = unit.name;
|
|
553
|
+
}
|
|
554
|
+
|
|
537
555
|
if (unit.drivers?.artifactStorageDrivers !== undefined) {
|
|
538
556
|
for (const [name, factory] of unit.drivers.artifactStorageDrivers) {
|
|
539
557
|
if ((BUILT_IN_STORAGE_TYPES as ReadonlyArray<string>).includes(name)) {
|
|
@@ -660,6 +678,7 @@ export function resolveExtensions(
|
|
|
660
678
|
policyGrantScope,
|
|
661
679
|
authorizationQueries,
|
|
662
680
|
licenseStatus,
|
|
681
|
+
outboundEgress,
|
|
663
682
|
},
|
|
664
683
|
services,
|
|
665
684
|
workers,
|
package/src/index.ts
CHANGED
|
@@ -119,6 +119,16 @@ export type {
|
|
|
119
119
|
LicenseStatusProvider,
|
|
120
120
|
LicenseStatusReport,
|
|
121
121
|
} from "./extensions/license-status.js";
|
|
122
|
+
// The outbound-egress seam (drivers.outboundEgress): which addresses the
|
|
123
|
+
// control plane may dial when it reaches a URL a user supplied. Both
|
|
124
|
+
// postures are exported so a composition registers one line and owns no
|
|
125
|
+
// copy of the address ranges (the classification lives in
|
|
126
|
+
// @stigmer/outbound/egress, shared with the runner's web_fetch guard).
|
|
127
|
+
export type { OutboundEgressPolicy } from "./extensions/outbound-egress.js";
|
|
128
|
+
export {
|
|
129
|
+
relaxedEgressPolicy,
|
|
130
|
+
strictEgressPolicy,
|
|
131
|
+
} from "./extensions/outbound-egress.js";
|
|
122
132
|
// The 20260911.11 identity-account seams (Q-IA-9): the store PORT a
|
|
123
133
|
// composition drives the domain through (drivers.identityAccountStore;
|
|
124
134
|
// a driver throws DuplicateAccountError for a held id), the federation
|
|
@@ -82,6 +82,22 @@ export const PLUGIN_LABEL = `${RESERVED_LABEL_PREFIX}plugin`;
|
|
|
82
82
|
*/
|
|
83
83
|
export const PLUGIN_VERSION_LABEL = `${RESERVED_LABEL_PREFIX}plugin-version`;
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* How an MCP server's `spec.auth` came to be. The one value, `endpoint`,
|
|
87
|
+
* says the server itself completed it: the author supplied a URL and
|
|
88
|
+
* nothing about authentication, the endpoint answered the save-time probe
|
|
89
|
+
* with an OAuth challenge, and the server wrote the token variable, its
|
|
90
|
+
* declaration and the Bearer header (domain/mcpserver/complete-endpoint-auth.ts).
|
|
91
|
+
* The label is provenance, not permission: it lets the next save tell a
|
|
92
|
+
* completion it may reuse from an author's own `auth` it must not touch.
|
|
93
|
+
* Stamped through the server-stamped mechanism; dropped when the author
|
|
94
|
+
* writes their own `auth`.
|
|
95
|
+
*/
|
|
96
|
+
export const MCP_AUTH_LABEL = `${RESERVED_LABEL_PREFIX}mcp-auth`;
|
|
97
|
+
|
|
98
|
+
/** The MCP_AUTH_LABEL value for a completion the endpoint's own challenge produced. */
|
|
99
|
+
export const MCP_AUTH_ENDPOINT = "endpoint";
|
|
100
|
+
|
|
85
101
|
/** The plugin id a resource's labels claim, or undefined when unlabelled. */
|
|
86
102
|
export function pluginIdOf(
|
|
87
103
|
metadata: ApiResourceMetadata | undefined,
|