mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
import { MAX_EXECUTION_ENVIRONMENT_VALUE_BYTES } from './executionContext';
|
||||
|
||||
export const LOCAL_SECRET_ALGORITHM = 'aes-256-gcm';
|
||||
export const MAX_LOCAL_SECRET_NAME_LENGTH = 128;
|
||||
export const MAX_LOCAL_SECRET_VERSION = 2_147_483_647;
|
||||
export const MAX_LOCAL_SECRET_REF_LENGTH = 512;
|
||||
export const MAX_LOCAL_SECRET_MUTATION_ID_LENGTH = 64;
|
||||
export const MAX_LOCAL_SECRET_KEY_ID_LENGTH = 128;
|
||||
|
||||
const SECRET_REF_PREFIX = 'qlsecret:v1:';
|
||||
const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/;
|
||||
const KEY_ID_PATTERN = /^[A-Za-z0-9._-]+$/;
|
||||
|
||||
export interface LocalSecretReference {
|
||||
projectId: string;
|
||||
name: string;
|
||||
version?: number;
|
||||
}
|
||||
|
||||
export interface LocalSecretEnvelope {
|
||||
projectId: string;
|
||||
name: string;
|
||||
version: number;
|
||||
mutationId: string;
|
||||
keyId: string;
|
||||
algorithm: typeof LOCAL_SECRET_ALGORITHM;
|
||||
nonce: string;
|
||||
ciphertext: string;
|
||||
authTag: string;
|
||||
createdAtMs: number;
|
||||
}
|
||||
|
||||
export class InvalidLocalSecretError extends TypeError {
|
||||
constructor(message: string) {
|
||||
super(`Local Secret value is invalid: ${message}`);
|
||||
this.name = 'InvalidLocalSecretError';
|
||||
}
|
||||
}
|
||||
|
||||
export class LocalSecretUnavailableError extends Error {
|
||||
readonly code = 'LOCAL_SECRET_UNAVAILABLE';
|
||||
|
||||
constructor() {
|
||||
super('Local Secret is unavailable');
|
||||
this.name = 'LocalSecretUnavailableError';
|
||||
}
|
||||
}
|
||||
|
||||
export class LocalSecretVersionConflictError extends Error {
|
||||
readonly code = 'LOCAL_SECRET_VERSION_CONFLICT';
|
||||
|
||||
constructor() {
|
||||
super('Local Secret current version changed');
|
||||
this.name = 'LocalSecretVersionConflictError';
|
||||
}
|
||||
}
|
||||
|
||||
export class LocalSecretMutationConflictError extends Error {
|
||||
readonly code = 'LOCAL_SECRET_MUTATION_CONFLICT';
|
||||
|
||||
constructor() {
|
||||
super('Local Secret mutation does not match its previous request');
|
||||
this.name = 'LocalSecretMutationConflictError';
|
||||
}
|
||||
}
|
||||
|
||||
function assertIdentifier(name: string, value: string, maximum: number): void {
|
||||
if (
|
||||
typeof value !== 'string' ||
|
||||
value.length < 1 ||
|
||||
value.length > maximum ||
|
||||
/[\u0000-\u001f\u007f]/.test(value)
|
||||
) {
|
||||
throw new InvalidLocalSecretError(`${name} is invalid`);
|
||||
}
|
||||
}
|
||||
|
||||
export function assertLocalSecretProjectId(value: string): void {
|
||||
assertIdentifier('projectId', value, 128);
|
||||
}
|
||||
|
||||
export function assertLocalSecretName(value: string): void {
|
||||
assertIdentifier('name', value, MAX_LOCAL_SECRET_NAME_LENGTH);
|
||||
}
|
||||
|
||||
export function assertLocalSecretVersion(value: number): void {
|
||||
if (
|
||||
!Number.isSafeInteger(value) ||
|
||||
value < 1 ||
|
||||
value > MAX_LOCAL_SECRET_VERSION
|
||||
) {
|
||||
throw new InvalidLocalSecretError('version is invalid');
|
||||
}
|
||||
}
|
||||
|
||||
export function assertLocalSecretMutationId(value: string): void {
|
||||
assertIdentifier('mutationId', value, MAX_LOCAL_SECRET_MUTATION_ID_LENGTH);
|
||||
}
|
||||
|
||||
export function assertLocalSecretKeyId(value: string): void {
|
||||
if (
|
||||
typeof value !== 'string' ||
|
||||
value.length < 1 ||
|
||||
value.length > MAX_LOCAL_SECRET_KEY_ID_LENGTH ||
|
||||
!KEY_ID_PATTERN.test(value)
|
||||
) {
|
||||
throw new InvalidLocalSecretError('keyId is invalid');
|
||||
}
|
||||
}
|
||||
|
||||
export function assertLocalSecretPlaintext(value: string): void {
|
||||
if (
|
||||
typeof value !== 'string' ||
|
||||
value.includes('\0') ||
|
||||
Buffer.byteLength(value, 'utf8') > MAX_EXECUTION_ENVIRONMENT_VALUE_BYTES
|
||||
) {
|
||||
throw new InvalidLocalSecretError('plaintext is invalid');
|
||||
}
|
||||
}
|
||||
|
||||
function decodeBase64Url(name: string, value: string, bytes?: number): Buffer {
|
||||
if (
|
||||
typeof value !== 'string' ||
|
||||
(value.length > 0 && !BASE64URL_PATTERN.test(value))
|
||||
) {
|
||||
throw new InvalidLocalSecretError(`${name} is invalid`);
|
||||
}
|
||||
const decoded = Buffer.from(value, 'base64url');
|
||||
if (
|
||||
decoded.toString('base64url') !== value ||
|
||||
(bytes && decoded.length !== bytes)
|
||||
) {
|
||||
throw new InvalidLocalSecretError(`${name} is invalid`);
|
||||
}
|
||||
return decoded;
|
||||
}
|
||||
|
||||
export function localSecretBinary(
|
||||
name: 'nonce' | 'ciphertext' | 'authTag',
|
||||
value: string,
|
||||
): Buffer {
|
||||
const decoded = decodeBase64Url(
|
||||
name,
|
||||
value,
|
||||
name === 'nonce' ? 12 : name === 'authTag' ? 16 : undefined,
|
||||
);
|
||||
if (
|
||||
name === 'ciphertext' &&
|
||||
decoded.length > MAX_EXECUTION_ENVIRONMENT_VALUE_BYTES
|
||||
) {
|
||||
throw new InvalidLocalSecretError('ciphertext is too large');
|
||||
}
|
||||
return decoded;
|
||||
}
|
||||
|
||||
export function createLocalSecretRef(reference: LocalSecretReference): string {
|
||||
assertLocalSecretProjectId(reference.projectId);
|
||||
assertLocalSecretName(reference.name);
|
||||
if (reference.version !== undefined) {
|
||||
assertLocalSecretVersion(reference.version);
|
||||
}
|
||||
const payload = JSON.stringify({
|
||||
projectId: reference.projectId,
|
||||
name: reference.name,
|
||||
...(reference.version === undefined ? {} : { version: reference.version }),
|
||||
});
|
||||
const value =
|
||||
SECRET_REF_PREFIX + Buffer.from(payload, 'utf8').toString('base64url');
|
||||
if (value.length > MAX_LOCAL_SECRET_REF_LENGTH) {
|
||||
throw new InvalidLocalSecretError('reference is too large');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function parseLocalSecretRef(value: string): LocalSecretReference {
|
||||
if (
|
||||
typeof value !== 'string' ||
|
||||
value.length > MAX_LOCAL_SECRET_REF_LENGTH ||
|
||||
!value.startsWith(SECRET_REF_PREFIX)
|
||||
) {
|
||||
throw new InvalidLocalSecretError('reference is invalid');
|
||||
}
|
||||
const encoded = value.slice(SECRET_REF_PREFIX.length);
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(decodeBase64Url('reference', encoded).toString('utf8'));
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidLocalSecretError) throw error;
|
||||
throw new InvalidLocalSecretError('reference is invalid');
|
||||
}
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
throw new InvalidLocalSecretError('reference is invalid');
|
||||
}
|
||||
const record = parsed as Record<string, unknown>;
|
||||
const keys = Object.keys(record).sort();
|
||||
const expectedKeys =
|
||||
record.version === undefined
|
||||
? ['name', 'projectId']
|
||||
: ['name', 'projectId', 'version'];
|
||||
if (
|
||||
keys.length !== expectedKeys.length ||
|
||||
keys.some((key, index) => key !== expectedKeys[index])
|
||||
) {
|
||||
throw new InvalidLocalSecretError('reference is invalid');
|
||||
}
|
||||
const reference: LocalSecretReference = {
|
||||
projectId: record.projectId as string,
|
||||
name: record.name as string,
|
||||
...(record.version === undefined
|
||||
? {}
|
||||
: { version: record.version as number }),
|
||||
};
|
||||
if (createLocalSecretRef(reference) !== value) {
|
||||
throw new InvalidLocalSecretError('reference is not canonical');
|
||||
}
|
||||
return Object.freeze(reference);
|
||||
}
|
||||
|
||||
export function normalizeLocalSecretEnvelope(
|
||||
envelope: LocalSecretEnvelope,
|
||||
): LocalSecretEnvelope {
|
||||
if (!envelope || typeof envelope !== 'object' || Array.isArray(envelope)) {
|
||||
throw new InvalidLocalSecretError('envelope must be an object');
|
||||
}
|
||||
assertLocalSecretProjectId(envelope.projectId);
|
||||
assertLocalSecretName(envelope.name);
|
||||
assertLocalSecretVersion(envelope.version);
|
||||
assertLocalSecretMutationId(envelope.mutationId);
|
||||
assertLocalSecretKeyId(envelope.keyId);
|
||||
if (envelope.algorithm !== LOCAL_SECRET_ALGORITHM) {
|
||||
throw new InvalidLocalSecretError('algorithm is invalid');
|
||||
}
|
||||
localSecretBinary('nonce', envelope.nonce);
|
||||
localSecretBinary('ciphertext', envelope.ciphertext);
|
||||
localSecretBinary('authTag', envelope.authTag);
|
||||
if (!Number.isSafeInteger(envelope.createdAtMs) || envelope.createdAtMs < 0) {
|
||||
throw new InvalidLocalSecretError('createdAtMs is invalid');
|
||||
}
|
||||
return Object.freeze({
|
||||
projectId: envelope.projectId,
|
||||
name: envelope.name,
|
||||
version: envelope.version,
|
||||
mutationId: envelope.mutationId,
|
||||
keyId: envelope.keyId,
|
||||
algorithm: LOCAL_SECRET_ALGORITHM,
|
||||
nonce: envelope.nonce,
|
||||
ciphertext: envelope.ciphertext,
|
||||
authTag: envelope.authTag,
|
||||
createdAtMs: envelope.createdAtMs,
|
||||
});
|
||||
}
|
||||
|
||||
export function localSecretEnvelopeAad(
|
||||
envelope: Pick<
|
||||
LocalSecretEnvelope,
|
||||
'projectId' | 'name' | 'version' | 'mutationId' | 'keyId' | 'algorithm'
|
||||
>,
|
||||
): Buffer {
|
||||
return Buffer.from(
|
||||
JSON.stringify({
|
||||
projectId: envelope.projectId,
|
||||
name: envelope.name,
|
||||
version: envelope.version,
|
||||
mutationId: envelope.mutationId,
|
||||
keyId: envelope.keyId,
|
||||
algorithm: envelope.algorithm,
|
||||
}),
|
||||
'utf8',
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user