UNPKG

@mesh-tech/mesh-cli

Version:

CLI for Mesh platform development utilities

233 lines (230 loc) 7.46 kB
// libs/mesh-cli/src/utils/temporal-codec.ts import { execFileSync } from "child_process"; import { webcrypto as crypto } from "node:crypto"; // libs/mesh-cli/src/utils/log.ts import chalk from "chalk"; function logPrefix(opts) { const enabled = opts.envFlag === "1" || opts.envFlag !== "0" && !opts.isTTY; return enabled ? `[${opts.now.toISOString().slice(11, 19)}] ` : ""; } function prefix() { return chalk.dim( logPrefix({ isTTY: !!process.stderr.isTTY, envFlag: process.env.MESH_LOG_TIMESTAMPS, now: /* @__PURE__ */ new Date() }) ); } function logSuccess(message) { console.error(prefix() + chalk.green("\u2713"), message); } function logWarn(message) { console.error(prefix() + chalk.yellow("\u26A0"), message); } // libs/mesh-cli/src/utils/temporal-codec.ts function resolveTemporalEncodingKeyFromK8s(namespace) { const secretName = `${namespace}-temporal-encoding-key`; try { const b64 = execFileSync( "kubectl", [ "get", "secret", secretName, "-n", namespace, "-o", "jsonpath={.data.TEMPORAL_ENCODING_KEY}" ], { encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] } ).trim(); if (!b64) return void 0; return Buffer.from(b64, "base64").toString("utf-8"); } catch { return void 0; } } var ENCRYPTED_ENCODING = "binary/encrypted"; var IV_LENGTH = 12; async function deriveKey(encodingKey) { const keyData = await crypto.subtle.digest( "SHA-256", new TextEncoder().encode(encodingKey) ); return crypto.subtle.importKey( "raw", keyData, { name: "AES-GCM" }, false, ["decrypt"] ); } async function aesGcmDecrypt(data, key) { const iv = data.slice(0, IV_LENGTH); const ciphertextWithTag = data.slice(IV_LENGTH); const decrypted = await crypto.subtle.decrypt( { name: "AES-GCM", iv }, key, ciphertextWithTag ); return new Uint8Array(decrypted); } async function createPayloadDecrypter(encodingKey) { const key = await deriveKey(encodingKey); return async (payload) => { if (!payload?.metadata) return null; const encodingBytes = payload.metadata["encoding"]; if (!encodingBytes) return null; const encoding = new TextDecoder().decode(encodingBytes); if (encoding !== ENCRYPTED_ENCODING) return null; if (!payload.data) return "[encrypted payload missing data]"; let inner; try { const decrypted = await aesGcmDecrypt(payload.data, key); inner = decodePayloadProtobuf(decrypted); } catch (err) { const msg = err instanceof Error ? err.message : String(err); return `[undecryptable: ${msg}]`; } return decodeInnerPayload(inner); }; } async function createRawPayloadDecrypter(encodingKey) { const key = await deriveKey(encodingKey); return async (payload) => { if (!payload?.metadata) return null; const encodingBytes = payload.metadata["encoding"]; if (!encodingBytes) return null; const encoding = new TextDecoder().decode(encodingBytes); if (encoding !== ENCRYPTED_ENCODING) return null; if (!payload.data) { throw new Error("encrypted payload has no data to decrypt"); } const decrypted = await aesGcmDecrypt(payload.data, key); return decodePayloadProtobuf(decrypted); }; } function readVarint(buf, pos) { let value = 0; let shift = 0; let next = pos; while (next < buf.length) { const byte = buf[next]; next += 1; value |= (byte & 127) << shift; if ((byte & 128) === 0) return { value, next }; shift += 7; if (shift >= 32) throw new Error("varint too large"); } throw new Error("truncated varint"); } function readLengthDelimited(buf, pos) { const { value: length, next: afterLen } = readVarint(buf, pos); const end = afterLen + length; if (end > buf.length) throw new Error("truncated length-delimited field"); return { bytes: buf.slice(afterLen, end), next: end }; } function decodePayloadProtobuf(buf) { const metadata = {}; let data; let pos = 0; while (pos < buf.length) { const { value: tag, next: afterTag } = readVarint(buf, pos); const fieldNumber = tag >>> 3; const wireType = tag & 7; pos = afterTag; if (wireType === 2) { const { bytes, next } = readLengthDelimited(buf, pos); pos = next; if (fieldNumber === 1) { const entry = decodeMetadataEntry(bytes); if (entry) metadata[entry.key] = entry.value; } else if (fieldNumber === 2) { data = bytes; } } else if (wireType === 0) { pos = readVarint(buf, pos).next; } else if (wireType === 1) { pos += 8; } else if (wireType === 5) { pos += 4; } else { throw new Error(`unsupported wire type ${wireType} at pos ${pos}`); } } return { metadata, data: data ?? new Uint8Array(0) }; } function decodeMetadataEntry(buf) { let key; let value; let pos = 0; while (pos < buf.length) { const { value: tag, next: afterTag } = readVarint(buf, pos); const fieldNumber = tag >>> 3; const wireType = tag & 7; pos = afterTag; if (wireType !== 2) { if (wireType === 0) pos = readVarint(buf, pos).next; else if (wireType === 1) pos += 8; else if (wireType === 5) pos += 4; else throw new Error(`unsupported metadata wire type ${wireType}`); continue; } const { bytes, next } = readLengthDelimited(buf, pos); pos = next; if (fieldNumber === 1) key = new TextDecoder().decode(bytes); else if (fieldNumber === 2) value = bytes; } if (key === void 0 || value === void 0) return null; return { key, value }; } function decodeInnerPayload(payload) { const meta = payload.metadata ?? {}; const encodingBytes = meta["encoding"]; const encoding = encodingBytes ? new TextDecoder().decode(encodingBytes) : "unknown"; const data = payload.data ?? new Uint8Array(0); if (encoding === "binary/null") return "null"; if (encoding === "json/plain") { const text = new TextDecoder().decode(data); try { return JSON.stringify(JSON.parse(text), null, 2); } catch { return text; } } if (encoding === "binary/plain") return `[binary, ${data.length} bytes]`; return `[encoding=${encoding}, ${data.length} bytes]`; } function resolveEncodingKey(namespace) { const fromK8s = resolveTemporalEncodingKeyFromK8s(namespace); const key = fromK8s ?? process.env.TEMPORAL_ENCODING_KEY; if (!key) { logWarn( `Could not read TEMPORAL_ENCODING_KEY from secret ${namespace}/${namespace}-temporal-encoding-key, and TEMPORAL_ENCODING_KEY is not set. Configure kubectl (namespace read access), or export TEMPORAL_ENCODING_KEY (e.g. from a \`mesh dev\` worker env).` ); return void 0; } logSuccess( fromK8s ? `Resolved encoding key from ${namespace}-temporal-encoding-key` : "Resolved encoding key from TEMPORAL_ENCODING_KEY env" ); return key; } async function buildDecrypterForNamespace(namespace) { const key = resolveEncodingKey(namespace); if (!key) return void 0; return createPayloadDecrypter(key); } async function buildRawDecrypterForNamespace(namespace) { const key = resolveEncodingKey(namespace); if (!key) return void 0; return createRawPayloadDecrypter(key); } export { buildDecrypterForNamespace, buildRawDecrypterForNamespace, createPayloadDecrypter, createRawPayloadDecrypter, resolveEncodingKey, resolveTemporalEncodingKeyFromK8s };