Action
Mymind (Public)
Posted by taurean,
Last update
about 4 hours ago
Share to Mymind
shares the current text note as a note in mymind. The first “# H1” is used as the notes title instead of being included in the body.
Edit MYMIND_KID and MYMIND_SECRET_BASE_64
Steps
-
script
// Drafts action: "Save to mymind" // Creates a mymind object (note) from the current draft via the mymind API. // Docs: https://access.mymind.com/api // Runs silently — no prompts. Title is pulled from the first "# " line; // if none exists, title is omitted and mymind derives one from the content. // === Configuration === const MYMIND_BASE_URL = "https://api.mymind.com"; const USER_AGENT = "drafts-mymind-action/1.0"; // mymind access key — create with "Full access" at access.mymind.com/extensions. // The secret is shown only once, at creation. const MYMIND_KID = "abc…"; const MYMIND_SECRET_BASE64 = "abc…"; // === Base64 (own implementation — operates on byte arrays directly) === const BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; const BASE64URL_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; function base64Decode(base64String) { const clean = base64String.replace(/[^A-Za-z0-9+/]/g, ""); const byteLength = Math.floor((clean.length * 6) / 8); const bytes = new Uint8Array(byteLength); let bitBuffer = 0; let bitCount = 0; let byteIndex = 0; for (let i = 0; i < clean.length; i++) { const value = BASE64_CHARS.indexOf(clean[i]); bitBuffer = (bitBuffer << 6) | value; bitCount += 6; if (bitCount >= 8) { bitCount -= 8; bytes[byteIndex] = (bitBuffer >> bitCount) & 0xff; byteIndex++; } } return bytes; } function base64UrlEncode(bytes) { let result = ""; let i = 0; for (; i + 3 <= bytes.length; i += 3) { const chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]; result += BASE64URL_CHARS[(chunk >> 18) & 0x3f]; result += BASE64URL_CHARS[(chunk >> 12) & 0x3f]; result += BASE64URL_CHARS[(chunk >> 6) & 0x3f]; result += BASE64URL_CHARS[chunk & 0x3f]; } const remaining = bytes.length - i; if (remaining === 1) { const chunk = bytes[i] << 16; result += BASE64URL_CHARS[(chunk >> 18) & 0x3f]; result += BASE64URL_CHARS[(chunk >> 12) & 0x3f]; } else if (remaining === 2) { const chunk = (bytes[i] << 16) | (bytes[i + 1] << 8); result += BASE64URL_CHARS[(chunk >> 18) & 0x3f]; result += BASE64URL_CHARS[(chunk >> 12) & 0x3f]; result += BASE64URL_CHARS[(chunk >> 6) & 0x3f]; } // JWS base64url is unpadded — no trailing "=" characters. return result; } // === UTF-8 encoding (JSON header/payload -> bytes for hashing) === function utf8Encode(str) { const bytes = []; for (let i = 0; i < str.length; i++) { const codePoint = str.codePointAt(i); if (codePoint > 0xffff) { i++; // this code point consumed a UTF-16 surrogate pair } if (codePoint <= 0x7f) { bytes.push(codePoint); } else if (codePoint <= 0x7ff) { bytes.push(0xc0 | (codePoint >> 6)); bytes.push(0x80 | (codePoint & 0x3f)); } else if (codePoint <= 0xffff) { bytes.push(0xe0 | (codePoint >> 12)); bytes.push(0x80 | ((codePoint >> 6) & 0x3f)); bytes.push(0x80 | (codePoint & 0x3f)); } else { bytes.push(0xf0 | (codePoint >> 18)); bytes.push(0x80 | ((codePoint >> 12) & 0x3f)); bytes.push(0x80 | ((codePoint >> 6) & 0x3f)); bytes.push(0x80 | (codePoint & 0x3f)); } } return new Uint8Array(bytes); } function concatBytes(a, b) { const result = new Uint8Array(a.length + b.length); result.set(a, 0); result.set(b, a.length); return result; } // === SHA-256 (FIPS 180-4) === const SHA256_K = [ 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 rotr(x, n) { return ((x >>> n) | (x << (32 - n))) >>> 0; } function sha256(bytes) { const messageLength = bytes.length; const bitLength = messageLength * 8; // Pad: message || 0x80 || zeros || 64-bit big-endian bit length, // total length rounded up to a multiple of 64 bytes. const paddedLength = Math.ceil((messageLength + 9) / 64) * 64; const padded = new Uint8Array(paddedLength); padded.set(bytes); padded[messageLength] = 0x80; const view = new DataView(padded.buffer); const highBits = Math.floor(bitLength / 0x100000000); const lowBits = bitLength >>> 0; view.setUint32(paddedLength - 8, highBits, false); view.setUint32(paddedLength - 4, lowBits, false); let h0 = 0x6a09e667, h1 = 0xbb67ae85, h2 = 0x3c6ef372, h3 = 0xa54ff53a; let h4 = 0x510e527f, h5 = 0x9b05688c, h6 = 0x1f83d9ab, h7 = 0x5be0cd19; const w = new Uint32Array(64); for (let chunkStart = 0; chunkStart < paddedLength; chunkStart += 64) { for (let i = 0; i < 16; i++) { w[i] = view.getUint32(chunkStart + i * 4, false); } for (let i = 16; i < 64; i++) { const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >>> 3); const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10); w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0; } let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7; for (let i = 0; i < 64; i++) { const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25); const ch = (e & f) ^ (~e & g); const temp1 = (h + s1 + ch + SHA256_K[i] + w[i]) >>> 0; const s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22); const maj = (a & b) ^ (a & c) ^ (b & c); const temp2 = (s0 + maj) >>> 0; h = g; g = f; f = e; e = (d + temp1) >>> 0; d = c; c = b; b = a; a = (temp1 + temp2) >>> 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 + h) >>> 0; } const digest = new Uint8Array(32); const digestView = new DataView(digest.buffer); digestView.setUint32(0, h0, false); digestView.setUint32(4, h1, false); digestView.setUint32(8, h2, false); digestView.setUint32(12, h3, false); digestView.setUint32(16, h4, false); digestView.setUint32(20, h5, false); digestView.setUint32(24, h6, false); digestView.setUint32(28, h7, false); return digest; } function hmacSha256(keyBytes, messageBytes) { const blockSize = 64; let key = keyBytes; if (key.length > blockSize) { key = sha256(key); } if (key.length < blockSize) { const padded = new Uint8Array(blockSize); padded.set(key); key = padded; } const innerPad = new Uint8Array(blockSize); const outerPad = new Uint8Array(blockSize); for (let i = 0; i < blockSize; i++) { innerPad[i] = key[i] ^ 0x36; outerPad[i] = key[i] ^ 0x5c; } const innerHash = sha256(concatBytes(innerPad, messageBytes)); return sha256(concatBytes(outerPad, innerHash)); } // === JWT signing per mymind's auth spec === // https://access.mymind.com/api/authentication function buildMymindJwt(kid, secretBytes, path, method) { const header = { alg: "HS256", kid: kid }; const now = Math.floor(Date.now() / 1000); const claims = { path: path, method: method, iat: now, exp: now + 300 }; const encodedHeader = base64UrlEncode(utf8Encode(JSON.stringify(header))); const encodedClaims = base64UrlEncode(utf8Encode(JSON.stringify(claims))); const signingInput = encodedHeader + "." + encodedClaims; const signature = hmacSha256(secretBytes, utf8Encode(signingInput)); const encodedSignature = base64UrlEncode(signature); return signingInput + "." + encodedSignature; } // === Title extraction: first "# " line, or none === // Matches a literal single "#" followed by whitespace, so "## " / "### " // subheadings are correctly excluded. function extractH1(content) { const lines = content.split("\n"); for (let i = 0; i < lines.length; i++) { const match = lines[i].match(/^#\s+(.+)$/); if (match) { return { title: match[1].trim(), lineIndex: i }; } } return null; } // === Main === if (draft.content.trim().length === 0) { app.displayErrorMessage("Draft is empty — nothing to save."); context.fail(); } else { const heading = extractH1(draft.content); let title; let body; if (heading) { title = heading.title; const lines = draft.content.split("\n"); lines.splice(heading.lineIndex, 1); body = lines.join("\n").trim(); } else { body = draft.content; } const requestBody = { content: { type: "text/markdown", body: body }, tags: draft.tags.map((name) => ({ name: name })) }; if (title) { requestBody.title = title; } const secretBytes = base64Decode(MYMIND_SECRET_BASE64); const path = "/objects"; const jwt = buildMymindJwt(MYMIND_KID, secretBytes, path, "POST"); const http = HTTP.create(); const response = http.request({ url: MYMIND_BASE_URL + path, method: "POST", data: requestBody, encoding: "json", headers: { Authorization: "Bearer " + jwt, "Content-Type": "application/json", "User-Agent": USER_AGENT } }); if (response.success) { const savedObject = response.responseData; app.displaySuccessMessage("Saved to mymind: " + savedObject.title); } else { console.log("mymind save failed: " + response.statusCode + " " + response.responseText); app.displayErrorMessage("mymind save failed (" + response.statusCode + ")"); context.fail(); } }
Options
-
After Success Archive , Tags: #mymind Notification Info Log Level Info
Items available in the Drafts Directory are uploaded by community members. Use appropriate caution reviewing downloaded items before use.