@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.
Files changed (92) hide show
  1. package/README.md +1 -1
  2. package/dist/boot/compose.d.ts.map +1 -1
  3. package/dist/boot/compose.js +42 -6
  4. package/dist/boot/compose.js.map +1 -1
  5. package/dist/boot/config.d.ts +6 -2
  6. package/dist/boot/config.d.ts.map +1 -1
  7. package/dist/boot/config.js.map +1 -1
  8. package/dist/boot/oauth-redirect-uri.d.ts +54 -0
  9. package/dist/boot/oauth-redirect-uri.d.ts.map +1 -0
  10. package/dist/boot/oauth-redirect-uri.js +37 -0
  11. package/dist/boot/oauth-redirect-uri.js.map +1 -0
  12. package/dist/domain/mcpserver/complete-endpoint-auth.d.ts +16 -0
  13. package/dist/domain/mcpserver/complete-endpoint-auth.d.ts.map +1 -0
  14. package/dist/domain/mcpserver/complete-endpoint-auth.js +260 -0
  15. package/dist/domain/mcpserver/complete-endpoint-auth.js.map +1 -0
  16. package/dist/domain/mcpserver/complete-oauth-connect.d.ts.map +1 -1
  17. package/dist/domain/mcpserver/complete-oauth-connect.js +1 -1
  18. package/dist/domain/mcpserver/complete-oauth-connect.js.map +1 -1
  19. package/dist/domain/mcpserver/connect.d.ts +25 -0
  20. package/dist/domain/mcpserver/connect.d.ts.map +1 -1
  21. package/dist/domain/mcpserver/connect.js +8 -23
  22. package/dist/domain/mcpserver/connect.js.map +1 -1
  23. package/dist/domain/mcpserver/controller.d.ts.map +1 -1
  24. package/dist/domain/mcpserver/controller.js +12 -0
  25. package/dist/domain/mcpserver/controller.js.map +1 -1
  26. package/dist/domain/mcpserver/initiate-oauth-connect.d.ts.map +1 -1
  27. package/dist/domain/mcpserver/initiate-oauth-connect.js +16 -13
  28. package/dist/domain/mcpserver/initiate-oauth-connect.js.map +1 -1
  29. package/dist/domain/mcpserver/oauth/dcr.d.ts +10 -1
  30. package/dist/domain/mcpserver/oauth/dcr.d.ts.map +1 -1
  31. package/dist/domain/mcpserver/oauth/dcr.js +1 -9
  32. package/dist/domain/mcpserver/oauth/dcr.js.map +1 -1
  33. package/dist/domain/mcpserver/oauth/discovery.d.ts +43 -20
  34. package/dist/domain/mcpserver/oauth/discovery.d.ts.map +1 -1
  35. package/dist/domain/mcpserver/oauth/discovery.js +69 -54
  36. package/dist/domain/mcpserver/oauth/discovery.js.map +1 -1
  37. package/dist/domain/mcpserver/oauth/preflight.d.ts +27 -1
  38. package/dist/domain/mcpserver/oauth/preflight.d.ts.map +1 -1
  39. package/dist/domain/mcpserver/oauth/preflight.js +1 -26
  40. package/dist/domain/mcpserver/oauth/preflight.js.map +1 -1
  41. package/dist/domain/mcpserver/oauth/refresh.d.ts +2 -1
  42. package/dist/domain/mcpserver/oauth/refresh.d.ts.map +1 -1
  43. package/dist/domain/mcpserver/oauth/refresh.js +1 -1
  44. package/dist/domain/mcpserver/oauth/refresh.js.map +1 -1
  45. package/dist/domain/mcpserver/oauth/token.d.ts +9 -2
  46. package/dist/domain/mcpserver/oauth/token.d.ts.map +1 -1
  47. package/dist/domain/mcpserver/oauth/token.js +2 -8
  48. package/dist/domain/mcpserver/oauth/token.js.map +1 -1
  49. package/dist/extensions/drivers.d.ts +17 -0
  50. package/dist/extensions/drivers.d.ts.map +1 -1
  51. package/dist/extensions/outbound-egress.d.ts +49 -0
  52. package/dist/extensions/outbound-egress.d.ts.map +1 -0
  53. package/dist/extensions/outbound-egress.js +51 -0
  54. package/dist/extensions/outbound-egress.js.map +1 -0
  55. package/dist/extensions/registry.d.ts +6 -0
  56. package/dist/extensions/registry.d.ts.map +1 -1
  57. package/dist/extensions/registry.js +10 -0
  58. package/dist/extensions/registry.js.map +1 -1
  59. package/dist/index.d.ts +2 -0
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +1 -0
  62. package/dist/index.js.map +1 -1
  63. package/dist/pipeline/apiresource-labels.d.ts +14 -0
  64. package/dist/pipeline/apiresource-labels.d.ts.map +1 -1
  65. package/dist/pipeline/apiresource-labels.js +14 -0
  66. package/dist/pipeline/apiresource-labels.js.map +1 -1
  67. package/package.json +6 -5
  68. package/src/boot/__tests__/oauth-redirect-uri.test.ts +38 -0
  69. package/src/boot/compose.ts +47 -8
  70. package/src/boot/config.ts +6 -2
  71. package/src/boot/oauth-redirect-uri.ts +53 -0
  72. package/src/domain/mcpserver/__tests__/complete-endpoint-auth.test.ts +270 -0
  73. package/src/domain/mcpserver/__tests__/connect-authorization.test.ts +3 -0
  74. package/src/domain/mcpserver/__tests__/connect.test.ts +31 -0
  75. package/src/domain/mcpserver/complete-endpoint-auth.ts +277 -0
  76. package/src/domain/mcpserver/complete-oauth-connect.ts +1 -0
  77. package/src/domain/mcpserver/connect.ts +21 -15
  78. package/src/domain/mcpserver/controller.ts +16 -0
  79. package/src/domain/mcpserver/initiate-oauth-connect.ts +16 -12
  80. package/src/domain/mcpserver/oauth/__tests__/discovery.test.ts +120 -91
  81. package/src/domain/mcpserver/oauth/__tests__/token-refresh.test.ts +3 -1
  82. package/src/domain/mcpserver/oauth/dcr.ts +2 -1
  83. package/src/domain/mcpserver/oauth/discovery.ts +120 -89
  84. package/src/domain/mcpserver/oauth/preflight.ts +2 -1
  85. package/src/domain/mcpserver/oauth/refresh.ts +2 -1
  86. package/src/domain/mcpserver/oauth/token.ts +4 -3
  87. package/src/extensions/__tests__/outbound-egress-points.test.ts +58 -0
  88. package/src/extensions/drivers.ts +17 -0
  89. package/src/extensions/outbound-egress.ts +55 -0
  90. package/src/extensions/registry.ts +19 -0
  91. package/src/index.ts +10 -0
  92. 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,