@beignet/core
Version:
Core framework primitives for Beignet
174 lines • 6.67 kB
JavaScript
/** 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