Showing the changes to packages/codemode-core/src (3 of 77 files). Show all
Unified · Split
| @@ -0,0 +1,164 @@ | ||
| 1 | +export type HttpRequestPlacements = { | |
| 2 | + headers?: Readonly<Record<string, string>>; | |
| 3 | + queryParams?: Readonly<Record<string, string>>; | |
| 4 | + cookies?: Readonly<Record<string, string>>; | |
| 5 | + bodyValues?: Readonly<Record<string, string>>; | |
| 6 | +}; | |
| 7 | + | |
| 8 | +const isRecord = (value: unknown): value is Record<string, unknown> => | |
| 9 | + typeof value === "object" && value !== null && !Array.isArray(value); | |
| 10 | + | |
| 11 | +const decodeJsonPointerSegment = (value: string): string => | |
| 12 | + value.replaceAll("~1", "/").replaceAll("~0", "~"); | |
| 13 | + | |
| 14 | +const pathSegmentsFromPlacementPath = (value: string): ReadonlyArray<string> => { | |
| 15 | + const trimmed = value.trim(); | |
| 16 | + if (trimmed.length === 0) { | |
| 17 | + return []; | |
| 18 | + } | |
| 19 | + | |
| 20 | + if (trimmed.startsWith("/")) { | |
| 21 | + return trimmed | |
| 22 | + .split("/") | |
| 23 | + .slice(1) | |
| 24 | + .map(decodeJsonPointerSegment) | |
| 25 | + .filter((segment) => segment.length > 0); | |
| 26 | + } | |
| 27 | + | |
| 28 | + return trimmed.split(".").filter((segment) => segment.length > 0); | |
| 29 | +}; | |
| 30 | + | |
| 31 | +const findHeaderKey = ( | |
| 32 | + headers: Readonly<Record<string, string>>, | |
| 33 | + name: string, | |
| 34 | +): string | null => { | |
| 35 | + const lowered = name.toLowerCase(); | |
| 36 | + for (const key of Object.keys(headers)) { | |
| 37 | + if (key.toLowerCase() === lowered) { | |
| 38 | + return key; | |
| 39 | + } | |
| 40 | + } | |
| 41 | + | |
| 42 | + return null; | |
| 43 | +}; | |
| 44 | + | |
| 45 | +const parseCookieHeader = (value: string): Record<string, string> => { | |
| 46 | + const parsed: Record<string, string> = {}; | |
| 47 | + | |
| 48 | + for (const part of value.split(";")) { | |
| 49 | + const trimmed = part.trim(); | |
| 50 | + if (trimmed.length === 0) { | |
| 51 | + continue; | |
| 52 | + } | |
| 53 | + | |
| 54 | + const separatorIndex = trimmed.indexOf("="); | |
| 55 | + if (separatorIndex === -1) { | |
| 56 | + parsed[trimmed] = ""; | |
| 57 | + continue; | |
| 58 | + } | |
| 59 | + | |
| 60 | + const name = trimmed.slice(0, separatorIndex).trim(); | |
| 61 | + const cookieValue = trimmed.slice(separatorIndex + 1).trim(); | |
| 62 | + if (name.length > 0) { | |
| 63 | + parsed[name] = cookieValue; | |
| 64 | + } | |
| 65 | + } | |
| 66 | + | |
| 67 | + return parsed; | |
| 68 | +}; | |
| 69 | + | |
| 70 | +const setNestedRecordValue = ( | |
| 71 | + target: Record<string, unknown>, | |
| 72 | + path: ReadonlyArray<string>, | |
| 73 | + value: string, | |
| 74 | +) => { | |
| 75 | + let current = target; | |
| 76 | + | |
| 77 | + for (let index = 0; index < path.length - 1; index += 1) { | |
| 78 | + const segment = path[index]!; | |
| 79 | + const next = current[segment]; | |
| 80 | + if (isRecord(next)) { | |
| 81 | + current = next; | |
| 82 | + continue; | |
| 83 | + } | |
| 84 | + | |
| 85 | + const replacement: Record<string, unknown> = {}; | |
| 86 | + current[segment] = replacement; | |
| 87 | + current = replacement; | |
| 88 | + } | |
| 89 | + | |
| 90 | + current[path[path.length - 1]!] = value; | |
| 91 | +}; | |
| 92 | + | |
| 93 | +export const applyHttpQueryPlacementsToUrl = (input: { | |
| 94 | + url: string | URL; | |
| 95 | + queryParams?: Readonly<Record<string, string>>; | |
| 96 | +}): URL => { | |
| 97 | + const url = input.url instanceof URL ? new URL(input.url.toString()) : new URL(input.url); | |
| 98 | + | |
| 99 | + for (const [key, value] of Object.entries(input.queryParams ?? {})) { | |
| 100 | + url.searchParams.set(key, value); | |
| 101 | + } | |
| 102 | + | |
| 103 | + return url; | |
| 104 | +}; | |
| 105 | + | |
| 106 | +export const applyCookiePlacementsToHeaders = (input: { | |
| 107 | + headers: Readonly<Record<string, string>>; | |
| 108 | + cookies?: Readonly<Record<string, string>>; | |
| 109 | +}): Record<string, string> => { | |
| 110 | + const cookies = input.cookies ?? {}; | |
| 111 | + if (Object.keys(cookies).length === 0) { | |
| 112 | + return { ...input.headers }; | |
| 113 | + } | |
| 114 | + | |
| 115 | + const cookieHeaderKey = findHeaderKey(input.headers, "cookie"); | |
| 116 | + const cookieHeaderValue = cookieHeaderKey ? input.headers[cookieHeaderKey] : null; | |
| 117 | + const mergedCookies = { | |
| 118 | + ...(cookieHeaderValue ? parseCookieHeader(cookieHeaderValue) : {}), | |
| 119 | + ...cookies, | |
| 120 | + }; | |
| 121 | + const headers = { ...input.headers }; | |
| 122 | + | |
| 123 | + headers[cookieHeaderKey ?? "cookie"] = Object.entries(mergedCookies) | |
| 124 | + .map(([name, value]) => `${name}=${encodeURIComponent(value)}`) | |
| 125 | + .join("; "); | |
| 126 | + | |
| 127 | + return headers; | |
| 128 | +}; | |
| 129 | + | |
| 130 | +export const applyJsonBodyPlacements = (input: { | |
| 131 | + body: unknown; | |
| 132 | + bodyValues?: Readonly<Record<string, string>>; | |
| 133 | + label?: string; | |
| 134 | +}): unknown => { | |
| 135 | + const bodyValues = input.bodyValues ?? {}; | |
| 136 | + if (Object.keys(bodyValues).length === 0) { | |
| 137 | + return input.body; | |
| 138 | + } | |
| 139 | + | |
| 140 | + const root = | |
| 141 | + input.body == null | |
| 142 | + ? {} | |
| 143 | + : isRecord(input.body) | |
| 144 | + ? structuredClone(input.body) | |
| 145 | + : null; | |
| 146 | + | |
| 147 | + if (root === null) { | |
| 148 | + throw new Error( | |
| 149 | + `${input.label ?? "HTTP request"} auth body placements require an object JSON body`, | |
| 150 | + ); | |
| 151 | + } | |
| 152 | + | |
| 153 | + for (const [rawPath, value] of Object.entries(bodyValues)) { | |
| 154 | + const path = pathSegmentsFromPlacementPath(rawPath); | |
| 155 | + if (path.length === 0) { | |
| 156 | + throw new Error( | |
| 157 | + `${input.label ?? "HTTP request"} auth body placement path cannot be empty`, | |
| 158 | + ); | |
| 159 | + } | |
| 160 | + setNestedRecordValue(root, path, value); | |
| 161 | + } | |
| 162 | + | |
| 163 | + return root; | |
| 164 | +}; | |
| @@ -4,3 +4,5 @@ export * from "./schema-types"; · 3 unmodified lines | ||
| 4 | 4 | export * from "./tool-map"; |
| 5 | 5 | export * from "./discovery"; |
| 6 | 6 | export * from "./system-tools"; |
| 7 | +export * from "./http-request-placements"; | |
| 8 | +export * from "./isomorphic-hash"; | |
| @@ -0,0 +1,132 @@ | ||
| 1 | +/** | |
| 2 | + * Isomorphic SHA-256 implementation that works in both Node.js and the browser. | |
| 3 | + * | |
| 4 | + * Avoids importing `node:crypto`, which Vite externalizes for browser builds, | |
| 5 | + * causing runtime errors when this module is pulled into client bundles. | |
| 6 | + * | |
| 7 | + * Uses the same SHA-256 algorithm as `node:crypto`, producing identical hex output. | |
| 8 | + */ | |
| 9 | + | |
| 10 | +// SHA-256 constants: first 32 bits of the fractional parts of the cube roots of the first 64 primes | |
| 11 | +const K = new Uint32Array([ | |
| 12 | + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, | |
| 13 | + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, | |
| 14 | + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, | |
| 15 | + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, | |
| 16 | + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, | |
| 17 | + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, | |
| 18 | + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, | |
| 19 | + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, | |
| 20 | +]); | |
| 21 | + | |
| 22 | +const rotr = (x: number, n: number): number => (x >>> n) | (x << (32 - n)); | |
| 23 | + | |
| 24 | +const sha256Compress = (state: Uint32Array, block: Uint32Array): void => { | |
| 25 | + const w = new Uint32Array(64); | |
| 26 | + for (let i = 0; i < 16; i++) w[i] = block[i]; | |
| 27 | + for (let i = 16; i < 64; i++) { | |
| 28 | + const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >>> 3); | |
| 29 | + const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10); | |
| 30 | + w[i] = (w[i - 16] + s0 + w[i - 7] + s1) | 0; | |
| 31 | + } | |
| 32 | + | |
| 33 | + let a = state[0], b = state[1], c = state[2], d = state[3]; | |
| 34 | + let e = state[4], f = state[5], g = state[6], h = state[7]; | |
| 35 | + | |
| 36 | + for (let i = 0; i < 64; i++) { | |
| 37 | + const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25); | |
| 38 | + const ch = (e & f) ^ (~e & g); | |
| 39 | + const temp1 = (h + S1 + ch + K[i] + w[i]) | 0; | |
| 40 | + const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22); | |
| 41 | + const maj = (a & b) ^ (a & c) ^ (b & c); | |
| 42 | + const temp2 = (S0 + maj) | 0; | |
| 43 | + | |
| 44 | + h = g; g = f; f = e; e = (d + temp1) | 0; | |
| 45 | + d = c; c = b; b = a; a = (temp1 + temp2) | 0; | |
| 46 | + } | |
| 47 | + | |
| 48 | + state[0] = (state[0] + a) | 0; | |
| 49 | + state[1] = (state[1] + b) | 0; | |
| 50 | + state[2] = (state[2] + c) | 0; | |
| 51 | + state[3] = (state[3] + d) | 0; | |
| 52 | + state[4] = (state[4] + e) | 0; | |
| 53 | + state[5] = (state[5] + f) | 0; | |
| 54 | + state[6] = (state[6] + g) | 0; | |
| 55 | + state[7] = (state[7] + h) | 0; | |
| 56 | +}; | |
| 57 | + | |
| 58 | +const utf8Encode = (str: string): Uint8Array => { | |
| 59 | + if (typeof TextEncoder !== "undefined") { | |
| 60 | + return new TextEncoder().encode(str); | |
| 61 | + } | |
| 62 | + | |
| 63 | + // Fallback for environments without TextEncoder | |
| 64 | + const bytes: number[] = []; | |
| 65 | + for (let i = 0; i < str.length; i++) { | |
| 66 | + let c = str.charCodeAt(i); | |
| 67 | + if (c < 0x80) { | |
| 68 | + bytes.push(c); | |
| 69 | + } else if (c < 0x800) { | |
| 70 | + bytes.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f)); | |
| 71 | + } else if (c >= 0xd800 && c < 0xdc00 && i + 1 < str.length) { | |
| 72 | + const next = str.charCodeAt(++i); | |
| 73 | + c = 0x10000 + ((c - 0xd800) << 10) + (next - 0xdc00); | |
| 74 | + bytes.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 0x3f), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f)); | |
| 75 | + } else { | |
| 76 | + bytes.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f)); | |
| 77 | + } | |
| 78 | + } | |
| 79 | + return new Uint8Array(bytes); | |
| 80 | +}; | |
| 81 | + | |
| 82 | +const sha256 = (data: Uint8Array): Uint8Array => { | |
| 83 | + const state = new Uint32Array([ | |
| 84 | + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, | |
| 85 | + 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19, | |
| 86 | + ]); | |
| 87 | + | |
| 88 | + const bitLen = data.length * 8; | |
| 89 | + | |
| 90 | + // Padding: append 1 bit, then zeros, then 64-bit big-endian length | |
| 91 | + const padded = new Uint8Array(Math.ceil((data.length + 9) / 64) * 64); | |
| 92 | + padded.set(data); | |
| 93 | + padded[data.length] = 0x80; | |
| 94 | + // Write the 64-bit big-endian bit length at the end | |
| 95 | + const view = new DataView(padded.buffer); | |
| 96 | + // For strings up to ~256 MB, the high 32 bits of the bit length are zero | |
| 97 | + view.setUint32(padded.length - 4, bitLen, false); | |
| 98 | + if (bitLen > 0xffffffff) { | |
| 99 | + view.setUint32(padded.length - 8, Math.floor(bitLen / 0x100000000), false); | |
| 100 | + } | |
| 101 | + | |
| 102 | + // Process each 64-byte block | |
| 103 | + const block = new Uint32Array(16); | |
| 104 | + for (let offset = 0; offset < padded.length; offset += 64) { | |
| 105 | + for (let i = 0; i < 16; i++) { | |
| 106 | + block[i] = view.getUint32(offset + i * 4, false); | |
| 107 | + } | |
| 108 | + sha256Compress(state, block); | |
| 109 | + } | |
| 110 | + | |
| 111 | + const result = new Uint8Array(32); | |
| 112 | + const resultView = new DataView(result.buffer); | |
| 113 | + for (let i = 0; i < 8; i++) { | |
| 114 | + resultView.setUint32(i * 4, state[i], false); | |
| 115 | + } | |
| 116 | + return result; | |
| 117 | +}; | |
| 118 | + | |
| 119 | +const hexFromBytes = (bytes: Uint8Array): string => { | |
| 120 | + let hex = ""; | |
| 121 | + for (let i = 0; i < bytes.length; i++) { | |
| 122 | + hex += bytes[i].toString(16).padStart(2, "0"); | |
| 123 | + } | |
| 124 | + return hex; | |
| 125 | +}; | |
| 126 | + | |
| 127 | +/** | |
| 128 | + * Compute a SHA-256 hex digest of a UTF-8 string. | |
| 129 | + * Drop-in replacement for `createHash("sha256").update(value).digest("hex")`. | |
| 130 | + */ | |
| 131 | +export const sha256Hex = (value: string): string => | |
| 132 | + hexFromBytes(sha256(utf8Encode(value))); | |