Created
April 10, 2026 04:49
-
-
Save sam-saffron-jarvis/17681670f1fd10c566fbd19fd47fb262 to your computer and use it in GitHub Desktop.
Discourse AI Custom Tool: List BigQuery Datasets
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* eslint-disable no-undef, no-unused-vars */ | |
| // ---- Base64 ---- | |
| const B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; | |
| function b64Dec(s) { | |
| s = s.replace(/[\s=]/g, ""); | |
| const o = []; | |
| let buf = 0, bits = 0; | |
| for (let i = 0; i < s.length; i++) { | |
| buf = (buf << 6) | B64.indexOf(s[i]); | |
| bits += 6; | |
| if (bits >= 8) { bits -= 8; o.push((buf >> bits) & 0xff); } | |
| } | |
| return o; | |
| } | |
| function b64Enc(b) { | |
| let s = ""; | |
| for (let i = 0; i < b.length; i += 3) { | |
| const x = b[i], y = b[i + 1] || 0, z = b[i + 2] || 0; | |
| s += B64[x >> 2] + B64[((x & 3) << 4) | (y >> 4)]; | |
| if (i + 1 < b.length) s += B64[((y & 15) << 2) | (z >> 6)]; | |
| if (i + 2 < b.length) s += B64[z & 63]; | |
| } | |
| return s.replace(/\+/g, "-").replace(/\//g, "_"); | |
| } | |
| function str2b(s) { | |
| const b = []; | |
| for (let i = 0; i < s.length; i++) b.push(s.charCodeAt(i)); | |
| return b; | |
| } | |
| // ---- SHA-256 ---- | |
| const SK = [ | |
| 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, | |
| 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, | |
| 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, | |
| 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, | |
| 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, | |
| 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, | |
| 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, | |
| 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2 | |
| ]; | |
| function rr(x, n) { return ((x >>> n) | (x << (32 - n))) >>> 0; } | |
| function sha256(msg) { | |
| const p = msg.slice(); | |
| const bl = msg.length * 8; | |
| p.push(0x80); | |
| while (p.length % 64 !== 56) p.push(0); | |
| p.push(0, 0, 0, 0, (bl >>> 24) & 0xff, (bl >>> 16) & 0xff, (bl >>> 8) & 0xff, bl & 0xff); | |
| let h0 = 0x6a09e667, h1 = 0xbb67ae85, h2 = 0x3c6ef372, h3 = 0xa54ff53a; | |
| let h4 = 0x510e527f, h5 = 0x9b05688c, h6 = 0x1f83d9ab, h7 = 0x5be0cd19; | |
| for (let off = 0; off < p.length; off += 64) { | |
| const w = Array(64); | |
| for (let i = 0; i < 16; i++) | |
| w[i] = ((p[off+i*4] << 24) | (p[off+i*4+1] << 16) | | |
| (p[off+i*4+2] << 8) | p[off+i*4+3]) >>> 0; | |
| for (let i = 16; i < 64; i++) | |
| w[i] = (w[i-16] + (rr(w[i-15],7) ^ rr(w[i-15],18) ^ (w[i-15]>>>3)) + | |
| w[i-7] + (rr(w[i-2],17) ^ rr(w[i-2],19) ^ (w[i-2]>>>10))) >>> 0; | |
| let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, hh = h7; | |
| for (let i = 0; i < 64; i++) { | |
| const t1 = (hh + (rr(e,6) ^ rr(e,11) ^ rr(e,25)) + | |
| ((e & f) ^ (~e & g)) + SK[i] + w[i]) >>> 0; | |
| const t2 = ((rr(a,2) ^ rr(a,13) ^ rr(a,22)) + | |
| ((a & b) ^ (a & c) ^ (b & c))) >>> 0; | |
| hh = g; g = f; f = e; e = (d + t1) >>> 0; | |
| d = c; c = b; b = a; a = (t1 + t2) >>> 0; | |
| } | |
| h0 = (h0+a)>>>0; h1 = (h1+b)>>>0; h2 = (h2+c)>>>0; h3 = (h3+d)>>>0; | |
| h4 = (h4+e)>>>0; h5 = (h5+f)>>>0; h6 = (h6+g)>>>0; h7 = (h7+hh)>>>0; | |
| } | |
| const out = []; | |
| for (const v of [h0, h1, h2, h3, h4, h5, h6, h7]) | |
| out.push((v >>> 24) & 0xff, (v >>> 16) & 0xff, (v >>> 8) & 0xff, v & 0xff); | |
| return out; | |
| } | |
| // ---- ASN.1 DER parser (PKCS8 → RSA key) ---- | |
| function parsePKCS8(pem) { | |
| const der = b64Dec(pem.replace(/-----[^-]+-----/g, "").replace(/\s/g, "")); | |
| let p = 0; | |
| function readLen() { | |
| var l = der[p++]; | |
| if (l & 0x80) { var nb = l & 0x7f; l = 0; for (var i = 0; i < nb; i++) l = (l << 8) | der[p++]; } | |
| return l; | |
| } | |
| function readSeq() { p++; readLen(); } | |
| function readInt() { p++; var l = readLen(); var v = der.slice(p, p + l); p += l; return v; } | |
| function readSkip() { p++; var l = readLen(); p += l; } | |
| readSeq(); | |
| readInt(); | |
| readSkip(); | |
| p++; readLen(); | |
| readSeq(); | |
| readInt(); | |
| var n = readInt(); | |
| readInt(); | |
| var d = readInt(); | |
| return { n: n, d: d }; | |
| } | |
| // ---- RSA PKCS1v15-SHA256 sign ---- | |
| function b2bi(bytes) { | |
| if (bytes.length === 0) return 0n; | |
| let h = "0x"; | |
| for (let i = 0; i < bytes.length; i++) h += bytes[i].toString(16).padStart(2, "0"); | |
| return BigInt(h); | |
| } | |
| function bi2b(n, len) { | |
| let h = n.toString(16); | |
| if (h.length & 1) h = "0" + h; | |
| while (h.length < len * 2) h = "00" + h; | |
| const o = []; | |
| for (let i = 0; i < h.length; i += 2) o.push(parseInt(h.substr(i, 2), 16)); | |
| return o; | |
| } | |
| function modpow(base, exp, mod) { | |
| let r = 1n; | |
| base = base % mod; | |
| while (exp > 0n) { | |
| if (exp & 1n) r = r * base % mod; | |
| exp >>= 1n; | |
| base = base * base % mod; | |
| } | |
| return r; | |
| } | |
| const DI_PREFIX = [ | |
| 0x30,0x31,0x30,0x0d,0x06,0x09,0x60,0x86,0x48,0x01, | |
| 0x65,0x03,0x04,0x02,0x01,0x05,0x00,0x04,0x20 | |
| ]; | |
| function rsaSign(hash, key) { | |
| const nBI = b2bi(key.n), dBI = b2bi(key.d); | |
| const kLen = Math.ceil(nBI.toString(16).length / 2); | |
| const di = DI_PREFIX.concat(hash); | |
| const em = [0x00, 0x01]; | |
| for (let i = 0; i < kLen - di.length - 3; i++) em.push(0xff); | |
| em.push(0x00, ...di); | |
| return bi2b(modpow(b2bi(em), dBI, nBI), kLen); | |
| } | |
| // ---- JWT + OAuth2 ---- | |
| function signJWT(cred) { | |
| const now = Math.floor(Date.now() / 1000); | |
| const hdr = b64Enc(str2b(JSON.stringify({ alg: "RS256", typ: "JWT" }))); | |
| const pay = b64Enc(str2b(JSON.stringify({ | |
| iss: cred.client_email, | |
| scope: "https://www.googleapis.com/auth/bigquery.readonly", | |
| aud: "https://oauth2.googleapis.com/token", | |
| iat: now, | |
| exp: now + 3600 | |
| }))); | |
| const msg = hdr + "." + pay; | |
| const sig = rsaSign(sha256(str2b(msg)), parsePKCS8(cred.private_key)); | |
| return msg + "." + b64Enc(sig); | |
| } | |
| function getToken(cred) { | |
| const jwt = signJWT(cred); | |
| const r = http.post("https://oauth2.googleapis.com/token", { | |
| headers: { "Content-Type": "application/x-www-form-urlencoded" }, | |
| body: "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=" + jwt | |
| }); | |
| if (r.status !== 200) throw new Error("OAuth2 failed (" + r.status + "): " + r.body); | |
| return JSON.parse(r.body).access_token; | |
| } | |
| // ---- BigQuery List Datasets ---- | |
| function invoke(params) { | |
| const cred = JSON.parse(secrets.get("json_cred")); | |
| const token = getToken(cred); | |
| const project = cred.project_id; | |
| const url = "https://bigquery.googleapis.com/bigquery/v2/projects/" + | |
| encodeURIComponent(project) + "/datasets"; | |
| const r = http.get(url, { | |
| headers: { Authorization: "Bearer " + token } | |
| }); | |
| if (r.status !== 200) throw new Error("BigQuery (" + r.status + "): " + r.body); | |
| const body = JSON.parse(r.body); | |
| var datasets = (body.datasets || []).map(function(ds) { | |
| return ds.datasetReference.datasetId; | |
| }); | |
| return { project: project, datasets: datasets }; | |
| } | |
| function details() { | |
| return "Lists all BigQuery datasets in the project"; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment