diff --git a/fern.schema.json b/fern.schema.json index 550823626108..e21d8fc2f1bf 100644 --- a/fern.schema.json +++ b/fern.schema.json @@ -3240,6 +3240,254 @@ } ] }, + "webhooks.WebhookSignatureAlgorithmSchema": { + "type": "string", + "enum": [ + "sha256", + "sha1", + "sha384", + "sha512" + ] + }, + "webhooks.WebhookSignatureEncodingSchema": { + "type": "string", + "enum": [ + "base64", + "hex" + ] + }, + "webhooks.WebhookPayloadComponentSchema": { + "type": "string", + "enum": [ + "body", + "timestamp", + "notification-url", + "message-id" + ] + }, + "webhooks.WebhookPayloadFormatSchema": { + "type": "object", + "properties": { + "components": { + "type": "array", + "items": { + "$ref": "#/definitions/webhooks.WebhookPayloadComponentSchema" + } + }, + "delimiter": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "components" + ], + "additionalProperties": false + }, + "webhooks.WebhookTimestampFormatSchema": { + "type": "string", + "enum": [ + "unix-seconds", + "unix-millis", + "iso8601" + ] + }, + "webhooks.WebhookTimestampSchema": { + "type": "object", + "properties": { + "header": { + "type": "string" + }, + "format": { + "oneOf": [ + { + "$ref": "#/definitions/webhooks.WebhookTimestampFormatSchema" + }, + { + "type": "null" + } + ] + }, + "tolerance": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "header" + ], + "additionalProperties": false + }, + "webhooks.AsymmetricAlgorithmSchema": { + "type": "string", + "enum": [ + "rsa-sha256", + "rsa-sha384", + "rsa-sha512", + "ecdsa-sha256", + "ecdsa-sha384", + "ecdsa-sha512", + "ed25519" + ] + }, + "webhooks.WebhookSignatureSchema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "hmac", + "asymmetric" + ] + } + }, + "oneOf": [ + { + "properties": { + "type": { + "const": "hmac" + }, + "header": { + "type": "string" + }, + "algorithm": { + "oneOf": [ + { + "$ref": "#/definitions/webhooks.WebhookSignatureAlgorithmSchema" + }, + { + "type": "null" + } + ] + }, + "encoding": { + "oneOf": [ + { + "$ref": "#/definitions/webhooks.WebhookSignatureEncodingSchema" + }, + { + "type": "null" + } + ] + }, + "signature-prefix": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "payload-format": { + "oneOf": [ + { + "$ref": "#/definitions/webhooks.WebhookPayloadFormatSchema" + }, + { + "type": "null" + } + ] + }, + "timestamp": { + "oneOf": [ + { + "$ref": "#/definitions/webhooks.WebhookTimestampSchema" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "type", + "header" + ] + }, + { + "properties": { + "type": { + "const": "asymmetric" + }, + "header": { + "type": "string" + }, + "asymmetric-algorithm": { + "$ref": "#/definitions/webhooks.AsymmetricAlgorithmSchema" + }, + "encoding": { + "oneOf": [ + { + "$ref": "#/definitions/webhooks.WebhookSignatureEncodingSchema" + }, + { + "type": "null" + } + ] + }, + "signature-prefix": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "jwks-url": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "key-id-header": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "timestamp": { + "oneOf": [ + { + "$ref": "#/definitions/webhooks.WebhookTimestampSchema" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "type", + "header", + "asymmetric-algorithm" + ] + } + ] + }, "examples.ExampleWebhookCallSchema": { "type": "object", "properties": { @@ -3347,6 +3595,16 @@ "payload": { "$ref": "#/definitions/webhooks.WebhookPayloadSchema" }, + "signature": { + "oneOf": [ + { + "$ref": "#/definitions/webhooks.WebhookSignatureSchema" + }, + { + "type": "null" + } + ] + }, "response": { "oneOf": [ { diff --git a/fern/apis/fern-definition/definition/webhooks.yml b/fern/apis/fern-definition/definition/webhooks.yml index 45af5a24c393..a574a73d58af 100644 --- a/fern/apis/fern-definition/definition/webhooks.yml +++ b/fern/apis/fern-definition/definition/webhooks.yml @@ -15,6 +15,7 @@ types: method: WebhookMethodSchema headers: optional> payload: WebhookPayloadSchema + signature: optional response: optional response-stream: optional examples: optional> @@ -42,3 +43,111 @@ types: name: string extends: optional properties: optional> + + WebhookSignatureSchema: + discriminant: type + union: + hmac: HmacSignatureSchema + asymmetric: AsymmetricSignatureSchema + + HmacSignatureSchema: + properties: + header: string + algorithm: + type: optional + docs: Defaults to sha256. + encoding: + type: optional + docs: Defaults to base64. + signature-prefix: + type: optional + docs: | + Prefix in the header value before the signature (e.g. "sha256="). + payload-format: + type: optional + docs: | + Defaults to body-only (components: [body], delimiter: ""). + timestamp: optional + + AsymmetricSignatureSchema: + properties: + header: string + asymmetric-algorithm: AsymmetricAlgorithmSchema + encoding: + type: optional + docs: Defaults to base64. + signature-prefix: + type: optional + docs: | + Prefix in the header value before the signature. + jwks-url: + type: optional + docs: JWKS endpoint URL. When omitted, a static public key is expected at runtime. + key-id-header: + type: optional + docs: HTTP header containing the key ID for JWKS key selection. + timestamp: optional + + WebhookSignatureAlgorithmSchema: + enum: + - sha256 + - sha1 + - sha384 + - sha512 + + WebhookSignatureEncodingSchema: + enum: + - base64 + - hex + + WebhookTimestampSchema: + properties: + header: string + format: + type: optional + docs: Defaults to unix-seconds. + tolerance: + type: optional + docs: Allowed clock skew in seconds. Defaults to 300. + + WebhookTimestampFormatSchema: + enum: + - name: unixSeconds + value: unix-seconds + - name: unixMillis + value: unix-millis + - name: iso8601 + value: iso8601 + + WebhookPayloadFormatSchema: + properties: + components: list + delimiter: + type: optional + docs: Defaults to empty string. + + WebhookPayloadComponentSchema: + enum: + - body + - timestamp + - name: notificationUrl + value: notification-url + - name: messageId + value: message-id + + AsymmetricAlgorithmSchema: + enum: + - name: rsaSha256 + value: rsa-sha256 + - name: rsaSha384 + value: rsa-sha384 + - name: rsaSha512 + value: rsa-sha512 + - name: ecdsaSha256 + value: ecdsa-sha256 + - name: ecdsaSha384 + value: ecdsa-sha384 + - name: ecdsaSha512 + value: ecdsa-sha512 + - name: ed25519 + value: ed25519 diff --git a/generators/go/internal/generator/model.go b/generators/go/internal/generator/model.go index ec5a9b3c2673..3125d8ab2b2b 100644 --- a/generators/go/internal/generator/model.go +++ b/generators/go/internal/generator/model.go @@ -1220,7 +1220,6 @@ func (t *typeVisitor) visitObjectProperties( dates = append(dates, extendedObjectProperties.dates...) } for _, property := range object.Properties { - t.writer.WriteDocs(property.Docs) if property.ValueType.Container != nil && property.ValueType.Container.Literal != nil { literals = append(literals, &literal{Name: property.Name, Value: property.ValueType.Container.Literal}) if !includeLiterals { @@ -1229,6 +1228,7 @@ func (t *typeVisitor) visitObjectProperties( } else { names = append(names, goExportedFieldName(property.Name.Name.PascalCase.UnsafeName)) } + t.writer.WriteDocs(property.Docs) if date := maybeDateProperty(property.ValueType, property.Name, false); date != nil { dates = append(dates, date) } diff --git a/generators/go/internal/generator/sdk.go b/generators/go/internal/generator/sdk.go index 31bba339a994..65239e5cbfa6 100644 --- a/generators/go/internal/generator/sdk.go +++ b/generators/go/internal/generator/sdk.go @@ -3132,7 +3132,6 @@ func (f *fileWriter) WriteRequestType( var literals []*literal f.P("type ", typeName, " struct {") for _, header := range append(serviceHeaders, endpoint.Headers...) { - f.WriteDocs(header.Docs) if header.ValueType.Container != nil && header.ValueType.Container.Literal != nil { literals = append( literals, @@ -3143,6 +3142,7 @@ func (f *fileWriter) WriteRequestType( ) continue } + f.WriteDocs(header.Docs) goType := typeReferenceToGoType(header.ValueType, f.types, f.scope, f.baseImportPath, importPath, false) f.P(goExportedFieldName(header.Name.Name.PascalCase.UnsafeName), " ", goType, " `json:\"-\" url:\"-\"`") } @@ -3158,7 +3158,6 @@ func (f *fileWriter) WriteRequestType( if queryParam.AllowMultiple { value = fmt.Sprintf("[]%s", value) } - f.WriteDocs(queryParam.Docs) if queryParam.ValueType.Container != nil && queryParam.ValueType.Container.Literal != nil { literals = append( literals, @@ -3169,6 +3168,7 @@ func (f *fileWriter) WriteRequestType( ) continue } + f.WriteDocs(queryParam.Docs) f.P(goExportedFieldName(queryParam.Name.Name.PascalCase.UnsafeName), " ", value, urlTagForType(queryParam.Name.WireValue, queryParam.ValueType, f.types, f.alwaysSendRequiredProperties)) } if endpoint.RequestBody == nil { diff --git a/generators/go/sdk/versions.yml b/generators/go/sdk/versions.yml index e160713cf747..716905577963 100644 --- a/generators/go/sdk/versions.yml +++ b/generators/go/sdk/versions.yml @@ -1,4 +1,15 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 1.25.0-rc2 + changelogEntry: + - summary: | + Fix literal property doc comments leaking into the next field's comments in generated + Go structs. When a property has a literal type (e.g. stream boolean) and is excluded + from the struct, its documentation was still being written, causing it to appear as + part of the next field's comments. + type: fix + createdAt: "2026-02-19" + irVersion: 61 + - version: 1.25.0-rc1 changelogEntry: - summary: | diff --git a/generators/java/sdk/versions.yml b/generators/java/sdk/versions.yml index a91a1f7710c0..a60bc576ef41 100644 --- a/generators/java/sdk/versions.yml +++ b/generators/java/sdk/versions.yml @@ -1,4 +1,15 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 3.38.1 + changelogEntry: + - summary: | + Preserve existing README.md when the generator does not produce one. Previously, + `overwriteLocalContents` deleted all files before copying generator output, so if + README generation failed silently the file was removed from the target repository. + The method now skips deleting README.md when the source directory does not include it. + type: fix + createdAt: "2026-02-23" + irVersion: 65 + - version: 3.38.0 changelogEntry: - summary: | diff --git a/generators/python-v2/sdk/src/SdkCustomConfig.ts b/generators/python-v2/sdk/src/SdkCustomConfig.ts index 8a523db4e06f..5cfb1457efae 100644 --- a/generators/python-v2/sdk/src/SdkCustomConfig.ts +++ b/generators/python-v2/sdk/src/SdkCustomConfig.ts @@ -50,6 +50,7 @@ export const SdkCustomConfigSchema = z.object({ /** @deprecated Use `wire_tests.enabled` instead */ enable_wire_tests: z.boolean().optional(), package_path: relativePathSchema.optional(), + package_name: z.string().optional(), client: ClientConfigSchema.optional(), client_class_name: z.string().optional(), inline_request_params: z.boolean().optional(), diff --git a/generators/python-v2/sdk/src/SdkGeneratorContext.ts b/generators/python-v2/sdk/src/SdkGeneratorContext.ts index e001bf5a713c..d2eb06ef6770 100644 --- a/generators/python-v2/sdk/src/SdkGeneratorContext.ts +++ b/generators/python-v2/sdk/src/SdkGeneratorContext.ts @@ -32,4 +32,23 @@ export class SdkGeneratorContext extends AbstractPythonGeneratorContext modulePath is ["seed"] - return [this.context.config.organization]; + return [this.context.getModulePath()]; } private getClientClassName(): string { diff --git a/generators/python-v2/sdk/src/wire-tests/WireTestSetupGenerator.ts b/generators/python-v2/sdk/src/wire-tests/WireTestSetupGenerator.ts index dac3ec07a473..39e54199bc1d 100644 --- a/generators/python-v2/sdk/src/wire-tests/WireTestSetupGenerator.ts +++ b/generators/python-v2/sdk/src/wire-tests/WireTestSetupGenerator.ts @@ -394,23 +394,10 @@ def pytest_unconfigure(config: pytest.Config) -> None: */ private getClientImport(): string { const clientClassName = this.getClientClassName(); - const modulePath = this.getModulePath(); + const modulePath = this.context.getModulePath(); return `from ${modulePath}.client import ${clientClassName}`; } - /** - * Gets the full module path including package_path if set. - */ - private getModulePath(): string { - const orgName = this.context.config.organization; - const packagePath = this.context.customConfig.package_path; - if (packagePath) { - const packagePathDotted = packagePath.replace(/\//g, "."); - return `${orgName}.${packagePathDotted}`; - } - return orgName; - } - /** * Builds the environment setup for the conftest.py file. * Returns an object with imports and the parameter to use in the client constructor. @@ -434,7 +421,7 @@ def pytest_unconfigure(config: pytest.Config) -> None: if (environments?.environments.type === "multipleBaseUrls") { const envConfig = environments.environments; const environmentClassName = this.getEnvironmentClassName(); - const modulePath = this.getModulePath(); + const modulePath = this.context.getModulePath(); // Build kwargs for all base URLs using dynamic base_url variable const baseUrlKwargsDynamic = envConfig.baseUrls diff --git a/generators/python/sdk/versions.yml b/generators/python/sdk/versions.yml index c48aa9e17f54..d9bad59755bf 100644 --- a/generators/python/sdk/versions.yml +++ b/generators/python/sdk/versions.yml @@ -1,5 +1,11 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json # For unreleased changes, use unreleased.yml +- version: 4.59.1 + changelogEntry: + - summary: Fix wire test imports to respect package_name custom config, preventing import errors when users specify custom package names + type: fix + createdAt: "2026-02-23" + irVersion: 65 - version: 4.59.0 changelogEntry: - summary: | diff --git a/package-yml.schema.json b/package-yml.schema.json index 38fcca39fb8f..1c6fca586369 100644 --- a/package-yml.schema.json +++ b/package-yml.schema.json @@ -3260,6 +3260,254 @@ } ] }, + "webhooks.WebhookSignatureAlgorithmSchema": { + "type": "string", + "enum": [ + "sha256", + "sha1", + "sha384", + "sha512" + ] + }, + "webhooks.WebhookSignatureEncodingSchema": { + "type": "string", + "enum": [ + "base64", + "hex" + ] + }, + "webhooks.WebhookPayloadComponentSchema": { + "type": "string", + "enum": [ + "body", + "timestamp", + "notification-url", + "message-id" + ] + }, + "webhooks.WebhookPayloadFormatSchema": { + "type": "object", + "properties": { + "components": { + "type": "array", + "items": { + "$ref": "#/definitions/webhooks.WebhookPayloadComponentSchema" + } + }, + "delimiter": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "components" + ], + "additionalProperties": false + }, + "webhooks.WebhookTimestampFormatSchema": { + "type": "string", + "enum": [ + "unix-seconds", + "unix-millis", + "iso8601" + ] + }, + "webhooks.WebhookTimestampSchema": { + "type": "object", + "properties": { + "header": { + "type": "string" + }, + "format": { + "oneOf": [ + { + "$ref": "#/definitions/webhooks.WebhookTimestampFormatSchema" + }, + { + "type": "null" + } + ] + }, + "tolerance": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "header" + ], + "additionalProperties": false + }, + "webhooks.AsymmetricAlgorithmSchema": { + "type": "string", + "enum": [ + "rsa-sha256", + "rsa-sha384", + "rsa-sha512", + "ecdsa-sha256", + "ecdsa-sha384", + "ecdsa-sha512", + "ed25519" + ] + }, + "webhooks.WebhookSignatureSchema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "hmac", + "asymmetric" + ] + } + }, + "oneOf": [ + { + "properties": { + "type": { + "const": "hmac" + }, + "header": { + "type": "string" + }, + "algorithm": { + "oneOf": [ + { + "$ref": "#/definitions/webhooks.WebhookSignatureAlgorithmSchema" + }, + { + "type": "null" + } + ] + }, + "encoding": { + "oneOf": [ + { + "$ref": "#/definitions/webhooks.WebhookSignatureEncodingSchema" + }, + { + "type": "null" + } + ] + }, + "signature-prefix": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "payload-format": { + "oneOf": [ + { + "$ref": "#/definitions/webhooks.WebhookPayloadFormatSchema" + }, + { + "type": "null" + } + ] + }, + "timestamp": { + "oneOf": [ + { + "$ref": "#/definitions/webhooks.WebhookTimestampSchema" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "type", + "header" + ] + }, + { + "properties": { + "type": { + "const": "asymmetric" + }, + "header": { + "type": "string" + }, + "asymmetric-algorithm": { + "$ref": "#/definitions/webhooks.AsymmetricAlgorithmSchema" + }, + "encoding": { + "oneOf": [ + { + "$ref": "#/definitions/webhooks.WebhookSignatureEncodingSchema" + }, + { + "type": "null" + } + ] + }, + "signature-prefix": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "jwks-url": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "key-id-header": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "timestamp": { + "oneOf": [ + { + "$ref": "#/definitions/webhooks.WebhookTimestampSchema" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "type", + "header", + "asymmetric-algorithm" + ] + } + ] + }, "examples.ExampleWebhookCallSchema": { "type": "object", "properties": { @@ -3367,6 +3615,16 @@ "payload": { "$ref": "#/definitions/webhooks.WebhookPayloadSchema" }, + "signature": { + "oneOf": [ + { + "$ref": "#/definitions/webhooks.WebhookSignatureSchema" + }, + { + "type": "null" + } + ] + }, "response": { "oneOf": [ { diff --git a/packages/cli/api-importers/openapi-to-ir/src/3.1/paths/operations/WebhookConverter.ts b/packages/cli/api-importers/openapi-to-ir/src/3.1/paths/operations/WebhookConverter.ts index 7ed1feabea3f..70dda04ea840 100644 --- a/packages/cli/api-importers/openapi-to-ir/src/3.1/paths/operations/WebhookConverter.ts +++ b/packages/cli/api-importers/openapi-to-ir/src/3.1/paths/operations/WebhookConverter.ts @@ -107,6 +107,7 @@ export class WebhookConverter extends AbstractOperationConverter { method: httpMethod, headers, payload, + signatureVerification: undefined, fileUploadPayload, responses: responses.length > 0 ? responses : undefined, examples: [], diff --git a/packages/cli/api-importers/openapi/openapi-ir-parser/src/openapi/v3/converters/operation/convertWebhookOperation.ts b/packages/cli/api-importers/openapi/openapi-ir-parser/src/openapi/v3/converters/operation/convertWebhookOperation.ts index 2f1e808ae879..fea60b8239fd 100644 --- a/packages/cli/api-importers/openapi/openapi-ir-parser/src/openapi/v3/converters/operation/convertWebhookOperation.ts +++ b/packages/cli/api-importers/openapi/openapi-ir-parser/src/openapi/v3/converters/operation/convertWebhookOperation.ts @@ -14,6 +14,7 @@ import { getGeneratedTypeName } from "../../../../schema/utils/getSchemaName.js" import { isReferenceObject } from "../../../../schema/utils/isReferenceObject.js"; import { AbstractOpenAPIV3ParserContext } from "../../AbstractOpenAPIV3ParserContext.js"; import { FernOpenAPIExtension } from "../../extensions/fernExtensions.js"; +import { getFernWebhookSignatureExtension } from "../../extensions/getFernWebhookSignatureExtension.js"; import { OperationContext } from "../contexts.js"; import { convertParameters } from "../endpoint/convertParameters.js"; import { convertRequest } from "../endpoint/convertRequest.js"; @@ -118,6 +119,8 @@ export function convertWebhookOperation({ payload = request.schema; } + const signatureVerification = getFernWebhookSignatureExtension(document, operation); + const webhook: WebhookWithExample = { summary: operation.summary, audiences: getExtension(operation, FernOpenAPIExtension.AUDIENCES) ?? [], @@ -129,6 +132,7 @@ export function convertWebhookOperation({ headers: convertedParameters.headers, generatedPayloadName: getGeneratedTypeName(payloadBreadcrumbs, context.options.preserveSchemaIds), payload, + signatureVerification, multipartFormData, response: convertedResponse?.value, description: operation.description, diff --git a/packages/cli/api-importers/openapi/openapi-ir-parser/src/openapi/v3/extensions/fernExtensions.ts b/packages/cli/api-importers/openapi/openapi-ir-parser/src/openapi/v3/extensions/fernExtensions.ts index a6ffe85d95ea..e46c504cb227 100644 --- a/packages/cli/api-importers/openapi/openapi-ir-parser/src/openapi/v3/extensions/fernExtensions.ts +++ b/packages/cli/api-importers/openapi/openapi-ir-parser/src/openapi/v3/extensions/fernExtensions.ts @@ -97,6 +97,38 @@ export const FernOpenAPIExtension = { */ TYPE_DEFINITION: "x-fern-type", + /** + * Used to specify webhook signature verification configuration. + * Can be set at the document level (applies to all webhooks) or + * on individual webhook operations (overrides the document default). + * + * Document-level usage (all webhooks inherit): + * x-fern-webhook-signature: + * type: hmac + * header: x-webhook-signature + * algorithm: sha256 + * encoding: hex + * + * Operation-level usage (HMAC): + * x-fern-webhook-signature: + * type: hmac + * header: x-hub-signature-256 + * algorithm: sha256 + * encoding: hex + * signature-prefix: "sha256=" + * + * Operation-level usage (asymmetric): + * x-fern-webhook-signature: + * type: asymmetric + * header: x-signature + * asymmetric-algorithm: rsa-sha256 + * jwks-url: https://api.example.com/.well-known/jwks.json + * + * Inherit document-level config explicitly: + * x-fern-webhook-signature: true + */ + WEBHOOK_SIGNATURE: "x-fern-webhook-signature", + /** * Used to specify if an endpoint should be generated * as a streaming endpoint. diff --git a/packages/cli/api-importers/openapi/openapi-ir-parser/src/openapi/v3/extensions/getFernWebhookSignatureExtension.ts b/packages/cli/api-importers/openapi/openapi-ir-parser/src/openapi/v3/extensions/getFernWebhookSignatureExtension.ts new file mode 100644 index 000000000000..114a8e67a2a0 --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir-parser/src/openapi/v3/extensions/getFernWebhookSignatureExtension.ts @@ -0,0 +1,202 @@ +import { finalIr } from "@fern-api/openapi-ir"; +import { OpenAPIV3 } from "openapi-types"; +import { getExtension } from "../../../getExtension.js"; +import { FernOpenAPIExtension } from "./fernExtensions.js"; + +type AsymmetricAlgorithmString = + | "rsa-sha256" + | "rsa-sha384" + | "rsa-sha512" + | "ecdsa-sha256" + | "ecdsa-sha384" + | "ecdsa-sha512" + | "ed25519"; + +interface WebhookTimestampExtensionSchema { + header: string; + format?: "unix-seconds" | "unix-millis" | "iso8601"; + tolerance?: number; +} + +interface WebhookPayloadFormatExtensionSchema { + components: Array<"body" | "timestamp" | "notification-url" | "message-id">; + delimiter?: string; +} + +interface WebhookSignatureExtensionSchema { + type: "hmac" | "asymmetric"; + header: string; + // HMAC fields + algorithm?: "sha256" | "sha1" | "sha384" | "sha512"; + encoding?: "base64" | "hex"; + "signature-prefix"?: string; + "payload-format"?: WebhookPayloadFormatExtensionSchema; + timestamp?: WebhookTimestampExtensionSchema; + // Asymmetric fields + "asymmetric-algorithm"?: AsymmetricAlgorithmString; + "jwks-url"?: string; + "key-id-header"?: string; +} + +function convertTimestampFormat(format: "unix-seconds" | "unix-millis" | "iso8601"): finalIr.WebhookTimestampFormat { + switch (format) { + case "unix-seconds": + return finalIr.WebhookTimestampFormat.UnixSeconds; + case "unix-millis": + return finalIr.WebhookTimestampFormat.UnixMillis; + case "iso8601": + return finalIr.WebhookTimestampFormat.Iso8601; + } +} + +function convertTimestamp( + timestamp: WebhookTimestampExtensionSchema | undefined +): finalIr.WebhookTimestamp | undefined { + if (timestamp == null) { + return undefined; + } + return { + header: timestamp.header, + format: timestamp.format != null ? convertTimestampFormat(timestamp.format) : undefined, + tolerance: timestamp.tolerance + }; +} + +function convertPayloadFormat( + payloadFormat: WebhookPayloadFormatExtensionSchema | undefined +): finalIr.WebhookPayloadFormat | undefined { + if (payloadFormat == null) { + return undefined; + } + return { + components: payloadFormat.components.map((component) => { + switch (component) { + case "body": + return finalIr.WebhookPayloadComponent.Body; + case "timestamp": + return finalIr.WebhookPayloadComponent.Timestamp; + case "notification-url": + return finalIr.WebhookPayloadComponent.NotificationUrl; + case "message-id": + return finalIr.WebhookPayloadComponent.MessageId; + } + }), + delimiter: payloadFormat.delimiter + }; +} + +function convertAlgorithm( + algorithm: "sha256" | "sha1" | "sha384" | "sha512" | undefined +): finalIr.WebhookSignatureAlgorithm | undefined { + if (algorithm == null) { + return undefined; + } + switch (algorithm) { + case "sha256": + return finalIr.WebhookSignatureAlgorithm.Sha256; + case "sha1": + return finalIr.WebhookSignatureAlgorithm.Sha1; + case "sha384": + return finalIr.WebhookSignatureAlgorithm.Sha384; + case "sha512": + return finalIr.WebhookSignatureAlgorithm.Sha512; + } +} + +function convertEncoding(encoding: "base64" | "hex" | undefined): finalIr.WebhookSignatureEncoding | undefined { + if (encoding == null) { + return undefined; + } + switch (encoding) { + case "base64": + return finalIr.WebhookSignatureEncoding.Base64; + case "hex": + return finalIr.WebhookSignatureEncoding.Hex; + } +} + +function convertAsymmetricAlgorithm( + algorithm: AsymmetricAlgorithmString | undefined +): finalIr.AsymmetricAlgorithm | undefined { + if (algorithm == null) { + return undefined; + } + switch (algorithm) { + case "rsa-sha256": + return finalIr.AsymmetricAlgorithm.RsaSha256; + case "rsa-sha384": + return finalIr.AsymmetricAlgorithm.RsaSha384; + case "rsa-sha512": + return finalIr.AsymmetricAlgorithm.RsaSha512; + case "ecdsa-sha256": + return finalIr.AsymmetricAlgorithm.EcdsaSha256; + case "ecdsa-sha384": + return finalIr.AsymmetricAlgorithm.EcdsaSha384; + case "ecdsa-sha512": + return finalIr.AsymmetricAlgorithm.EcdsaSha512; + case "ed25519": + return finalIr.AsymmetricAlgorithm.Ed25519; + } +} + +function convertSignatureSchema( + extension: WebhookSignatureExtensionSchema +): finalIr.WebhookSignatureVerification | undefined { + if (extension.type === "hmac") { + return finalIr.WebhookSignatureVerification.hmac({ + header: extension.header, + algorithm: convertAlgorithm(extension.algorithm), + encoding: convertEncoding(extension.encoding), + signaturePrefix: extension["signature-prefix"], + payloadFormat: convertPayloadFormat(extension["payload-format"]), + timestamp: convertTimestamp(extension.timestamp) + }); + } + + if (extension.type === "asymmetric") { + const asymmetricAlgorithm = convertAsymmetricAlgorithm(extension["asymmetric-algorithm"]); + if (asymmetricAlgorithm == null) { + return undefined; + } + return finalIr.WebhookSignatureVerification.asymmetric({ + header: extension.header, + asymmetricAlgorithm, + encoding: convertEncoding(extension.encoding), + signaturePrefix: extension["signature-prefix"], + jwksUrl: extension["jwks-url"], + keyIdHeader: extension["key-id-header"], + timestamp: convertTimestamp(extension.timestamp) + }); + } + + return undefined; +} + +function getDocumentLevelSignature(document: OpenAPIV3.Document): finalIr.WebhookSignatureVerification | undefined { + const extension = getExtension(document, FernOpenAPIExtension.WEBHOOK_SIGNATURE); + if (extension == null || typeof extension === "boolean") { + return undefined; + } + return convertSignatureSchema(extension); +} + +export function getFernWebhookSignatureExtension( + document: OpenAPIV3.Document, + operation: OpenAPIV3.OperationObject +): finalIr.WebhookSignatureVerification | undefined { + const extension = getExtension( + operation, + FernOpenAPIExtension.WEBHOOK_SIGNATURE + ); + + if (extension != null) { + if (typeof extension === "boolean") { + // Operation says "true" → inherit from document-level config + return getDocumentLevelSignature(document); + } + return convertSignatureSchema(extension); + } + + // No operation-level extension; fall back to document-level default for all webhooks + return getDocumentLevelSignature(document); +} diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir/x-fern-webhook-signature.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir/x-fern-webhook-signature.json new file mode 100644 index 000000000000..9f9692364cba --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir/x-fern-webhook-signature.json @@ -0,0 +1,368 @@ +{ + "title": "Webhook Signature Verification Test", + "servers": [], + "websocketServers": [], + "tags": { + "tagsById": {} + }, + "hasEndpointsMarkedInternal": false, + "endpoints": [], + "webhooks": [ + { + "audiences": [], + "sdkName": { + "groupName": [ + "webhooks" + ], + "methodName": "userCreated" + }, + "method": "POST", + "operationId": "userCreated", + "tags": [], + "headers": [], + "generatedPayloadName": "UserCreatedWebhooksPayload", + "payload": { + "generatedName": "UserCreatedWebhooksPayload", + "schema": "UserEvent", + "source": { + "file": "../openapi.yml", + "type": "openapi" + }, + "type": "reference" + }, + "signatureVerification": { + "header": "x-webhook-signature", + "algorithm": "sha256", + "encoding": "hex", + "signaturePrefix": "sha256=", + "payloadFormat": { + "components": [ + "timestamp", + "body" + ], + "delimiter": "." + }, + "timestamp": { + "header": "x-webhook-timestamp", + "format": "unix-seconds", + "tolerance": 300 + }, + "type": "hmac" + }, + "examples": [ + { + "payload": { + "properties": { + "userId": { + "value": { + "value": "userId", + "type": "string" + }, + "type": "primitive" + }, + "action": { + "value": { + "value": "action", + "type": "string" + }, + "type": "primitive" + } + }, + "type": "object" + } + } + ], + "source": { + "file": "../openapi.yml", + "type": "openapi" + } + }, + { + "audiences": [], + "sdkName": { + "groupName": [ + "webhooks" + ], + "methodName": "orderPlaced" + }, + "method": "POST", + "operationId": "orderPlaced", + "tags": [], + "headers": [], + "generatedPayloadName": "OrderPlacedWebhooksPayload", + "payload": { + "generatedName": "OrderPlacedWebhooksPayload", + "schema": "OrderEvent", + "source": { + "file": "../openapi.yml", + "type": "openapi" + }, + "type": "reference" + }, + "signatureVerification": { + "header": "x-webhook-signature", + "algorithm": "sha256", + "encoding": "hex", + "signaturePrefix": "sha256=", + "payloadFormat": { + "components": [ + "timestamp", + "body" + ], + "delimiter": "." + }, + "timestamp": { + "header": "x-webhook-timestamp", + "format": "unix-seconds", + "tolerance": 300 + }, + "type": "hmac" + }, + "examples": [ + { + "payload": { + "properties": { + "orderId": { + "value": { + "value": "orderId", + "type": "string" + }, + "type": "primitive" + } + }, + "type": "object" + } + } + ], + "source": { + "file": "../openapi.yml", + "type": "openapi" + } + }, + { + "audiences": [], + "sdkName": { + "groupName": [ + "webhooks" + ], + "methodName": "paymentProcessed" + }, + "method": "POST", + "operationId": "paymentProcessed", + "tags": [], + "headers": [], + "generatedPayloadName": "PaymentProcessedWebhooksPayload", + "payload": { + "generatedName": "PaymentProcessedWebhooksPayload", + "schema": "PaymentEvent", + "source": { + "file": "../openapi.yml", + "type": "openapi" + }, + "type": "reference" + }, + "signatureVerification": { + "header": "x-signature", + "asymmetricAlgorithm": "rsa-sha256", + "encoding": "base64", + "signaturePrefix": "rsa=", + "jwksUrl": "https://api.example.com/.well-known/jwks.json", + "keyIdHeader": "x-key-id", + "timestamp": { + "header": "x-timestamp", + "format": "iso8601" + }, + "type": "asymmetric" + }, + "examples": [ + { + "payload": { + "properties": { + "paymentId": { + "value": { + "value": "paymentId", + "type": "string" + }, + "type": "primitive" + }, + "amount": { + "value": { + "value": 1.1, + "type": "double" + }, + "type": "primitive" + } + }, + "type": "object" + } + } + ], + "source": { + "file": "../openapi.yml", + "type": "openapi" + } + } + ], + "channels": {}, + "groupedSchemas": { + "rootSchemas": { + "UserEvent": { + "allOf": [], + "properties": [ + { + "conflict": {}, + "generatedName": "userEventUserId", + "key": "userId", + "schema": { + "schema": { + "type": "string" + }, + "generatedName": "UserEventUserId", + "groupName": [], + "type": "primitive" + }, + "audiences": [] + }, + { + "conflict": {}, + "generatedName": "userEventAction", + "key": "action", + "schema": { + "schema": { + "type": "string" + }, + "generatedName": "UserEventAction", + "groupName": [], + "type": "primitive" + }, + "audiences": [] + } + ], + "allOfPropertyConflicts": [], + "generatedName": "UserEvent", + "groupName": [], + "additionalProperties": false, + "source": { + "file": "../openapi.yml", + "type": "openapi" + }, + "type": "object" + }, + "OrderEvent": { + "allOf": [], + "properties": [ + { + "conflict": {}, + "generatedName": "orderEventOrderId", + "key": "orderId", + "schema": { + "schema": { + "type": "string" + }, + "generatedName": "OrderEventOrderId", + "groupName": [], + "type": "primitive" + }, + "audiences": [] + }, + { + "conflict": {}, + "generatedName": "orderEventTotal", + "key": "total", + "schema": { + "generatedName": "OrderEventTotal", + "value": { + "schema": { + "type": "double" + }, + "generatedName": "OrderEventTotal", + "groupName": [], + "type": "primitive" + }, + "groupName": [], + "type": "optional" + }, + "audiences": [] + } + ], + "allOfPropertyConflicts": [], + "generatedName": "OrderEvent", + "groupName": [], + "additionalProperties": false, + "source": { + "file": "../openapi.yml", + "type": "openapi" + }, + "type": "object" + }, + "PaymentEvent": { + "allOf": [], + "properties": [ + { + "conflict": {}, + "generatedName": "paymentEventPaymentId", + "key": "paymentId", + "schema": { + "schema": { + "type": "string" + }, + "generatedName": "PaymentEventPaymentId", + "groupName": [], + "type": "primitive" + }, + "audiences": [] + }, + { + "conflict": {}, + "generatedName": "paymentEventAmount", + "key": "amount", + "schema": { + "schema": { + "type": "double" + }, + "generatedName": "PaymentEventAmount", + "groupName": [], + "type": "primitive" + }, + "audiences": [] + }, + { + "conflict": {}, + "generatedName": "paymentEventCurrency", + "key": "currency", + "schema": { + "generatedName": "PaymentEventCurrency", + "value": { + "schema": { + "type": "string" + }, + "generatedName": "PaymentEventCurrency", + "groupName": [], + "type": "primitive" + }, + "groupName": [], + "type": "optional" + }, + "audiences": [] + } + ], + "allOfPropertyConflicts": [], + "generatedName": "PaymentEvent", + "groupName": [], + "additionalProperties": false, + "source": { + "file": "../openapi.yml", + "type": "openapi" + }, + "type": "object" + } + }, + "namespacedSchemas": {} + }, + "variables": {}, + "nonRequestReferencedSchemas": {}, + "securitySchemes": {}, + "globalHeaders": [], + "idempotencyHeaders": [], + "groups": {} +} \ No newline at end of file diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/ada.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/ada.json index 6dc963cae96d..670ff7b5871e 100644 --- a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/ada.json +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/ada.json @@ -1295,6 +1295,7 @@ types: "headers": {}, "method": "POST", "payload": "root.EndUserCreatedWebhookPayload", + "signature": undefined, }, "v1EndUserUpdatedPost83bfd265": { "audiences": [], @@ -1343,6 +1344,7 @@ types: "headers": {}, "method": "POST", "payload": "root.EndUserUpdatedWebhookPayload", + "signature": undefined, }, }, }, diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/top-level-webhooks.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/top-level-webhooks.json index ec9b237db712..bb303b89e48f 100644 --- a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/top-level-webhooks.json +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/top-level-webhooks.json @@ -63,6 +63,7 @@ "headers": {}, "method": "POST", "payload": "root.Update", + "signature": undefined, }, "updateInlined": { "audiences": [], @@ -77,6 +78,7 @@ "headers": {}, "method": "POST", "payload": "UpdateInlinedStatusPayload", + "signature": undefined, }, }, }, diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/webhooks.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/webhooks.json index c31966d70e2a..a8406e6abc63 100644 --- a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/webhooks.json +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/webhooks.json @@ -37,6 +37,7 @@ "headers": {}, "method": "POST", "payload": "Pet", + "signature": undefined, }, }, }, diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/x-fern-webhook-signature.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/x-fern-webhook-signature.json new file mode 100644 index 000000000000..16ca4029fe20 --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/x-fern-webhook-signature.json @@ -0,0 +1,267 @@ +{ + "absoluteFilePath": "/DUMMY_PATH", + "importedDefinitions": {}, + "namedDefinitionFiles": { + "__package__.yml": { + "absoluteFilepath": "/DUMMY_PATH", + "contents": { + "types": { + "OrderEvent": { + "docs": undefined, + "inline": undefined, + "properties": { + "orderId": "string", + "total": "optional", + }, + "source": { + "openapi": "../openapi.yml", + }, + }, + "PaymentEvent": { + "docs": undefined, + "inline": undefined, + "properties": { + "amount": "double", + "currency": "optional", + "paymentId": "string", + }, + "source": { + "openapi": "../openapi.yml", + }, + }, + "UserEvent": { + "docs": undefined, + "inline": undefined, + "properties": { + "action": "string", + "userId": "string", + }, + "source": { + "openapi": "../openapi.yml", + }, + }, + }, + }, + "rawContents": "types: + UserEvent: + properties: + userId: string + action: string + source: + openapi: ../openapi.yml + OrderEvent: + properties: + orderId: string + total: optional + source: + openapi: ../openapi.yml + PaymentEvent: + properties: + paymentId: string + amount: double + currency: optional + source: + openapi: ../openapi.yml +", + }, + "webhooks.yml": { + "absoluteFilepath": "/DUMMY_PATH", + "contents": { + "imports": { + "root": "__package__.yml", + }, + "webhooks": { + "orderPlaced": { + "audiences": [], + "display-name": undefined, + "examples": [ + { + "docs": undefined, + "name": undefined, + "payload": { + "orderId": "orderId", + }, + }, + ], + "headers": {}, + "method": "POST", + "payload": "root.OrderEvent", + "signature": { + "algorithm": "sha256", + "encoding": "hex", + "header": "x-webhook-signature", + "payload-format": { + "components": [ + "timestamp", + "body", + ], + "delimiter": ".", + }, + "signature-prefix": "sha256=", + "timestamp": { + "format": "unix-seconds", + "header": "x-webhook-timestamp", + "tolerance": 300, + }, + "type": "hmac", + }, + }, + "paymentProcessed": { + "audiences": [], + "display-name": undefined, + "examples": [ + { + "docs": undefined, + "name": undefined, + "payload": { + "amount": 1.1, + "paymentId": "paymentId", + }, + }, + ], + "headers": {}, + "method": "POST", + "payload": "root.PaymentEvent", + "signature": { + "asymmetric-algorithm": "rsa-sha256", + "encoding": "base64", + "header": "x-signature", + "jwks-url": "https://api.example.com/.well-known/jwks.json", + "key-id-header": "x-key-id", + "signature-prefix": "rsa=", + "timestamp": { + "format": "iso8601", + "header": "x-timestamp", + "tolerance": undefined, + }, + "type": "asymmetric", + }, + }, + "userCreated": { + "audiences": [], + "display-name": undefined, + "examples": [ + { + "docs": undefined, + "name": undefined, + "payload": { + "action": "action", + "userId": "userId", + }, + }, + ], + "headers": {}, + "method": "POST", + "payload": "root.UserEvent", + "signature": { + "algorithm": "sha256", + "encoding": "hex", + "header": "x-webhook-signature", + "payload-format": { + "components": [ + "timestamp", + "body", + ], + "delimiter": ".", + }, + "signature-prefix": "sha256=", + "timestamp": { + "format": "unix-seconds", + "header": "x-webhook-timestamp", + "tolerance": 300, + }, + "type": "hmac", + }, + }, + }, + }, + "rawContents": "imports: + root: __package__.yml +webhooks: + userCreated: + audiences: [] + method: POST + headers: {} + payload: root.UserEvent + signature: + type: hmac + header: x-webhook-signature + algorithm: sha256 + encoding: hex + signature-prefix: sha256= + payload-format: + components: + - timestamp + - body + delimiter: . + timestamp: + header: x-webhook-timestamp + format: unix-seconds + tolerance: 300 + examples: + - payload: + userId: userId + action: action + orderPlaced: + audiences: [] + method: POST + headers: {} + payload: root.OrderEvent + signature: + type: hmac + header: x-webhook-signature + algorithm: sha256 + encoding: hex + signature-prefix: sha256= + payload-format: + components: + - timestamp + - body + delimiter: . + timestamp: + header: x-webhook-timestamp + format: unix-seconds + tolerance: 300 + examples: + - payload: + orderId: orderId + paymentProcessed: + audiences: [] + method: POST + headers: {} + payload: root.PaymentEvent + signature: + type: asymmetric + header: x-signature + asymmetric-algorithm: rsa-sha256 + encoding: base64 + signature-prefix: rsa= + jwks-url: https://api.example.com/.well-known/jwks.json + key-id-header: x-key-id + timestamp: + header: x-timestamp + format: iso8601 + examples: + - payload: + paymentId: paymentId + amount: 1.1 +", + }, + }, + "packageMarkers": {}, + "rootApiFile": { + "contents": { + "display-name": "Webhook Signature Verification Test", + "error-discrimination": { + "strategy": "status-code", + }, + "name": "api", + }, + "defaultUrl": undefined, + "rawContents": "name: api +error-discrimination: + strategy: status-code +display-name: Webhook Signature Verification Test +", + }, +} \ No newline at end of file diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/x-fern-webhook.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/x-fern-webhook.json index 9dffc060f02f..d8204252b516 100644 --- a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/x-fern-webhook.json +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/x-fern-webhook.json @@ -63,6 +63,7 @@ "headers": {}, "method": "POST", "payload": "root.Update", + "signature": undefined, }, "updateInlined": { "audiences": [], @@ -77,6 +78,7 @@ "headers": {}, "method": "POST", "payload": "UpdateInlinedStatusPayload", + "signature": undefined, }, }, }, @@ -131,6 +133,7 @@ types: "headers": {}, "method": "POST", "payload": "root.Update", + "signature": undefined, }, }, }, diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/x-fern-webhook-signature/fern/fern.config.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/x-fern-webhook-signature/fern/fern.config.json new file mode 100644 index 000000000000..7980537f5644 --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/x-fern-webhook-signature/fern/fern.config.json @@ -0,0 +1,4 @@ +{ + "organization": "fern", + "version": "*" +} \ No newline at end of file diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/x-fern-webhook-signature/fern/generators.yml b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/x-fern-webhook-signature/fern/generators.yml new file mode 100644 index 000000000000..5b01f1e0833d --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/x-fern-webhook-signature/fern/generators.yml @@ -0,0 +1,4 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json +api: + specs: + - openapi: ../openapi.yml diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/x-fern-webhook-signature/openapi.yml b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/x-fern-webhook-signature/openapi.yml new file mode 100644 index 000000000000..dad640463855 --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/x-fern-webhook-signature/openapi.yml @@ -0,0 +1,109 @@ +openapi: 3.1.0 +info: + title: Webhook Signature Verification Test + version: 1.0.0 + +# Document-level default: all webhooks inherit HMAC unless overridden +x-fern-webhook-signature: + type: hmac + header: x-webhook-signature + algorithm: sha256 + encoding: hex + signature-prefix: "sha256=" + payload-format: + components: + - timestamp + - body + delimiter: "." + timestamp: + header: x-webhook-timestamp + format: unix-seconds + tolerance: 300 + +paths: + # Webhook that explicitly inherits document-level config via boolean + /webhooks/user-created: + post: + operationId: userCreated + x-fern-webhook: true + x-fern-webhook-signature: true + x-fern-sdk-group-name: webhooks + x-fern-sdk-method-name: userCreated + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/UserEvent" + + # Webhook that falls back to document-level config (no explicit extension) + /webhooks/order-placed: + post: + operationId: orderPlaced + x-fern-webhook: true + x-fern-sdk-group-name: webhooks + x-fern-sdk-method-name: orderPlaced + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/OrderEvent" + + # Webhook that overrides with asymmetric signature + /webhooks/payment-processed: + post: + operationId: paymentProcessed + x-fern-webhook: true + x-fern-sdk-group-name: webhooks + x-fern-sdk-method-name: paymentProcessed + x-fern-webhook-signature: + type: asymmetric + header: x-signature + asymmetric-algorithm: rsa-sha256 + encoding: base64 + signature-prefix: "rsa=" + jwks-url: https://api.example.com/.well-known/jwks.json + key-id-header: x-key-id + timestamp: + header: x-timestamp + format: iso8601 + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/PaymentEvent" + +components: + schemas: + UserEvent: + type: object + properties: + userId: + type: string + action: + type: string + required: + - userId + - action + + OrderEvent: + type: object + properties: + orderId: + type: string + total: + type: number + required: + - orderId + + PaymentEvent: + type: object + properties: + paymentId: + type: string + amount: + type: number + currency: + type: string + required: + - paymentId + - amount diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern/src/buildWebhooks.ts b/packages/cli/api-importers/openapi/openapi-ir-to-fern/src/buildWebhooks.ts index 535d8fd34876..6032ad46dd5e 100644 --- a/packages/cli/api-importers/openapi/openapi-ir-to-fern/src/buildWebhooks.ts +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern/src/buildWebhooks.ts @@ -1,6 +1,16 @@ import { FERN_PACKAGE_MARKER_FILENAME } from "@fern-api/configuration"; import { RawSchemas } from "@fern-api/fern-definition-schema"; -import { Webhook } from "@fern-api/openapi-ir"; +import { + AsymmetricAlgorithm, + Webhook, + WebhookPayloadComponent, + WebhookPayloadFormat, + WebhookSignatureAlgorithm, + WebhookSignatureEncoding, + WebhookSignatureVerification, + WebhookTimestamp, + WebhookTimestampFormat +} from "@fern-api/openapi-ir"; import { join, RelativeFilePath } from "@fern-api/path-utils"; import { camelCase, isEqual } from "lodash-es"; import { buildHeader } from "./buildHeader.js"; @@ -43,6 +53,7 @@ export function buildWebhooks(context: OpenApiIrConverterContext): void { namespace: maybeWebhookNamespace, declarationDepth: 0 }), + signature: convertSignatureVerification(webhook.signatureVerification), examples: webhook.examples != null ? webhook.examples.map((exampleWebhookCall) => { @@ -247,3 +258,145 @@ function getWebhookLocation({ location: getUnresolvedWebhookLocation({ webhook, context }) }); } + +function convertSignatureVerification( + signatureVerification: WebhookSignatureVerification | undefined +): RawSchemas.WebhookSignatureSchema | undefined { + if (signatureVerification == null) { + return undefined; + } + + switch (signatureVerification.type) { + case "hmac": + return { + type: "hmac", + header: signatureVerification.header, + algorithm: convertSignatureAlgorithm(signatureVerification.algorithm), + encoding: convertSignatureEncoding(signatureVerification.encoding), + "signature-prefix": signatureVerification.signaturePrefix, + "payload-format": convertPayloadFormat(signatureVerification.payloadFormat), + timestamp: convertTimestamp(signatureVerification.timestamp) + }; + case "asymmetric": + return { + type: "asymmetric", + header: signatureVerification.header, + "asymmetric-algorithm": convertAsymmetricAlgorithm(signatureVerification.asymmetricAlgorithm), + encoding: convertSignatureEncoding(signatureVerification.encoding), + "signature-prefix": signatureVerification.signaturePrefix, + "jwks-url": signatureVerification.jwksUrl, + "key-id-header": signatureVerification.keyIdHeader, + timestamp: convertTimestamp(signatureVerification.timestamp) + }; + default: + return undefined; + } +} + +function convertSignatureAlgorithm( + algorithm: WebhookSignatureAlgorithm | undefined +): RawSchemas.WebhookSignatureAlgorithmSchema | undefined { + if (algorithm == null) { + return undefined; + } + switch (algorithm) { + case "sha256": + return "sha256"; + case "sha1": + return "sha1"; + case "sha384": + return "sha384"; + case "sha512": + return "sha512"; + default: + return undefined; + } +} + +function convertSignatureEncoding( + encoding: WebhookSignatureEncoding | undefined +): RawSchemas.WebhookSignatureEncodingSchema | undefined { + if (encoding == null) { + return undefined; + } + switch (encoding) { + case "base64": + return "base64"; + case "hex": + return "hex"; + default: + return undefined; + } +} + +function convertAsymmetricAlgorithm(algorithm: AsymmetricAlgorithm): RawSchemas.AsymmetricAlgorithmSchema { + switch (algorithm) { + case "rsa-sha256": + return "rsa-sha256"; + case "rsa-sha384": + return "rsa-sha384"; + case "rsa-sha512": + return "rsa-sha512"; + case "ecdsa-sha256": + return "ecdsa-sha256"; + case "ecdsa-sha384": + return "ecdsa-sha384"; + case "ecdsa-sha512": + return "ecdsa-sha512"; + case "ed25519": + return "ed25519"; + default: + return "rsa-sha256"; + } +} + +function convertPayloadFormat( + payloadFormat: WebhookPayloadFormat | undefined +): RawSchemas.WebhookPayloadFormatSchema | undefined { + if (payloadFormat == null) { + return undefined; + } + return { + components: payloadFormat.components.map(convertPayloadComponent), + delimiter: payloadFormat.delimiter + }; +} + +function convertPayloadComponent(component: WebhookPayloadComponent): RawSchemas.WebhookPayloadComponentSchema { + switch (component) { + case "body": + return "body"; + case "timestamp": + return "timestamp"; + case "notification-url": + return "notification-url"; + case "message-id": + return "message-id"; + default: + return "body"; + } +} + +function convertTimestampFormat(format: WebhookTimestampFormat): RawSchemas.WebhookTimestampFormatSchema | undefined { + switch (format) { + case "unix-seconds": + return "unix-seconds"; + case "unix-millis": + return "unix-millis"; + case "iso8601": + return "iso8601"; + default: + return undefined; + } +} + +function convertTimestamp(timestamp: WebhookTimestamp | undefined): RawSchemas.WebhookTimestampSchema | undefined { + if (timestamp == null) { + return undefined; + } + return { + header: timestamp.header, + format: timestamp.format != null ? convertTimestampFormat(timestamp.format) : undefined, + tolerance: timestamp.tolerance + }; +} diff --git a/packages/cli/api-importers/openapi/openapi-ir/fern/definition/finalIr.yml b/packages/cli/api-importers/openapi/openapi-ir/fern/definition/finalIr.yml index 46aa4e7266b7..b4f16da542e6 100644 --- a/packages/cli/api-importers/openapi/openapi-ir/fern/definition/finalIr.yml +++ b/packages/cli/api-importers/openapi/openapi-ir/fern/definition/finalIr.yml @@ -106,6 +106,7 @@ types: Optional multipart form data payload for webhooks that use multipart/form-data content type. This is populated when the webhook request body uses multipart/form-data encoding. response: optional + signatureVerification: optional examples: list MultipartFormDataWebhookPayload: @@ -180,6 +181,89 @@ types: - GET - POST + WebhookSignatureVerification: + discriminant: type + union: + hmac: HmacWebhookSignatureVerification + asymmetric: AsymmetricWebhookSignatureVerification + + HmacWebhookSignatureVerification: + properties: + header: string + algorithm: optional + encoding: optional + signaturePrefix: optional + payloadFormat: optional + timestamp: optional + + AsymmetricWebhookSignatureVerification: + properties: + header: string + asymmetricAlgorithm: AsymmetricAlgorithm + encoding: optional + signaturePrefix: optional + jwksUrl: optional + keyIdHeader: optional + timestamp: optional + + WebhookSignatureAlgorithm: + enum: + - sha256 + - sha1 + - sha384 + - sha512 + + WebhookSignatureEncoding: + enum: + - base64 + - hex + + WebhookTimestamp: + properties: + header: string + format: optional + tolerance: optional + + WebhookTimestampFormat: + enum: + - name: unixSeconds + value: unix-seconds + - name: unixMillis + value: unix-millis + - name: iso8601 + value: iso8601 + + WebhookPayloadFormat: + properties: + components: list + delimiter: optional + + WebhookPayloadComponent: + enum: + - body + - timestamp + - name: notificationUrl + value: notification-url + - name: messageId + value: message-id + + AsymmetricAlgorithm: + enum: + - name: rsaSha256 + value: rsa-sha256 + - name: rsaSha384 + value: rsa-sha384 + - name: rsaSha512 + value: rsa-sha512 + - name: ecdsaSha256 + value: ecdsa-sha256 + - name: ecdsaSha384 + value: ecdsa-sha384 + - name: ecdsaSha512 + value: ecdsa-sha512 + - name: ed25519 + value: ed25519 + Availability: enum: - GenerallyAvailable diff --git a/packages/cli/api-importers/openapi/openapi-ir/fern/definition/parseIr.yml b/packages/cli/api-importers/openapi/openapi-ir/fern/definition/parseIr.yml index 09e2d7ae0f53..2e4d91ab7069 100644 --- a/packages/cli/api-importers/openapi/openapi-ir/fern/definition/parseIr.yml +++ b/packages/cli/api-importers/openapi/openapi-ir/fern/definition/parseIr.yml @@ -94,6 +94,7 @@ types: Optional multipart form data payload for webhooks that use multipart/form-data content type. This is populated when the webhook request body uses multipart/form-data encoding. response: optional + signatureVerification: optional examples: list MultipartFormDataWebhookPayloadWithExample: diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/AsymmetricAlgorithm.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/AsymmetricAlgorithm.ts new file mode 100644 index 000000000000..c221cf81e5be --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/AsymmetricAlgorithm.ts @@ -0,0 +1,50 @@ +// This file was auto-generated by Fern from our API Definition. + +const AsymmetricAlgorithmValues = { + RsaSha256: "rsa-sha256", + RsaSha384: "rsa-sha384", + RsaSha512: "rsa-sha512", + EcdsaSha256: "ecdsa-sha256", + EcdsaSha384: "ecdsa-sha384", + EcdsaSha512: "ecdsa-sha512", + Ed25519: "ed25519", +} as const; +export type AsymmetricAlgorithm = (typeof AsymmetricAlgorithmValues)[keyof typeof AsymmetricAlgorithmValues]; +export const AsymmetricAlgorithm: typeof AsymmetricAlgorithmValues & { + _visit: (value: AsymmetricAlgorithm, visitor: AsymmetricAlgorithm.Visitor) => R; +} = { + ...AsymmetricAlgorithmValues, + _visit: (value: AsymmetricAlgorithm, visitor: AsymmetricAlgorithm.Visitor): R => { + switch (value) { + case AsymmetricAlgorithm.RsaSha256: + return visitor.rsaSha256(); + case AsymmetricAlgorithm.RsaSha384: + return visitor.rsaSha384(); + case AsymmetricAlgorithm.RsaSha512: + return visitor.rsaSha512(); + case AsymmetricAlgorithm.EcdsaSha256: + return visitor.ecdsaSha256(); + case AsymmetricAlgorithm.EcdsaSha384: + return visitor.ecdsaSha384(); + case AsymmetricAlgorithm.EcdsaSha512: + return visitor.ecdsaSha512(); + case AsymmetricAlgorithm.Ed25519: + return visitor.ed25519(); + default: + return visitor._other(); + } + }, +}; + +export namespace AsymmetricAlgorithm { + export interface Visitor { + rsaSha256: () => R; + rsaSha384: () => R; + rsaSha512: () => R; + ecdsaSha256: () => R; + ecdsaSha384: () => R; + ecdsaSha512: () => R; + ed25519: () => R; + _other: () => R; + } +} diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/AsymmetricWebhookSignatureVerification.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/AsymmetricWebhookSignatureVerification.ts new file mode 100644 index 000000000000..e04eb83aa273 --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/AsymmetricWebhookSignatureVerification.ts @@ -0,0 +1,13 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernOpenapiIr from "../../../index.js"; + +export interface AsymmetricWebhookSignatureVerification { + header: string; + asymmetricAlgorithm: FernOpenapiIr.AsymmetricAlgorithm; + encoding: FernOpenapiIr.WebhookSignatureEncoding | undefined; + signaturePrefix: string | undefined; + jwksUrl: string | undefined; + keyIdHeader: string | undefined; + timestamp: FernOpenapiIr.WebhookTimestamp | undefined; +} diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/HmacWebhookSignatureVerification.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/HmacWebhookSignatureVerification.ts new file mode 100644 index 000000000000..4373533078fe --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/HmacWebhookSignatureVerification.ts @@ -0,0 +1,12 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernOpenapiIr from "../../../index.js"; + +export interface HmacWebhookSignatureVerification { + header: string; + algorithm: FernOpenapiIr.WebhookSignatureAlgorithm | undefined; + encoding: FernOpenapiIr.WebhookSignatureEncoding | undefined; + signaturePrefix: string | undefined; + payloadFormat: FernOpenapiIr.WebhookPayloadFormat | undefined; + timestamp: FernOpenapiIr.WebhookTimestamp | undefined; +} diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/Webhook.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/Webhook.ts index 9b8683530bbf..3ed8dae08e1c 100644 --- a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/Webhook.ts +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/Webhook.ts @@ -19,5 +19,6 @@ export interface Webhook extends FernOpenapiIr.WithDescription, FernOpenapiIr.Wi */ multipartFormData: FernOpenapiIr.MultipartFormDataWebhookPayload | undefined; response: FernOpenapiIr.Response | undefined; + signatureVerification: FernOpenapiIr.WebhookSignatureVerification | undefined; examples: FernOpenapiIr.WebhookExampleCall[]; } diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/WebhookPayloadComponent.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/WebhookPayloadComponent.ts new file mode 100644 index 000000000000..b5d434ffbfd4 --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/WebhookPayloadComponent.ts @@ -0,0 +1,39 @@ +// This file was auto-generated by Fern from our API Definition. + +const WebhookPayloadComponentValues = { + Body: "body", + Timestamp: "timestamp", + NotificationUrl: "notification-url", + MessageId: "message-id", +} as const; +export type WebhookPayloadComponent = + (typeof WebhookPayloadComponentValues)[keyof typeof WebhookPayloadComponentValues]; +export const WebhookPayloadComponent: typeof WebhookPayloadComponentValues & { + _visit: (value: WebhookPayloadComponent, visitor: WebhookPayloadComponent.Visitor) => R; +} = { + ...WebhookPayloadComponentValues, + _visit: (value: WebhookPayloadComponent, visitor: WebhookPayloadComponent.Visitor): R => { + switch (value) { + case WebhookPayloadComponent.Body: + return visitor.body(); + case WebhookPayloadComponent.Timestamp: + return visitor.timestamp(); + case WebhookPayloadComponent.NotificationUrl: + return visitor.notificationUrl(); + case WebhookPayloadComponent.MessageId: + return visitor.messageId(); + default: + return visitor._other(); + } + }, +}; + +export namespace WebhookPayloadComponent { + export interface Visitor { + body: () => R; + timestamp: () => R; + notificationUrl: () => R; + messageId: () => R; + _other: () => R; + } +} diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/WebhookPayloadFormat.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/WebhookPayloadFormat.ts new file mode 100644 index 000000000000..5c302a2ecbd3 --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/WebhookPayloadFormat.ts @@ -0,0 +1,8 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernOpenapiIr from "../../../index.js"; + +export interface WebhookPayloadFormat { + components: FernOpenapiIr.WebhookPayloadComponent[]; + delimiter: string | undefined; +} diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/WebhookSignatureAlgorithm.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/WebhookSignatureAlgorithm.ts new file mode 100644 index 000000000000..68d97e894d74 --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/WebhookSignatureAlgorithm.ts @@ -0,0 +1,39 @@ +// This file was auto-generated by Fern from our API Definition. + +const WebhookSignatureAlgorithmValues = { + Sha256: "sha256", + Sha1: "sha1", + Sha384: "sha384", + Sha512: "sha512", +} as const; +export type WebhookSignatureAlgorithm = + (typeof WebhookSignatureAlgorithmValues)[keyof typeof WebhookSignatureAlgorithmValues]; +export const WebhookSignatureAlgorithm: typeof WebhookSignatureAlgorithmValues & { + _visit: (value: WebhookSignatureAlgorithm, visitor: WebhookSignatureAlgorithm.Visitor) => R; +} = { + ...WebhookSignatureAlgorithmValues, + _visit: (value: WebhookSignatureAlgorithm, visitor: WebhookSignatureAlgorithm.Visitor): R => { + switch (value) { + case WebhookSignatureAlgorithm.Sha256: + return visitor.sha256(); + case WebhookSignatureAlgorithm.Sha1: + return visitor.sha1(); + case WebhookSignatureAlgorithm.Sha384: + return visitor.sha384(); + case WebhookSignatureAlgorithm.Sha512: + return visitor.sha512(); + default: + return visitor._other(); + } + }, +}; + +export namespace WebhookSignatureAlgorithm { + export interface Visitor { + sha256: () => R; + sha1: () => R; + sha384: () => R; + sha512: () => R; + _other: () => R; + } +} diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/WebhookSignatureEncoding.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/WebhookSignatureEncoding.ts new file mode 100644 index 000000000000..b81e4717d14b --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/WebhookSignatureEncoding.ts @@ -0,0 +1,31 @@ +// This file was auto-generated by Fern from our API Definition. + +const WebhookSignatureEncodingValues = { + Base64: "base64", + Hex: "hex", +} as const; +export type WebhookSignatureEncoding = + (typeof WebhookSignatureEncodingValues)[keyof typeof WebhookSignatureEncodingValues]; +export const WebhookSignatureEncoding: typeof WebhookSignatureEncodingValues & { + _visit: (value: WebhookSignatureEncoding, visitor: WebhookSignatureEncoding.Visitor) => R; +} = { + ...WebhookSignatureEncodingValues, + _visit: (value: WebhookSignatureEncoding, visitor: WebhookSignatureEncoding.Visitor): R => { + switch (value) { + case WebhookSignatureEncoding.Base64: + return visitor.base64(); + case WebhookSignatureEncoding.Hex: + return visitor.hex(); + default: + return visitor._other(); + } + }, +}; + +export namespace WebhookSignatureEncoding { + export interface Visitor { + base64: () => R; + hex: () => R; + _other: () => R; + } +} diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/WebhookSignatureVerification.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/WebhookSignatureVerification.ts new file mode 100644 index 000000000000..a8bc5950a18a --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/WebhookSignatureVerification.ts @@ -0,0 +1,71 @@ +// This file was auto-generated by Fern from our API Definition. + +import * as FernOpenapiIr from "../../../index.js"; + +export type WebhookSignatureVerification = + | FernOpenapiIr.WebhookSignatureVerification.Hmac + | FernOpenapiIr.WebhookSignatureVerification.Asymmetric; + +export namespace WebhookSignatureVerification { + export interface Hmac extends FernOpenapiIr.HmacWebhookSignatureVerification, _Utils { + type: "hmac"; + } + + export interface Asymmetric extends FernOpenapiIr.AsymmetricWebhookSignatureVerification, _Utils { + type: "asymmetric"; + } + + export interface _Utils { + _visit: <_Result>(visitor: FernOpenapiIr.WebhookSignatureVerification._Visitor<_Result>) => _Result; + } + + export interface _Visitor<_Result> { + hmac: (value: FernOpenapiIr.HmacWebhookSignatureVerification) => _Result; + asymmetric: (value: FernOpenapiIr.AsymmetricWebhookSignatureVerification) => _Result; + _other: (value: { type: string }) => _Result; + } +} + +export const WebhookSignatureVerification = { + hmac: (value: FernOpenapiIr.HmacWebhookSignatureVerification): FernOpenapiIr.WebhookSignatureVerification.Hmac => { + return { + ...value, + type: "hmac", + _visit: function <_Result>( + this: FernOpenapiIr.WebhookSignatureVerification.Hmac, + visitor: FernOpenapiIr.WebhookSignatureVerification._Visitor<_Result>, + ) { + return FernOpenapiIr.WebhookSignatureVerification._visit(this, visitor); + }, + }; + }, + + asymmetric: ( + value: FernOpenapiIr.AsymmetricWebhookSignatureVerification, + ): FernOpenapiIr.WebhookSignatureVerification.Asymmetric => { + return { + ...value, + type: "asymmetric", + _visit: function <_Result>( + this: FernOpenapiIr.WebhookSignatureVerification.Asymmetric, + visitor: FernOpenapiIr.WebhookSignatureVerification._Visitor<_Result>, + ) { + return FernOpenapiIr.WebhookSignatureVerification._visit(this, visitor); + }, + }; + }, + + _visit: <_Result>( + value: FernOpenapiIr.WebhookSignatureVerification, + visitor: FernOpenapiIr.WebhookSignatureVerification._Visitor<_Result>, + ): _Result => { + switch (value.type) { + case "hmac": + return visitor.hmac(value); + case "asymmetric": + return visitor.asymmetric(value); + default: + return visitor._other(value); + } + }, +} as const; diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/WebhookTimestamp.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/WebhookTimestamp.ts new file mode 100644 index 000000000000..fe37d1714aec --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/WebhookTimestamp.ts @@ -0,0 +1,9 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernOpenapiIr from "../../../index.js"; + +export interface WebhookTimestamp { + header: string; + format: FernOpenapiIr.WebhookTimestampFormat | undefined; + tolerance: number | undefined; +} diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/WebhookTimestampFormat.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/WebhookTimestampFormat.ts new file mode 100644 index 000000000000..c5c479d4eb62 --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/WebhookTimestampFormat.ts @@ -0,0 +1,34 @@ +// This file was auto-generated by Fern from our API Definition. + +const WebhookTimestampFormatValues = { + UnixSeconds: "unix-seconds", + UnixMillis: "unix-millis", + Iso8601: "iso8601", +} as const; +export type WebhookTimestampFormat = (typeof WebhookTimestampFormatValues)[keyof typeof WebhookTimestampFormatValues]; +export const WebhookTimestampFormat: typeof WebhookTimestampFormatValues & { + _visit: (value: WebhookTimestampFormat, visitor: WebhookTimestampFormat.Visitor) => R; +} = { + ...WebhookTimestampFormatValues, + _visit: (value: WebhookTimestampFormat, visitor: WebhookTimestampFormat.Visitor): R => { + switch (value) { + case WebhookTimestampFormat.UnixSeconds: + return visitor.unixSeconds(); + case WebhookTimestampFormat.UnixMillis: + return visitor.unixMillis(); + case WebhookTimestampFormat.Iso8601: + return visitor.iso8601(); + default: + return visitor._other(); + } + }, +}; + +export namespace WebhookTimestampFormat { + export interface Visitor { + unixSeconds: () => R; + unixMillis: () => R; + iso8601: () => R; + _other: () => R; + } +} diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/index.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/index.ts index bc4296e51cdc..d9cd4399b008 100644 --- a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/index.ts +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/finalIr/types/index.ts @@ -1,5 +1,7 @@ export * from "./AllOfPropertyConflict.js"; export * from "./ArraySchema.js"; +export * from "./AsymmetricAlgorithm.js"; +export * from "./AsymmetricWebhookSignatureVerification.js"; export * from "./Availability.js"; export * from "./BooleanSchema.js"; export * from "./BytesResponse.js"; @@ -30,6 +32,7 @@ export * from "./GlobalHeader.js"; export * from "./GlobalSecurity.js"; export * from "./Header.js"; export * from "./HeaderExample.js"; +export * from "./HmacWebhookSignatureVerification.js"; export * from "./HttpEndpointServer.js"; export * from "./HttpError.js"; export * from "./HttpMethod.js"; @@ -80,6 +83,13 @@ export * from "./UriPagination.js"; export * from "./Webhook.js"; export * from "./WebhookExampleCall.js"; export * from "./WebhookHttpMethod.js"; +export * from "./WebhookPayloadComponent.js"; +export * from "./WebhookPayloadFormat.js"; +export * from "./WebhookSignatureAlgorithm.js"; +export * from "./WebhookSignatureEncoding.js"; +export * from "./WebhookSignatureVerification.js"; +export * from "./WebhookTimestamp.js"; +export * from "./WebhookTimestampFormat.js"; export * from "./WebsocketChannel.js"; export * from "./WebsocketHandshake.js"; export * from "./WebsocketMessageExample.js"; diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/parseIr/types/WebhookWithExample.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/parseIr/types/WebhookWithExample.ts index 2dd07fe1516f..8e2eaa160e35 100644 --- a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/parseIr/types/WebhookWithExample.ts +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/api/resources/parseIr/types/WebhookWithExample.ts @@ -22,5 +22,6 @@ export interface WebhookWithExample */ multipartFormData: FernOpenapiIr.MultipartFormDataWebhookPayloadWithExample | undefined; response: FernOpenapiIr.ResponseWithExample | undefined; + signatureVerification: FernOpenapiIr.WebhookSignatureVerification | undefined; examples: FernOpenapiIr.WebhookExampleCall[]; } diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/AsymmetricAlgorithm.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/AsymmetricAlgorithm.ts new file mode 100644 index 000000000000..4e7418d319ca --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/AsymmetricAlgorithm.ts @@ -0,0 +1,29 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernOpenapiIr from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; + +export const AsymmetricAlgorithm: core.serialization.Schema< + serializers.AsymmetricAlgorithm.Raw, + FernOpenapiIr.AsymmetricAlgorithm +> = core.serialization.enum_([ + "rsa-sha256", + "rsa-sha384", + "rsa-sha512", + "ecdsa-sha256", + "ecdsa-sha384", + "ecdsa-sha512", + "ed25519", +]); + +export declare namespace AsymmetricAlgorithm { + export type Raw = + | "rsa-sha256" + | "rsa-sha384" + | "rsa-sha512" + | "ecdsa-sha256" + | "ecdsa-sha384" + | "ecdsa-sha512" + | "ed25519"; +} diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/AsymmetricWebhookSignatureVerification.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/AsymmetricWebhookSignatureVerification.ts new file mode 100644 index 000000000000..945b9de29ebd --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/AsymmetricWebhookSignatureVerification.ts @@ -0,0 +1,33 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernOpenapiIr from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; +import { AsymmetricAlgorithm } from "./AsymmetricAlgorithm.js"; +import { WebhookSignatureEncoding } from "./WebhookSignatureEncoding.js"; +import { WebhookTimestamp } from "./WebhookTimestamp.js"; + +export const AsymmetricWebhookSignatureVerification: core.serialization.ObjectSchema< + serializers.AsymmetricWebhookSignatureVerification.Raw, + FernOpenapiIr.AsymmetricWebhookSignatureVerification +> = core.serialization.objectWithoutOptionalProperties({ + header: core.serialization.string(), + asymmetricAlgorithm: AsymmetricAlgorithm, + encoding: WebhookSignatureEncoding.optional(), + signaturePrefix: core.serialization.string().optional(), + jwksUrl: core.serialization.string().optional(), + keyIdHeader: core.serialization.string().optional(), + timestamp: WebhookTimestamp.optional(), +}); + +export declare namespace AsymmetricWebhookSignatureVerification { + export interface Raw { + header: string; + asymmetricAlgorithm: AsymmetricAlgorithm.Raw; + encoding?: WebhookSignatureEncoding.Raw | null; + signaturePrefix?: string | null; + jwksUrl?: string | null; + keyIdHeader?: string | null; + timestamp?: WebhookTimestamp.Raw | null; + } +} diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/HmacWebhookSignatureVerification.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/HmacWebhookSignatureVerification.ts new file mode 100644 index 000000000000..7606fa560b77 --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/HmacWebhookSignatureVerification.ts @@ -0,0 +1,32 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernOpenapiIr from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; +import { WebhookPayloadFormat } from "./WebhookPayloadFormat.js"; +import { WebhookSignatureAlgorithm } from "./WebhookSignatureAlgorithm.js"; +import { WebhookSignatureEncoding } from "./WebhookSignatureEncoding.js"; +import { WebhookTimestamp } from "./WebhookTimestamp.js"; + +export const HmacWebhookSignatureVerification: core.serialization.ObjectSchema< + serializers.HmacWebhookSignatureVerification.Raw, + FernOpenapiIr.HmacWebhookSignatureVerification +> = core.serialization.objectWithoutOptionalProperties({ + header: core.serialization.string(), + algorithm: WebhookSignatureAlgorithm.optional(), + encoding: WebhookSignatureEncoding.optional(), + signaturePrefix: core.serialization.string().optional(), + payloadFormat: WebhookPayloadFormat.optional(), + timestamp: WebhookTimestamp.optional(), +}); + +export declare namespace HmacWebhookSignatureVerification { + export interface Raw { + header: string; + algorithm?: WebhookSignatureAlgorithm.Raw | null; + encoding?: WebhookSignatureEncoding.Raw | null; + signaturePrefix?: string | null; + payloadFormat?: WebhookPayloadFormat.Raw | null; + timestamp?: WebhookTimestamp.Raw | null; + } +} diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/Webhook.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/Webhook.ts index 0b99dc67b25f..ae431b9c24f0 100644 --- a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/Webhook.ts +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/Webhook.ts @@ -13,6 +13,7 @@ import { MultipartFormDataWebhookPayload } from "./MultipartFormDataWebhookPaylo import { Response } from "./Response.js"; import { WebhookExampleCall } from "./WebhookExampleCall.js"; import { WebhookHttpMethod } from "./WebhookHttpMethod.js"; +import { WebhookSignatureVerification } from "./WebhookSignatureVerification.js"; export const Webhook: core.serialization.ObjectSchema = core.serialization @@ -28,6 +29,7 @@ export const Webhook: core.serialization.ObjectSchema serializers.Schema), multipartFormData: MultipartFormDataWebhookPayload.optional(), response: Response.optional(), + signatureVerification: WebhookSignatureVerification.optional(), examples: core.serialization.list(WebhookExampleCall), }) .extend(WithDescription) @@ -47,6 +49,7 @@ export declare namespace Webhook { payload: serializers.Schema.Raw; multipartFormData?: MultipartFormDataWebhookPayload.Raw | null; response?: Response.Raw | null; + signatureVerification?: WebhookSignatureVerification.Raw | null; examples: WebhookExampleCall.Raw[]; } } diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/WebhookPayloadComponent.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/WebhookPayloadComponent.ts new file mode 100644 index 000000000000..ac98f456b81b --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/WebhookPayloadComponent.ts @@ -0,0 +1,14 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernOpenapiIr from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; + +export const WebhookPayloadComponent: core.serialization.Schema< + serializers.WebhookPayloadComponent.Raw, + FernOpenapiIr.WebhookPayloadComponent +> = core.serialization.enum_(["body", "timestamp", "notification-url", "message-id"]); + +export declare namespace WebhookPayloadComponent { + export type Raw = "body" | "timestamp" | "notification-url" | "message-id"; +} diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/WebhookPayloadFormat.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/WebhookPayloadFormat.ts new file mode 100644 index 000000000000..861d022e1816 --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/WebhookPayloadFormat.ts @@ -0,0 +1,21 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernOpenapiIr from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; +import { WebhookPayloadComponent } from "./WebhookPayloadComponent.js"; + +export const WebhookPayloadFormat: core.serialization.ObjectSchema< + serializers.WebhookPayloadFormat.Raw, + FernOpenapiIr.WebhookPayloadFormat +> = core.serialization.objectWithoutOptionalProperties({ + components: core.serialization.list(WebhookPayloadComponent), + delimiter: core.serialization.string().optional(), +}); + +export declare namespace WebhookPayloadFormat { + export interface Raw { + components: WebhookPayloadComponent.Raw[]; + delimiter?: string | null; + } +} diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/WebhookSignatureAlgorithm.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/WebhookSignatureAlgorithm.ts new file mode 100644 index 000000000000..d1c74c2f6662 --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/WebhookSignatureAlgorithm.ts @@ -0,0 +1,14 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernOpenapiIr from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; + +export const WebhookSignatureAlgorithm: core.serialization.Schema< + serializers.WebhookSignatureAlgorithm.Raw, + FernOpenapiIr.WebhookSignatureAlgorithm +> = core.serialization.enum_(["sha256", "sha1", "sha384", "sha512"]); + +export declare namespace WebhookSignatureAlgorithm { + export type Raw = "sha256" | "sha1" | "sha384" | "sha512"; +} diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/WebhookSignatureEncoding.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/WebhookSignatureEncoding.ts new file mode 100644 index 000000000000..f5147dc95cba --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/WebhookSignatureEncoding.ts @@ -0,0 +1,14 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernOpenapiIr from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; + +export const WebhookSignatureEncoding: core.serialization.Schema< + serializers.WebhookSignatureEncoding.Raw, + FernOpenapiIr.WebhookSignatureEncoding +> = core.serialization.enum_(["base64", "hex"]); + +export declare namespace WebhookSignatureEncoding { + export type Raw = "base64" | "hex"; +} diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/WebhookSignatureVerification.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/WebhookSignatureVerification.ts new file mode 100644 index 000000000000..102de7c054d9 --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/WebhookSignatureVerification.ts @@ -0,0 +1,41 @@ +// This file was auto-generated by Fern from our API Definition. + +import * as FernOpenapiIr from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; +import { AsymmetricWebhookSignatureVerification } from "./AsymmetricWebhookSignatureVerification.js"; +import { HmacWebhookSignatureVerification } from "./HmacWebhookSignatureVerification.js"; + +export const WebhookSignatureVerification: core.serialization.Schema< + serializers.WebhookSignatureVerification.Raw, + FernOpenapiIr.WebhookSignatureVerification +> = core.serialization + .union("type", { + hmac: HmacWebhookSignatureVerification, + asymmetric: AsymmetricWebhookSignatureVerification, + }) + .transform({ + transform: (value) => { + switch (value.type) { + case "hmac": + return FernOpenapiIr.WebhookSignatureVerification.hmac(value); + case "asymmetric": + return FernOpenapiIr.WebhookSignatureVerification.asymmetric(value); + default: + return value as FernOpenapiIr.WebhookSignatureVerification; + } + }, + untransform: ({ _visit, ...value }) => value as any, + }); + +export declare namespace WebhookSignatureVerification { + export type Raw = WebhookSignatureVerification.Hmac | WebhookSignatureVerification.Asymmetric; + + export interface Hmac extends HmacWebhookSignatureVerification.Raw { + type: "hmac"; + } + + export interface Asymmetric extends AsymmetricWebhookSignatureVerification.Raw { + type: "asymmetric"; + } +} diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/WebhookTimestamp.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/WebhookTimestamp.ts new file mode 100644 index 000000000000..972705381e3b --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/WebhookTimestamp.ts @@ -0,0 +1,23 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernOpenapiIr from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; +import { WebhookTimestampFormat } from "./WebhookTimestampFormat.js"; + +export const WebhookTimestamp: core.serialization.ObjectSchema< + serializers.WebhookTimestamp.Raw, + FernOpenapiIr.WebhookTimestamp +> = core.serialization.objectWithoutOptionalProperties({ + header: core.serialization.string(), + format: WebhookTimestampFormat.optional(), + tolerance: core.serialization.number().optional(), +}); + +export declare namespace WebhookTimestamp { + export interface Raw { + header: string; + format?: WebhookTimestampFormat.Raw | null; + tolerance?: number | null; + } +} diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/WebhookTimestampFormat.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/WebhookTimestampFormat.ts new file mode 100644 index 000000000000..06356b495638 --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/WebhookTimestampFormat.ts @@ -0,0 +1,14 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernOpenapiIr from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; + +export const WebhookTimestampFormat: core.serialization.Schema< + serializers.WebhookTimestampFormat.Raw, + FernOpenapiIr.WebhookTimestampFormat +> = core.serialization.enum_(["unix-seconds", "unix-millis", "iso8601"]); + +export declare namespace WebhookTimestampFormat { + export type Raw = "unix-seconds" | "unix-millis" | "iso8601"; +} diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/index.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/index.ts index bc4296e51cdc..d9cd4399b008 100644 --- a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/index.ts +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/finalIr/types/index.ts @@ -1,5 +1,7 @@ export * from "./AllOfPropertyConflict.js"; export * from "./ArraySchema.js"; +export * from "./AsymmetricAlgorithm.js"; +export * from "./AsymmetricWebhookSignatureVerification.js"; export * from "./Availability.js"; export * from "./BooleanSchema.js"; export * from "./BytesResponse.js"; @@ -30,6 +32,7 @@ export * from "./GlobalHeader.js"; export * from "./GlobalSecurity.js"; export * from "./Header.js"; export * from "./HeaderExample.js"; +export * from "./HmacWebhookSignatureVerification.js"; export * from "./HttpEndpointServer.js"; export * from "./HttpError.js"; export * from "./HttpMethod.js"; @@ -80,6 +83,13 @@ export * from "./UriPagination.js"; export * from "./Webhook.js"; export * from "./WebhookExampleCall.js"; export * from "./WebhookHttpMethod.js"; +export * from "./WebhookPayloadComponent.js"; +export * from "./WebhookPayloadFormat.js"; +export * from "./WebhookSignatureAlgorithm.js"; +export * from "./WebhookSignatureEncoding.js"; +export * from "./WebhookSignatureVerification.js"; +export * from "./WebhookTimestamp.js"; +export * from "./WebhookTimestampFormat.js"; export * from "./WebsocketChannel.js"; export * from "./WebsocketHandshake.js"; export * from "./WebsocketMessageExample.js"; diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/parseIr/types/WebhookWithExample.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/parseIr/types/WebhookWithExample.ts index e08035072e1b..48074fc0f72a 100644 --- a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/parseIr/types/WebhookWithExample.ts +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/serialization/resources/parseIr/types/WebhookWithExample.ts @@ -10,6 +10,7 @@ import { WithSource } from "../../commons/types/WithSource.js"; import { EndpointSdkName } from "../../finalIr/types/EndpointSdkName.js"; import { WebhookExampleCall } from "../../finalIr/types/WebhookExampleCall.js"; import { WebhookHttpMethod } from "../../finalIr/types/WebhookHttpMethod.js"; +import { WebhookSignatureVerification } from "../../finalIr/types/WebhookSignatureVerification.js"; import { HeaderWithExample } from "./HeaderWithExample.js"; import { MultipartFormDataWebhookPayloadWithExample } from "./MultipartFormDataWebhookPayloadWithExample.js"; import { ResponseWithExample } from "./ResponseWithExample.js"; @@ -30,6 +31,7 @@ export const WebhookWithExample: core.serialization.ObjectSchema< payload: core.serialization.lazy(() => serializers.SchemaWithExample), multipartFormData: MultipartFormDataWebhookPayloadWithExample.optional(), response: ResponseWithExample.optional(), + signatureVerification: WebhookSignatureVerification.optional(), examples: core.serialization.list(WebhookExampleCall), }) .extend(WithDescription) @@ -49,6 +51,7 @@ export declare namespace WebhookWithExample { payload: serializers.SchemaWithExample.Raw; multipartFormData?: MultipartFormDataWebhookPayloadWithExample.Raw | null; response?: ResponseWithExample.Raw | null; + signatureVerification?: WebhookSignatureVerification.Raw | null; examples: WebhookExampleCall.Raw[]; } } diff --git a/packages/cli/cli-v2/src/api/config/converter/ApiDefinitionConverter.ts b/packages/cli/cli-v2/src/api/config/converter/ApiDefinitionConverter.ts index 39dfb564131b..8dcf3ce0efa1 100644 --- a/packages/cli/cli-v2/src/api/config/converter/ApiDefinitionConverter.ts +++ b/packages/cli/cli-v2/src/api/config/converter/ApiDefinitionConverter.ts @@ -152,28 +152,33 @@ export class ApiDefinitionConverter { if (isNullish(sourcedApis)) { return {}; } - const result: Record = {}; - for (const [apiName, apiDef] of Object.entries(apis)) { - const sourcedApiDef = sourcedApis[apiName]; - if (isNullish(sourcedApiDef)) { - continue; - } - const specs = await this.convertSpecs({ - absoluteFernYmlPath, - specs: apiDef.specs, - sourced: sourcedApiDef.specs - }); - result[apiName] = { - specs, - auth: apiDef.auth, - authSchemes: apiDef.authSchemes, - defaultUrl: apiDef.defaultUrl, - defaultEnvironment: apiDef.defaultEnvironment, - environments: apiDef.environments, - headers: apiDef.headers - }; - } - return result; + const apiEntries = Object.entries(apis) + .filter(([apiName]) => !isNullish(sourcedApis[apiName])) + .map(([apiName, apiDef]) => ({ apiName, apiDef, sourcedApiDef: sourcedApis[apiName] })) + .filter( + (entry): entry is typeof entry & { sourcedApiDef: NonNullable } => + !isNullish(entry.sourcedApiDef) + ); + const convertedEntries = await Promise.all( + apiEntries.map(async ({ apiName, apiDef, sourcedApiDef }) => { + const specs = await this.convertSpecs({ + absoluteFernYmlPath, + specs: apiDef.specs, + sourced: sourcedApiDef.specs + }); + const definition: ApiDefinition = { + specs, + auth: apiDef.auth, + authSchemes: apiDef.authSchemes, + defaultUrl: apiDef.defaultUrl, + defaultEnvironment: apiDef.defaultEnvironment, + environments: apiDef.environments, + headers: apiDef.headers + }; + return [apiName, definition] as const; + }) + ); + return Object.fromEntries(convertedEntries); } private async convertSpecs({ @@ -188,16 +193,20 @@ export class ApiDefinitionConverter { // Validate spec combinations before conversion. this.validateSpecCombinations({ specs, sourced }); - const results: ApiSpec[] = []; + const specEntries: { spec: schemas.ApiSpecSchema; sourced: Sourced }[] = []; for (let i = 0; i < specs.length; i++) { const spec = specs[i]; const sourcedSpec = sourced[i]; if (spec == null || isNullish(sourcedSpec)) { continue; } - results.push(await this.convertSpec({ absoluteFernYmlPath, spec, sourced: sourcedSpec })); + specEntries.push({ spec, sourced: sourcedSpec }); } - return results; + return await Promise.all( + specEntries.map(({ spec, sourced: sourcedSpec }) => + this.convertSpec({ absoluteFernYmlPath, spec, sourced: sourcedSpec }) + ) + ); } private async convertSpec({ diff --git a/packages/cli/cli-v2/src/workspace/WorkspaceLoader.ts b/packages/cli/cli-v2/src/workspace/WorkspaceLoader.ts index 74fd2c6ea060..f4cc7815ba89 100644 --- a/packages/cli/cli-v2/src/workspace/WorkspaceLoader.ts +++ b/packages/cli/cli-v2/src/workspace/WorkspaceLoader.ts @@ -59,10 +59,12 @@ export class WorkspaceLoader { public async load({ fernYml }: { fernYml: FernYmlSchemaLoader.Success }): Promise { const ai = this.convertAi({ fernYml }); - const apis = await this.convertApis({ fernYml }); - const cliVersion = await this.convertCliVersion({ fernYml }); const docs = this.convertDocs({ fernYml }); - const sdks = await this.convertSdks({ fernYml }); + const [apis, cliVersion, sdks] = await Promise.all([ + this.convertApis({ fernYml }), + this.convertCliVersion({ fernYml }), + this.convertSdks({ fernYml }) + ]); if (this.issues.length > 0) { return { success: false, diff --git a/packages/cli/cli/versions.yml b/packages/cli/cli/versions.yml index 8e1091cbadf7..dcf3a9dd67fe 100644 --- a/packages/cli/cli/versions.yml +++ b/packages/cli/cli/versions.yml @@ -1,22 +1,98 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json -- version: 3.82.0 +- version: 3.84.0 changelogEntry: - summary: | - Add `terminator` field support for SSE streaming throughout the OpenAPI to Fern pipeline. - The field was already present in the Fern definition schema but missing from the OpenAPI - extension parsing, OpenAPI-IR schema, and the OpenAPI-IR to Fern conversion layers. + Add workspace validation caching and skipValidation flag to improve toFernWorkspace() + performance. A global validation cache eliminates redundant JSON schema validation and + Zod parsing when the same files are processed by multiple workspace instances. The + skipValidation option allows callers to bypass JSON schema validation for known-good files. type: feat - createdAt: "2026-02-20" + createdAt: "2026-02-23" + irVersion: 65 +- version: 3.83.3 + changelogEntry: + - summary: | + Preserve existing README.md during local SDK generation when the generator + does not produce one. Previously, `copyGeneratedFiles` deleted all files + before copying generator output, so if README generation failed silently + the file was removed from the target repository. The file copy methods now + skip deleting README.md when the generated output directory does not + include it. + type: fix + createdAt: "2026-02-23" + irVersion: 65 +- version: 3.83.2 + changelogEntry: + - summary: | + Improve CLI startup performance by loading workspace files concurrently. + type: chore + createdAt: "2026-02-23" irVersion: 65 -- version: 3.81.1 +- version: 3.83.1 changelogEntry: - summary: | - Fix example object keys starting with `$` (e.g. `$ref`) not being unescaped when - generating the IR `jsonExample`. The Fern definition escapes `$`-prefixed keys with - a backslash to avoid collision with example references, but the backslash was not - removed when building the wire-format JSON example, causing generators to emit - invalid code (e.g. `"\$ref"` in Go). + Fix S3 signature mismatch for docs assets outside fern/ folder. Paths containing "../" + are now sanitized to prevent HTTP client URL normalization from breaking S3 presigned URLs. type: fix + createdAt: "2026-02-23" + irVersion: 65 +- version: 3.83.0 + changelogEntry: + - summary: | + Add webhook signature verification support. Webhooks can now declare a `signature` + configuration with `type: hmac` or `type: asymmetric`, including algorithm, encoding, + signature prefix parsing, payload format composition, and timestamp-based replay protection. + Supported via Fern Definition `signature` field, OpenAPI `x-fern-webhook-signature` extension, + and the IR `WebhookSignatureVerification` union. + + **OpenAPI example** — set `x-fern-webhook-signature` at the document level so + all webhooks inherit the same signature configuration: + + ```yaml + openapi: 3.1.0 + info: + title: My API + version: 1.0.0 + x-fern-webhook-signature: + type: hmac + header: x-webhook-signature + algorithm: sha256 + encoding: hex + signature-prefix: "sha256=" + payload-format: + components: + - timestamp + - body + delimiter: "." + timestamp: + header: x-webhook-timestamp + format: unix-seconds + tolerance: 300 + webhooks: + orderCreated: + post: + operationId: orderCreated + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OrderEvent' + ``` + + Individual webhook operations can also override the document-level default by + specifying their own `x-fern-webhook-signature` configuration inline. + type: feat + createdAt: "2026-02-23" + irVersion: 65 +- version: 3.82.0 + changelogEntry: + - summary: | + Add webhook signature verification configuration to the IR. Webhooks can now + specify a `signature` block (in Fern Definition) or `x-fern-webhook-signature` + extension (in OpenAPI) with algorithm, encoding, header name, and payload format. + SDK generators can use this to produce signature verification utilities. + type: feat createdAt: "2026-02-19" irVersion: 65 - version: 3.81.0 diff --git a/packages/cli/ete-tests/src/tests/ir/__snapshots__/ir.test.ts.snap b/packages/cli/ete-tests/src/tests/ir/__snapshots__/ir.test.ts.snap index e545d080274b..c12e3d5f3185 100644 --- a/packages/cli/ete-tests/src/tests/ir/__snapshots__/ir.test.ts.snap +++ b/packages/cli/ete-tests/src/tests/ir/__snapshots__/ir.test.ts.snap @@ -43473,6 +43473,7 @@ exports[`ir > {"name":"webhooks"} 1`] = ` } ] }, + "signatureVerification": null, "fileUploadPayload": null, "responses": null, "examples": null, diff --git a/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/AsymmetricAlgorithmSchema.ts b/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/AsymmetricAlgorithmSchema.ts new file mode 100644 index 000000000000..d8ef8e6378ab --- /dev/null +++ b/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/AsymmetricAlgorithmSchema.ts @@ -0,0 +1,12 @@ +// This file was auto-generated by Fern from our API Definition. + +export const AsymmetricAlgorithmSchema = { + RsaSha256: "rsa-sha256", + RsaSha384: "rsa-sha384", + RsaSha512: "rsa-sha512", + EcdsaSha256: "ecdsa-sha256", + EcdsaSha384: "ecdsa-sha384", + EcdsaSha512: "ecdsa-sha512", + Ed25519: "ed25519", +} as const; +export type AsymmetricAlgorithmSchema = (typeof AsymmetricAlgorithmSchema)[keyof typeof AsymmetricAlgorithmSchema]; diff --git a/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/AsymmetricSignatureSchema.ts b/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/AsymmetricSignatureSchema.ts new file mode 100644 index 000000000000..e01996751a65 --- /dev/null +++ b/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/AsymmetricSignatureSchema.ts @@ -0,0 +1,17 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernDefinition from "../../../index.js"; + +export interface AsymmetricSignatureSchema { + header: string; + "asymmetric-algorithm": FernDefinition.AsymmetricAlgorithmSchema; + /** Defaults to base64. */ + encoding?: FernDefinition.WebhookSignatureEncodingSchema; + /** Prefix in the header value before the signature. */ + "signature-prefix"?: string; + /** JWKS endpoint URL. When omitted, a static public key is expected at runtime. */ + "jwks-url"?: string; + /** HTTP header containing the key ID for JWKS key selection. */ + "key-id-header"?: string; + timestamp?: FernDefinition.WebhookTimestampSchema; +} diff --git a/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/HmacSignatureSchema.ts b/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/HmacSignatureSchema.ts new file mode 100644 index 000000000000..9a5d4a3bb16b --- /dev/null +++ b/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/HmacSignatureSchema.ts @@ -0,0 +1,16 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernDefinition from "../../../index.js"; + +export interface HmacSignatureSchema { + header: string; + /** Defaults to sha256. */ + algorithm?: FernDefinition.WebhookSignatureAlgorithmSchema; + /** Defaults to base64. */ + encoding?: FernDefinition.WebhookSignatureEncodingSchema; + /** Prefix in the header value before the signature (e.g. "sha256="). */ + "signature-prefix"?: string; + /** Defaults to body-only (components: [body], delimiter: ""). */ + "payload-format"?: FernDefinition.WebhookPayloadFormatSchema; + timestamp?: FernDefinition.WebhookTimestampSchema; +} diff --git a/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/WebhookPayloadComponentSchema.ts b/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/WebhookPayloadComponentSchema.ts new file mode 100644 index 000000000000..35e82727ef81 --- /dev/null +++ b/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/WebhookPayloadComponentSchema.ts @@ -0,0 +1,10 @@ +// This file was auto-generated by Fern from our API Definition. + +export const WebhookPayloadComponentSchema = { + Body: "body", + Timestamp: "timestamp", + NotificationUrl: "notification-url", + MessageId: "message-id", +} as const; +export type WebhookPayloadComponentSchema = + (typeof WebhookPayloadComponentSchema)[keyof typeof WebhookPayloadComponentSchema]; diff --git a/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/WebhookPayloadFormatSchema.ts b/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/WebhookPayloadFormatSchema.ts new file mode 100644 index 000000000000..58ebdbaf62e9 --- /dev/null +++ b/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/WebhookPayloadFormatSchema.ts @@ -0,0 +1,9 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernDefinition from "../../../index.js"; + +export interface WebhookPayloadFormatSchema { + components: FernDefinition.WebhookPayloadComponentSchema[]; + /** Defaults to empty string. */ + delimiter?: string; +} diff --git a/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/WebhookSchema.ts b/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/WebhookSchema.ts index 0bc867744e22..231f675f0c71 100644 --- a/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/WebhookSchema.ts +++ b/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/WebhookSchema.ts @@ -10,6 +10,7 @@ export interface WebhookSchema method: FernDefinition.WebhookMethodSchema; headers?: Record; payload: FernDefinition.WebhookPayloadSchema; + signature?: FernDefinition.WebhookSignatureSchema; response?: FernDefinition.HttpResponseSchema; "response-stream"?: FernDefinition.HttpResponseStreamSchema; examples?: FernDefinition.ExampleWebhookCallSchema[]; diff --git a/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/WebhookSignatureAlgorithmSchema.ts b/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/WebhookSignatureAlgorithmSchema.ts new file mode 100644 index 000000000000..7279f627580f --- /dev/null +++ b/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/WebhookSignatureAlgorithmSchema.ts @@ -0,0 +1,10 @@ +// This file was auto-generated by Fern from our API Definition. + +export const WebhookSignatureAlgorithmSchema = { + Sha256: "sha256", + Sha1: "sha1", + Sha384: "sha384", + Sha512: "sha512", +} as const; +export type WebhookSignatureAlgorithmSchema = + (typeof WebhookSignatureAlgorithmSchema)[keyof typeof WebhookSignatureAlgorithmSchema]; diff --git a/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/WebhookSignatureEncodingSchema.ts b/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/WebhookSignatureEncodingSchema.ts new file mode 100644 index 000000000000..614892467cb2 --- /dev/null +++ b/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/WebhookSignatureEncodingSchema.ts @@ -0,0 +1,8 @@ +// This file was auto-generated by Fern from our API Definition. + +export const WebhookSignatureEncodingSchema = { + Base64: "base64", + Hex: "hex", +} as const; +export type WebhookSignatureEncodingSchema = + (typeof WebhookSignatureEncodingSchema)[keyof typeof WebhookSignatureEncodingSchema]; diff --git a/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/WebhookSignatureSchema.ts b/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/WebhookSignatureSchema.ts new file mode 100644 index 000000000000..f8260edec172 --- /dev/null +++ b/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/WebhookSignatureSchema.ts @@ -0,0 +1,17 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernDefinition from "../../../index.js"; + +export type WebhookSignatureSchema = + | FernDefinition.WebhookSignatureSchema.Hmac + | FernDefinition.WebhookSignatureSchema.Asymmetric; + +export namespace WebhookSignatureSchema { + export interface Hmac extends FernDefinition.HmacSignatureSchema { + type: "hmac"; + } + + export interface Asymmetric extends FernDefinition.AsymmetricSignatureSchema { + type: "asymmetric"; + } +} diff --git a/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/WebhookTimestampFormatSchema.ts b/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/WebhookTimestampFormatSchema.ts new file mode 100644 index 000000000000..a2eccb5b6a1c --- /dev/null +++ b/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/WebhookTimestampFormatSchema.ts @@ -0,0 +1,9 @@ +// This file was auto-generated by Fern from our API Definition. + +export const WebhookTimestampFormatSchema = { + UnixSeconds: "unix-seconds", + UnixMillis: "unix-millis", + Iso8601: "iso8601", +} as const; +export type WebhookTimestampFormatSchema = + (typeof WebhookTimestampFormatSchema)[keyof typeof WebhookTimestampFormatSchema]; diff --git a/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/WebhookTimestampSchema.ts b/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/WebhookTimestampSchema.ts new file mode 100644 index 000000000000..4e7383335234 --- /dev/null +++ b/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/WebhookTimestampSchema.ts @@ -0,0 +1,11 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernDefinition from "../../../index.js"; + +export interface WebhookTimestampSchema { + header: string; + /** Defaults to unix-seconds. */ + format?: FernDefinition.WebhookTimestampFormatSchema; + /** Allowed clock skew in seconds. Defaults to 300. */ + tolerance?: number; +} diff --git a/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/index.ts b/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/index.ts index af6a8c453aaf..3d759feb1224 100644 --- a/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/index.ts +++ b/packages/cli/fern-definition/schema/src/schemas/api/resources/webhooks/types/index.ts @@ -1,5 +1,15 @@ +export * from "./AsymmetricAlgorithmSchema.js"; +export * from "./AsymmetricSignatureSchema.js"; +export * from "./HmacSignatureSchema.js"; export * from "./WebhookInlinedPayloadSchema.js"; export * from "./WebhookMethodSchema.js"; +export * from "./WebhookPayloadComponentSchema.js"; +export * from "./WebhookPayloadFormatSchema.js"; export * from "./WebhookPayloadSchema.js"; export * from "./WebhookReferencedPayloadSchema.js"; export * from "./WebhookSchema.js"; +export * from "./WebhookSignatureAlgorithmSchema.js"; +export * from "./WebhookSignatureEncodingSchema.js"; +export * from "./WebhookSignatureSchema.js"; +export * from "./WebhookTimestampFormatSchema.js"; +export * from "./WebhookTimestampSchema.js"; diff --git a/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/AsymmetricAlgorithmSchema.ts b/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/AsymmetricAlgorithmSchema.ts new file mode 100644 index 000000000000..268b26dd9ce3 --- /dev/null +++ b/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/AsymmetricAlgorithmSchema.ts @@ -0,0 +1,29 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernDefinition from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; + +export const AsymmetricAlgorithmSchema: core.serialization.Schema< + serializers.AsymmetricAlgorithmSchema.Raw, + FernDefinition.AsymmetricAlgorithmSchema +> = core.serialization.enum_([ + "rsa-sha256", + "rsa-sha384", + "rsa-sha512", + "ecdsa-sha256", + "ecdsa-sha384", + "ecdsa-sha512", + "ed25519", +]); + +export declare namespace AsymmetricAlgorithmSchema { + export type Raw = + | "rsa-sha256" + | "rsa-sha384" + | "rsa-sha512" + | "ecdsa-sha256" + | "ecdsa-sha384" + | "ecdsa-sha512" + | "ed25519"; +} diff --git a/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/AsymmetricSignatureSchema.ts b/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/AsymmetricSignatureSchema.ts new file mode 100644 index 000000000000..f889bc02af3e --- /dev/null +++ b/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/AsymmetricSignatureSchema.ts @@ -0,0 +1,33 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernDefinition from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; +import { AsymmetricAlgorithmSchema } from "./AsymmetricAlgorithmSchema.js"; +import { WebhookSignatureEncodingSchema } from "./WebhookSignatureEncodingSchema.js"; +import { WebhookTimestampSchema } from "./WebhookTimestampSchema.js"; + +export const AsymmetricSignatureSchema: core.serialization.ObjectSchema< + serializers.AsymmetricSignatureSchema.Raw, + FernDefinition.AsymmetricSignatureSchema +> = core.serialization.object({ + header: core.serialization.string(), + "asymmetric-algorithm": AsymmetricAlgorithmSchema, + encoding: WebhookSignatureEncodingSchema.optional(), + "signature-prefix": core.serialization.string().optional(), + "jwks-url": core.serialization.string().optional(), + "key-id-header": core.serialization.string().optional(), + timestamp: WebhookTimestampSchema.optional(), +}); + +export declare namespace AsymmetricSignatureSchema { + export interface Raw { + header: string; + "asymmetric-algorithm": AsymmetricAlgorithmSchema.Raw; + encoding?: WebhookSignatureEncodingSchema.Raw | null; + "signature-prefix"?: string | null; + "jwks-url"?: string | null; + "key-id-header"?: string | null; + timestamp?: WebhookTimestampSchema.Raw | null; + } +} diff --git a/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/HmacSignatureSchema.ts b/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/HmacSignatureSchema.ts new file mode 100644 index 000000000000..e100f31eaf39 --- /dev/null +++ b/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/HmacSignatureSchema.ts @@ -0,0 +1,32 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernDefinition from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; +import { WebhookPayloadFormatSchema } from "./WebhookPayloadFormatSchema.js"; +import { WebhookSignatureAlgorithmSchema } from "./WebhookSignatureAlgorithmSchema.js"; +import { WebhookSignatureEncodingSchema } from "./WebhookSignatureEncodingSchema.js"; +import { WebhookTimestampSchema } from "./WebhookTimestampSchema.js"; + +export const HmacSignatureSchema: core.serialization.ObjectSchema< + serializers.HmacSignatureSchema.Raw, + FernDefinition.HmacSignatureSchema +> = core.serialization.object({ + header: core.serialization.string(), + algorithm: WebhookSignatureAlgorithmSchema.optional(), + encoding: WebhookSignatureEncodingSchema.optional(), + "signature-prefix": core.serialization.string().optional(), + "payload-format": WebhookPayloadFormatSchema.optional(), + timestamp: WebhookTimestampSchema.optional(), +}); + +export declare namespace HmacSignatureSchema { + export interface Raw { + header: string; + algorithm?: WebhookSignatureAlgorithmSchema.Raw | null; + encoding?: WebhookSignatureEncodingSchema.Raw | null; + "signature-prefix"?: string | null; + "payload-format"?: WebhookPayloadFormatSchema.Raw | null; + timestamp?: WebhookTimestampSchema.Raw | null; + } +} diff --git a/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/WebhookPayloadComponentSchema.ts b/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/WebhookPayloadComponentSchema.ts new file mode 100644 index 000000000000..2a295e3b696d --- /dev/null +++ b/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/WebhookPayloadComponentSchema.ts @@ -0,0 +1,14 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernDefinition from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; + +export const WebhookPayloadComponentSchema: core.serialization.Schema< + serializers.WebhookPayloadComponentSchema.Raw, + FernDefinition.WebhookPayloadComponentSchema +> = core.serialization.enum_(["body", "timestamp", "notification-url", "message-id"]); + +export declare namespace WebhookPayloadComponentSchema { + export type Raw = "body" | "timestamp" | "notification-url" | "message-id"; +} diff --git a/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/WebhookPayloadFormatSchema.ts b/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/WebhookPayloadFormatSchema.ts new file mode 100644 index 000000000000..136055a1c54a --- /dev/null +++ b/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/WebhookPayloadFormatSchema.ts @@ -0,0 +1,21 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernDefinition from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; +import { WebhookPayloadComponentSchema } from "./WebhookPayloadComponentSchema.js"; + +export const WebhookPayloadFormatSchema: core.serialization.ObjectSchema< + serializers.WebhookPayloadFormatSchema.Raw, + FernDefinition.WebhookPayloadFormatSchema +> = core.serialization.object({ + components: core.serialization.list(WebhookPayloadComponentSchema), + delimiter: core.serialization.string().optional(), +}); + +export declare namespace WebhookPayloadFormatSchema { + export interface Raw { + components: WebhookPayloadComponentSchema.Raw[]; + delimiter?: string | null; + } +} diff --git a/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/WebhookSchema.ts b/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/WebhookSchema.ts index 2f35f58e29d1..0cf7c595843f 100644 --- a/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/WebhookSchema.ts +++ b/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/WebhookSchema.ts @@ -13,6 +13,7 @@ import { HttpResponseSchema } from "../../service/types/HttpResponseSchema.js"; import { HttpResponseStreamSchema } from "../../service/types/HttpResponseStreamSchema.js"; import { WebhookMethodSchema } from "./WebhookMethodSchema.js"; import { WebhookPayloadSchema } from "./WebhookPayloadSchema.js"; +import { WebhookSignatureSchema } from "./WebhookSignatureSchema.js"; export const WebhookSchema: core.serialization.ObjectSchema< serializers.WebhookSchema.Raw, @@ -22,6 +23,7 @@ export const WebhookSchema: core.serialization.ObjectSchema< method: WebhookMethodSchema, headers: core.serialization.record(core.serialization.string(), HttpHeaderSchema).optional(), payload: WebhookPayloadSchema, + signature: WebhookSignatureSchema.optional(), response: HttpResponseSchema.optional(), "response-stream": HttpResponseStreamSchema.optional(), examples: core.serialization.list(ExampleWebhookCallSchema).optional(), @@ -36,6 +38,7 @@ export declare namespace WebhookSchema { method: WebhookMethodSchema.Raw; headers?: Record | null; payload: WebhookPayloadSchema.Raw; + signature?: WebhookSignatureSchema.Raw | null; response?: HttpResponseSchema.Raw | null; "response-stream"?: HttpResponseStreamSchema.Raw | null; examples?: ExampleWebhookCallSchema.Raw[] | null; diff --git a/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/WebhookSignatureAlgorithmSchema.ts b/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/WebhookSignatureAlgorithmSchema.ts new file mode 100644 index 000000000000..0ad40da09ea6 --- /dev/null +++ b/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/WebhookSignatureAlgorithmSchema.ts @@ -0,0 +1,14 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernDefinition from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; + +export const WebhookSignatureAlgorithmSchema: core.serialization.Schema< + serializers.WebhookSignatureAlgorithmSchema.Raw, + FernDefinition.WebhookSignatureAlgorithmSchema +> = core.serialization.enum_(["sha256", "sha1", "sha384", "sha512"]); + +export declare namespace WebhookSignatureAlgorithmSchema { + export type Raw = "sha256" | "sha1" | "sha384" | "sha512"; +} diff --git a/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/WebhookSignatureEncodingSchema.ts b/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/WebhookSignatureEncodingSchema.ts new file mode 100644 index 000000000000..2ba2ce64b827 --- /dev/null +++ b/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/WebhookSignatureEncodingSchema.ts @@ -0,0 +1,14 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernDefinition from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; + +export const WebhookSignatureEncodingSchema: core.serialization.Schema< + serializers.WebhookSignatureEncodingSchema.Raw, + FernDefinition.WebhookSignatureEncodingSchema +> = core.serialization.enum_(["base64", "hex"]); + +export declare namespace WebhookSignatureEncodingSchema { + export type Raw = "base64" | "hex"; +} diff --git a/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/WebhookSignatureSchema.ts b/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/WebhookSignatureSchema.ts new file mode 100644 index 000000000000..ed4c50d056a9 --- /dev/null +++ b/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/WebhookSignatureSchema.ts @@ -0,0 +1,32 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernDefinition from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; +import { AsymmetricSignatureSchema } from "./AsymmetricSignatureSchema.js"; +import { HmacSignatureSchema } from "./HmacSignatureSchema.js"; + +export const WebhookSignatureSchema: core.serialization.Schema< + serializers.WebhookSignatureSchema.Raw, + FernDefinition.WebhookSignatureSchema +> = core.serialization + .union("type", { + hmac: HmacSignatureSchema, + asymmetric: AsymmetricSignatureSchema, + }) + .transform({ + transform: (value) => value, + untransform: (value) => value, + }); + +export declare namespace WebhookSignatureSchema { + export type Raw = WebhookSignatureSchema.Hmac | WebhookSignatureSchema.Asymmetric; + + export interface Hmac extends HmacSignatureSchema.Raw { + type: "hmac"; + } + + export interface Asymmetric extends AsymmetricSignatureSchema.Raw { + type: "asymmetric"; + } +} diff --git a/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/WebhookTimestampFormatSchema.ts b/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/WebhookTimestampFormatSchema.ts new file mode 100644 index 000000000000..b4521561a335 --- /dev/null +++ b/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/WebhookTimestampFormatSchema.ts @@ -0,0 +1,14 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernDefinition from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; + +export const WebhookTimestampFormatSchema: core.serialization.Schema< + serializers.WebhookTimestampFormatSchema.Raw, + FernDefinition.WebhookTimestampFormatSchema +> = core.serialization.enum_(["unix-seconds", "unix-millis", "iso8601"]); + +export declare namespace WebhookTimestampFormatSchema { + export type Raw = "unix-seconds" | "unix-millis" | "iso8601"; +} diff --git a/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/WebhookTimestampSchema.ts b/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/WebhookTimestampSchema.ts new file mode 100644 index 000000000000..b341c5a1f215 --- /dev/null +++ b/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/WebhookTimestampSchema.ts @@ -0,0 +1,23 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernDefinition from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; +import { WebhookTimestampFormatSchema } from "./WebhookTimestampFormatSchema.js"; + +export const WebhookTimestampSchema: core.serialization.ObjectSchema< + serializers.WebhookTimestampSchema.Raw, + FernDefinition.WebhookTimestampSchema +> = core.serialization.object({ + header: core.serialization.string(), + format: WebhookTimestampFormatSchema.optional(), + tolerance: core.serialization.number().optional(), +}); + +export declare namespace WebhookTimestampSchema { + export interface Raw { + header: string; + format?: WebhookTimestampFormatSchema.Raw | null; + tolerance?: number | null; + } +} diff --git a/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/index.ts b/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/index.ts index af6a8c453aaf..3d759feb1224 100644 --- a/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/index.ts +++ b/packages/cli/fern-definition/schema/src/schemas/serialization/resources/webhooks/types/index.ts @@ -1,5 +1,15 @@ +export * from "./AsymmetricAlgorithmSchema.js"; +export * from "./AsymmetricSignatureSchema.js"; +export * from "./HmacSignatureSchema.js"; export * from "./WebhookInlinedPayloadSchema.js"; export * from "./WebhookMethodSchema.js"; +export * from "./WebhookPayloadComponentSchema.js"; +export * from "./WebhookPayloadFormatSchema.js"; export * from "./WebhookPayloadSchema.js"; export * from "./WebhookReferencedPayloadSchema.js"; export * from "./WebhookSchema.js"; +export * from "./WebhookSignatureAlgorithmSchema.js"; +export * from "./WebhookSignatureEncodingSchema.js"; +export * from "./WebhookSignatureSchema.js"; +export * from "./WebhookTimestampFormatSchema.js"; +export * from "./WebhookTimestampSchema.js"; diff --git a/packages/cli/fern-definition/validator/src/ast/visitors/visitWebhooks.ts b/packages/cli/fern-definition/validator/src/ast/visitors/visitWebhooks.ts index 27c3300b7376..2c98e51f49cc 100644 --- a/packages/cli/fern-definition/validator/src/ast/visitors/visitWebhooks.ts +++ b/packages/cli/fern-definition/validator/src/ast/visitors/visitWebhooks.ts @@ -94,6 +94,7 @@ export function visitWebhooks({ } }); }, + signature: noop, response: noop, "response-stream": noop, audiences: noop, diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/fixtures/audiences/fern/definition/webhooks.yml b/packages/cli/generation/ir-generator-tests/src/ir/__test__/fixtures/audiences/fern/definition/webhooks.yml index fcc6b668e5b9..e7385864f4e0 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/fixtures/audiences/fern/definition/webhooks.yml +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/fixtures/audiences/fern/definition/webhooks.yml @@ -5,6 +5,21 @@ webhooks: display-name: User Added method: POST payload: User + signature: + type: hmac + header: x-webhook-signature + algorithm: sha256 + encoding: hex + signature-prefix: "sha256=" + payload-format: + components: + - timestamp + - body + delimiter: "." + timestamp: + header: x-webhook-timestamp + format: unix-seconds + tolerance: 300 userDeleted: display-name: User Added diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/irs/audiences.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/irs/audiences.json index 50e48688f321..9fa0a397f791 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/irs/audiences.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/irs/audiences.json @@ -1395,6 +1395,67 @@ }, "docs": null }, + "signatureVerification": { + "type": "hmac", + "signatureHeaderName": { + "name": { + "originalName": "x-webhook-signature", + "camelCase": { + "unsafeName": "xWebhookSignature", + "safeName": "xWebhookSignature" + }, + "snakeCase": { + "unsafeName": "x_webhook_signature", + "safeName": "x_webhook_signature" + }, + "screamingSnakeCase": { + "unsafeName": "X_WEBHOOK_SIGNATURE", + "safeName": "X_WEBHOOK_SIGNATURE" + }, + "pascalCase": { + "unsafeName": "XWebhookSignature", + "safeName": "XWebhookSignature" + } + }, + "wireValue": "x-webhook-signature" + }, + "algorithm": "SHA256", + "encoding": "HEX", + "signaturePrefix": "sha256=", + "payloadFormat": { + "components": [ + "TIMESTAMP", + "BODY" + ], + "delimiter": "." + }, + "timestamp": { + "headerName": { + "name": { + "originalName": "x-webhook-timestamp", + "camelCase": { + "unsafeName": "xWebhookTimestamp", + "safeName": "xWebhookTimestamp" + }, + "snakeCase": { + "unsafeName": "x_webhook_timestamp", + "safeName": "x_webhook_timestamp" + }, + "screamingSnakeCase": { + "unsafeName": "X_WEBHOOK_TIMESTAMP", + "safeName": "X_WEBHOOK_TIMESTAMP" + }, + "pascalCase": { + "unsafeName": "XWebhookTimestamp", + "safeName": "XWebhookTimestamp" + } + }, + "wireValue": "x-webhook-timestamp" + }, + "format": "UNIX_SECONDS", + "tolerance": 300 + } + }, "fileUploadPayload": null, "responses": null, "examples": null, diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/irs/environmentAudiences.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/irs/environmentAudiences.json index 50c7520de346..f56fea715ea4 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/irs/environmentAudiences.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/irs/environmentAudiences.json @@ -5195,6 +5195,67 @@ }, "docs": null }, + "signatureVerification": { + "type": "hmac", + "signatureHeaderName": { + "name": { + "originalName": "x-webhook-signature", + "camelCase": { + "unsafeName": "xWebhookSignature", + "safeName": "xWebhookSignature" + }, + "snakeCase": { + "unsafeName": "x_webhook_signature", + "safeName": "x_webhook_signature" + }, + "screamingSnakeCase": { + "unsafeName": "X_WEBHOOK_SIGNATURE", + "safeName": "X_WEBHOOK_SIGNATURE" + }, + "pascalCase": { + "unsafeName": "XWebhookSignature", + "safeName": "XWebhookSignature" + } + }, + "wireValue": "x-webhook-signature" + }, + "algorithm": "SHA256", + "encoding": "HEX", + "signaturePrefix": "sha256=", + "payloadFormat": { + "components": [ + "TIMESTAMP", + "BODY" + ], + "delimiter": "." + }, + "timestamp": { + "headerName": { + "name": { + "originalName": "x-webhook-timestamp", + "camelCase": { + "unsafeName": "xWebhookTimestamp", + "safeName": "xWebhookTimestamp" + }, + "snakeCase": { + "unsafeName": "x_webhook_timestamp", + "safeName": "x_webhook_timestamp" + }, + "screamingSnakeCase": { + "unsafeName": "X_WEBHOOK_TIMESTAMP", + "safeName": "X_WEBHOOK_TIMESTAMP" + }, + "pascalCase": { + "unsafeName": "XWebhookTimestamp", + "safeName": "XWebhookTimestamp" + } + }, + "wireValue": "x-webhook-timestamp" + }, + "format": "UNIX_SECONDS", + "tolerance": 300 + } + }, "fileUploadPayload": null, "responses": null, "examples": null, @@ -5299,6 +5360,7 @@ }, "docs": null }, + "signatureVerification": null, "fileUploadPayload": null, "responses": null, "examples": null, diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/irs/environmentAudiencesAllHack.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/irs/environmentAudiencesAllHack.json index 4c345afa9d66..e9aa2ac55f4b 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/irs/environmentAudiencesAllHack.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/irs/environmentAudiencesAllHack.json @@ -4030,6 +4030,7 @@ }, "docs": null }, + "signatureVerification": null, "fileUploadPayload": null, "responses": null, "examples": null, @@ -4134,6 +4135,7 @@ }, "docs": null }, + "signatureVerification": null, "fileUploadPayload": null, "responses": null, "examples": null, diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/irs/environmentAudiencesSelectHack.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/irs/environmentAudiencesSelectHack.json index bf5c5feaa77f..14da4aefbbe7 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/irs/environmentAudiencesSelectHack.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/irs/environmentAudiencesSelectHack.json @@ -1421,6 +1421,7 @@ }, "docs": null }, + "signatureVerification": null, "fileUploadPayload": null, "responses": null, "examples": null, diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/dollar-string-examples.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/dollar-string-examples.json index 24b265a5a0a0..c1f80b942757 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/dollar-string-examples.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/dollar-string-examples.json @@ -314,6 +314,7 @@ }, "docs": null }, + "signatureVerification": null, "fileUploadPayload": null, "responses": null, "examples": [ diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/webhook-audience.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/webhook-audience.json index fffb6aa41ee5..19326e84b549 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/webhook-audience.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/webhook-audience.json @@ -410,6 +410,7 @@ }, "docs": null }, + "signatureVerification": null, "fileUploadPayload": null, "responses": null, "examples": [ @@ -588,6 +589,7 @@ }, "docs": null }, + "signatureVerification": null, "fileUploadPayload": null, "responses": null, "examples": [ @@ -766,6 +768,7 @@ }, "docs": null }, + "signatureVerification": null, "fileUploadPayload": null, "responses": null, "examples": [ diff --git a/packages/cli/generation/ir-generator/src/converters/convertWebhookGroup.ts b/packages/cli/generation/ir-generator/src/converters/convertWebhookGroup.ts index 5af134c3e99d..18cc9f9b3a00 100644 --- a/packages/cli/generation/ir-generator/src/converters/convertWebhookGroup.ts +++ b/packages/cli/generation/ir-generator/src/converters/convertWebhookGroup.ts @@ -8,8 +8,13 @@ import { RawSchemas } from "@fern-api/fern-definition-schema"; import { + AsymmetricAlgorithm, + AsymmetricKeySignatureVerification, + AsymmetricKeySource, Availability, ExampleWebhookCall, + HmacAlgorithm, + HmacSignatureVerification, HttpResponse, HttpResponseBody, InlinedWebhookPayloadProperty, @@ -17,7 +22,13 @@ import { StreamingResponse, Webhook, WebhookGroup, - WebhookPayload + WebhookPayload, + WebhookPayloadComponent, + WebhookPayloadFormat, + WebhookSignatureEncoding, + WebhookSignatureVerification, + WebhookTimestampConfig, + WebhookTimestampFormat } from "@fern-api/ir-sdk"; import { IdGenerator, isReferencedWebhookPayloadSchema } from "@fern-api/ir-utils"; @@ -60,6 +71,7 @@ export function convertWebhookGroup({ ) : [], payload: convertWebhookPayloadSchema({ payload: webhook.payload, file }), + signatureVerification: convertWebhookSignatureSchema({ signature: webhook.signature, file }), fileUploadPayload: undefined, responses: convertWebhookResponses({ webhook, file, typeResolver }), examples: @@ -360,3 +372,189 @@ function convertWebhookJsonResponse( }) ); } + +function convertWebhookSignatureSchema({ + signature, + file +}: { + signature: RawSchemas.WebhookSignatureSchema | undefined; + file: FernFileContext; +}): WebhookSignatureVerification | undefined { + if (signature == null) { + return undefined; + } + + switch (signature.type) { + case "hmac": + return WebhookSignatureVerification.hmac(convertHmacSignature({ hmac: signature, file })); + case "asymmetric": + return WebhookSignatureVerification.asymmetric(convertAsymmetricSignature({ asymmetric: signature, file })); + default: + return undefined; + } +} + +function convertHmacSignature({ + hmac, + file +}: { + hmac: RawSchemas.HmacSignatureSchema; + file: FernFileContext; +}): HmacSignatureVerification { + return { + signatureHeaderName: file.casingsGenerator.generateNameAndWireValue({ + wireValue: hmac.header, + name: hmac.header + }), + algorithm: convertHmacAlgorithm(hmac.algorithm), + encoding: convertSignatureEncoding(hmac.encoding), + signaturePrefix: hmac["signature-prefix"], + payloadFormat: convertPayloadFormat(hmac["payload-format"]), + timestamp: convertTimestampConfig({ timestamp: hmac.timestamp, file }) + }; +} + +function convertAsymmetricSignature({ + asymmetric, + file +}: { + asymmetric: RawSchemas.AsymmetricSignatureSchema; + file: FernFileContext; +}): AsymmetricKeySignatureVerification { + return { + signatureHeaderName: file.casingsGenerator.generateNameAndWireValue({ + wireValue: asymmetric.header, + name: asymmetric.header + }), + algorithm: convertAsymmetricAlgorithm(asymmetric["asymmetric-algorithm"]), + encoding: convertSignatureEncoding(asymmetric.encoding), + signaturePrefix: asymmetric["signature-prefix"], + keySource: convertKeySource({ asymmetric, file }), + timestamp: convertTimestampConfig({ timestamp: asymmetric.timestamp, file }) + }; +} + +function convertHmacAlgorithm(algorithm: RawSchemas.WebhookSignatureAlgorithmSchema | undefined): HmacAlgorithm { + switch (algorithm) { + case "sha1": + return HmacAlgorithm.Sha1; + case "sha384": + return HmacAlgorithm.Sha384; + case "sha512": + return HmacAlgorithm.Sha512; + case "sha256": + case undefined: + return HmacAlgorithm.Sha256; + } +} + +function convertSignatureEncoding( + encoding: RawSchemas.WebhookSignatureEncodingSchema | undefined +): WebhookSignatureEncoding { + switch (encoding) { + case "hex": + return WebhookSignatureEncoding.Hex; + case "base64": + case undefined: + return WebhookSignatureEncoding.Base64; + } +} + +function convertAsymmetricAlgorithm(algorithm: RawSchemas.AsymmetricAlgorithmSchema): AsymmetricAlgorithm { + switch (algorithm) { + case "rsa-sha256": + return AsymmetricAlgorithm.RsaSha256; + case "rsa-sha384": + return AsymmetricAlgorithm.RsaSha384; + case "rsa-sha512": + return AsymmetricAlgorithm.RsaSha512; + case "ecdsa-sha256": + return AsymmetricAlgorithm.EcdsaSha256; + case "ecdsa-sha384": + return AsymmetricAlgorithm.EcdsaSha384; + case "ecdsa-sha512": + return AsymmetricAlgorithm.EcdsaSha512; + case "ed25519": + return AsymmetricAlgorithm.Ed25519; + } +} + +function convertKeySource({ + asymmetric, + file +}: { + asymmetric: RawSchemas.AsymmetricSignatureSchema; + file: FernFileContext; +}): AsymmetricKeySource { + if (asymmetric["jwks-url"] != null) { + return AsymmetricKeySource.jwks({ + url: asymmetric["jwks-url"], + keyIdHeader: + asymmetric["key-id-header"] != null + ? file.casingsGenerator.generateNameAndWireValue({ + wireValue: asymmetric["key-id-header"], + name: asymmetric["key-id-header"] + }) + : undefined + }); + } + return AsymmetricKeySource.static({}); +} + +function convertPayloadFormat(payloadFormat: RawSchemas.WebhookPayloadFormatSchema | undefined): WebhookPayloadFormat { + if (payloadFormat == null) { + return { + components: [WebhookPayloadComponent.Body], + delimiter: "" + }; + } + return { + components: payloadFormat.components.map(convertPayloadComponent), + delimiter: payloadFormat.delimiter ?? "" + }; +} + +function convertPayloadComponent(component: RawSchemas.WebhookPayloadComponentSchema): WebhookPayloadComponent { + switch (component) { + case "body": + return WebhookPayloadComponent.Body; + case "timestamp": + return WebhookPayloadComponent.Timestamp; + case "notification-url": + return WebhookPayloadComponent.NotificationUrl; + case "message-id": + return WebhookPayloadComponent.MessageId; + } +} + +function convertTimestampConfig({ + timestamp, + file +}: { + timestamp: RawSchemas.WebhookTimestampSchema | undefined; + file: FernFileContext; +}): WebhookTimestampConfig | undefined { + if (timestamp == null) { + return undefined; + } + return { + headerName: file.casingsGenerator.generateNameAndWireValue({ + wireValue: timestamp.header, + name: timestamp.header + }), + format: convertTimestampFormat(timestamp.format), + tolerance: timestamp.tolerance + }; +} + +function convertTimestampFormat(format: RawSchemas.WebhookTimestampFormatSchema | undefined): WebhookTimestampFormat { + switch (format) { + case "unix-millis": + return WebhookTimestampFormat.UnixMillis; + case "iso8601": + return WebhookTimestampFormat.Iso8601; + case "unix-seconds": + case undefined: + return WebhookTimestampFormat.UnixSeconds; + } +} diff --git a/packages/cli/generation/local-generation/local-workspace-runner/src/LocalTaskHandler.ts b/packages/cli/generation/local-generation/local-workspace-runner/src/LocalTaskHandler.ts index 1300f0c52416..c465fea3e5f5 100644 --- a/packages/cli/generation/local-generation/local-workspace-runner/src/LocalTaskHandler.ts +++ b/packages/cli/generation/local-generation/local-workspace-runner/src/LocalTaskHandler.ts @@ -277,6 +277,10 @@ export class LocalTaskHandler { ); const fernIgnorePaths = await getFernIgnorePaths({ absolutePathToFernignore }); + // Also preserve README.md if the generated output doesn't include one, + // to prevent accidental deletion when README generation fails silently. + const pathsToPreserve = await this.getPathsToPreserve(fernIgnorePaths); + const response = await this.runGitCommand(["config", "--list"], this.absolutePathToLocalOutput); if (!response.includes("user.name")) { await this.runGitCommand(["config", "user.name", "fern-api"], this.absolutePathToLocalOutput); @@ -295,9 +299,9 @@ export class LocalTaskHandler { // If absolutePathToLocalOutput is already a git repository, work directly in it await this.runGitCommand(["add", "."], this.absolutePathToLocalOutput); - // Undo changes to fernignore paths - await this.runGitCommand(["reset", "--", ...fernIgnorePaths], this.absolutePathToLocalOutput); - await this.runGitCommand(["clean", "-fd", "--", ...fernIgnorePaths], this.absolutePathToLocalOutput); + // Undo changes to preserved paths (fernignore + README.md if missing from output) + await this.runGitCommand(["reset", "--", ...pathsToPreserve], this.absolutePathToLocalOutput); + await this.runGitCommand(["clean", "-fd", "--", ...pathsToPreserve], this.absolutePathToLocalOutput); await this.runGitCommand(["restore", "."], this.absolutePathToLocalOutput); } @@ -311,6 +315,9 @@ export class LocalTaskHandler { ); const fernIgnorePaths = await getFernIgnorePaths({ absolutePathToFernignore }); + // Also preserve README.md if the generated output doesn't include one. + const pathsToPreserve = await this.getPathsToPreserve(fernIgnorePaths); + // Copy files from local output to tmp directory await cp(this.absolutePathToLocalOutput, tmpOutputResolutionDir, { recursive: true }); @@ -333,9 +340,9 @@ export class LocalTaskHandler { await this.runGitCommand(["add", "."], tmpOutputResolutionDir); - // Undo changes to fernignore paths - await this.runGitCommand(["reset", "--", ...fernIgnorePaths], tmpOutputResolutionDir); - await this.runGitCommand(["clean", "-fd", "--", ...fernIgnorePaths], tmpOutputResolutionDir); + // Undo changes to preserved paths (fernignore + README.md if missing from output) + await this.runGitCommand(["reset", "--", ...pathsToPreserve], tmpOutputResolutionDir); + await this.runGitCommand(["clean", "-fd", "--", ...pathsToPreserve], tmpOutputResolutionDir); await this.runGitCommand(["restore", "."], tmpOutputResolutionDir); // remove .git dir before copying files over @@ -352,10 +359,16 @@ export class LocalTaskHandler { // Read directory contents const contents = await readdir(this.absolutePathToLocalOutput); - // Delete everything except .git + // Build list of items to preserve: always .git, plus README.md if not in generated output + const itemsToPreserve = [".git"]; + if (await this.generatedOutputMissingReadme()) { + itemsToPreserve.push("README.md"); + } + + // Delete everything except preserved items await Promise.all( contents - .filter((item) => item !== ".git") + .filter((item) => !itemsToPreserve.includes(item)) .map((item) => rm(join(this.absolutePathToLocalOutput, RelativeFilePath.of(item)), { force: true, @@ -410,6 +423,38 @@ export class LocalTaskHandler { await cp(absolutePathToTmpSnippetJSON, absolutePathToLocalSnippetJSON); } + /** + * Checks whether the generated output is missing README.md. + * When true, the existing README.md should be preserved to prevent + * accidental deletion caused by silent README generation failures. + */ + private async generatedOutputMissingReadme(): Promise { + try { + const contents = await readdir(this.absolutePathToTmpOutputDirectory); + // If the output is a single zip file we can't inspect its contents, + // so conservatively assume it includes a README. + if (contents.length === 1 && contents[0] != null && contents[0].endsWith(".zip")) { + return false; + } + return !contents.includes("README.md"); + } catch { + // If we can't check the generated output, preserve the existing README to be safe + return true; + } + } + + /** + * Returns the list of paths that should be preserved during file copy. + * Starts with fernignore paths, and adds README.md if the generated + * output does not include one. + */ + private async getPathsToPreserve(fernIgnorePaths: string[]): Promise { + if (await this.generatedOutputMissingReadme()) { + return [...fernIgnorePaths, "README.md"]; + } + return fernIgnorePaths; + } + private async runGitCommand(options: string[], cwd: AbsoluteFilePath): Promise { const response = await loggingExeca(this.context.logger, "git", options, { cwd, diff --git a/packages/cli/generation/remote-generation/remote-workspace-runner/src/__test__/publishDocs.test.ts b/packages/cli/generation/remote-generation/remote-workspace-runner/src/__test__/publishDocs.test.ts new file mode 100644 index 000000000000..3782964045fa --- /dev/null +++ b/packages/cli/generation/remote-generation/remote-workspace-runner/src/__test__/publishDocs.test.ts @@ -0,0 +1,87 @@ +import { RelativeFilePath } from "@fern-api/fs-utils"; + +// Define the function locally to avoid import issues - tests the same logic +function sanitizeRelativePathForS3(relativeFilePath: RelativeFilePath): RelativeFilePath { + // Replace ../ segments with _dot_dot_/ to prevent HTTP client normalization issues + // that cause S3 signature mismatches when paths contain parent directory references + return relativeFilePath.replace(/\.\.\//g, "_dot_dot_/") as RelativeFilePath; +} + +describe("publishDocs S3 path sanitization", () => { + describe("sanitizeRelativePathForS3", () => { + it("leaves normal relative paths unchanged", () => { + const path = RelativeFilePath.of("docs/assets/logo.svg"); + const result = sanitizeRelativePathForS3(path); + expect(result).toBe("docs/assets/logo.svg"); + }); + + it("replaces single ../ with _dot_dot_/", () => { + const path = RelativeFilePath.of("../docs/assets/logo.svg"); + const result = sanitizeRelativePathForS3(path); + expect(result).toBe("_dot_dot_/docs/assets/logo.svg"); + expect(result).not.toContain("../"); + }); + + it("replaces multiple ../ segments with _dot_dot_/", () => { + const path = RelativeFilePath.of("../../assets/images/favicon.ico"); + const result = sanitizeRelativePathForS3(path); + expect(result).toBe("_dot_dot_/_dot_dot_/assets/images/favicon.ico"); + expect(result).not.toContain("../"); + }); + + it("handles mixed ../ segments throughout path", () => { + const path = RelativeFilePath.of("../docs/../assets/logo.svg"); + const result = sanitizeRelativePathForS3(path); + expect(result).toBe("_dot_dot_/docs/_dot_dot_/assets/logo.svg"); + expect(result).not.toContain("../"); + }); + + it("handles deeply nested ../ segments", () => { + const path = RelativeFilePath.of("../../../../other-project/assets/logo.svg"); + const result = sanitizeRelativePathForS3(path); + expect(result).toBe("_dot_dot_/_dot_dot_/_dot_dot_/_dot_dot_/other-project/assets/logo.svg"); + expect(result).not.toContain("../"); + }); + + it("preserves path that already contains _dot_dot_/", () => { + // Edge case: if somehow a path already contains _dot_dot_/, it should be preserved + const path = RelativeFilePath.of("docs/_dot_dot_/assets/logo.svg"); + const result = sanitizeRelativePathForS3(path); + expect(result).toBe("docs/_dot_dot_/assets/logo.svg"); + }); + + it("ensures consistent output for same input", () => { + // Same input should always produce same output (idempotent) + const path = RelativeFilePath.of("../docs/assets/logo.svg"); + const result1 = sanitizeRelativePathForS3(path); + const result2 = sanitizeRelativePathForS3(path); + expect(result1).toBe(result2); + expect(result1).toBe("_dot_dot_/docs/assets/logo.svg"); + }); + + it("handles root-relative paths starting with ../", () => { + const path = RelativeFilePath.of("../favicon.ico"); + const result = sanitizeRelativePathForS3(path); + expect(result).toBe("_dot_dot_/favicon.ico"); + expect(result).not.toContain("../"); + }); + + it("prevents S3 signature mismatch by avoiding HTTP normalization", () => { + // This test documents the core issue we're fixing: + // HTTP clients normalize ../ in URLs, but S3 signatures are computed + // against the original path. By replacing ../ with _dot_dot_/, we prevent + // HTTP normalization from changing the request path. + const path = RelativeFilePath.of("../assets/logo.svg"); + const result = sanitizeRelativePathForS3(path); + + // The sanitized path should not be affected by HTTP URL normalization + expect(result).toBe("_dot_dot_/assets/logo.svg"); + expect(result).not.toContain("../"); + + // Simulate what would happen with HTTP normalization on original path: + // "../assets/logo.svg" would become "assets/logo.svg" + // But our sanitized path "_dot_dot_/assets/logo.svg" stays unchanged + expect(result).not.toBe("assets/logo.svg"); + }); + }); +}); diff --git a/packages/cli/generation/remote-generation/remote-workspace-runner/src/publishDocs.ts b/packages/cli/generation/remote-generation/remote-workspace-runner/src/publishDocs.ts index 4fbade766602..d0b10c240392 100644 --- a/packages/cli/generation/remote-generation/remote-workspace-runner/src/publishDocs.ts +++ b/packages/cli/generation/remote-generation/remote-workspace-runner/src/publishDocs.ts @@ -38,11 +38,21 @@ interface FileWithMimeType { relativeFilePath: RelativeFilePath; } +interface FileWithSanitizedPathAndMimeType extends FileWithMimeType { + sanitizedPath: RelativeFilePath; +} + export async function calculateFileHash(absoluteFilePath: AbsoluteFilePath | string): Promise { const fileBuffer = await readFile(absoluteFilePath); return createHash("sha256").update(new Uint8Array(fileBuffer)).digest("hex"); } +export function sanitizeRelativePathForS3(relativeFilePath: RelativeFilePath): RelativeFilePath { + // Replace ../ segments with _dot_dot_/ to prevent HTTP client normalization issues + // that cause S3 signature mismatches when paths contain parent directory references + return relativeFilePath.replace(/\.\.\//g, "_dot_dot_/") as RelativeFilePath; +} + export async function publishDocs({ token, organization, @@ -114,13 +124,24 @@ export async function publishDocs({ taskContext: context, editThisPage, uploadFiles: async (files) => { - const filesMap = new Map(files.map((file) => [file.absoluteFilePath, file])); - const filesWithMimeType: FileWithMimeType[] = files + // Pre-compute sanitized paths and attach to file objects + const filesWithSanitizedPaths = files.map((file) => ({ + ...file, + sanitizedPath: sanitizeRelativePathForS3(file.relativeFilePath) + })); + + const filesMap = new Map(filesWithSanitizedPaths.map((file) => [file.absoluteFilePath, file])); + const sanitizedToAbsoluteMap = new Map( + filesWithSanitizedPaths.map((file) => [file.sanitizedPath, file.absoluteFilePath]) + ); + const filesWithMimeType: FileWithSanitizedPathAndMimeType[] = filesWithSanitizedPaths .map((fileMetadata) => ({ ...fileMetadata, mediaType: mime.lookup(fileMetadata.absoluteFilePath) })) - .filter((fileMetadata): fileMetadata is FileWithMimeType => fileMetadata.mediaType !== false); + .filter( + (fileMetadata): fileMetadata is FileWithSanitizedPathAndMimeType => fileMetadata.mediaType !== false + ); const imagesToMeasure = filesWithMimeType .filter((file) => MediaType.parse(file.mediaType)?.isImage() ?? false) @@ -141,10 +162,9 @@ export async function publishDocs({ return null; } + const sanitizedPath = filePath.sanitizedPath; const obj = { - filePath: CjsFdrSdk.docs.v1.write.FilePath( - convertToFernHostRelativeFilePath(filePath.relativeFilePath) - ), + filePath: CjsFdrSdk.docs.v1.write.FilePath(convertToFernHostRelativeFilePath(sanitizedPath)), width: image.width, height: image.height, blurDataUrl: image.blurDataUrl, @@ -161,7 +181,9 @@ export async function publishDocs({ const hashImageTime = performance.now() - hashImageStart; context.logger.debug(`Hashed ${images.length} images in ${hashImageTime.toFixed(0)}ms`); - const nonImageFiles = files.filter(({ absoluteFilePath }) => !measuredImages.has(absoluteFilePath)); + const nonImageFiles = filesWithSanitizedPaths.filter( + ({ absoluteFilePath }) => !measuredImages.has(absoluteFilePath) + ); context.logger.debug( `Hashing ${nonImageFiles.length} non-image files with concurrency ${HASH_CONCURRENCY}...` @@ -170,10 +192,12 @@ export async function publishDocs({ const filepaths: CjsFdrSdk.docs.v2.write.FilePathInput[] = await asyncPool( HASH_CONCURRENCY, nonImageFiles, - async (file) => ({ - path: CjsFdrSdk.docs.v1.write.FilePath(convertToFernHostRelativeFilePath(file.relativeFilePath)), - fileHash: await calculateFileHash(file.absoluteFilePath) - }) + async (file) => { + return { + path: CjsFdrSdk.docs.v1.write.FilePath(convertToFernHostRelativeFilePath(file.sanitizedPath)), + fileHash: await calculateFileHash(file.absoluteFilePath) + }; + } ); const hashNonImageTime = performance.now() - hashNonImageStart; context.logger.debug(`Hashed ${filepaths.length} non-image files in ${hashNonImageTime.toFixed(0)}ms`); @@ -210,7 +234,8 @@ export async function publishDocs({ urlsToUpload, docsWorkspace.absoluteFilePath, context, - UPLOAD_FILE_BATCH_SIZE + UPLOAD_FILE_BATCH_SIZE, + sanitizedToAbsoluteMap ); } else { context.logger.debug(`No files to upload (all ${skippedCount} up to date)`); @@ -218,7 +243,8 @@ export async function publishDocs({ } return convertToFilePathPairs( startDocsRegisterResponse.body.uploadUrls, - docsWorkspace.absoluteFilePath + docsWorkspace.absoluteFilePath, + sanitizedToAbsoluteMap ); } else { return await startDocsRegisterFailed(startDocsRegisterResponse.error, context, organization); @@ -263,7 +289,8 @@ export async function publishDocs({ urlsToUpload, docsWorkspace.absoluteFilePath, context, - UPLOAD_FILE_BATCH_SIZE + UPLOAD_FILE_BATCH_SIZE, + sanitizedToAbsoluteMap ); } else { context.logger.info("No files to upload (all up to date)"); @@ -271,7 +298,8 @@ export async function publishDocs({ } return convertToFilePathPairs( startDocsRegisterResponse.body.uploadUrls, - docsWorkspace.absoluteFilePath + docsWorkspace.absoluteFilePath, + sanitizedToAbsoluteMap ); } else { return startDocsRegisterFailed(startDocsRegisterResponse.error, context, organization); @@ -496,7 +524,8 @@ async function uploadFiles( filesToUpload: Record, docsWorkspacePath: AbsoluteFilePath, context: TaskContext, - batchSize: number + batchSize: number, + sanitizedToAbsoluteMap: Map ): Promise { const startTime = Date.now(); const totalFiles = Object.keys(filesToUpload).length; @@ -506,8 +535,9 @@ async function uploadFiles( for (const chunkedFilepaths of chunkedFilepathsToUpload) { await Promise.all( chunkedFilepaths.map(async ([key, { uploadUrl }]) => { - const relativeFilePath = RelativeFilePath.of(key); - const absoluteFilePath = resolve(docsWorkspacePath, relativeFilePath); + // Use the mapping to get the original absolute path instead of reconstructing from sanitized key + const absoluteFilePath = + sanitizedToAbsoluteMap.get(key) || resolve(docsWorkspacePath, RelativeFilePath.of(key)); try { const mimeType = mime.lookup(absoluteFilePath); await axios.put(uploadUrl, await readFile(absoluteFilePath), { @@ -533,12 +563,14 @@ async function uploadFiles( function convertToFilePathPairs( uploadUrls: Record, - docsWorkspacePath: AbsoluteFilePath + docsWorkspacePath: AbsoluteFilePath, + sanitizedToAbsoluteMap?: Map ): UploadedFile[] { const toRet: UploadedFile[] = []; for (const [key, value] of Object.entries(uploadUrls)) { const relativeFilePath = RelativeFilePath.of(key); - const absoluteFilePath = resolve(docsWorkspacePath, relativeFilePath); + // Use the mapping to get the original absolute path instead of reconstructing from sanitized key + const absoluteFilePath = sanitizedToAbsoluteMap?.get(key) || resolve(docsWorkspacePath, relativeFilePath); toRet.push({ relativeFilePath, absoluteFilePath, diff --git a/packages/cli/project-loader/src/loadProject.ts b/packages/cli/project-loader/src/loadProject.ts index 3eebfcf7242c..ad25a059fbf8 100644 --- a/packages/cli/project-loader/src/loadProject.ts +++ b/packages/cli/project-loader/src/loadProject.ts @@ -65,18 +65,18 @@ export async function loadProjectFromDirectory({ }: loadProject.LoadProjectFromDirectoryArgs): Promise { let apiWorkspaces: AbstractAPIWorkspace[] = []; - if ( - (await doesPathExist(join(absolutePathToFernDirectory, RelativeFilePath.of(APIS_DIRECTORY)))) || - (await doesPathExist(join(absolutePathToFernDirectory, RelativeFilePath.of(DEFINITION_DIRECTORY)))) || - (await doesPathExist( - join(absolutePathToFernDirectory, RelativeFilePath.of(GENERATORS_CONFIGURATION_FILENAME)) - )) || - (await doesPathExist( + const [apisExists, defExists, genExists, genAltExists, openapiExists, asyncapiExists] = await Promise.all([ + doesPathExist(join(absolutePathToFernDirectory, RelativeFilePath.of(APIS_DIRECTORY))), + doesPathExist(join(absolutePathToFernDirectory, RelativeFilePath.of(DEFINITION_DIRECTORY))), + doesPathExist(join(absolutePathToFernDirectory, RelativeFilePath.of(GENERATORS_CONFIGURATION_FILENAME))), + doesPathExist( join(absolutePathToFernDirectory, RelativeFilePath.of(GENERATORS_CONFIGURATION_FILENAME_ALTERNATIVE)) - )) || - (await doesPathExist(join(absolutePathToFernDirectory, RelativeFilePath.of(OPENAPI_DIRECTORY)))) || - (await doesPathExist(join(absolutePathToFernDirectory, RelativeFilePath.of(ASYNCAPI_DIRECTORY)))) - ) { + ), + doesPathExist(join(absolutePathToFernDirectory, RelativeFilePath.of(OPENAPI_DIRECTORY))), + doesPathExist(join(absolutePathToFernDirectory, RelativeFilePath.of(ASYNCAPI_DIRECTORY))) + ]); + + if (apisExists || defExists || genExists || genAltExists || openapiExists || asyncapiExists) { apiWorkspaces = await loadApis({ cliName, fernDirectory: absolutePathToFernDirectory, diff --git a/packages/cli/register/src/ir-to-fdr-converter/__test__/__snapshots__/webhook-multipart-form-data-ir.snap b/packages/cli/register/src/ir-to-fdr-converter/__test__/__snapshots__/webhook-multipart-form-data-ir.snap index 26799045bd49..580705f5cefb 100644 --- a/packages/cli/register/src/ir-to-fdr-converter/__test__/__snapshots__/webhook-multipart-form-data-ir.snap +++ b/packages/cli/register/src/ir-to-fdr-converter/__test__/__snapshots__/webhook-multipart-form-data-ir.snap @@ -1541,6 +1541,7 @@ "statusCode": 500, }, ], + "signatureVerification": undefined, "v2Examples": { "autogeneratedExamples": { "documentManagementDocumentUploadedWebhookExample": { diff --git a/packages/cli/register/src/ir-to-fdr-converter/__test__/__snapshots__/webhook-openapi-responses-ir.snap b/packages/cli/register/src/ir-to-fdr-converter/__test__/__snapshots__/webhook-openapi-responses-ir.snap index 895d99acd24b..1696c92315c3 100644 --- a/packages/cli/register/src/ir-to-fdr-converter/__test__/__snapshots__/webhook-openapi-responses-ir.snap +++ b/packages/cli/register/src/ir-to-fdr-converter/__test__/__snapshots__/webhook-openapi-responses-ir.snap @@ -2186,6 +2186,7 @@ "statusCode": 200, }, ], + "signatureVerification": undefined, "v2Examples": { "autogeneratedExamples": {}, "userSpecifiedExamples": { @@ -2304,6 +2305,7 @@ "statusCode": 500, }, ], + "signatureVerification": undefined, "v2Examples": { "autogeneratedExamples": {}, "userSpecifiedExamples": { diff --git a/packages/cli/workspace/lazy-fern-workspace/src/LazyFernWorkspace.ts b/packages/cli/workspace/lazy-fern-workspace/src/LazyFernWorkspace.ts index 0b46fbe44572..161049b21ea5 100644 --- a/packages/cli/workspace/lazy-fern-workspace/src/LazyFernWorkspace.ts +++ b/packages/cli/workspace/lazy-fern-workspace/src/LazyFernWorkspace.ts @@ -39,7 +39,7 @@ export class LazyFernWorkspace extends AbstractAPIWorkspace { const key = hash(settings ?? {}); @@ -63,7 +63,8 @@ export class LazyFernWorkspace extends AbstractAPIWorkspace { - const documents: Document[] = []; - for (const spec of specs) { - try { - const contents = (await readFile(spec.absoluteFilepath)).toString(); - let sourceRelativePath = relative(this.absoluteFilePath, spec.source.file); - if (spec.source.relativePathToDependency != null) { - sourceRelativePath = join(spec.source.relativePathToDependency, sourceRelativePath); - } - const source = - spec.source.type === "protobuf" - ? OpenApiIrSource.protobuf({ file: sourceRelativePath }) - : OpenApiIrSource.openapi({ file: sourceRelativePath }); + const results = await Promise.all(specs.map((spec) => this.loadDocument({ context, spec, loadAiExamples }))); + return results.filter((doc): doc is Document => doc != null); + } - if (contents.includes("openapi") || contents.includes("swagger")) { - try { - const openAPI = await loadOpenAPI({ - absolutePathToOpenAPI: spec.absoluteFilepath, - context, - absolutePathToOpenAPIOverrides: spec.absoluteFilepathToOverrides, - absolutePathToOpenAPIOverlays: spec.absoluteFilepathToOverlays, - loadAiExamples - }); - if (isOpenAPIV3(openAPI)) { - documents.push({ - type: "openapi", - value: openAPI, - source, - namespace: spec.namespace, - settings: getParseOptions({ options: spec.settings }) - }); - continue; - } else if (isOpenAPIV2(openAPI)) { - // default to https to produce a valid URL - if (!openAPI.schemes || openAPI.schemes.length === 0) { - openAPI.schemes = ["https"]; - } - const convertedOpenAPI = await convertOpenAPIV2ToV3(openAPI); - documents.push({ - type: "openapi", - value: convertedOpenAPI, - source, - namespace: spec.namespace, - settings: getParseOptions({ options: spec.settings }) - }); - continue; - } - } catch (error) { - context.logger.debug( - `Failed to parse OpenAPI document at ${spec.absoluteFilepath}: ${error}. Skipping...` - ); - continue; - } - } + private async loadDocument({ + context, + spec, + loadAiExamples + }: { + context: TaskContext; + spec: OpenAPISpec; + loadAiExamples: boolean; + }): Promise { + try { + const contents = (await readFile(spec.absoluteFilepath)).toString(); + let sourceRelativePath = relative(this.absoluteFilePath, spec.source.file); + if (spec.source.relativePathToDependency != null) { + sourceRelativePath = join(spec.source.relativePathToDependency, sourceRelativePath); + } + const source = + spec.source.type === "protobuf" + ? OpenApiIrSource.protobuf({ file: sourceRelativePath }) + : OpenApiIrSource.openapi({ file: sourceRelativePath }); - if (contents.includes("asyncapi")) { - try { - const asyncAPI = await loadAsyncAPI({ - context, - absoluteFilePath: spec.absoluteFilepath, - absoluteFilePathToOverrides: spec.absoluteFilepathToOverrides - }); - documents.push({ - type: "asyncapi", - value: asyncAPI, + if (contents.includes("openapi") || contents.includes("swagger")) { + try { + const openAPI = await loadOpenAPI({ + absolutePathToOpenAPI: spec.absoluteFilepath, + context, + absolutePathToOpenAPIOverrides: spec.absoluteFilepathToOverrides, + absolutePathToOpenAPIOverlays: spec.absoluteFilepathToOverlays, + loadAiExamples + }); + if (isOpenAPIV3(openAPI)) { + return { + type: "openapi", + value: openAPI, source, namespace: spec.namespace, settings: getParseOptions({ options: spec.settings }) - }); - continue; - } catch (error) { - context.logger.error( - `Failed to parse AsyncAPI document at ${spec.absoluteFilepath}: ${error}. Skipping...` - ); - continue; - } - } - - if (contents.includes("openrpc")) { - try { - const asyncAPI = await loadAsyncAPI({ - context, - absoluteFilePath: spec.absoluteFilepath, - absoluteFilePathToOverrides: spec.absoluteFilepathToOverrides - }); - documents.push({ - type: "asyncapi", - value: asyncAPI, + }; + } else if (isOpenAPIV2(openAPI)) { + // default to https to produce a valid URL + if (!openAPI.schemes || openAPI.schemes.length === 0) { + openAPI.schemes = ["https"]; + } + const convertedOpenAPI = await convertOpenAPIV2ToV3(openAPI); + return { + type: "openapi", + value: convertedOpenAPI, source, namespace: spec.namespace, settings: getParseOptions({ options: spec.settings }) - }); - continue; - } catch (error) { - context.logger.error( - `Failed to parse OpenRPC document at ${spec.absoluteFilepath}: ${error}. Skipping...` - ); - continue; + }; } + } catch (error) { + context.logger.debug( + `Failed to parse OpenAPI document at ${spec.absoluteFilepath}: ${error}. Skipping...` + ); + return undefined; } + } - context.logger.warn( - `${spec.absoluteFilepath} is not a valid OpenAPI, AsyncAPI, or OpenRPC file. Skipping...` - ); - } catch (error) { - context.logger.error(`Failed to read or process file ${spec.absoluteFilepath}: ${error}. Skipping...`); - continue; + if (contents.includes("asyncapi")) { + try { + const asyncAPI = await loadAsyncAPI({ + context, + absoluteFilePath: spec.absoluteFilepath, + absoluteFilePathToOverrides: spec.absoluteFilepathToOverrides + }); + return { + type: "asyncapi", + value: asyncAPI, + source, + namespace: spec.namespace, + settings: getParseOptions({ options: spec.settings }) + }; + } catch (error) { + context.logger.error( + `Failed to parse AsyncAPI document at ${spec.absoluteFilepath}: ${error}. Skipping...` + ); + return undefined; + } } + + if (contents.includes("openrpc")) { + try { + const asyncAPI = await loadAsyncAPI({ + context, + absoluteFilePath: spec.absoluteFilepath, + absoluteFilePathToOverrides: spec.absoluteFilepathToOverrides + }); + return { + type: "asyncapi", + value: asyncAPI, + source, + namespace: spec.namespace, + settings: getParseOptions({ options: spec.settings }) + }; + } catch (error) { + context.logger.error( + `Failed to parse OpenRPC document at ${spec.absoluteFilepath}: ${error}. Skipping...` + ); + return undefined; + } + } + + context.logger.warn( + `${spec.absoluteFilepath} is not a valid OpenAPI, AsyncAPI, or OpenRPC file. Skipping...` + ); + return undefined; + } catch (error) { + context.logger.error(`Failed to read or process file ${spec.absoluteFilepath}: ${error}. Skipping...`); + return undefined; } - return documents; } } diff --git a/packages/cli/workspace/lazy-fern-workspace/src/package-yml.schema.json b/packages/cli/workspace/lazy-fern-workspace/src/package-yml.schema.json index 38fcca39fb8f..1c6fca586369 100644 --- a/packages/cli/workspace/lazy-fern-workspace/src/package-yml.schema.json +++ b/packages/cli/workspace/lazy-fern-workspace/src/package-yml.schema.json @@ -3260,6 +3260,254 @@ } ] }, + "webhooks.WebhookSignatureAlgorithmSchema": { + "type": "string", + "enum": [ + "sha256", + "sha1", + "sha384", + "sha512" + ] + }, + "webhooks.WebhookSignatureEncodingSchema": { + "type": "string", + "enum": [ + "base64", + "hex" + ] + }, + "webhooks.WebhookPayloadComponentSchema": { + "type": "string", + "enum": [ + "body", + "timestamp", + "notification-url", + "message-id" + ] + }, + "webhooks.WebhookPayloadFormatSchema": { + "type": "object", + "properties": { + "components": { + "type": "array", + "items": { + "$ref": "#/definitions/webhooks.WebhookPayloadComponentSchema" + } + }, + "delimiter": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "components" + ], + "additionalProperties": false + }, + "webhooks.WebhookTimestampFormatSchema": { + "type": "string", + "enum": [ + "unix-seconds", + "unix-millis", + "iso8601" + ] + }, + "webhooks.WebhookTimestampSchema": { + "type": "object", + "properties": { + "header": { + "type": "string" + }, + "format": { + "oneOf": [ + { + "$ref": "#/definitions/webhooks.WebhookTimestampFormatSchema" + }, + { + "type": "null" + } + ] + }, + "tolerance": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "header" + ], + "additionalProperties": false + }, + "webhooks.AsymmetricAlgorithmSchema": { + "type": "string", + "enum": [ + "rsa-sha256", + "rsa-sha384", + "rsa-sha512", + "ecdsa-sha256", + "ecdsa-sha384", + "ecdsa-sha512", + "ed25519" + ] + }, + "webhooks.WebhookSignatureSchema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "hmac", + "asymmetric" + ] + } + }, + "oneOf": [ + { + "properties": { + "type": { + "const": "hmac" + }, + "header": { + "type": "string" + }, + "algorithm": { + "oneOf": [ + { + "$ref": "#/definitions/webhooks.WebhookSignatureAlgorithmSchema" + }, + { + "type": "null" + } + ] + }, + "encoding": { + "oneOf": [ + { + "$ref": "#/definitions/webhooks.WebhookSignatureEncodingSchema" + }, + { + "type": "null" + } + ] + }, + "signature-prefix": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "payload-format": { + "oneOf": [ + { + "$ref": "#/definitions/webhooks.WebhookPayloadFormatSchema" + }, + { + "type": "null" + } + ] + }, + "timestamp": { + "oneOf": [ + { + "$ref": "#/definitions/webhooks.WebhookTimestampSchema" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "type", + "header" + ] + }, + { + "properties": { + "type": { + "const": "asymmetric" + }, + "header": { + "type": "string" + }, + "asymmetric-algorithm": { + "$ref": "#/definitions/webhooks.AsymmetricAlgorithmSchema" + }, + "encoding": { + "oneOf": [ + { + "$ref": "#/definitions/webhooks.WebhookSignatureEncodingSchema" + }, + { + "type": "null" + } + ] + }, + "signature-prefix": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "jwks-url": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "key-id-header": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "timestamp": { + "oneOf": [ + { + "$ref": "#/definitions/webhooks.WebhookTimestampSchema" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "type", + "header", + "asymmetric-algorithm" + ] + } + ] + }, "examples.ExampleWebhookCallSchema": { "type": "object", "properties": { @@ -3367,6 +3615,16 @@ "payload": { "$ref": "#/definitions/webhooks.WebhookPayloadSchema" }, + "signature": { + "oneOf": [ + { + "$ref": "#/definitions/webhooks.WebhookSignatureSchema" + }, + { + "type": "null" + } + ] + }, "response": { "oneOf": [ { diff --git a/packages/cli/workspace/lazy-fern-workspace/src/utils/listFernFiles.ts b/packages/cli/workspace/lazy-fern-workspace/src/utils/listFernFiles.ts index ab16b4ad6ba0..d51a0983ca17 100644 --- a/packages/cli/workspace/lazy-fern-workspace/src/utils/listFernFiles.ts +++ b/packages/cli/workspace/lazy-fern-workspace/src/utils/listFernFiles.ts @@ -3,18 +3,15 @@ import { AbsoluteFilePath, listFiles, RelativeFilePath, relative } from "@fern-a import { readFile } from "fs/promises"; export async function listFernFiles(root: AbsoluteFilePath, extensionGlob: string): Promise { - const files: FernFile[] = []; - - for (const absoluteFilepath of await listFiles(root, extensionGlob)) { - files.push( - await createFernFile({ + const absoluteFilepaths = await listFiles(root, extensionGlob); + return await Promise.all( + absoluteFilepaths.map((absoluteFilepath) => + createFernFile({ relativeFilepath: relative(root, absoluteFilepath), absoluteFilepath }) - ); - } - - return files; + ) + ); } async function createFernFile({ diff --git a/packages/cli/workspace/lazy-fern-workspace/src/utils/validateStructureOfYamlFiles.ts b/packages/cli/workspace/lazy-fern-workspace/src/utils/validateStructureOfYamlFiles.ts index 99f7a77a2f55..371e3befb9db 100644 --- a/packages/cli/workspace/lazy-fern-workspace/src/utils/validateStructureOfYamlFiles.ts +++ b/packages/cli/workspace/lazy-fern-workspace/src/utils/validateStructureOfYamlFiles.ts @@ -1,7 +1,12 @@ import { OnDiskNamedDefinitionFile, ParsedFernFile } from "@fern-api/api-workspace-commons"; import { FERN_PACKAGE_MARKER_FILENAME, ROOT_API_FILENAME } from "@fern-api/configuration-loader"; import { entries, validateAgainstJsonSchema } from "@fern-api/core-utils"; -import { PackageMarkerFileSchema, RawSchemas, RootApiFileSchema } from "@fern-api/fern-definition-schema"; +import { + DefinitionFileSchema, + PackageMarkerFileSchema, + RawSchemas, + RootApiFileSchema +} from "@fern-api/fern-definition-schema"; import { AbsoluteFilePath, join, RelativeFilePath } from "@fern-api/fs-utils"; import path from "path"; @@ -10,6 +15,22 @@ import * as DefinitionFileJsonSchema from "../fern.schema.json"; import * as PackageMarkerFileJsonSchema from "../package-yml.schema.json"; import { WorkspaceLoader, WorkspaceLoaderFailureType } from "./Result.js"; +/** Cast a JSON schema import to the type expected by validateAgainstJsonSchema. */ +function asJsonSchema(schema: Record): Parameters[1] { + return schema as Parameters[1]; +} + +/** + * Module-level caches for validated/parsed file contents. + * Keyed on the raw YAML string (file contents), these caches avoid redundant + * JSON schema validation and schema parseOrThrow calls when the same file + * is processed by multiple workspace instances (e.g., multiple generators + * in a group, or multiple test runs against the same fixtures). + */ +const rootApiParsedCache = new Map(); +const definitionParsedCache = new Map(); +const packageMarkerParsedCache = new Map(); + export declare namespace validateStructureOfYamlFiles { export type Return = SuccessfulResult | FailedResult; @@ -31,10 +52,18 @@ export declare namespace validateStructureOfYamlFiles { export function validateStructureOfYamlFiles({ files, - absolutePathToDefinition + absolutePathToDefinition, + skipValidation }: { files: Record>; absolutePathToDefinition: AbsoluteFilePath; + /** + * When true, skips JSON schema validation (validateAgainstJsonSchema). + * Schema parsing (parseOrThrow) is still performed on cache miss to ensure + * type-safe output. Use this when files are known to be valid (e.g., test + * fixtures, or when re-generating IR for a second generator in the same group). + */ + skipValidation?: boolean; }): validateStructureOfYamlFiles.Return { let rootApiFile: ParsedFernFile | undefined = undefined; const namesDefinitionFiles: Record = {}; @@ -56,36 +85,81 @@ export function validateStructureOfYamlFiles({ }; if (relativeFilepath === ROOT_API_FILENAME) { - // biome-ignore lint/suspicious/noExplicitAny: allow explicit any - const result = validateAgainstJsonSchema(parsedFileContents, RootApiFileJsonSchema as any); - if (result.success) { + // Check cache first + const cached = rootApiParsedCache.get(file.rawContents); + if (cached != null) { + rootApiFile = { + defaultUrl: cached["default-url"], + contents: cached, + rawContents: file.rawContents + }; + } else if (skipValidation) { + // Skip JSON schema validation, just run parseOrThrow const contents = RawSchemas.serialization.RootApiFileSchema.parseOrThrow(parsedFileContents); + rootApiParsedCache.set(file.rawContents, contents); rootApiFile = { defaultUrl: contents["default-url"], contents, rawContents: file.rawContents }; } else { - addFailure(result); + const result = validateAgainstJsonSchema(parsedFileContents, asJsonSchema(RootApiFileJsonSchema)); + if (result.success) { + const contents = RawSchemas.serialization.RootApiFileSchema.parseOrThrow(parsedFileContents); + rootApiParsedCache.set(file.rawContents, contents); + rootApiFile = { + defaultUrl: contents["default-url"], + contents, + rawContents: file.rawContents + }; + } else { + addFailure(result); + } } } else if (path.basename(relativeFilepath) === FERN_PACKAGE_MARKER_FILENAME) { - // biome-ignore lint/suspicious/noExplicitAny: allow explicit any - const result = validateAgainstJsonSchema(parsedFileContents, PackageMarkerFileJsonSchema as any); - if (result.success) { + // Check cache first + const cached = packageMarkerParsedCache.get(file.rawContents); + if (cached != null) { + packageMarkers[relativeFilepath] = { + defaultUrl: typeof cached.export === "object" ? cached.export.url : undefined, + contents: cached, + rawContents: file.rawContents + }; + } else if (skipValidation) { const contents = RawSchemas.serialization.PackageMarkerFileSchema.parseOrThrow(parsedFileContents); + packageMarkerParsedCache.set(file.rawContents, contents); packageMarkers[relativeFilepath] = { defaultUrl: typeof contents.export === "object" ? contents.export.url : undefined, contents, rawContents: file.rawContents }; } else { - addFailure(result); + const result = validateAgainstJsonSchema(parsedFileContents, asJsonSchema(PackageMarkerFileJsonSchema)); + if (result.success) { + const contents = RawSchemas.serialization.PackageMarkerFileSchema.parseOrThrow(parsedFileContents); + packageMarkerParsedCache.set(file.rawContents, contents); + packageMarkers[relativeFilepath] = { + defaultUrl: typeof contents.export === "object" ? contents.export.url : undefined, + contents, + rawContents: file.rawContents + }; + } else { + addFailure(result); + } } } else { - // biome-ignore lint/suspicious/noExplicitAny: allow explicit any - const result = validateAgainstJsonSchema(parsedFileContents, DefinitionFileJsonSchema as any); - if (result.success) { + // Check cache first + const cached = definitionParsedCache.get(file.rawContents); + if (cached != null) { + namesDefinitionFiles[relativeFilepath] = { + defaultUrl: undefined, + contents: cached, + rawContents: file.rawContents, + absoluteFilePath: join(absolutePathToDefinition, relativeFilepath) + }; + } else if (skipValidation) { const contents = RawSchemas.serialization.DefinitionFileSchema.parseOrThrow(parsedFileContents); + definitionParsedCache.set(file.rawContents, contents); namesDefinitionFiles[relativeFilepath] = { defaultUrl: undefined, contents, @@ -93,7 +167,19 @@ export function validateStructureOfYamlFiles({ absoluteFilePath: join(absolutePathToDefinition, relativeFilepath) }; } else { - addFailure(result); + const result = validateAgainstJsonSchema(parsedFileContents, asJsonSchema(DefinitionFileJsonSchema)); + if (result.success) { + const contents = RawSchemas.serialization.DefinitionFileSchema.parseOrThrow(parsedFileContents); + definitionParsedCache.set(file.rawContents, contents); + namesDefinitionFiles[relativeFilepath] = { + defaultUrl: undefined, + contents, + rawContents: file.rawContents, + absoluteFilePath: join(absolutePathToDefinition, relativeFilepath) + }; + } else { + addFailure(result); + } } } } diff --git a/packages/cli/workspace/loader/src/loadAPIWorkspace.ts b/packages/cli/workspace/loader/src/loadAPIWorkspace.ts index c908bdd32447..96f8cc7d67e0 100644 --- a/packages/cli/workspace/loader/src/loadAPIWorkspace.ts +++ b/packages/cli/workspace/loader/src/loadAPIWorkspace.ts @@ -192,16 +192,13 @@ export async function loadAPIWorkspace({ cliVersion: string; workspaceName: string | undefined; }): Promise { - const generatorsConfiguration = await loadGeneratorsConfiguration({ - absolutePathToWorkspace, - context - }); - - let changelog = undefined; - try { - changelog = await loadAPIChangelog({ absolutePathToWorkspace }); - // biome-ignore lint/suspicious/noEmptyBlockStatements: allow - } catch (err) {} + const [generatorsConfiguration, changelog] = await Promise.all([ + loadGeneratorsConfiguration({ + absolutePathToWorkspace, + context + }), + loadAPIChangelog({ absolutePathToWorkspace }).catch(() => undefined) + ]); if (generatorsConfiguration?.api != null && generatorsConfiguration?.api.type === "conjure") { return { @@ -237,12 +234,17 @@ export async function loadAPIWorkspace({ } specs.push(...maybeSpecs); } else { - for (const [namespace, definitions] of Object.entries(generatorsConfiguration.api.definitions)) { - const maybeSpecs = await loadSingleNamespaceAPIWorkspace({ - absolutePathToWorkspace, - namespace, - definitions - }); + const namespaceEntries = Object.entries(generatorsConfiguration.api.definitions); + const namespaceResults = await Promise.all( + namespaceEntries.map(([namespace, definitions]) => + loadSingleNamespaceAPIWorkspace({ + absolutePathToWorkspace, + namespace, + definitions + }) + ) + ); + for (const maybeSpecs of namespaceResults) { if (!Array.isArray(maybeSpecs)) { return maybeSpecs; } diff --git a/packages/commons/github/src/ClonedRepository.ts b/packages/commons/github/src/ClonedRepository.ts index 9e4d97974727..9f3bf6eb40b7 100644 --- a/packages/commons/github/src/ClonedRepository.ts +++ b/packages/commons/github/src/ClonedRepository.ts @@ -254,9 +254,16 @@ export class ClonedRepository { readdir(this.clonePath) ]); + // Build list of files to preserve: always preserve .git, .gitignore, .fernignore. + // Also preserve README.md if the source doesn't include one, to prevent accidental + // deletion when a generator fails to produce a README (e.g. silent try/catch failure). + const filesToPreserve = sourceContents.includes(README_FILEPATH) + ? DEFAULT_IGNORED_FILES + : [...DEFAULT_IGNORED_FILES, README_FILEPATH]; + await Promise.all( destContents - .filter((content) => !DEFAULT_IGNORED_FILES.includes(content)) + .filter((content) => !filesToPreserve.includes(content)) .map(async (content) => { await rm(resolve(this.clonePath, content), { recursive: true, diff --git a/packages/ir-sdk/fern/apis/ir-types-latest/VERSION b/packages/ir-sdk/fern/apis/ir-types-latest/VERSION index 9746452b4aa1..4d350a654b28 100644 --- a/packages/ir-sdk/fern/apis/ir-types-latest/VERSION +++ b/packages/ir-sdk/fern/apis/ir-types-latest/VERSION @@ -1 +1 @@ -65.1.0 +65.2.0 diff --git a/packages/ir-sdk/fern/apis/ir-types-latest/changelog/CHANGELOG.md b/packages/ir-sdk/fern/apis/ir-types-latest/changelog/CHANGELOG.md index 5dcbc478d1ad..0164c2ff29fe 100644 --- a/packages/ir-sdk/fern/apis/ir-types-latest/changelog/CHANGELOG.md +++ b/packages/ir-sdk/fern/apis/ir-types-latest/changelog/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v65.2.0] - 2026-02-23 +- Feature: Add `WebhookSignatureVerification` discriminated union to the `Webhook` type. + Supports HMAC-based and asymmetric key signature verification with configurable algorithms, + encoding, signature prefix parsing, payload format composition, and timestamp-based replay protection. + New types: `HmacSignatureVerification`, `AsymmetricKeySignatureVerification`, `HmacAlgorithm`, + `AsymmetricAlgorithm`, `WebhookSignatureEncoding`, `WebhookPayloadFormat`, `WebhookPayloadComponent`, + `WebhookTimestampConfig`, `WebhookTimestampFormat`, `AsymmetricKeySource`, `JwksKeySource`, `StaticKeySource`. + +## [v65.1.0] - 2026-02-23 +- Feature: Add optional `signatureVerification` field to the `Webhook` type for webhook signature verification. + Includes algorithm (SHA256/SHA1/SHA384/SHA512), encoding (base64/hex), signature header name, and payload format (bodyOnly/urlPrefixed). + ## [v65.0.0] - 2026-02-03 - Feature: Add new pagination types where the cursor is to be consumed as the full URI, or full path (HATEOS-style) diff --git a/packages/ir-sdk/fern/apis/ir-types-latest/definition/webhooks.yml b/packages/ir-sdk/fern/apis/ir-types-latest/definition/webhooks.yml index 76e744902012..fed8978438f6 100644 --- a/packages/ir-sdk/fern/apis/ir-types-latest/definition/webhooks.yml +++ b/packages/ir-sdk/fern/apis/ir-types-latest/definition/webhooks.yml @@ -16,6 +16,11 @@ types: method: WebhookHttpMethod headers: list payload: WebhookPayload + signatureVerification: + type: optional + docs: | + Configuration for verifying webhook signatures. When present, SDK generators + can produce utilities that verify the signature of incoming webhook requests. fileUploadPayload: type: optional docs: | @@ -64,3 +69,137 @@ types: properties: name: optional payload: types.ExampleTypeReference + + WebhookSignatureVerification: + docs: | + Configuration for verifying webhook signatures. When present, SDK generators + can produce utilities that verify the signature of incoming webhook requests. + union: + hmac: + type: HmacSignatureVerification + docs: HMAC-based signature verification using a shared secret. + asymmetric: + type: AsymmetricKeySignatureVerification + docs: Asymmetric key signature verification using a public key or JWKS endpoint. + + HmacSignatureVerification: + properties: + signatureHeaderName: + type: commons.NameAndWireValue + docs: The HTTP header that contains the webhook signature. + algorithm: HmacAlgorithm + encoding: WebhookSignatureEncoding + signaturePrefix: + type: optional + docs: | + Prefix prepended to the signature in the header value. + For example, GitHub uses "sha256=" so the header value is "sha256=". + When set, the prefix is stripped before comparing signatures. + payloadFormat: WebhookPayloadFormat + timestamp: optional + + HmacAlgorithm: + docs: The HMAC algorithm used to compute the webhook signature. + enum: + - SHA256 + - SHA1 + - SHA384 + - SHA512 + + AsymmetricKeySignatureVerification: + properties: + signatureHeaderName: + type: commons.NameAndWireValue + docs: The HTTP header that contains the webhook signature. + algorithm: AsymmetricAlgorithm + encoding: WebhookSignatureEncoding + signaturePrefix: + type: optional + docs: | + Prefix prepended to the signature in the header value. + When set, the prefix is stripped before comparing signatures. + keySource: AsymmetricKeySource + timestamp: optional + + AsymmetricAlgorithm: + docs: The asymmetric signing algorithm. + enum: + - RSA_SHA256 + - RSA_SHA384 + - RSA_SHA512 + - ECDSA_SHA256 + - ECDSA_SHA384 + - ECDSA_SHA512 + - ED25519 + + AsymmetricKeySource: + union: + jwks: + type: JwksKeySource + docs: Fetch public keys from a JWKS endpoint. + static: + type: StaticKeySource + docs: Use a static public key provided at runtime. + + JwksKeySource: + properties: + url: + type: string + docs: The JWKS endpoint URL. + keyIdHeader: + type: optional + docs: Optional HTTP header containing the key ID to select from the JWKS set. + + StaticKeySource: + properties: {} + + WebhookSignatureEncoding: + docs: The encoding of the computed signature. + enum: + - BASE64 + - HEX + + WebhookTimestampConfig: + docs: | + Configuration for timestamp-based replay protection. + When present, the webhook consumer should reject requests + where the timestamp is outside the allowed tolerance window. + properties: + headerName: + type: commons.NameAndWireValue + docs: The HTTP header containing the delivery timestamp. + format: WebhookTimestampFormat + tolerance: + type: optional + docs: Allowed clock skew in seconds. Defaults to 300 (5 minutes). + + WebhookTimestampFormat: + docs: The format of the timestamp value in the header. + enum: + - UNIX_SECONDS + - UNIX_MILLIS + - ISO8601 + + WebhookPayloadFormat: + docs: | + Describes how the signed payload is constructed from webhook request components. + Components are concatenated in order using the specified delimiter. + properties: + components: + type: list + docs: Ordered list of components to concatenate when computing the signature. + delimiter: + type: string + docs: String used to join components. Use empty string for direct concatenation. + + WebhookPayloadComponent: + docs: A component included in the signed payload. + enum: + - value: BODY + docs: The raw request body. + - value: TIMESTAMP + docs: The delivery timestamp value from the timestamp header. + - value: NOTIFICATION_URL + docs: The notification/callback URL. + - value: MESSAGE_ID + docs: A provider-assigned message identifier. diff --git a/packages/ir-sdk/src/sdk/api/resources/webhooks/types/AsymmetricAlgorithm.ts b/packages/ir-sdk/src/sdk/api/resources/webhooks/types/AsymmetricAlgorithm.ts new file mode 100644 index 000000000000..b49e35409c5e --- /dev/null +++ b/packages/ir-sdk/src/sdk/api/resources/webhooks/types/AsymmetricAlgorithm.ts @@ -0,0 +1,51 @@ +// This file was auto-generated by Fern from our API Definition. + +/** The asymmetric signing algorithm. */ +const AsymmetricAlgorithmValues = { + RsaSha256: "RSA_SHA256", + RsaSha384: "RSA_SHA384", + RsaSha512: "RSA_SHA512", + EcdsaSha256: "ECDSA_SHA256", + EcdsaSha384: "ECDSA_SHA384", + EcdsaSha512: "ECDSA_SHA512", + Ed25519: "ED25519", +} as const; +export type AsymmetricAlgorithm = (typeof AsymmetricAlgorithmValues)[keyof typeof AsymmetricAlgorithmValues]; +export const AsymmetricAlgorithm: typeof AsymmetricAlgorithmValues & { + _visit: (value: AsymmetricAlgorithm, visitor: AsymmetricAlgorithm.Visitor) => R; +} = { + ...AsymmetricAlgorithmValues, + _visit: (value: AsymmetricAlgorithm, visitor: AsymmetricAlgorithm.Visitor): R => { + switch (value) { + case AsymmetricAlgorithm.RsaSha256: + return visitor.rsaSha256(); + case AsymmetricAlgorithm.RsaSha384: + return visitor.rsaSha384(); + case AsymmetricAlgorithm.RsaSha512: + return visitor.rsaSha512(); + case AsymmetricAlgorithm.EcdsaSha256: + return visitor.ecdsaSha256(); + case AsymmetricAlgorithm.EcdsaSha384: + return visitor.ecdsaSha384(); + case AsymmetricAlgorithm.EcdsaSha512: + return visitor.ecdsaSha512(); + case AsymmetricAlgorithm.Ed25519: + return visitor.ed25519(); + default: + return visitor._other(); + } + }, +}; + +export namespace AsymmetricAlgorithm { + export interface Visitor { + rsaSha256: () => R; + rsaSha384: () => R; + rsaSha512: () => R; + ecdsaSha256: () => R; + ecdsaSha384: () => R; + ecdsaSha512: () => R; + ed25519: () => R; + _other: () => R; + } +} diff --git a/packages/ir-sdk/src/sdk/api/resources/webhooks/types/AsymmetricKeySignatureVerification.ts b/packages/ir-sdk/src/sdk/api/resources/webhooks/types/AsymmetricKeySignatureVerification.ts new file mode 100644 index 000000000000..868940173e8e --- /dev/null +++ b/packages/ir-sdk/src/sdk/api/resources/webhooks/types/AsymmetricKeySignatureVerification.ts @@ -0,0 +1,17 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernIr from "../../../index.js"; + +export interface AsymmetricKeySignatureVerification { + /** The HTTP header that contains the webhook signature. */ + signatureHeaderName: FernIr.NameAndWireValue; + algorithm: FernIr.AsymmetricAlgorithm; + encoding: FernIr.WebhookSignatureEncoding; + /** + * Prefix prepended to the signature in the header value. + * When set, the prefix is stripped before comparing signatures. + */ + signaturePrefix: string | undefined; + keySource: FernIr.AsymmetricKeySource; + timestamp: FernIr.WebhookTimestampConfig | undefined; +} diff --git a/packages/ir-sdk/src/sdk/api/resources/webhooks/types/AsymmetricKeySource.ts b/packages/ir-sdk/src/sdk/api/resources/webhooks/types/AsymmetricKeySource.ts new file mode 100644 index 000000000000..215b82618fe8 --- /dev/null +++ b/packages/ir-sdk/src/sdk/api/resources/webhooks/types/AsymmetricKeySource.ts @@ -0,0 +1,73 @@ +// This file was auto-generated by Fern from our API Definition. + +import * as FernIr from "../../../index.js"; + +export type AsymmetricKeySource = + /** + * Fetch public keys from a JWKS endpoint. */ + | FernIr.AsymmetricKeySource.Jwks + /** + * Use a static public key provided at runtime. */ + | FernIr.AsymmetricKeySource.Static; + +export namespace AsymmetricKeySource { + export interface Jwks extends FernIr.JwksKeySource, _Utils { + type: "jwks"; + } + + export interface Static extends FernIr.StaticKeySource, _Utils { + type: "static"; + } + + export interface _Utils { + _visit: <_Result>(visitor: FernIr.AsymmetricKeySource._Visitor<_Result>) => _Result; + } + + export interface _Visitor<_Result> { + jwks: (value: FernIr.JwksKeySource) => _Result; + static: (value: FernIr.StaticKeySource) => _Result; + _other: (value: { type: string }) => _Result; + } +} + +export const AsymmetricKeySource = { + jwks: (value: FernIr.JwksKeySource): FernIr.AsymmetricKeySource.Jwks => { + return { + ...value, + type: "jwks", + _visit: function <_Result>( + this: FernIr.AsymmetricKeySource.Jwks, + visitor: FernIr.AsymmetricKeySource._Visitor<_Result>, + ) { + return FernIr.AsymmetricKeySource._visit(this, visitor); + }, + }; + }, + + static: (value: FernIr.StaticKeySource): FernIr.AsymmetricKeySource.Static => { + return { + ...value, + type: "static", + _visit: function <_Result>( + this: FernIr.AsymmetricKeySource.Static, + visitor: FernIr.AsymmetricKeySource._Visitor<_Result>, + ) { + return FernIr.AsymmetricKeySource._visit(this, visitor); + }, + }; + }, + + _visit: <_Result>( + value: FernIr.AsymmetricKeySource, + visitor: FernIr.AsymmetricKeySource._Visitor<_Result>, + ): _Result => { + switch (value.type) { + case "jwks": + return visitor.jwks(value); + case "static": + return visitor.static(value); + default: + return visitor._other(value); + } + }, +} as const; diff --git a/packages/ir-sdk/src/sdk/api/resources/webhooks/types/HmacAlgorithm.ts b/packages/ir-sdk/src/sdk/api/resources/webhooks/types/HmacAlgorithm.ts new file mode 100644 index 000000000000..622d637bcbea --- /dev/null +++ b/packages/ir-sdk/src/sdk/api/resources/webhooks/types/HmacAlgorithm.ts @@ -0,0 +1,39 @@ +// This file was auto-generated by Fern from our API Definition. + +/** The HMAC algorithm used to compute the webhook signature. */ +const HmacAlgorithmValues = { + Sha256: "SHA256", + Sha1: "SHA1", + Sha384: "SHA384", + Sha512: "SHA512", +} as const; +export type HmacAlgorithm = (typeof HmacAlgorithmValues)[keyof typeof HmacAlgorithmValues]; +export const HmacAlgorithm: typeof HmacAlgorithmValues & { + _visit: (value: HmacAlgorithm, visitor: HmacAlgorithm.Visitor) => R; +} = { + ...HmacAlgorithmValues, + _visit: (value: HmacAlgorithm, visitor: HmacAlgorithm.Visitor): R => { + switch (value) { + case HmacAlgorithm.Sha256: + return visitor.sha256(); + case HmacAlgorithm.Sha1: + return visitor.sha1(); + case HmacAlgorithm.Sha384: + return visitor.sha384(); + case HmacAlgorithm.Sha512: + return visitor.sha512(); + default: + return visitor._other(); + } + }, +}; + +export namespace HmacAlgorithm { + export interface Visitor { + sha256: () => R; + sha1: () => R; + sha384: () => R; + sha512: () => R; + _other: () => R; + } +} diff --git a/packages/ir-sdk/src/sdk/api/resources/webhooks/types/HmacSignatureVerification.ts b/packages/ir-sdk/src/sdk/api/resources/webhooks/types/HmacSignatureVerification.ts new file mode 100644 index 000000000000..0ed960658e2e --- /dev/null +++ b/packages/ir-sdk/src/sdk/api/resources/webhooks/types/HmacSignatureVerification.ts @@ -0,0 +1,18 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernIr from "../../../index.js"; + +export interface HmacSignatureVerification { + /** The HTTP header that contains the webhook signature. */ + signatureHeaderName: FernIr.NameAndWireValue; + algorithm: FernIr.HmacAlgorithm; + encoding: FernIr.WebhookSignatureEncoding; + /** + * Prefix prepended to the signature in the header value. + * For example, GitHub uses "sha256=" so the header value is "sha256=". + * When set, the prefix is stripped before comparing signatures. + */ + signaturePrefix: string | undefined; + payloadFormat: FernIr.WebhookPayloadFormat; + timestamp: FernIr.WebhookTimestampConfig | undefined; +} diff --git a/packages/ir-sdk/src/sdk/api/resources/webhooks/types/JwksKeySource.ts b/packages/ir-sdk/src/sdk/api/resources/webhooks/types/JwksKeySource.ts new file mode 100644 index 000000000000..d86a754e4d9c --- /dev/null +++ b/packages/ir-sdk/src/sdk/api/resources/webhooks/types/JwksKeySource.ts @@ -0,0 +1,10 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernIr from "../../../index.js"; + +export interface JwksKeySource { + /** The JWKS endpoint URL. */ + url: string; + /** Optional HTTP header containing the key ID to select from the JWKS set. */ + keyIdHeader: FernIr.NameAndWireValue | undefined; +} diff --git a/packages/ir-sdk/src/sdk/api/resources/webhooks/types/StaticKeySource.ts b/packages/ir-sdk/src/sdk/api/resources/webhooks/types/StaticKeySource.ts new file mode 100644 index 000000000000..8e5a1b637f94 --- /dev/null +++ b/packages/ir-sdk/src/sdk/api/resources/webhooks/types/StaticKeySource.ts @@ -0,0 +1,3 @@ +// This file was auto-generated by Fern from our API Definition. + +export type StaticKeySource = {}; diff --git a/packages/ir-sdk/src/sdk/api/resources/webhooks/types/Webhook.ts b/packages/ir-sdk/src/sdk/api/resources/webhooks/types/Webhook.ts index a307f9bd1984..18702c2a2f05 100644 --- a/packages/ir-sdk/src/sdk/api/resources/webhooks/types/Webhook.ts +++ b/packages/ir-sdk/src/sdk/api/resources/webhooks/types/Webhook.ts @@ -9,6 +9,11 @@ export interface Webhook extends FernIr.Declaration { method: FernIr.WebhookHttpMethod; headers: FernIr.HttpHeader[]; payload: FernIr.WebhookPayload; + /** + * Configuration for verifying webhook signatures. When present, SDK generators + * can produce utilities that verify the signature of incoming webhook requests. + */ + signatureVerification: FernIr.WebhookSignatureVerification | undefined; /** * Optional multipart form data payload for webhooks that use multipart/form-data content type. * This is populated when the webhook request body uses multipart/form-data encoding. diff --git a/packages/ir-sdk/src/sdk/api/resources/webhooks/types/WebhookPayloadComponent.ts b/packages/ir-sdk/src/sdk/api/resources/webhooks/types/WebhookPayloadComponent.ts new file mode 100644 index 000000000000..dbf7b43c8bde --- /dev/null +++ b/packages/ir-sdk/src/sdk/api/resources/webhooks/types/WebhookPayloadComponent.ts @@ -0,0 +1,48 @@ +// This file was auto-generated by Fern from our API Definition. + +/** A component included in the signed payload. */ +const WebhookPayloadComponentValues = { + /** + * The raw request body. */ + Body: "BODY", + /** + * The delivery timestamp value from the timestamp header. */ + Timestamp: "TIMESTAMP", + /** + * The notification/callback URL. */ + NotificationUrl: "NOTIFICATION_URL", + /** + * A provider-assigned message identifier. */ + MessageId: "MESSAGE_ID", +} as const; +export type WebhookPayloadComponent = + (typeof WebhookPayloadComponentValues)[keyof typeof WebhookPayloadComponentValues]; +export const WebhookPayloadComponent: typeof WebhookPayloadComponentValues & { + _visit: (value: WebhookPayloadComponent, visitor: WebhookPayloadComponent.Visitor) => R; +} = { + ...WebhookPayloadComponentValues, + _visit: (value: WebhookPayloadComponent, visitor: WebhookPayloadComponent.Visitor): R => { + switch (value) { + case WebhookPayloadComponent.Body: + return visitor.body(); + case WebhookPayloadComponent.Timestamp: + return visitor.timestamp(); + case WebhookPayloadComponent.NotificationUrl: + return visitor.notificationUrl(); + case WebhookPayloadComponent.MessageId: + return visitor.messageId(); + default: + return visitor._other(); + } + }, +}; + +export namespace WebhookPayloadComponent { + export interface Visitor { + body: () => R; + timestamp: () => R; + notificationUrl: () => R; + messageId: () => R; + _other: () => R; + } +} diff --git a/packages/ir-sdk/src/sdk/api/resources/webhooks/types/WebhookPayloadFormat.ts b/packages/ir-sdk/src/sdk/api/resources/webhooks/types/WebhookPayloadFormat.ts new file mode 100644 index 000000000000..52cef4fa77e7 --- /dev/null +++ b/packages/ir-sdk/src/sdk/api/resources/webhooks/types/WebhookPayloadFormat.ts @@ -0,0 +1,14 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernIr from "../../../index.js"; + +/** + * Describes how the signed payload is constructed from webhook request components. + * Components are concatenated in order using the specified delimiter. + */ +export interface WebhookPayloadFormat { + /** Ordered list of components to concatenate when computing the signature. */ + components: FernIr.WebhookPayloadComponent[]; + /** String used to join components. Use empty string for direct concatenation. */ + delimiter: string; +} diff --git a/packages/ir-sdk/src/sdk/api/resources/webhooks/types/WebhookSignatureEncoding.ts b/packages/ir-sdk/src/sdk/api/resources/webhooks/types/WebhookSignatureEncoding.ts new file mode 100644 index 000000000000..cd19afd6e114 --- /dev/null +++ b/packages/ir-sdk/src/sdk/api/resources/webhooks/types/WebhookSignatureEncoding.ts @@ -0,0 +1,32 @@ +// This file was auto-generated by Fern from our API Definition. + +/** The encoding of the computed signature. */ +const WebhookSignatureEncodingValues = { + Base64: "BASE64", + Hex: "HEX", +} as const; +export type WebhookSignatureEncoding = + (typeof WebhookSignatureEncodingValues)[keyof typeof WebhookSignatureEncodingValues]; +export const WebhookSignatureEncoding: typeof WebhookSignatureEncodingValues & { + _visit: (value: WebhookSignatureEncoding, visitor: WebhookSignatureEncoding.Visitor) => R; +} = { + ...WebhookSignatureEncodingValues, + _visit: (value: WebhookSignatureEncoding, visitor: WebhookSignatureEncoding.Visitor): R => { + switch (value) { + case WebhookSignatureEncoding.Base64: + return visitor.base64(); + case WebhookSignatureEncoding.Hex: + return visitor.hex(); + default: + return visitor._other(); + } + }, +}; + +export namespace WebhookSignatureEncoding { + export interface Visitor { + base64: () => R; + hex: () => R; + _other: () => R; + } +} diff --git a/packages/ir-sdk/src/sdk/api/resources/webhooks/types/WebhookSignatureVerification.ts b/packages/ir-sdk/src/sdk/api/resources/webhooks/types/WebhookSignatureVerification.ts new file mode 100644 index 000000000000..0b4bbe371f69 --- /dev/null +++ b/packages/ir-sdk/src/sdk/api/resources/webhooks/types/WebhookSignatureVerification.ts @@ -0,0 +1,77 @@ +// This file was auto-generated by Fern from our API Definition. + +import * as FernIr from "../../../index.js"; + +/** + * Configuration for verifying webhook signatures. When present, SDK generators + * can produce utilities that verify the signature of incoming webhook requests. + */ +export type WebhookSignatureVerification = + /** + * HMAC-based signature verification using a shared secret. */ + | FernIr.WebhookSignatureVerification.Hmac + /** + * Asymmetric key signature verification using a public key or JWKS endpoint. */ + | FernIr.WebhookSignatureVerification.Asymmetric; + +export namespace WebhookSignatureVerification { + export interface Hmac extends FernIr.HmacSignatureVerification, _Utils { + type: "hmac"; + } + + export interface Asymmetric extends FernIr.AsymmetricKeySignatureVerification, _Utils { + type: "asymmetric"; + } + + export interface _Utils { + _visit: <_Result>(visitor: FernIr.WebhookSignatureVerification._Visitor<_Result>) => _Result; + } + + export interface _Visitor<_Result> { + hmac: (value: FernIr.HmacSignatureVerification) => _Result; + asymmetric: (value: FernIr.AsymmetricKeySignatureVerification) => _Result; + _other: (value: { type: string }) => _Result; + } +} + +export const WebhookSignatureVerification = { + hmac: (value: FernIr.HmacSignatureVerification): FernIr.WebhookSignatureVerification.Hmac => { + return { + ...value, + type: "hmac", + _visit: function <_Result>( + this: FernIr.WebhookSignatureVerification.Hmac, + visitor: FernIr.WebhookSignatureVerification._Visitor<_Result>, + ) { + return FernIr.WebhookSignatureVerification._visit(this, visitor); + }, + }; + }, + + asymmetric: (value: FernIr.AsymmetricKeySignatureVerification): FernIr.WebhookSignatureVerification.Asymmetric => { + return { + ...value, + type: "asymmetric", + _visit: function <_Result>( + this: FernIr.WebhookSignatureVerification.Asymmetric, + visitor: FernIr.WebhookSignatureVerification._Visitor<_Result>, + ) { + return FernIr.WebhookSignatureVerification._visit(this, visitor); + }, + }; + }, + + _visit: <_Result>( + value: FernIr.WebhookSignatureVerification, + visitor: FernIr.WebhookSignatureVerification._Visitor<_Result>, + ): _Result => { + switch (value.type) { + case "hmac": + return visitor.hmac(value); + case "asymmetric": + return visitor.asymmetric(value); + default: + return visitor._other(value); + } + }, +} as const; diff --git a/packages/ir-sdk/src/sdk/api/resources/webhooks/types/WebhookTimestampConfig.ts b/packages/ir-sdk/src/sdk/api/resources/webhooks/types/WebhookTimestampConfig.ts new file mode 100644 index 000000000000..99b77746d2ea --- /dev/null +++ b/packages/ir-sdk/src/sdk/api/resources/webhooks/types/WebhookTimestampConfig.ts @@ -0,0 +1,16 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernIr from "../../../index.js"; + +/** + * Configuration for timestamp-based replay protection. + * When present, the webhook consumer should reject requests + * where the timestamp is outside the allowed tolerance window. + */ +export interface WebhookTimestampConfig { + /** The HTTP header containing the delivery timestamp. */ + headerName: FernIr.NameAndWireValue; + format: FernIr.WebhookTimestampFormat; + /** Allowed clock skew in seconds. Defaults to 300 (5 minutes). */ + tolerance: number | undefined; +} diff --git a/packages/ir-sdk/src/sdk/api/resources/webhooks/types/WebhookTimestampFormat.ts b/packages/ir-sdk/src/sdk/api/resources/webhooks/types/WebhookTimestampFormat.ts new file mode 100644 index 000000000000..8b43b32ac590 --- /dev/null +++ b/packages/ir-sdk/src/sdk/api/resources/webhooks/types/WebhookTimestampFormat.ts @@ -0,0 +1,35 @@ +// This file was auto-generated by Fern from our API Definition. + +/** The format of the timestamp value in the header. */ +const WebhookTimestampFormatValues = { + UnixSeconds: "UNIX_SECONDS", + UnixMillis: "UNIX_MILLIS", + Iso8601: "ISO8601", +} as const; +export type WebhookTimestampFormat = (typeof WebhookTimestampFormatValues)[keyof typeof WebhookTimestampFormatValues]; +export const WebhookTimestampFormat: typeof WebhookTimestampFormatValues & { + _visit: (value: WebhookTimestampFormat, visitor: WebhookTimestampFormat.Visitor) => R; +} = { + ...WebhookTimestampFormatValues, + _visit: (value: WebhookTimestampFormat, visitor: WebhookTimestampFormat.Visitor): R => { + switch (value) { + case WebhookTimestampFormat.UnixSeconds: + return visitor.unixSeconds(); + case WebhookTimestampFormat.UnixMillis: + return visitor.unixMillis(); + case WebhookTimestampFormat.Iso8601: + return visitor.iso8601(); + default: + return visitor._other(); + } + }, +}; + +export namespace WebhookTimestampFormat { + export interface Visitor { + unixSeconds: () => R; + unixMillis: () => R; + iso8601: () => R; + _other: () => R; + } +} diff --git a/packages/ir-sdk/src/sdk/api/resources/webhooks/types/index.ts b/packages/ir-sdk/src/sdk/api/resources/webhooks/types/index.ts index 26d3a3bb9e30..2f81b171af8f 100644 --- a/packages/ir-sdk/src/sdk/api/resources/webhooks/types/index.ts +++ b/packages/ir-sdk/src/sdk/api/resources/webhooks/types/index.ts @@ -1,9 +1,22 @@ +export * from "./AsymmetricAlgorithm.js"; +export * from "./AsymmetricKeySignatureVerification.js"; +export * from "./AsymmetricKeySource.js"; export * from "./ExampleWebhookCall.js"; +export * from "./HmacAlgorithm.js"; +export * from "./HmacSignatureVerification.js"; export * from "./InlinedWebhookPayload.js"; export * from "./InlinedWebhookPayloadProperty.js"; +export * from "./JwksKeySource.js"; +export * from "./StaticKeySource.js"; export * from "./Webhook.js"; export * from "./WebhookGroup.js"; export * from "./WebhookHttpMethod.js"; export * from "./WebhookName.js"; export * from "./WebhookPayload.js"; +export * from "./WebhookPayloadComponent.js"; +export * from "./WebhookPayloadFormat.js"; export * from "./WebhookPayloadReference.js"; +export * from "./WebhookSignatureEncoding.js"; +export * from "./WebhookSignatureVerification.js"; +export * from "./WebhookTimestampConfig.js"; +export * from "./WebhookTimestampFormat.js"; diff --git a/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/AsymmetricAlgorithm.ts b/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/AsymmetricAlgorithm.ts new file mode 100644 index 000000000000..9aa6754f3315 --- /dev/null +++ b/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/AsymmetricAlgorithm.ts @@ -0,0 +1,29 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernIr from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; + +export const AsymmetricAlgorithm: core.serialization.Schema< + serializers.AsymmetricAlgorithm.Raw, + FernIr.AsymmetricAlgorithm +> = core.serialization.enum_([ + "RSA_SHA256", + "RSA_SHA384", + "RSA_SHA512", + "ECDSA_SHA256", + "ECDSA_SHA384", + "ECDSA_SHA512", + "ED25519", +]); + +export declare namespace AsymmetricAlgorithm { + export type Raw = + | "RSA_SHA256" + | "RSA_SHA384" + | "RSA_SHA512" + | "ECDSA_SHA256" + | "ECDSA_SHA384" + | "ECDSA_SHA512" + | "ED25519"; +} diff --git a/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/AsymmetricKeySignatureVerification.ts b/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/AsymmetricKeySignatureVerification.ts new file mode 100644 index 000000000000..64cf621ffcfd --- /dev/null +++ b/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/AsymmetricKeySignatureVerification.ts @@ -0,0 +1,33 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernIr from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; +import { NameAndWireValue } from "../../commons/types/NameAndWireValue.js"; +import { AsymmetricAlgorithm } from "./AsymmetricAlgorithm.js"; +import { AsymmetricKeySource } from "./AsymmetricKeySource.js"; +import { WebhookSignatureEncoding } from "./WebhookSignatureEncoding.js"; +import { WebhookTimestampConfig } from "./WebhookTimestampConfig.js"; + +export const AsymmetricKeySignatureVerification: core.serialization.ObjectSchema< + serializers.AsymmetricKeySignatureVerification.Raw, + FernIr.AsymmetricKeySignatureVerification +> = core.serialization.objectWithoutOptionalProperties({ + signatureHeaderName: NameAndWireValue, + algorithm: AsymmetricAlgorithm, + encoding: WebhookSignatureEncoding, + signaturePrefix: core.serialization.string().optional(), + keySource: AsymmetricKeySource, + timestamp: WebhookTimestampConfig.optional(), +}); + +export declare namespace AsymmetricKeySignatureVerification { + export interface Raw { + signatureHeaderName: NameAndWireValue.Raw; + algorithm: AsymmetricAlgorithm.Raw; + encoding: WebhookSignatureEncoding.Raw; + signaturePrefix?: string | null; + keySource: AsymmetricKeySource.Raw; + timestamp?: WebhookTimestampConfig.Raw | null; + } +} diff --git a/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/AsymmetricKeySource.ts b/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/AsymmetricKeySource.ts new file mode 100644 index 000000000000..acb4d9f9475e --- /dev/null +++ b/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/AsymmetricKeySource.ts @@ -0,0 +1,41 @@ +// This file was auto-generated by Fern from our API Definition. + +import * as FernIr from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; +import { JwksKeySource } from "./JwksKeySource.js"; +import { StaticKeySource } from "./StaticKeySource.js"; + +export const AsymmetricKeySource: core.serialization.Schema< + serializers.AsymmetricKeySource.Raw, + FernIr.AsymmetricKeySource +> = core.serialization + .union("type", { + jwks: JwksKeySource, + static: StaticKeySource, + }) + .transform({ + transform: (value) => { + switch (value.type) { + case "jwks": + return FernIr.AsymmetricKeySource.jwks(value); + case "static": + return FernIr.AsymmetricKeySource.static(value); + default: + return value as FernIr.AsymmetricKeySource; + } + }, + untransform: ({ _visit, ...value }) => value as any, + }); + +export declare namespace AsymmetricKeySource { + export type Raw = AsymmetricKeySource.Jwks | AsymmetricKeySource.Static; + + export interface Jwks extends JwksKeySource.Raw { + type: "jwks"; + } + + export interface Static extends StaticKeySource.Raw { + type: "static"; + } +} diff --git a/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/HmacAlgorithm.ts b/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/HmacAlgorithm.ts new file mode 100644 index 000000000000..22e22892a97b --- /dev/null +++ b/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/HmacAlgorithm.ts @@ -0,0 +1,12 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernIr from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; + +export const HmacAlgorithm: core.serialization.Schema = + core.serialization.enum_(["SHA256", "SHA1", "SHA384", "SHA512"]); + +export declare namespace HmacAlgorithm { + export type Raw = "SHA256" | "SHA1" | "SHA384" | "SHA512"; +} diff --git a/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/HmacSignatureVerification.ts b/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/HmacSignatureVerification.ts new file mode 100644 index 000000000000..d625774246cb --- /dev/null +++ b/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/HmacSignatureVerification.ts @@ -0,0 +1,33 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernIr from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; +import { NameAndWireValue } from "../../commons/types/NameAndWireValue.js"; +import { HmacAlgorithm } from "./HmacAlgorithm.js"; +import { WebhookPayloadFormat } from "./WebhookPayloadFormat.js"; +import { WebhookSignatureEncoding } from "./WebhookSignatureEncoding.js"; +import { WebhookTimestampConfig } from "./WebhookTimestampConfig.js"; + +export const HmacSignatureVerification: core.serialization.ObjectSchema< + serializers.HmacSignatureVerification.Raw, + FernIr.HmacSignatureVerification +> = core.serialization.objectWithoutOptionalProperties({ + signatureHeaderName: NameAndWireValue, + algorithm: HmacAlgorithm, + encoding: WebhookSignatureEncoding, + signaturePrefix: core.serialization.string().optional(), + payloadFormat: WebhookPayloadFormat, + timestamp: WebhookTimestampConfig.optional(), +}); + +export declare namespace HmacSignatureVerification { + export interface Raw { + signatureHeaderName: NameAndWireValue.Raw; + algorithm: HmacAlgorithm.Raw; + encoding: WebhookSignatureEncoding.Raw; + signaturePrefix?: string | null; + payloadFormat: WebhookPayloadFormat.Raw; + timestamp?: WebhookTimestampConfig.Raw | null; + } +} diff --git a/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/JwksKeySource.ts b/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/JwksKeySource.ts new file mode 100644 index 000000000000..76830103a28f --- /dev/null +++ b/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/JwksKeySource.ts @@ -0,0 +1,19 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernIr from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; +import { NameAndWireValue } from "../../commons/types/NameAndWireValue.js"; + +export const JwksKeySource: core.serialization.ObjectSchema = + core.serialization.objectWithoutOptionalProperties({ + url: core.serialization.string(), + keyIdHeader: NameAndWireValue.optional(), + }); + +export declare namespace JwksKeySource { + export interface Raw { + url: string; + keyIdHeader?: NameAndWireValue.Raw | null; + } +} diff --git a/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/StaticKeySource.ts b/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/StaticKeySource.ts new file mode 100644 index 000000000000..6a9476c2a039 --- /dev/null +++ b/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/StaticKeySource.ts @@ -0,0 +1,12 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernIr from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; + +export const StaticKeySource: core.serialization.ObjectSchema = + core.serialization.objectWithoutOptionalProperties({}); + +export declare namespace StaticKeySource { + export type Raw = {}; +} diff --git a/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/Webhook.ts b/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/Webhook.ts index d38e6406e5b7..689e348b8f68 100644 --- a/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/Webhook.ts +++ b/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/Webhook.ts @@ -13,6 +13,7 @@ import { ExampleWebhookCall } from "./ExampleWebhookCall.js"; import { WebhookHttpMethod } from "./WebhookHttpMethod.js"; import { WebhookName } from "./WebhookName.js"; import { WebhookPayload } from "./WebhookPayload.js"; +import { WebhookSignatureVerification } from "./WebhookSignatureVerification.js"; export const Webhook: core.serialization.ObjectSchema = core.serialization .objectWithoutOptionalProperties({ @@ -22,6 +23,7 @@ export const Webhook: core.serialization.ObjectSchema = core.serialization.enum_(["BODY", "TIMESTAMP", "NOTIFICATION_URL", "MESSAGE_ID"]); + +export declare namespace WebhookPayloadComponent { + export type Raw = "BODY" | "TIMESTAMP" | "NOTIFICATION_URL" | "MESSAGE_ID"; +} diff --git a/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/WebhookPayloadFormat.ts b/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/WebhookPayloadFormat.ts new file mode 100644 index 000000000000..7aece7314044 --- /dev/null +++ b/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/WebhookPayloadFormat.ts @@ -0,0 +1,21 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernIr from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; +import { WebhookPayloadComponent } from "./WebhookPayloadComponent.js"; + +export const WebhookPayloadFormat: core.serialization.ObjectSchema< + serializers.WebhookPayloadFormat.Raw, + FernIr.WebhookPayloadFormat +> = core.serialization.objectWithoutOptionalProperties({ + components: core.serialization.list(WebhookPayloadComponent), + delimiter: core.serialization.string(), +}); + +export declare namespace WebhookPayloadFormat { + export interface Raw { + components: WebhookPayloadComponent.Raw[]; + delimiter: string; + } +} diff --git a/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/WebhookSignatureEncoding.ts b/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/WebhookSignatureEncoding.ts new file mode 100644 index 000000000000..d4c559c51233 --- /dev/null +++ b/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/WebhookSignatureEncoding.ts @@ -0,0 +1,14 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernIr from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; + +export const WebhookSignatureEncoding: core.serialization.Schema< + serializers.WebhookSignatureEncoding.Raw, + FernIr.WebhookSignatureEncoding +> = core.serialization.enum_(["BASE64", "HEX"]); + +export declare namespace WebhookSignatureEncoding { + export type Raw = "BASE64" | "HEX"; +} diff --git a/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/WebhookSignatureVerification.ts b/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/WebhookSignatureVerification.ts new file mode 100644 index 000000000000..fd8a76c4a286 --- /dev/null +++ b/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/WebhookSignatureVerification.ts @@ -0,0 +1,41 @@ +// This file was auto-generated by Fern from our API Definition. + +import * as FernIr from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; +import { AsymmetricKeySignatureVerification } from "./AsymmetricKeySignatureVerification.js"; +import { HmacSignatureVerification } from "./HmacSignatureVerification.js"; + +export const WebhookSignatureVerification: core.serialization.Schema< + serializers.WebhookSignatureVerification.Raw, + FernIr.WebhookSignatureVerification +> = core.serialization + .union("type", { + hmac: HmacSignatureVerification, + asymmetric: AsymmetricKeySignatureVerification, + }) + .transform({ + transform: (value) => { + switch (value.type) { + case "hmac": + return FernIr.WebhookSignatureVerification.hmac(value); + case "asymmetric": + return FernIr.WebhookSignatureVerification.asymmetric(value); + default: + return value as FernIr.WebhookSignatureVerification; + } + }, + untransform: ({ _visit, ...value }) => value as any, + }); + +export declare namespace WebhookSignatureVerification { + export type Raw = WebhookSignatureVerification.Hmac | WebhookSignatureVerification.Asymmetric; + + export interface Hmac extends HmacSignatureVerification.Raw { + type: "hmac"; + } + + export interface Asymmetric extends AsymmetricKeySignatureVerification.Raw { + type: "asymmetric"; + } +} diff --git a/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/WebhookTimestampConfig.ts b/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/WebhookTimestampConfig.ts new file mode 100644 index 000000000000..5632b6335283 --- /dev/null +++ b/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/WebhookTimestampConfig.ts @@ -0,0 +1,24 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernIr from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; +import { NameAndWireValue } from "../../commons/types/NameAndWireValue.js"; +import { WebhookTimestampFormat } from "./WebhookTimestampFormat.js"; + +export const WebhookTimestampConfig: core.serialization.ObjectSchema< + serializers.WebhookTimestampConfig.Raw, + FernIr.WebhookTimestampConfig +> = core.serialization.objectWithoutOptionalProperties({ + headerName: NameAndWireValue, + format: WebhookTimestampFormat, + tolerance: core.serialization.number().optional(), +}); + +export declare namespace WebhookTimestampConfig { + export interface Raw { + headerName: NameAndWireValue.Raw; + format: WebhookTimestampFormat.Raw; + tolerance?: number | null; + } +} diff --git a/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/WebhookTimestampFormat.ts b/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/WebhookTimestampFormat.ts new file mode 100644 index 000000000000..e11a53eccdf6 --- /dev/null +++ b/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/WebhookTimestampFormat.ts @@ -0,0 +1,14 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as FernIr from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; + +export const WebhookTimestampFormat: core.serialization.Schema< + serializers.WebhookTimestampFormat.Raw, + FernIr.WebhookTimestampFormat +> = core.serialization.enum_(["UNIX_SECONDS", "UNIX_MILLIS", "ISO8601"]); + +export declare namespace WebhookTimestampFormat { + export type Raw = "UNIX_SECONDS" | "UNIX_MILLIS" | "ISO8601"; +} diff --git a/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/index.ts b/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/index.ts index 26d3a3bb9e30..2f81b171af8f 100644 --- a/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/index.ts +++ b/packages/ir-sdk/src/sdk/serialization/resources/webhooks/types/index.ts @@ -1,9 +1,22 @@ +export * from "./AsymmetricAlgorithm.js"; +export * from "./AsymmetricKeySignatureVerification.js"; +export * from "./AsymmetricKeySource.js"; export * from "./ExampleWebhookCall.js"; +export * from "./HmacAlgorithm.js"; +export * from "./HmacSignatureVerification.js"; export * from "./InlinedWebhookPayload.js"; export * from "./InlinedWebhookPayloadProperty.js"; +export * from "./JwksKeySource.js"; +export * from "./StaticKeySource.js"; export * from "./Webhook.js"; export * from "./WebhookGroup.js"; export * from "./WebhookHttpMethod.js"; export * from "./WebhookName.js"; export * from "./WebhookPayload.js"; +export * from "./WebhookPayloadComponent.js"; +export * from "./WebhookPayloadFormat.js"; export * from "./WebhookPayloadReference.js"; +export * from "./WebhookSignatureEncoding.js"; +export * from "./WebhookSignatureVerification.js"; +export * from "./WebhookTimestampConfig.js"; +export * from "./WebhookTimestampFormat.js";