@beignet/core
Version:
Core framework primitives for Beignet
237 lines (218 loc) • 7.7 kB
text/typescript
import type { HttpContractConfig } from "./types.js";
/**
* Lifecycle metadata for an HTTP contract that external clients should stop
* using.
*/
export type ContractDeprecationMeta = {
/** UTC ISO 8601 timestamp when the contract became deprecated. */
since: string;
/** Optional human-readable explanation. */
reason?: string;
/** UTC ISO 8601 timestamp after which the contract may stop being served. */
sunset?: string;
/** URI reference for the preferred replacement operation. */
replacement?: string;
/** Absolute HTTP(S) URL with migration or deprecation documentation. */
documentation?: string;
};
/** Stable code identifying invalid lifecycle or operation metadata. */
export type ContractLifecycleFindingCode =
| "INVALID_DEPRECATION_SINCE"
| "INVALID_DEPRECATION_SUNSET"
| "DEPRECATION_SUNSET_BEFORE_SINCE"
| "INVALID_DEPRECATION_REASON"
| "INVALID_DEPRECATION_REPLACEMENT"
| "INVALID_DEPRECATION_DOCUMENTATION"
| "INVALID_OPERATION_ID";
/** Error raised when contract lifecycle metadata is malformed. */
export class ContractLifecycleError extends Error {
/** Stable machine-readable finding code. */
readonly code: ContractLifecycleFindingCode;
/** Name of the invalid contract, or the group label during group setup. */
readonly contract: string;
constructor(args: {
code: ContractLifecycleFindingCode;
contract: string;
message: string;
}) {
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: string): number | undefined {
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: {
value: unknown;
field: "reason" | "replacement";
code: ContractLifecycleFindingCode;
contract: string;
}): string | undefined {
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: unknown,
contract: string,
): asserts deprecation is ContractDeprecationMeta {
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 as Record<string, unknown>;
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: number | undefined;
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: URL | undefined;
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 as string)
) {
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: Pick<HttpContractConfig, "name" | "metadata">,
): string {
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: Pick<HttpContractConfig, "name" | "metadata">,
): void {
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: Pick<HttpContractConfig, "name" | "metadata">,
): Record<string, string> {
const deprecation = contract.metadata.deprecation;
if (deprecation === undefined) return {};
assertValidContractDeprecation(deprecation, contract.name);
const headers: Record<string, string> = {
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;
}