stitchkit 0.22.0 → 0.24.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.
@@ -398,6 +398,8 @@ function generateOpenApiDocument(config) {
398
398
  for (const [key, method] of Object.entries(service.methods)) {
399
399
  if (method.expose && !method.expose.includes("HTTP"))
400
400
  continue;
401
+ if (config.includeMethod && !config.includeMethod(method))
402
+ continue;
401
403
  const servicePath = joinPath("/", service.prefix, method.path === "/" ? "" : method.path);
402
404
  const fullPath = toOpenApiPath(pathPrefix ? joinPath(pathPrefix, servicePath) : servicePath);
403
405
  const parameters = [];
@@ -10,7 +10,7 @@
10
10
  * emits — not a divergent code path. Schemas are inlined (valid OpenAPI);
11
11
  * `$ref` de-duplication can come later if a spec grows unwieldy.
12
12
  */
13
- import type { RawRoute, ServiceDef } from './types';
13
+ import type { MethodDef, RawRoute, ServiceDef } from './types';
14
14
  export interface OpenApiInfo {
15
15
  title: string;
16
16
  version: string;
@@ -31,6 +31,19 @@ export interface OpenApiConfig {
31
31
  }>;
32
32
  /** `servers` block for the spec. */
33
33
  servers?: OpenApiServer[];
34
+ /**
35
+ * Emit only the methods this predicate keeps — a curated public spec instead
36
+ * of the whole HTTP surface. The predicate decides the policy (the core stays
37
+ * generic): filter on `method.scope`, `method.meta` (the recommended
38
+ * declarative allowlist — mark endpoints `meta: { public: true }` and keep
39
+ * `(m) => m.meta?.public === true`), `method.key`, anything on the method.
40
+ * Omit to include every HTTP method (the default).
41
+ *
42
+ * This controls what the spec **advertises**, not access — a hidden endpoint
43
+ * is still callable; the auth `scope` gate is the actual guard. Build a
44
+ * separate filtered document for a public route (see the guide).
45
+ */
46
+ includeMethod?: (method: Readonly<MethodDef>) => boolean;
34
47
  }
35
48
  export interface OpenApiDocument {
36
49
  openapi: '3.1.0';
@@ -41,7 +54,9 @@ export interface OpenApiDocument {
41
54
  /**
42
55
  * Generate an OpenAPI 3.1 document from contract services. Only methods exposed
43
56
  * on HTTP are included — a method whose `expose` omits `'HTTP'` (an MCP/agent
44
- * only tool) is skipped, matching the router's own route-building rule.
57
+ * only tool) is skipped, matching the router's own route-building rule. Pass
58
+ * `includeMethod` to emit a curated subset (a public spec) instead of the whole
59
+ * surface.
45
60
  */
46
61
  export declare function generateOpenApiDocument(config: OpenApiConfig): OpenApiDocument;
47
62
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../../src/server/openapi.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEpD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,WAAW,CAAC;IAClB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,UAAU,EAAE,CAAC;IACxB,iFAAiF;IACjF,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,UAAU,EAAE,CAAA;KAAE,CAAC,CAAC;IAChE,oCAAoC;IACpC,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAChD;AAsED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,aAAa,GAAG,eAAe,CAgH9E;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,GAAG,QAAQ,CAM9E"}
1
+ {"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../../src/server/openapi.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE/D,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,WAAW,CAAC;IAClB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,UAAU,EAAE,CAAC;IACxB,iFAAiF;IACjF,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,UAAU,EAAE,CAAA;KAAE,CAAC,CAAC;IAChE,oCAAoC;IACpC,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B;;;;;;;;;;;OAWG;IACH,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC,SAAS,CAAC,KAAK,OAAO,CAAC;CAC1D;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAChD;AAsED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,aAAa,GAAG,eAAe,CAuH9E;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,GAAG,QAAQ,CAM9E"}
@@ -1,15 +1,25 @@
1
1
  import type { RawRoute } from '../server/types';
2
+ /**
3
+ * OpenID Connect DCR `application_type` (SEP-837). A `native` client (desktop /
4
+ * CLI) may register an `http` loopback redirect; a `web` client may not — the
5
+ * mismatch is the usual cause of a `redirect_uri` rejection for CLI clients.
6
+ */
7
+ export type ApplicationType = 'native' | 'web';
2
8
  /** A client as registered via DCR. Public clients (PKCE) carry no secret. */
3
9
  export interface RegisteredClient {
4
10
  clientId: string;
5
11
  redirectUris: string[];
6
12
  clientName?: string;
13
+ /** The `application_type` the client declared, when it declared one. */
14
+ applicationType?: ApplicationType;
7
15
  }
8
16
  /** Metadata posted to `/register` (RFC 7591) before a client id is assigned. */
9
17
  export interface ClientMetadata {
10
18
  redirectUris: string[];
11
19
  clientName?: string;
12
20
  tokenEndpointAuthMethod?: string;
21
+ /** `native` (desktop / CLI, loopback allowed) or `web` (https only). */
22
+ applicationType?: ApplicationType;
13
23
  }
14
24
  /** State persisted between `/authorize` and `/token`, keyed by the auth code. */
15
25
  export interface AuthCodeData {
@@ -1 +1 @@
1
- {"version":3,"file":"oauth-provider.d.ts","sourceRoot":"","sources":["../../src/tools/oauth-provider.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAIhD,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,gFAAgF;AAChF,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uBAAuB,CAAC,EAAE,MAAM,CAAC;CAClC;AAED,iFAAiF;AACjF,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,wFAAwF;IACxF,mBAAmB,EAAE,MAAM,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,2CAA2C;AAC3C,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,+EAA+E;AAC/E,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,mBAAmB;IAClC,oFAAoF;IACpF,MAAM,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,aAAa,EAAE,MAAM,CAAC;IACtB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC;IACjB,wDAAwD;IACxD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,4CAA4C;IAC5C,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,6BAA6B;IAC7B,OAAO,EAAE;QACP,QAAQ,CAAC,QAAQ,EAAE,cAAc,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;QAC9D,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;KACzD,CAAC;IACF,+EAA+E;IAC/E,KAAK,EAAE;QACL,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QACtD,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;KAClD,CAAC;IACF,uEAAuE;IACvE,aAAa,CAAC,EAAE;QACd,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QACtD,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;KAClD,CAAC;IAEF;;;;OAIG;IACH,aAAa,CACX,GAAG,EAAE,OAAO,EACZ,WAAW,EAAE,WAAW,GACvB,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,QAAQ,CAAC,CAAC;CAC3C;AAoED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,mBAAmB,GAAG,QAAQ,EAAE,CAkR1E"}
1
+ {"version":3,"file":"oauth-provider.d.ts","sourceRoot":"","sources":["../../src/tools/oauth-provider.ts"],"names":[],"mappings":"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"}
package/dist/tools.d.ts CHANGED
@@ -17,7 +17,7 @@ export { type DownloadToolConfig, mountDownload } from './tools/mount-download';
17
17
  export { mountUpload, type UploadToolConfig } from './tools/mount-upload';
18
18
  export { mountWait, type WaitToolConfig } from './tools/mount-wait';
19
19
  export { oauthProtectedResourceRoute, PROTECTED_RESOURCE_PATH, type ProtectedResourceConfig, protectedResourceMetadataUrl, wwwAuthenticateHeader, } from './tools/oauth-metadata';
20
- export { type AuthCodeData, type AuthRequest, type ClientMetadata, mountOAuthProvider, type OAuthProviderConfig, type RefreshData, type RegisteredClient, } from './tools/oauth-provider';
20
+ export { type ApplicationType, type AuthCodeData, type AuthRequest, type ClientMetadata, mountOAuthProvider, type OAuthProviderConfig, type RefreshData, type RegisteredClient, } from './tools/oauth-provider';
21
21
  export { type ImplementRemoteOptions, implementRemote } from './tools/remote';
22
22
  export { createToolLogger, type ToolCallRecord, type ToolLoggerConfig, } from './tools/tool-logger';
23
23
  export { createToolkit, type Toolkit } from './tools/toolkit';
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,gBAAgB,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AACrF,OAAO,EAAE,KAAK,SAAS,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxD,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7F,OAAO,EAAE,yBAAyB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EAAE,aAAa,EAAE,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC7E,OAAO,EACL,cAAc,EACd,KAAK,wBAAwB,EAC7B,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,QAAQ,EACR,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,2BAA2B,EAC3B,kBAAkB,EAClB,KAAK,SAAS,EACd,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,KAAK,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACpF,OAAO,EAAE,YAAY,EAAE,KAAK,aAAa,EAAE,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AAClF,OAAO,EAAE,KAAK,kBAAkB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAChF,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC1E,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,EACL,2BAA2B,EAC3B,uBAAuB,EACvB,KAAK,uBAAuB,EAC5B,4BAA4B,EAC5B,qBAAqB,GACtB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,kBAAkB,EAClB,KAAK,mBAAmB,EACxB,KAAK,WAAW,EAChB,KAAK,gBAAgB,GACtB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,KAAK,sBAAsB,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAC9E,OAAO,EACL,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,gBAAgB,GACtB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,KAAK,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC9D,OAAO,EACL,mBAAmB,EACnB,KAAK,eAAe,EACpB,KAAK,gBAAgB,GACtB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,KAAK,eAAe,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,gBAAgB,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AACrF,OAAO,EAAE,KAAK,SAAS,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxD,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7F,OAAO,EAAE,yBAAyB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EAAE,aAAa,EAAE,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC7E,OAAO,EACL,cAAc,EACd,KAAK,wBAAwB,EAC7B,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,QAAQ,EACR,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,2BAA2B,EAC3B,kBAAkB,EAClB,KAAK,SAAS,EACd,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,KAAK,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACpF,OAAO,EAAE,YAAY,EAAE,KAAK,aAAa,EAAE,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AAClF,OAAO,EAAE,KAAK,kBAAkB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAChF,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC1E,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,EACL,2BAA2B,EAC3B,uBAAuB,EACvB,KAAK,uBAAuB,EAC5B,4BAA4B,EAC5B,qBAAqB,GACtB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,kBAAkB,EAClB,KAAK,mBAAmB,EACxB,KAAK,WAAW,EAChB,KAAK,gBAAgB,GACtB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,KAAK,sBAAsB,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAC9E,OAAO,EACL,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,gBAAgB,GACtB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,KAAK,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC9D,OAAO,EACL,mBAAmB,EACnB,KAAK,eAAe,EACpB,KAAK,gBAAgB,GACtB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,KAAK,eAAe,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/tools.js CHANGED
@@ -615,13 +615,15 @@ function json(body, status = 200, extraHeaders) {
615
615
  function oauthError(error, description, status = 400) {
616
616
  return json({ error, error_description: description }, status);
617
617
  }
618
- function isHttpUri(value) {
618
+ function isRegistrableRedirectUri(value, applicationType) {
619
619
  try {
620
620
  const url = new URL(value);
621
621
  if (url.protocol === "https:")
622
622
  return true;
623
623
  if (url.protocol !== "http:")
624
624
  return false;
625
+ if (applicationType === "web")
626
+ return false;
625
627
  const host = url.hostname.replace(/^\[|\]$/g, "");
626
628
  return host === "127.0.0.1" || host === "::1" || host === "localhost";
627
629
  } catch {
@@ -655,6 +657,7 @@ function mountOAuthProvider(config) {
655
657
  const registerPath = `${base}/register`;
656
658
  const authorizePath = `${base}/authorize`;
657
659
  const tokenPath = `${base}/token`;
660
+ const redirectToClient = (uri, params) => redirectWith(uri, { ...params, iss: config.issuer });
658
661
  const metadataRoute = {
659
662
  method: "ALL",
660
663
  path: AS_METADATA_PATH,
@@ -670,6 +673,7 @@ function mountOAuthProvider(config) {
670
673
  grant_types_supported: config.refreshTokens ? ["authorization_code", "refresh_token"] : ["authorization_code"],
671
674
  code_challenge_methods_supported: ["S256"],
672
675
  token_endpoint_auth_methods_supported: ["none"],
676
+ authorization_response_iss_parameter_supported: true,
673
677
  ...config.scopesSupported && { scopes_supported: config.scopesSupported }
674
678
  });
675
679
  }
@@ -686,14 +690,20 @@ function mountOAuthProvider(config) {
686
690
  if (!isRecord(meta)) {
687
691
  return oauthError("invalid_client_metadata", "Body must be a JSON object");
688
692
  }
693
+ const rawAppType = meta.application_type;
694
+ if (rawAppType !== undefined && rawAppType !== "native" && rawAppType !== "web") {
695
+ return oauthError("invalid_client_metadata", "application_type must be 'native' or 'web'");
696
+ }
697
+ const applicationType = rawAppType;
689
698
  const redirectUris = meta.redirect_uris;
690
- if (!Array.isArray(redirectUris) || redirectUris.length === 0 || !redirectUris.every((u) => typeof u === "string" && isHttpUri(u))) {
691
- return oauthError("invalid_redirect_uri", "redirect_uris must be a non-empty array of absolute https URLs (http is allowed only on a loopback host)");
699
+ if (!Array.isArray(redirectUris) || redirectUris.length === 0 || !redirectUris.every((u) => typeof u === "string" && isRegistrableRedirectUri(u, applicationType))) {
700
+ return oauthError("invalid_redirect_uri", applicationType === "web" ? "redirect_uris must be a non-empty array of absolute https URLs (a web client cannot register an http loopback URI)" : "redirect_uris must be a non-empty array of absolute https URLs (http is allowed only on a loopback host)");
692
701
  }
693
702
  const client = await config.clients.register({
694
703
  redirectUris,
695
704
  clientName: typeof meta.client_name === "string" ? meta.client_name : undefined,
696
- tokenEndpointAuthMethod: typeof meta.token_endpoint_auth_method === "string" ? meta.token_endpoint_auth_method : undefined
705
+ tokenEndpointAuthMethod: typeof meta.token_endpoint_auth_method === "string" ? meta.token_endpoint_auth_method : undefined,
706
+ ...applicationType && { applicationType }
697
707
  });
698
708
  return json({
699
709
  client_id: client.clientId,
@@ -701,7 +711,8 @@ function mountOAuthProvider(config) {
701
711
  token_endpoint_auth_method: "none",
702
712
  grant_types: config.refreshTokens ? ["authorization_code", "refresh_token"] : ["authorization_code"],
703
713
  response_types: ["code"],
704
- ...client.clientName && { client_name: client.clientName }
714
+ ...client.clientName && { client_name: client.clientName },
715
+ ...client.applicationType && { application_type: client.applicationType }
705
716
  }, 201);
706
717
  }
707
718
  };
@@ -731,27 +742,27 @@ function mountOAuthProvider(config) {
731
742
  return oauthError("invalid_request", "redirect_uri does not match a registered URI");
732
743
  }
733
744
  if (responseType !== "code") {
734
- return redirectWith(redirectUri, {
745
+ return redirectToClient(redirectUri, {
735
746
  error: "unsupported_response_type",
736
747
  ...state && { state }
737
748
  });
738
749
  }
739
750
  if (!codeChallenge || codeChallengeMethod !== "S256") {
740
- return redirectWith(redirectUri, {
751
+ return redirectToClient(redirectUri, {
741
752
  error: "invalid_request",
742
753
  error_description: "PKCE S256 code_challenge is required",
743
754
  ...state && { state }
744
755
  });
745
756
  }
746
757
  if (!resource) {
747
- return redirectWith(redirectUri, {
758
+ return redirectToClient(redirectUri, {
748
759
  error: "invalid_target",
749
760
  error_description: "resource parameter is required",
750
761
  ...state && { state }
751
762
  });
752
763
  }
753
764
  if (resource !== config.resource) {
754
- return redirectWith(redirectUri, {
765
+ return redirectToClient(redirectUri, {
755
766
  error: "invalid_target",
756
767
  error_description: "resource is not served by this authorization server",
757
768
  ...state && { state }
@@ -772,7 +783,7 @@ function mountOAuthProvider(config) {
772
783
  userId: result.userId,
773
784
  expiresAt: Date.now() + AUTH_CODE_TTL_MS
774
785
  });
775
- return redirectWith(redirectUri, { code, ...state && { state } });
786
+ return redirectToClient(redirectUri, { code, ...state && { state } });
776
787
  }
777
788
  };
778
789
  const issueAccessToken = (userId, audience, clientId, scope) => signJwt({ scope, client_id: clientId }, config.signingSecret, {
package/llms-full.txt CHANGED
@@ -355,7 +355,10 @@ beforeHandle: (ctx, endpoint) => {
355
355
  }
356
356
  ```
357
357
 
358
- `meta` is **app-private** — it is never serialized into the OpenAPI document.
358
+ `meta` is **app-private** — it is never serialized into the OpenAPI document. It
359
+ can still *drive* generation: `generateOpenApiDocument`'s `includeMethod` reads
360
+ it to curate a public spec (e.g. `meta: { public: true }`), without ever emitting
361
+ `meta` itself — see [Curating the spec](./server.md#curating-the-spec--includemethod).
359
362
 
360
363
  > **Declare a meta type as a `type`, an inline literal, or with `satisfies` — not
361
364
  > an `interface`.** A TS `interface` has no implicit index signature (it can be
@@ -856,6 +859,60 @@ bus.emit('user.created', { id: '1' })
856
859
  A typed in-process pub/sub — decouple a handler from the side effects of its
857
860
  write without reaching for an external queue.
858
861
 
862
+ ## OpenAPI
863
+
864
+ `generateOpenApiDocument` builds an OpenAPI 3.1 document straight from the
865
+ contracts — the contract *is* the spec, no decorators or hand-maintained
866
+ annotations (→ ADR 0018). `openApiRoute` serves it as a raw route:
867
+
868
+ ```ts
869
+ import { generateOpenApiDocument, openApiRoute } from 'stitchkit/server'
870
+
871
+ const doc = generateOpenApiDocument({
872
+ info: { title: 'My API', version: '1.0.0' },
873
+ services: [users, orders],
874
+ })
875
+ createServer({ services: [users, orders], rawRoutes: [openApiRoute('/openapi.json', doc)] })
876
+ ```
877
+
878
+ Only HTTP-exposed methods appear (an MCP/agent-only tool is skipped).
879
+
880
+ ### Curating the spec — `includeMethod`
881
+
882
+ To publish a **subset** — a public spec that advertises only some methods
883
+ without revealing the rest — pass `includeMethod`. It keeps the core generic:
884
+ *you* decide the policy, filtering on anything the method carries. The
885
+ recommended declarative allowlist marks endpoints with the existing `meta`
886
+ passthrough and keeps those:
887
+
888
+ ```ts
889
+ // contract — declarative, one source of truth
890
+ getBalance: { method: 'GET', path: '/balance', desc: '…', scope: 'account',
891
+ meta: { public: true }, output: BalanceSchema }
892
+
893
+ // generation — the app's policy
894
+ const publicDoc = generateOpenApiDocument({
895
+ info: { title: 'Public API', version: '1.0.0' },
896
+ services: [account],
897
+ includeMethod: (m) => m.meta?.public === true,
898
+ })
899
+ ```
900
+
901
+ An excluded method's whole entry — path *and* every schema inlined within it —
902
+ is simply never emitted, so nothing about a hidden endpoint leaks.
903
+
904
+ > **The filter advertises; it does not authorize.** Hiding a method from the
905
+ > spec does **not** protect it — it is still callable, and the auth `scope` gate
906
+ > is the only thing guarding it. And because `openApiRoute` closes over the
907
+ > document you hand it, the filter only matters if you feed it a filtered one:
908
+ > serve **two** documents — a full internal spec and a filtered public spec on
909
+ > separate routes — never one unfiltered `openApiRoute` on a public path.
910
+
911
+ ```ts
912
+ const internal = openApiRoute('/internal/openapi.json', fullDoc) // behind auth
913
+ const publicSpec = openApiRoute('/openapi.json', publicDoc) // curated
914
+ ```
915
+
859
916
 
860
917
  ==============================================================================
861
918
  # Guide: Typed client (docs/guide/client.md)
@@ -1336,14 +1393,36 @@ createServer({
1336
1393
  })
1337
1394
  ```
1338
1395
 
1339
- Access tokens are signed HS256 JWTs (`signJwt`) whose `aud` is the resource —
1340
- validate them in `auth` with `verifyJwt(token, secret, { audience: resource })`.
1341
- `authorizeUser` is where the app authenticates the user (reuse an existing
1342
- session) and records consent; return `{ userId }` to issue a code, or a
1343
- `Response` to redirect the browser to a login page first. The AS and resource
1344
- server can co-locate or live on separate origins. See
1396
+ Access tokens are signed HS256 JWTs (`signJwt`) whose `aud` is the resource and
1397
+ whose `iss` is the issuer — validate both in `auth` with
1398
+ `verifyJwt(token, secret, { audience: resource, issuer })`. `authorizeUser` is
1399
+ where the app authenticates the user (reuse an existing session) and records
1400
+ consent; return `{ userId }` to issue a code, or a `Response` to redirect the
1401
+ browser to a login page first. The AS and resource server can co-locate or live
1402
+ on separate origins. See
1345
1403
  [ADR 0015](../decisions/0015-oauth-resource-server.md).
1346
1404
 
1405
+ ### Authorization hardening (MCP 2026-07-28)
1406
+
1407
+ - **`iss` on every authorization response (RFC 9207, SEP-2468).** Success *and*
1408
+ error redirects carry `iss`, and the AS metadata advertises
1409
+ `authorization_response_iss_parameter_supported: true`. A client that talks to
1410
+ several authorization servers validates `iss` before redeeming the code, which
1411
+ closes the **mix-up attack** — an attacker's server cannot pass its response
1412
+ off as this issuer's. Additive on the wire: a client that ignores `iss` is
1413
+ unaffected.
1414
+ - **`application_type` on registration (SEP-837).** A client may declare
1415
+ `"native"` (desktop / CLI) or `"web"` in its DCR body. A **native** client may
1416
+ register an `http` loopback redirect (`http://127.0.0.1:…`, RFC 8252 §7.3); a
1417
+ **web** client is held to `https` only — that mismatch is the usual cause of
1418
+ the `redirect_uri` rejection CLI clients hit. Omit the field and registration
1419
+ behaves exactly as before (loopback allowed); an unknown value is rejected
1420
+ rather than silently defaulted.
1421
+
1422
+ > Dynamic Client Registration is **deprecated** in the 2026-07-28 spec in favour
1423
+ > of Client ID Metadata Documents (CIMD), with a ≥12-month window. DCR keeps
1424
+ > working and stays supported here; CIMD support is tracked separately.
1425
+
1347
1426
  ## Proxying a remote API — `implementRemote`
1348
1427
 
1349
1428
  `implement` binds a contract to local handlers. `implementRemote` binds it to a
@@ -3286,7 +3365,7 @@ Also re-exports the error helpers from `stitchkit/contract`.
3286
3365
  |--------|------|---------|
3287
3366
  | `generateOpenApiDocument` | function | an OpenAPI 3.1 document from contract services — [ADR 0018](../decisions/0018-openapi-generation.md) |
3288
3367
  | `openApiRoute` | function | a `RawRoute` that serves the document as JSON |
3289
- | `OpenApiConfig` | _type_ | config for `generateOpenApiDocument` |
3368
+ | `OpenApiConfig` | _type_ | config for `generateOpenApiDocument` (incl. `includeMethod` — curate a public subset) — [guide](../guide/server.md#curating-the-spec--includemethod) |
3290
3369
  | `OpenApiDocument` | _type_ | the generated document |
3291
3370
  | `OpenApiInfo` | _type_ | the spec `info` block |
3292
3371
  | `OpenApiServer` | _type_ | a spec `servers` entry |
@@ -3413,6 +3492,7 @@ A native remote-connector auth surface for MCP — [guide](../guide/mcp-and-agen
3413
3492
  | `PROTECTED_RESOURCE_PATH` | const | the well-known metadata path |
3414
3493
  | `OAuthProviderConfig` | _type_ | config for `mountOAuthProvider` |
3415
3494
  | `ProtectedResourceConfig` | _type_ | config for `oauthProtectedResourceRoute` |
3495
+ | `ApplicationType` | _type_ | DCR `application_type` — `'native'` (loopback allowed) \| `'web'` (https only) |
3416
3496
  | `AuthCodeData` | _type_ | a stored authorization-code record |
3417
3497
  | `AuthRequest` | _type_ | a parsed authorization request |
3418
3498
  | `ClientMetadata` | _type_ | dynamic-client-registration metadata |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "description": "Contract-first backend framework — one defineContract() into an HTTP API, MCP tools, AI-agent tools and a typed client. Bun and Node.",
5
5
  "keywords": [
6
6
  "bun",