UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

174 lines 6.67 kB
/** Error raised when contract lifecycle metadata is malformed. */ export class ContractLifecycleError extends Error { /** Stable machine-readable finding code. */ code; /** Name of the invalid contract, or the group label during group setup. */ contract; constructor(args) { super(args.message); this.name = "ContractLifecycleError"; this.code = args.code; this.contract = args.contract; } } const ISO_8601_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/; const INVALID_URI_REFERENCE_CHARACTERS = /[\s<>"\\]/; const INVALID_PERCENT_ENCODING = /%(?![0-9A-Fa-f]{2})/; function parseUtcTimestamp(value) { if (!ISO_8601_UTC.test(value)) return undefined; const timestamp = Date.parse(value); if (!Number.isFinite(timestamp)) return undefined; const canonical = new Date(timestamp).toISOString(); if (value !== canonical && value !== canonical.replace(".000Z", "Z")) { return undefined; } return timestamp; } function assertNonEmptyOptionalString(args) { if (args.value === undefined) return undefined; if (typeof args.value !== "string" || args.value.trim().length === 0) { throw new ContractLifecycleError({ code: args.code, contract: args.contract, message: `Contract "${args.contract}" deprecation ${args.field} must be a non-empty string.`, }); } return args.value; } /** Validate deprecation metadata supplied by builders or raw contract configs. */ export function assertValidContractDeprecation(deprecation, contract) { if (typeof deprecation !== "object" || deprecation === null) { throw new ContractLifecycleError({ code: "INVALID_DEPRECATION_SINCE", contract, message: `Contract "${contract}" deprecation metadata must include a valid "since" timestamp.`, }); } const metadata = deprecation; const since = typeof metadata.since === "string" ? parseUtcTimestamp(metadata.since) : undefined; if (since === undefined) { throw new ContractLifecycleError({ code: "INVALID_DEPRECATION_SINCE", contract, message: `Contract "${contract}" deprecation "since" must be a valid UTC ISO 8601 timestamp such as 2026-07-11T00:00:00Z.`, }); } let sunset; if (metadata.sunset !== undefined) { sunset = typeof metadata.sunset === "string" ? parseUtcTimestamp(metadata.sunset) : undefined; if (sunset === undefined) { throw new ContractLifecycleError({ code: "INVALID_DEPRECATION_SUNSET", contract, message: `Contract "${contract}" deprecation "sunset" must be a valid UTC ISO 8601 timestamp.`, }); } if (sunset < since) { throw new ContractLifecycleError({ code: "DEPRECATION_SUNSET_BEFORE_SINCE", contract, message: `Contract "${contract}" deprecation "sunset" must not be earlier than "since".`, }); } } assertNonEmptyOptionalString({ value: metadata.reason, field: "reason", code: "INVALID_DEPRECATION_REASON", contract, }); const replacement = assertNonEmptyOptionalString({ value: metadata.replacement, field: "replacement", code: "INVALID_DEPRECATION_REPLACEMENT", contract, }); let replacementIsValid = true; if (replacement) { try { new URL(replacement, "https://beignet.invalid"); } catch { replacementIsValid = false; } } if (replacement && (!replacementIsValid || INVALID_URI_REFERENCE_CHARACTERS.test(replacement) || INVALID_PERCENT_ENCODING.test(replacement))) { throw new ContractLifecycleError({ code: "INVALID_DEPRECATION_REPLACEMENT", contract, message: `Contract "${contract}" deprecation "replacement" must be a valid URI reference without whitespace.`, }); } if (metadata.documentation !== undefined) { let documentation; if (typeof metadata.documentation === "string") { try { documentation = new URL(metadata.documentation); } catch { documentation = undefined; } } if (!documentation || (documentation.protocol !== "http:" && documentation.protocol !== "https:") || INVALID_URI_REFERENCE_CHARACTERS.test(metadata.documentation)) { throw new ContractLifecycleError({ code: "INVALID_DEPRECATION_DOCUMENTATION", contract, message: `Contract "${contract}" deprecation "documentation" must be an absolute HTTP(S) URL.`, }); } } } /** Return and validate the operation ID used by OpenAPI and route registries. */ export function getContractOperationId(contract) { const operationId = contract.metadata.openapi?.operationId ?? contract.name; if (typeof operationId !== "string" || operationId.trim().length === 0 || operationId !== operationId.trim()) { throw new ContractLifecycleError({ code: "INVALID_OPERATION_ID", contract: contract.name, message: `Contract "${contract.name}" operationId must be a non-empty string without surrounding whitespace.`, }); } return operationId; } /** Validate lifecycle metadata on a complete contract definition. */ export function assertValidContractLifecycle(contract) { getContractOperationId(contract); const deprecation = contract.metadata.deprecation; if (deprecation !== undefined) { assertValidContractDeprecation(deprecation, contract.name); } } /** Build standards-based HTTP response headers for a deprecated contract. */ export function contractLifecycleResponseHeaders(contract) { const deprecation = contract.metadata.deprecation; if (deprecation === undefined) return {}; assertValidContractDeprecation(deprecation, contract.name); const headers = { Deprecation: `@${Math.floor(Date.parse(deprecation.since) / 1000)}`, }; if (deprecation.sunset) { headers.Sunset = new Date(deprecation.sunset).toUTCString(); } if (deprecation.documentation) { headers.Link = `<${deprecation.documentation}>; rel="deprecation"`; } return headers; } //# sourceMappingURL=lifecycle.js.map