@scalar/mock-server 0.14.1 → 0.14.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @scalar/mock-server
2
2
 
3
+ ## 0.14.3
4
+
5
+ ## 0.14.2
6
+
7
+ ### Patch Changes
8
+
9
+ - [#10179](https://github.com/scalar/scalar/pull/10179): Serve OAuth2 authorization server metadata at the declared oauth2MetadataUrl, advertising local mock endpoints and the configured grants and scopes.
10
+
11
+ Normalize absolute OAuth token URLs to route paths when registering mock authentication routes.
12
+
13
+ - [#10179](https://github.com/scalar/scalar/pull/10179): Warn when OAuth2 metadata routes collide with declared API paths. Keep the OAuth2 metadata field in OpenAPI 3.2 schemas and document the HTTP exception for local development.
14
+
3
15
  ## 0.14.1
4
16
 
5
17
  ### Patch Changes
@@ -0,0 +1,14 @@
1
+ import type { OpenAPIV3_2 } from '@scalar/openapi-types';
2
+ /** The RFC8414 fields exposed by the mock authorization server. */
3
+ type OAuth2Metadata = {
4
+ issuer: string;
5
+ authorization_endpoint?: string;
6
+ token_endpoint?: string;
7
+ response_types_supported: string[];
8
+ grant_types_supported: string[];
9
+ scopes_supported: string[];
10
+ };
11
+ /** Advertises local mock endpoints instead of sending clients to the real authorization server. */
12
+ export declare const getOAuth2Metadata: (flows: OpenAPIV3_2.OAuth2SecurityScheme["flows"], origin: string) => OAuth2Metadata;
13
+ export {};
14
+ //# sourceMappingURL=get-oauth2-metadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-oauth2-metadata.d.ts","sourceRoot":"","sources":["../../src/utils/get-oauth2-metadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAIxD,mEAAmE;AACnE,KAAK,cAAc,GAAG;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,sBAAsB,CAAC,EAAE,MAAM,CAAA;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,wBAAwB,EAAE,MAAM,EAAE,CAAA;IAClC,qBAAqB,EAAE,MAAM,EAAE,CAAA;IAC/B,gBAAgB,EAAE,MAAM,EAAE,CAAA;CAC3B,CAAA;AAED,mGAAmG;AACnG,eAAO,MAAM,iBAAiB,GAAI,OAAO,WAAW,CAAC,oBAAoB,CAAC,OAAO,CAAC,EAAE,QAAQ,MAAM,KAAG,cAqBpG,CAAA"}
@@ -0,0 +1,23 @@
1
+ import { getPathFromUrl } from './get-open-auth-token-urls.js';
2
+ /** Advertises local mock endpoints instead of sending clients to the real authorization server. */
3
+ export const getOAuth2Metadata = (flows, origin) => {
4
+ const authorizationFlow = flows?.authorizationCode ?? flows?.implicit;
5
+ const tokenFlow = flows?.authorizationCode ?? flows?.clientCredentials ?? flows?.password;
6
+ const localUrl = (url) => new URL(getPathFromUrl(url), origin).href;
7
+ const supportedFlows = [
8
+ { flow: flows?.authorizationCode, grant: 'authorization_code', response: 'code' },
9
+ { flow: flows?.implicit, grant: 'implicit', response: 'token' },
10
+ { flow: flows?.clientCredentials, grant: 'client_credentials' },
11
+ { flow: flows?.password, grant: 'password' },
12
+ ].filter(({ flow }) => flow);
13
+ return {
14
+ issuer: origin,
15
+ ...(authorizationFlow
16
+ ? { authorization_endpoint: localUrl(authorizationFlow.authorizationUrl ?? '/oauth/authorize') }
17
+ : {}),
18
+ ...(tokenFlow ? { token_endpoint: localUrl(tokenFlow.tokenUrl ?? '/oauth/token') } : {}),
19
+ response_types_supported: supportedFlows.flatMap(({ response }) => (response ? [response] : [])),
20
+ grant_types_supported: supportedFlows.map(({ grant }) => grant),
21
+ scopes_supported: [...new Set(supportedFlows.flatMap(({ flow }) => Object.keys(flow?.scopes ?? {})))],
22
+ };
23
+ };
@@ -1,7 +1,10 @@
1
1
  import type { OpenAPI } from '@scalar/openapi-types';
2
2
  /**
3
- * Extract path from URL
3
+ * Extract path from URL. Metadata is fetched at its exact declared URL, so its routes
4
+ * preserve trailing slashes instead of using the token-route normalization.
4
5
  */
5
- export declare function getPathFromUrl(url: string): string;
6
+ export declare function getPathFromUrl(url: string, { preserveTrailingSlash }?: {
7
+ preserveTrailingSlash?: boolean;
8
+ }): string;
6
9
  export declare function getOpenAuthTokenUrls(schema?: OpenAPI.Document): string[];
7
10
  //# sourceMappingURL=get-open-auth-token-urls.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"get-open-auth-token-urls.d.ts","sourceRoot":"","sources":["../../src/utils/get-open-auth-token-urls.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAA0B,MAAM,uBAAuB,CAAA;AAG5E;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAYlD;AAiBD,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,QAAQ,GAAG,MAAM,EAAE,CAuCxE"}
1
+ {"version":3,"file":"get-open-auth-token-urls.d.ts","sourceRoot":"","sources":["../../src/utils/get-open-auth-token-urls.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAA0B,MAAM,uBAAuB,CAAA;AAG5E;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,EACX,EAAE,qBAA6B,EAAE,GAAE;IAAE,qBAAqB,CAAC,EAAE,OAAO,CAAA;CAAO,GAC1E,MAAM,CAYR;AAiBD,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,QAAQ,GAAG,MAAM,EAAE,CAuCxE"}
@@ -1,14 +1,15 @@
1
1
  import { getResolvedRef } from '@scalar/workspace-store/helpers/get-resolved-ref';
2
2
  /**
3
- * Extract path from URL
3
+ * Extract path from URL. Metadata is fetched at its exact declared URL, so its routes
4
+ * preserve trailing slashes instead of using the token-route normalization.
4
5
  */
5
- export function getPathFromUrl(url) {
6
+ export function getPathFromUrl(url, { preserveTrailingSlash = false } = {}) {
6
7
  try {
7
8
  // Handle relative URLs by prepending a base
8
9
  const urlObject = url.startsWith('http') ? new URL(url) : new URL(url, 'http://example.com');
9
10
  // Normalize: remove trailing slash except for root path
10
11
  const path = urlObject.pathname;
11
- return path === '/' ? path : path.replace(/\/$/, '');
12
+ return preserveTrailingSlash || path === '/' ? path : path.replace(/\/$/, '');
12
13
  }
13
14
  catch {
14
15
  // If URL is invalid, return the original string
@@ -1 +1 @@
1
- {"version":3,"file":"log-authentication-instructions.d.ts","sourceRoot":"","sources":["../../src/utils/log-authentication-instructions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAGxD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAI/C;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAC3C,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,oBAAoB,CAAC,EACjE,GAAG,GAAE,gBAA8C,QAwHpD"}
1
+ {"version":3,"file":"log-authentication-instructions.d.ts","sourceRoot":"","sources":["../../src/utils/log-authentication-instructions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAGxD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAI/C;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAC3C,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,oBAAoB,CAAC,EACjE,GAAG,GAAE,gBAA8C,QAiIpD"}
@@ -66,6 +66,13 @@ export function logAuthenticationInstructions(securitySchemes, log = (line) => c
66
66
  }
67
67
  break;
68
68
  case 'oauth2':
69
+ if ('oauth2MetadataUrl' in scheme &&
70
+ typeof scheme.oauth2MetadataUrl === 'string' &&
71
+ scheme.oauth2MetadataUrl.trim()) {
72
+ log('✅ OAuth 2.0 Authorization Server Metadata');
73
+ log(` GET ${getPathFromUrl(scheme.oauth2MetadataUrl, { preserveTrailingSlash: true })}`);
74
+ log('');
75
+ }
69
76
  if (scheme.flows) {
70
77
  Object.keys(scheme.flows).forEach((flow) => {
71
78
  switch (flow) {
@@ -1 +1 @@
1
- {"version":3,"file":"set-up-authentication-routes.d.ts","sourceRoot":"","sources":["../../src/utils/set-up-authentication-routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAA0B,MAAM,uBAAuB,CAAA;AAE5E,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAOhC;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,QAAQ,QAmG7E"}
1
+ {"version":3,"file":"set-up-authentication-routes.d.ts","sourceRoot":"","sources":["../../src/utils/set-up-authentication-routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAA0B,MAAM,uBAAuB,CAAA;AAE5E,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAQhC;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,QAAQ,QA+G7E"}
@@ -1,6 +1,7 @@
1
1
  import { getResolvedRef } from '@scalar/workspace-store/helpers/get-resolved-ref';
2
2
  import { respondWithAuthorizePage } from '../routes/respond-with-authorize-page.js';
3
3
  import { respondWithToken } from '../routes/respond-with-token.js';
4
+ import { getOAuth2Metadata } from './get-oauth2-metadata.js';
4
5
  import { getOpenAuthTokenUrls, getPathFromUrl } from './get-open-auth-token-urls.js';
5
6
  /**
6
7
  * Helper function to set up authentication routes for OAuth 2.0 flows
@@ -36,11 +37,20 @@ export function setUpAuthenticationRoutes(app, schema) {
36
37
  return;
37
38
  }
38
39
  if (scheme.type === 'oauth2') {
40
+ if ('oauth2MetadataUrl' in scheme &&
41
+ typeof scheme.oauth2MetadataUrl === 'string' &&
42
+ scheme.oauth2MetadataUrl.trim()) {
43
+ const metadataPath = getPathFromUrl(scheme.oauth2MetadataUrl, { preserveTrailingSlash: true });
44
+ if (schema?.paths && Object.hasOwn(schema.paths, metadataPath)) {
45
+ console.warn(`OAuth2 metadata route "${metadataPath}" collides with a declared API path.`);
46
+ }
47
+ app.get(metadataPath, (c) => c.json(getOAuth2Metadata(scheme.flows, new URL(c.req.url).origin)));
48
+ }
39
49
  if (scheme.flows?.authorizationCode) {
40
50
  const authorizeRoute = scheme.flows.authorizationCode.authorizationUrl ?? '/oauth/authorize';
41
51
  const tokenRoute = scheme.flows.authorizationCode.tokenUrl ?? '/oauth/token';
42
52
  authorizeUrls.add(getPathFromUrl(authorizeRoute));
43
- tokenUrls.add(tokenRoute);
53
+ tokenUrls.add(getPathFromUrl(tokenRoute));
44
54
  }
45
55
  if (scheme.flows?.implicit) {
46
56
  const authorizeRoute = scheme.flows.implicit.authorizationUrl ?? '/oauth/authorize';
@@ -48,11 +58,11 @@ export function setUpAuthenticationRoutes(app, schema) {
48
58
  }
49
59
  if (scheme.flows?.password) {
50
60
  const tokenRoute = scheme.flows.password.tokenUrl ?? '/oauth/token';
51
- tokenUrls.add(tokenRoute);
61
+ tokenUrls.add(getPathFromUrl(tokenRoute));
52
62
  }
53
63
  if (scheme.flows?.clientCredentials) {
54
64
  const tokenRoute = scheme.flows.clientCredentials.tokenUrl ?? '/oauth/token';
55
- tokenUrls.add(tokenRoute);
65
+ tokenUrls.add(getPathFromUrl(tokenRoute));
56
66
  }
57
67
  }
58
68
  else if (scheme.type === 'openIdConnect') {
@@ -74,7 +84,7 @@ export function setUpAuthenticationRoutes(app, schema) {
74
84
  const authorizeRoute = '/oauth/authorize';
75
85
  const tokenRoute = '/oauth/token';
76
86
  authorizeUrls.add(getPathFromUrl(authorizeRoute));
77
- tokenUrls.add(tokenRoute);
87
+ tokenUrls.add(getPathFromUrl(tokenRoute));
78
88
  }
79
89
  }
80
90
  });
package/package.json CHANGED
@@ -16,7 +16,7 @@
16
16
  "swagger",
17
17
  "cli"
18
18
  ],
19
- "version": "0.14.1",
19
+ "version": "0.14.3",
20
20
  "engines": {
21
21
  "node": ">=22"
22
22
  },
@@ -53,17 +53,17 @@
53
53
  "dependencies": {
54
54
  "@faker-js/faker": "10.4.0",
55
55
  "@hono/node-ws": "^1.2.0",
56
+ "@scalar/helpers": "0.12.0",
57
+ "@scalar/json-magic": "0.14.0",
58
+ "@scalar/openapi-types": "0.9.6",
59
+ "@scalar/openapi-upgrader": "0.2.17",
60
+ "@scalar/types": "0.20.0",
61
+ "@scalar/workspace-store": "0.63.0",
56
62
  "ajv": "^8.20.0",
57
63
  "ajv-formats": "^3.0.1",
58
64
  "hono": "^4.13.7",
59
65
  "quickjs-emscripten": "0.32.0",
60
- "yaml": "^2.9.0",
61
- "@scalar/helpers": "0.12.0",
62
- "@scalar/json-magic": "0.13.5",
63
- "@scalar/openapi-types": "0.9.6",
64
- "@scalar/openapi-upgrader": "0.2.16",
65
- "@scalar/workspace-store": "0.61.0",
66
- "@scalar/types": "0.20.0"
66
+ "yaml": "^2.9.0"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@types/node": "^24.1.0",