diff --git a/schema/staticwebapp.config.json b/schema/staticwebapp.config.json new file mode 100644 index 00000000..9b83b475 --- /dev/null +++ b/schema/staticwebapp.config.json @@ -0,0 +1,613 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "additionalProperties": false, + "default": { + "navigationFallback": { + "rewrite": "/index.html" + } + }, + "definitions": { + "route": { + "type": "object", + "required": ["route"], + "properties": { + "route": { + "type": "string", + "description": "Request route pattern to match. May contain valid wildcards. See documentation: https://aka.ms/swa/config-schema" + }, + "methods": { + "type": "array", + "description": "Request method(s) to match", + "items": { + "anyOf": [ + { + "type": "string", + "enum": ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "CONNECT", "OPTIONS", "TRACE"] + } + ] + } + }, + "allowedRoles": { + "type": "array", + "description": "Roles that are allowed to access this route. If not empty, only role(s) listed are authorized to access the route. Roles are only used for authorization; they are not used to evaluate whether the route matches the request.", + "items": { + "anyOf": [ + { + "type": "string", + "examples": ["anonymous", "authenticated"] + } + ] + } + }, + "headers": { + "type": "object", + "description": "Override any matching global headers", + "additionalProperties": true + }, + "redirect": { + "type": "string", + "description": "Redirect to a relative or absolute path, or an external URI. Default status code is 302, override with 301." + }, + "statusCode": { + "type": "integer", + "description": "Status code override" + }, + "rewrite": { + "type": "string", + "description": "A path to rewrite the request route to" + } + }, + "additionalProperties": false + }, + "auth": { + "type": "object", + "required": ["identityProviders"], + "properties": { + "rolesSource": { + "type": "string", + "description": "Route to API function for assigning roles. For example, \"/api/GetRoles\". See https://aka.ms/swa-roles-function" + }, + "identityProviders": { + "type": "object", + "properties": { + "azureActiveDirectory": { + "type": "object", + "required": ["registration"], + "properties": { + "enabled": { + "description": " if the azureActiveDirectory provider is not enabled, otherwise", + "type": "boolean", + "default": true + }, + "registration": { + "type": "object", + "required": ["openIdIssuer", "clientSecretSettingName"], + "properties": { + "openIdIssuer": { + "type": "string", + "description": "The endpoint for the OpenID configuration of the AAD tenant" + }, + "clientIdSettingName": { + "type": "string", + "description": "The name of the application setting containing the Application (client) ID for the Azure AD app registration" + }, + "clientSecretSettingName": { + "type": "string", + "description": "The name of the application setting containing the client secret for the Azure AD app registration" + } + }, + "additionalProperties": false + }, + "login": { + "type": "object", + "description": "", + "properties": { + "loginParameters": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "userDetailsClaim": { + "type": "string", + "description": "The name of the claim from which we should read user details" + } + }, + "additionalProperties": false + }, + "apple": { + "type": "object", + "required": ["registration"], + "properties": { + "enabled": { + "description": " if the apple provider is not enabled, otherwise", + "type": "boolean", + "default": true + }, + "registration": { + "type": "object", + "required": ["clientSecretSettingName"], + "properties": { + "clientIdSettingName": { + "type": "string", + "description": "The name of the application setting containing the Client ID" + }, + "clientSecretSettingName": { + "type": "string", + "description": "The name of the application setting containing the Client Secret" + } + }, + "additionalProperties": false + }, + "login": { + "type": "object", + "description": "", + "properties": { + "scopes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "userDetailsClaim": { + "type": "string", + "description": "The name of the claim from which we should read user details" + } + }, + "additionalProperties": false + }, + "facebook": { + "type": "object", + "required": ["registration"], + "properties": { + "enabled": { + "description": " if the facebook provider is not enabled, otherwise", + "type": "boolean", + "default": true + }, + "registration": { + "type": "object", + "required": ["appSecretSettingName"], + "properties": { + "appIdSettingName": { + "type": "string", + "description": "The name of the application setting containing the App ID" + }, + "appSecretSettingName": { + "type": "string", + "description": "The name of the application setting containing the App Secret" + } + }, + "additionalProperties": false + }, + "login": { + "type": "object", + "description": "", + "properties": { + "scopes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "userDetailsClaim": { + "type": "string", + "description": "The name of the claim from which we should read user details" + } + }, + "additionalProperties": false + }, + "github": { + "type": "object", + "required": ["registration"], + "properties": { + "enabled": { + "description": " if the gitHub provider is not enabled, otherwise", + "type": "boolean", + "default": true + }, + "registration": { + "type": "object", + "required": ["clientSecretSettingName"], + "properties": { + "clientIdSettingName": { + "type": "string", + "description": "The name of the application setting containing the Client ID" + }, + "clientSecretSettingName": { + "type": "string", + "description": "The name of the application setting containing the Client Secret" + } + }, + "additionalProperties": false + }, + "login": { + "type": "object", + "description": "", + "properties": { + "scopes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "userDetailsClaim": { + "type": "string", + "description": "The name of the claim from which we should read user details" + } + }, + "additionalProperties": false + }, + "google": { + "type": "object", + "required": ["registration"], + "properties": { + "enabled": { + "description": " if the google provider is not enabled, otherwise", + "type": "boolean", + "default": true + }, + "registration": { + "type": "object", + "required": ["clientSecretSettingName"], + "properties": { + "clientIdSettingName": { + "type": "string", + "description": "The name of the application setting containing the Client ID" + }, + "clientSecretSettingName": { + "type": "string", + "description": "The name of the application setting containing the Client Secret" + } + }, + "additionalProperties": false + }, + "login": { + "type": "object", + "description": "", + "properties": { + "scopes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "userDetailsClaim": { + "type": "string", + "description": "The name of the claim from which we should read user details" + } + }, + "additionalProperties": false + }, + "twitter": { + "type": "object", + "required": ["registration"], + "properties": { + "enabled": { + "description": " if the twitter provider is not enabled, otherwise", + "type": "boolean", + "default": true + }, + "registration": { + "type": "object", + "required": ["consumerSecretSettingName"], + "properties": { + "consumerKeySettingName": { + "type": "string", + "description": "The name of the application setting containing the Consumer Key" + }, + "consumerSecretSettingName": { + "type": "string", + "description": "The name of the application setting containing the Consumer Secret" + } + }, + "additionalProperties": false + }, + "userDetailsClaim": { + "type": "string", + "description": "The name of the claim from which we should read user details" + } + }, + "additionalProperties": false + }, + "customOpenIdConnectProviders": { + "type": "object", + "patternProperties": { + ".*": { + "required": ["registration", "login"], + "properties": { + "enabled": { + "description": " if the custom OpenID Connect provider is not enabled, otherwise", + "type": "boolean", + "default": true + }, + "registration": { + "type": "object", + "required": ["clientCredential", "openIdConnectConfiguration"], + "properties": { + "clientIdSettingName": { + "type": "string", + "description": "The name of the application setting containing the Client ID" + }, + "clientCredential": { + "type": "object", + "required": ["clientSecretSettingName"], + "properties": { + "clientSecretSettingName": { + "type": "string", + "description": "The name of the application setting containing the Client Secret" + } + } + }, + "openIdConnectConfiguration": { + "type": "object", + "properties": { + "authorizationEndpoint": { + "type": "string", + "description": "The path to the authorization endpoint" + }, + "tokenEndpoint": { + "type": "string", + "description": "The path to the token endpoint" + }, + "issuer": { + "type": "string", + "description": "The path to the issuer endpoint" + }, + "certificationUri": { + "type": "string", + "description": "The path to the jwks uri" + }, + "wellKnownOpenIdConfiguration": { + "type": "string", + "description": "The path to the well known configuration endpoint" + } + } + } + }, + "additionalProperties": false + }, + "login": { + "type": "object", + "description": "", + "properties": { + "nameClaimType": { + "type": "string" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "loginParameterNames": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "description": "Documentation: https://aka.ms/swa/config-schema", + "id": "https://json.schemastore.org/staticwebapp.config.json", + "properties": { + "$schema": { + "type": "string", + "default": "https://json.schemastore.org/staticwebapp.config.json", + "description": "JSON schema" + }, + "routes": { + "type": "array", + "description": "Route definitions to modify routing behavior", + "default": [ + { + "route": "/example", + "rewrite": "/example.html" + } + ], + "items": { + "examples": [ + { + "route": "/example", + "rewrite": "/example.html" + }, + { + "route": "/login", + "redirect": "/.auth/login/github" + } + ], + "anyOf": [ + { + "allOf": [ + { + "$ref": "#/definitions/route" + } + ] + } + ] + } + }, + "navigationFallback": { + "type": "object", + "description": "A default file to return if the request does not match a resource", + "default": { + "rewrite": "/index.html" + }, + "required": ["rewrite"], + "properties": { + "rewrite": { + "type": "string", + "description": "The default file to return if the request does not match a resource", + "default": "/index.html" + }, + "exclude": { + "type": "array", + "description": "Paths to exclude from the fallback route. May use valid wildcards. https://aka.ms/swa/config-schema", + "examples": [["*.{jpg,gif,png}", "assets/*"]] + } + }, + "additionalProperties": false + }, + "responseOverrides": { + "type": "object", + "description": "Custom error pages or redirects", + "examples": [ + { + "404": { + "rewrite": "/custom_404.html", + "statusCode": 200 + } + } + ], + "propertyNames": { + "pattern": "^\\d+$" + }, + "patternProperties": { + ".*": { + "oneOf": [ + { + "type": "object", + "properties": { + "redirect": { + "type": "string", + "description": "Redirect to a relative or absolute path, or an external URI. Default status code is 302, override with 301." + }, + "statusCode": { + "type": "integer", + "description": "Status code" + }, + "rewrite": { + "type": "string", + "description": "A path to rewrite the request route to" + } + } + } + ] + } + } + }, + "mimeTypes": { + "type": "object", + "description": "Custom mime types configuration", + "default": {}, + "examples": [ + { + ".config": "application/xml" + } + ], + "patternProperties": { + "^\\..+$": { + "type": "string" + } + }, + "additionalProperties": false + }, + "globalHeaders": { + "type": "object", + "description": "Default headers to set on all responses", + "additionalProperties": true + }, + "auth": { + "$ref": "#/definitions/auth" + }, + "networking": { + "type": "object", + "description": "Networking configuration", + "properties": { + "allowedIpRanges": { + "type": "array", + "description": "Restrict access to one or more IPv4 ranges. Supports CIDR notation (e.g., \"192.168.100.14/24\")", + "items": { + "type": "string" + }, + "examples": [["10.0.0.0/24", "192.1.1.1/10"]] + } + }, + "additionalProperties": false + }, + "forwardingGateway": { + "type": "object", + "description": "Forwarding gateway configuration", + "properties": { + "allowedForwardedHosts": { + "type": "array", + "description": "The value of `X-Forwarded-Host` to allow to be used when generating redirect URLs", + "items": { + "type": "string" + }, + "examples": [["example.org", "www.example.org", "staging.example.org"]] + }, + "requiredHeaders": { + "type": "object", + "description": "HTTP header name/value pairs that are required for access", + "examples": [ + { + "X-Azure-FDID": "10dd26ef" + } + ], + "additionalProperties": true + } + }, + "additionalProperties": false + }, + "platform": { + "type": "object", + "description": "Platform configuration", + "properties": { + "apiRuntime": { + "type": "string", + "enum": [ + "dotnet:3.1", + "dotnet:6.0", + "dotnet-isolated:6.0", + "dotnet-isolated:7.0", + "node:12", + "node:14", + "node:16", + "node:18", + "python:3.8", + "python:3.9", + "python:3.10" + ], + "description": "Language runtime for the managed functions API" + } + }, + "additionalProperties": false + }, + "trailingSlash": { + "type": "string", + "enum": ["always", "never", "auto"], + "description": "Trailing slash configuration" + } + }, + "title": "Azure Static Web Apps configuration file", + "type": "object" +} diff --git a/src/core/constants.ts b/src/core/constants.ts index 5c5a88f5..31059340 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -226,6 +226,8 @@ export const SWA_CONFIG_FILENAME = "staticwebapp.config.json"; export const SWA_CONFIG_FILENAME_LEGACY = "routes.json"; export const CUSTOM_URL_SCHEME = "swa://"; export const OVERRIDABLE_ERROR_CODES = [400, 401, 403, 404]; +export const SWA_CONFIG_SCHEME_URL = "https://json.schemastore.org/staticwebapp.config.json"; +export const SWA_CONFIG_SCHEME_FALLBACK_PATH = path.join(__dirname, "..", "..", "schema", SWA_CONFIG_FILENAME); // Constants related to Api runtime export const DEFAULT_VERSION = { diff --git a/src/core/utils/user-config.ts b/src/core/utils/user-config.ts index 622c8767..a2265860 100644 --- a/src/core/utils/user-config.ts +++ b/src/core/utils/user-config.ts @@ -5,7 +5,13 @@ import type http from "http"; import jsonMap from "json-source-map"; import fetch, { RequestInit } from "node-fetch"; import path from "path"; -import { SWA_CONFIG_FILENAME, SWA_CONFIG_FILENAME_LEGACY, SWA_RUNTIME_CONFIG_MAX_SIZE_IN_KB } from "../constants"; +import { + SWA_CONFIG_FILENAME, + SWA_CONFIG_FILENAME_LEGACY, + SWA_RUNTIME_CONFIG_MAX_SIZE_IN_KB, + SWA_CONFIG_SCHEME_URL, + SWA_CONFIG_SCHEME_FALLBACK_PATH, +} from "../constants"; import { logger } from "./logger"; import { isHttpUrl } from "./net"; const { readdir, readFile, stat } = fs.promises; @@ -165,11 +171,10 @@ function findLineAndColumnByPosition(content: string, position: number | undefin } async function loadSWAConfigSchema(): Promise | null> { - const schemaUrl = "https://json.schemastore.org/staticwebapp.config.json"; try { - const res = await fetch(schemaUrl, { timeout: 10 * 1000 } as RequestInit); + const res = await fetch(SWA_CONFIG_SCHEME_URL, { timeout: 10 * 1000 } as RequestInit); if (res.status === 200) { - logger.silly(`Schema loaded successfully from ${schemaUrl}`); + logger.silly(`Schema loaded successfully from ${SWA_CONFIG_SCHEME_URL}`); return (await res.json()) as JSONSchemaType; } logger.silly(`Status: ${res.status} ${res.statusText}.`); @@ -177,7 +182,18 @@ async function loadSWAConfigSchema(): Promise | logger.warn((err as any).message); } - logger.silly(`Failed to load schema from ${schemaUrl}`); + logger.warn(`Warning: Failed to load schema from ${SWA_CONFIG_SCHEME_URL}. Try to load fallback schema locally.`); + + try { + const data = fs.readFileSync(SWA_CONFIG_SCHEME_FALLBACK_PATH, "utf8"); + const config = JSON.parse(data); + logger.silly(`Schema loaded successfully from ${SWA_CONFIG_SCHEME_FALLBACK_PATH}`); + return config; + } catch (err) { + logger.warn((err as any).message); + logger.warn(`Warning: Failed to load staticwebapp.config.json schema from ${SWA_CONFIG_SCHEME_FALLBACK_PATH}.`); + } + return null; }