sindri
Version:
The Sindri Labs JavaScript SDK and CLI tool.
183 lines (169 loc) • 5.9 kB
text/typescript
import type { Logger } from "lib/logging";
let cachedDefaultMeta: Meta | null = null;
/**
* Retrieves the default metadata from the `SINDRI_META` environment variable.
*
* The `SINDRI_META` environment variable can be set to a JSON object (e.g., `{"key": "value"}`) or
* a colon-delimited string of key-value pairs (e.g., `key1=value1:key2=value2`). In the
* colon-delimited format, you can escape actual colons by doubling them (`::` will map to `:`).
*
* @param options.cache - Whether to cache the default metadata. Note that retrieving a cached value
* will not raise exceptions or log warnings, even if the `raiseExceptions` option is set to `true`.
* @param options.logger - The optional logger to use for warning messages.
* @param options.raiseExceptions - Whether to raise exceptions for invalid metadata entries.
* Warnings will not be logged if this is set to `true`.
*/
export function getDefaultMeta({
cache = true,
logger,
raiseExceptions = false,
}: { cache?: boolean; logger?: Logger; raiseExceptions?: boolean } = {}): Meta {
if (cache && cachedDefaultMeta) {
return cachedDefaultMeta;
}
// There are no environment variables in a browser.
if (process.env.BROWSER_BUILD) {
return (cachedDefaultMeta = {});
}
const { SINDRI_META } = process.env;
if (!SINDRI_META) {
return (cachedDefaultMeta = {});
}
// Handle the filtering, validation, logging, and/or error handling for each metadata entry.
const validationFilter = ([key, value]: [string, unknown]): boolean => {
if (typeof value !== "string") {
const errorMessage = `Invalid metadata entry for '${key}' (value must be a string).`;
if (raiseExceptions) {
throw new Error(errorMessage);
}
logger?.warn(
{
key,
value,
SINDRI_META,
},
errorMessage + " Ignoring.",
);
return false;
}
const validationError = validateMetaEntry(key, value);
if (validationError) {
if (raiseExceptions) {
throw new Error(validationError);
}
logger?.warn(
{
key,
value,
SINDRI_META,
},
validationError + " Ignoring.",
);
return false;
}
return true;
};
// Support specifying default metadata as JSON.
if (SINDRI_META.startsWith("{")) {
try {
return (cachedDefaultMeta = Object.fromEntries(
Object.entries(JSON.parse(SINDRI_META)).filter(validationFilter),
) as Meta);
} catch (error) {
const errorMessage = "Failed to parse 'SINDRI_META' as JSON.";
if (raiseExceptions) {
throw new Error(errorMessage);
}
logger?.warn(
{
SINDRI_META,
error: (error as Error).toString(),
},
errorMessage + " Using '{}' as the default.",
);
return (cachedDefaultMeta = {});
}
}
// Support the `key=value:key=value` format.
const colonPlaceholder = "\0";
// Split the string into :-delimited pieces, and unescape `::` to `:` in each segment.
return (cachedDefaultMeta = Object.fromEntries(
SINDRI_META.replace(/::/g, colonPlaceholder)
.split(":")
.map((segment) => segment.replace(new RegExp(colonPlaceholder, "g"), ":"))
// Split each piece into a key and value.
.filter((segment) => {
if (!segment.includes("=")) {
const errorMessage =
`Invalid 'SINDRI_META' metadata segment '${segment}' ` +
"(missing '=', try 'key=value').";
if (raiseExceptions) {
throw new Error(errorMessage);
}
logger?.warn({ segment, SINDRI_META }, errorMessage + " Ignoring.");
return false;
}
return true;
})
.map((segment): [key: string, value: string] => {
const index = segment.indexOf("=");
return [segment.slice(0, index), segment.slice(index + 1)];
})
// Validate the keys and values (logic should match the backend validation).
.filter(validationFilter),
));
}
export type Meta = Record<string, string>;
/**
* Validates the metadata and merges it with the default metadata.
*
* @param meta - The metadata to validate and merge.
* @returns The validated and merged metadata.
*/
export function validateMetaAndMergeWithDefaults(meta: Meta): Meta {
const defaultMeta = getDefaultMeta({ raiseExceptions: true });
Object.entries(meta).forEach(([key, value]) => {
const validationError = validateMetaEntry(key, value);
if (validationError) {
throw new Error(validationError);
}
});
return { ...defaultMeta, ...meta };
}
/**
* Validates a key-value pair of metadata.
*
* @param key - The metadata key.
* @param value - The metadata value.
* @returns An error message if the entry is invalid, otherwise `null`.
*/
export function validateMetaEntry(key: string, value: string): string | null {
// These validation constraints must be kept in sync with the backend.
const keyLengthLow = 1;
const keyLengthHigh = 64;
const valueLengthLow = 0;
const valueLengthHigh = 4096;
const keyRegex = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
// Validate the key.
if (key.length < keyLengthLow || key.length > keyLengthHigh) {
return (
`Invalid metadata key length for '${key}' (must be ` +
`${keyLengthLow}-${keyLengthHigh} characters).`
);
}
if (!keyRegex.test(key)) {
return (
`Invalid metadata key for '${key}' (must start with an alphabet character and only ` +
"include alphanumeric characters, underscores, and hyphens)."
);
}
// Validate the value.
if (value.length < valueLengthLow || value.length > valueLengthHigh) {
return (
`Invalid metadata value length for '${key}' (must be ` +
`${valueLengthLow}-${valueLengthHigh} characters).`
);
}
// Otherwise, the entry is valid.
return null;
}