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.