mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:00:24 +08:00
175 lines
5.3 KiB
TypeScript
175 lines
5.3 KiB
TypeScript
import { createHash } from 'crypto';
|
|
import { MAX_EXECUTION_ENVIRONMENT_ENTRIES } from './executionContext';
|
|
|
|
export const MAX_LOCAL_CONTEXT_REF_LENGTH = 512;
|
|
export const MAX_LOCAL_SECRET_REF_LENGTH = 512;
|
|
|
|
export type LocalExecutionEnvironmentBinding =
|
|
| {
|
|
name: string;
|
|
kind: 'public';
|
|
value: string;
|
|
}
|
|
| {
|
|
name: string;
|
|
kind: 'secret';
|
|
secretRef: string;
|
|
};
|
|
|
|
export interface LocalExecutionContextRecipe {
|
|
contextRef: string;
|
|
environment: readonly LocalExecutionEnvironmentBinding[];
|
|
}
|
|
|
|
export interface LocalExecutionContextRecipeRecord
|
|
extends LocalExecutionContextRecipe {
|
|
contentDigest: string;
|
|
createdAtMs: number;
|
|
}
|
|
|
|
function assertBoundedText(name: string, value: string, maximum: number): void {
|
|
if (
|
|
typeof value !== 'string' ||
|
|
value.length < 1 ||
|
|
value.length > maximum ||
|
|
/[\u0000-\u001f\u007f]/.test(value)
|
|
) {
|
|
throw new TypeError(`${name} is invalid`);
|
|
}
|
|
}
|
|
|
|
function assertEnvironmentName(value: string): void {
|
|
if (
|
|
typeof value !== 'string' ||
|
|
value.length < 1 ||
|
|
value.length > 255 ||
|
|
value.includes('=') ||
|
|
value.includes('\0')
|
|
) {
|
|
throw new TypeError('Local context environment name is invalid');
|
|
}
|
|
}
|
|
|
|
export function assertLocalExecutionContextRef(value: string): void {
|
|
assertBoundedText(
|
|
'Local execution contextRef',
|
|
value,
|
|
MAX_LOCAL_CONTEXT_REF_LENGTH,
|
|
);
|
|
}
|
|
|
|
export function normalizeLocalExecutionContextRecipe(
|
|
recipe: LocalExecutionContextRecipe,
|
|
): LocalExecutionContextRecipe {
|
|
if (!recipe || typeof recipe !== 'object' || Array.isArray(recipe)) {
|
|
throw new TypeError('Local execution context recipe must be an object');
|
|
}
|
|
assertLocalExecutionContextRef(recipe.contextRef);
|
|
if (!Array.isArray(recipe.environment)) {
|
|
throw new TypeError('Local context environment bindings must be an array');
|
|
}
|
|
if (recipe.environment.length > MAX_EXECUTION_ENVIRONMENT_ENTRIES) {
|
|
throw new RangeError('Local context has too many environment bindings');
|
|
}
|
|
const names = new Set<string>();
|
|
const environment = recipe.environment
|
|
.map((binding) => {
|
|
if (!binding || typeof binding !== 'object' || Array.isArray(binding)) {
|
|
throw new TypeError('Local context environment binding is invalid');
|
|
}
|
|
assertEnvironmentName(binding.name);
|
|
if (names.has(binding.name)) {
|
|
throw new TypeError('Local context environment binding is duplicated');
|
|
}
|
|
names.add(binding.name);
|
|
if (binding.kind === 'public') {
|
|
if (typeof binding.value !== 'string' || binding.value.includes('\0')) {
|
|
throw new TypeError('Local public environment value is invalid');
|
|
}
|
|
return Object.freeze({
|
|
name: binding.name,
|
|
kind: 'public' as const,
|
|
value: binding.value,
|
|
});
|
|
}
|
|
if (binding.kind === 'secret') {
|
|
assertBoundedText(
|
|
'Local environment secretRef',
|
|
binding.secretRef,
|
|
MAX_LOCAL_SECRET_REF_LENGTH,
|
|
);
|
|
return Object.freeze({
|
|
name: binding.name,
|
|
kind: 'secret' as const,
|
|
secretRef: binding.secretRef,
|
|
});
|
|
}
|
|
throw new TypeError('Local context environment binding kind is invalid');
|
|
})
|
|
.sort((left, right) =>
|
|
left.name < right.name ? -1 : left.name > right.name ? 1 : 0,
|
|
);
|
|
return Object.freeze({
|
|
contextRef: recipe.contextRef,
|
|
environment: Object.freeze(environment),
|
|
});
|
|
}
|
|
|
|
function environmentDigest(
|
|
environment: readonly LocalExecutionEnvironmentBinding[],
|
|
): string {
|
|
return createHash('sha256')
|
|
.update(JSON.stringify(environment), 'utf8')
|
|
.digest('hex');
|
|
}
|
|
|
|
export function createLocalExecutionContextRecipe(
|
|
environment: readonly LocalExecutionEnvironmentBinding[],
|
|
): LocalExecutionContextRecipe {
|
|
const placeholder = normalizeLocalExecutionContextRecipe({
|
|
contextRef: 'localctx:pending',
|
|
environment,
|
|
});
|
|
return normalizeLocalExecutionContextRecipe({
|
|
contextRef: `localctx:sha256:${environmentDigest(placeholder.environment)}`,
|
|
environment: placeholder.environment,
|
|
});
|
|
}
|
|
|
|
export function localExecutionContextRecipeDigest(
|
|
recipe: LocalExecutionContextRecipe,
|
|
): string {
|
|
const normalized = normalizeLocalExecutionContextRecipe(recipe);
|
|
return createHash('sha256')
|
|
.update(JSON.stringify(normalized), 'utf8')
|
|
.digest('hex');
|
|
}
|
|
|
|
export function assertContentAddressedLocalExecutionContextRecipe(
|
|
recipe: LocalExecutionContextRecipe,
|
|
): void {
|
|
const normalized = normalizeLocalExecutionContextRecipe(recipe);
|
|
const expected = createLocalExecutionContextRecipe(normalized.environment);
|
|
if (normalized.contextRef !== expected.contextRef) {
|
|
throw new TypeError(
|
|
'Local execution context recipe is not content-addressed',
|
|
);
|
|
}
|
|
}
|
|
|
|
export function createLocalExecutionContextRecipeRecord(
|
|
recipe: LocalExecutionContextRecipe,
|
|
createdAtMs: number,
|
|
): LocalExecutionContextRecipeRecord {
|
|
if (!Number.isSafeInteger(createdAtMs) || createdAtMs < 0) {
|
|
throw new RangeError('createdAtMs must be a non-negative safe integer');
|
|
}
|
|
const normalized = normalizeLocalExecutionContextRecipe(recipe);
|
|
assertContentAddressedLocalExecutionContextRecipe(normalized);
|
|
return Object.freeze({
|
|
...normalized,
|
|
contentDigest: localExecutionContextRecipeDigest(normalized),
|
|
createdAtMs,
|
|
});
|
|
}
|