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 | export * from "./tool-map"; | 4 | export * from "./tool-map"; |
| 5 | export * from "./discovery"; | 5 | export * from "./discovery"; |
| 6 | export * from "./system-tools"; | 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))); | ||