@scalar/types
Version:
Types to work with Scalar packages
201 lines (200 loc) • 9.22 kB
JavaScript
import { z } from 'zod';
import { nanoidSchema } from '../utils/nanoid.js';
// ---------------------------------------------------------------------------
// COMMON PROPS FOR ALL SECURITY SCHEMES
/** Some common properties used in all security schemes */
const commonProps = z.object({
/* A description for security scheme. CommonMark syntax MAY be used for rich text representation. */
description: z.string().optional(),
});
const extendedSecuritySchema = z.object({
uid: nanoidSchema.brand(),
/** The name key that links a security requirement to a security object */
nameKey: z.string().optional().default(''),
});
// ---------------------------------------------------------------------------
// API KEY
const securitySchemeApiKeyIn = ['query', 'header', 'cookie'];
const oasSecuritySchemeApiKey = commonProps.extend({
type: z.literal('apiKey'),
/** REQUIRED. The name of the header, query or cookie parameter to be used. */
name: z.string().optional().default(''),
/** REQUIRED. The location of the API key. Valid values are "query", "header" or "cookie". */
in: z.enum(securitySchemeApiKeyIn).optional().default('header').catch('header'),
});
const apiKeyValueSchema = z.object({
value: z.string().default(''),
});
export const securityApiKeySchema = oasSecuritySchemeApiKey.merge(extendedSecuritySchema).merge(apiKeyValueSchema);
// ---------------------------------------------------------------------------
// HTTP
const oasSecuritySchemeHttp = commonProps.extend({
type: z.literal('http'),
/**
* REQUIRED. The name of the HTTP Authorization scheme to be used in the Authorization header as defined in
* [RFC7235]. The values used SHOULD be registered in the IANA Authentication Scheme registry.
*/
scheme: z
.string()
.toLowerCase()
.pipe(z.enum(['basic', 'bearer']))
.optional()
.default('basic'),
/**
* A hint to the client to identify how the bearer token is formatted.
* Bearer tokens are usually generated by an authorization server, so
* this information is primarily for documentation purposes.
*/
bearerFormat: z
.union([z.literal('JWT'), z.string()])
.optional()
.default('JWT'),
});
const httpValueSchema = z.object({
username: z.string().default(''),
password: z.string().default(''),
token: z.string().default(''),
});
export const securityHttpSchema = oasSecuritySchemeHttp.merge(extendedSecuritySchema).merge(httpValueSchema);
// ---------------------------------------------------------------------------
// OPENID CONNECT
const oasSecuritySchemeOpenId = commonProps.extend({
type: z.literal('openIdConnect'),
/**
* REQUIRED. OpenId Connect URL to discover OAuth2 configuration values. This MUST be in the
* form of a URL. The OpenID Connect standard requires the use of TLS.
*/
openIdConnectUrl: z.string().optional().default(''),
});
export const securityOpenIdSchema = oasSecuritySchemeOpenId.merge(extendedSecuritySchema);
// ---------------------------------------------------------------------------
/**
* REQUIRED. The authorization URL to be used for this flow. This MUST be in
* the form of a URL. The OAuth2 standard requires the use of TLS.
*/
const authorizationUrl = z.string().default('');
/**
* REQUIRED. The token URL to be used for this flow. This MUST be in the
* form of a URL. The OAuth2 standard requires the use of TLS.
*/
const tokenUrl = z.string().default('');
/** Common properties used across all oauth2 flows */
const flowsCommon = z.object({
/**
* The URL to be used for obtaining refresh tokens. This MUST be in the form of a
* URL. The OAuth2 standard requires the use of TLS.
*/
'refreshUrl': z.string().optional().default(''),
/**
* REQUIRED. The available scopes for the OAuth2 security scheme. A map
* between the scope name and a short description for it. The map MAY be empty.
*/
'scopes': z.record(z.string(), z.string().optional().default('')).optional().default({}).catch({}),
'selectedScopes': z.array(z.string()).optional().default([]),
/** Extension to save the client Id associated with an oauth flow */
'x-scalar-client-id': z.string().optional().default(''),
/** The auth token */
'token': z.string().default(''),
/** Additional query parameters for the OAuth authorization request. Example: { prompt: 'consent', audience: 'scalar' }. */
'x-scalar-security-query': z.record(z.string(), z.string()).optional(),
/** Additional body parameters for the OAuth token request. Example: { audience: 'foo' }. */
'x-scalar-security-body': z.record(z.string(), z.string()).optional(),
/** Extension to specify custom token name in the response (defaults to 'access_token') */
'x-tokenName': z.string().optional(),
});
/** Setup a default redirect uri if we can */
const defaultRedirectUri = typeof window !== 'undefined' ? window.location.origin + window.location.pathname : '';
/** Options for the x-usePkce extension */
export const pkceOptions = ['SHA-256', 'plain', 'no'];
const credentialsLocationExtension = z.enum(['header', 'body']).optional();
/** Oauth2 security scheme */
const oasSecuritySchemeOauth2 = commonProps.extend({
type: z.literal('oauth2'),
/** The default scopes for the oauth flow */
'x-default-scopes': z.array(z.string()).optional(),
/** REQUIRED. An object containing configuration information for the flow types supported. */
flows: z
.object({
/** Configuration for the OAuth Implicit flow */
implicit: flowsCommon.extend({
'type': z.literal('implicit').default('implicit'),
authorizationUrl,
'x-scalar-redirect-uri': z.string().optional().default(defaultRedirectUri),
}),
/** Configuration for the OAuth Resource Owner Password flow */
password: flowsCommon.extend({
type: z.literal('password').default('password'),
tokenUrl,
clientSecret: z.string().default(''),
username: z.string().default(''),
password: z.string().default(''),
'x-scalar-credentials-location': credentialsLocationExtension,
}),
/** Configuration for the OAuth Client Credentials flow. Previously called application in OpenAPI 2.0. */
clientCredentials: flowsCommon.extend({
type: z.literal('clientCredentials').default('clientCredentials'),
tokenUrl,
clientSecret: z.string().default(''),
'x-scalar-credentials-location': credentialsLocationExtension,
}),
/** Configuration for the OAuth Authorization Code flow. Previously called accessCode in OpenAPI 2.0.*/
authorizationCode: flowsCommon.extend({
'type': z.literal('authorizationCode').default('authorizationCode'),
authorizationUrl,
'x-usePkce': z.enum(pkceOptions).optional().default('no'),
'x-scalar-redirect-uri': z.string().optional().default(defaultRedirectUri),
tokenUrl,
clientSecret: z.string().default(''),
'x-scalar-credentials-location': credentialsLocationExtension,
}),
})
.partial()
.default({
implicit: {
selectedScopes: [],
scopes: {},
'x-scalar-client-id': '',
refreshUrl: '',
token: '',
type: 'implicit',
authorizationUrl: 'http://localhost:8080',
'x-scalar-redirect-uri': defaultRedirectUri,
},
}),
});
export const securityOauthSchema = oasSecuritySchemeOauth2.merge(extendedSecuritySchema);
// ---------------------------------------------------------------------------
// Final Types
/**
* Security Requirement
* Lists the required security schemes to execute this operation OR the whole collection/spec.
* The name used for each property MUST correspond to a security scheme declared in the Security
* Schemes under the Components Object.
*
* The key (name) here will be matched to the key of the securityScheme for linking
*
* @see https://spec.openapis.org/oas/latest.html#security-requirement-object
*/
export const oasSecurityRequirementSchema = z.record(z.string(), z.array(z.string()).optional().default([]));
/** OAS Compliant security schemes */
export const oasSecuritySchemeSchema = z.union([
oasSecuritySchemeApiKey,
oasSecuritySchemeHttp,
oasSecuritySchemeOauth2,
oasSecuritySchemeOpenId,
]);
/** Extended security schemes for workspace usage */
export const securitySchemeSchema = z
.discriminatedUnion('type', [securityApiKeySchema, securityHttpSchema, securityOpenIdSchema, securityOauthSchema])
.transform((data) => {
// Set selected scopes from x-default-scopes
if (data.type === 'oauth2' && data['x-default-scopes']?.length) {
const keys = Object.keys(data.flows);
keys.forEach((key) => {
if (data.flows[key]?.selectedScopes && data['x-default-scopes']) {
data.flows[key].selectedScopes = [data['x-default-scopes']].flat();
}
});
}
return data;
});