import * as Variables from "./Variables.js"; let Token = localStorage.getItem(Variables.BlueskyAccessToken); if (Token == null) { console.error("No Bluesky access token!"); } // Getters // This gets the timeline. The cursor is a time in Z form. export async function GetTimeline(Cursor) { if (Token == null) { return ""; } let FetchThing = localStorage.getItem(Variables.BlueskyPDS) + "/xrpc/app.bsky.feed.getTimeline"; if (Cursor != "") { FetchThing += "?cursor=" + Cursor; } let DPoP = await ClientDPoPPDS("GET", FetchThing); let request = fetch(FetchThing, {method: "GET", headers: {"Authorization": "DPoP " + Token, "DPoP": DPoP}});; let body = await request.then((response) => response.json()); let status = await request.then((response) => response.status); let header = await request.then((response) => response.headers.get("dpop-nonce")); if (status == 401) { await HandleError(body, header); body = await GetTimeline(Cursor); } return body; } // Gets a "public" timeline (essentially a feed by bsky.app where you can discover posts). export async function GetPublicTimeline(Cursor) { if (Token == null) { return ""; } let FetchThing = localStorage.getItem(Variables.BlueskyPDS) + "/xrpc/app.bsky.feed.getFeed?feed=at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot"; if (Cursor != "") { FetchThing += "&cursor=" + Cursor; } let DPoP = await ClientDPoPPDS("GET", FetchThing); let request = fetch(FetchThing, {method: "GET", headers: {"Authorization": "DPoP " + Token, "DPoP": DPoP}}); let body = await request.then((response) => response.json()); let status = await request.then((response) => response.status); let header = await request.then((response) => response.headers.get("dpop-nonce")); if (status == 401) { await HandleError(body, header); body = await GetPublicTimeline(Cursor); } return body; } export async function GetProfile(DID) { if (Token == null) { return ""; } let FetchThing = localStorage.getItem(Variables.BlueskyPDS) + "/xrpc/app.bsky.actor.getProfile?actor=" + DID; let DPoP = await ClientDPoPPDS("GET", FetchThing); let request = fetch(FetchThing, { method: "GET", headers: {"Authorization": "DPoP " + Token, "DPoP": DPoP}}); let body = await request.then((response) => response.json()); let status = await request.then((response) => response.status); let header = await request.then((response) => response.headers.get("dpop-nonce")); if (status == 401) { await HandleError(body, header); body = await GetProfile(DID); } return body; } export async function GetProfileFeed(DID) { if (Token == null) { return ""; } let FetchThing = localStorage.getItem(Variables.BlueskyPDS) + "/xrpc/app.bsky.feed.getAuthorFeed?actor=" + DID; let DPoP = await ClientDPoPPDS("GET", FetchThing); let request = fetch(FetchThing, { method: "GET", headers: {"Authorization": "DPoP " + Token, "DPoP": DPoP}}); let body = await request.then((response) => response.json()); let status = await request.then((response) => response.status); let header = await request.then((response) => response.headers.get("dpop-nonce")); if (status == 401) { await HandleError(body, header); body = await GetProfileFeed(DID); } return body; } // This gets the post. If there are multiple URIs, they must be within an array. export async function GetPosts(URIs) { if (Token == null) { return ""; } let FetchThing = localStorage.getItem(Variables.BlueskyPDS) + "/xrpc/app.bsky.feed.getPosts?uris=" + URIs; let DPoP = await ClientDPoPPDS("GET", FetchThing); let request = fetch(FetchThing, { method: "GET", headers: {"Authorization": "DPoP " + Token, "DPoP": DPoP}}); let body = await request.then((response) => response.json()); let status = await request.then((response) => response.status); let header = await request.then((response) => response.headers.get("dpop-nonce")); if (status == 401) { await HandleError(body, header); body = await GetPosts(URIs); } return body; } // Get a blob (like an image or video). Authentication need not apply. export async function GetBlob(DID, CID) { let request = fetch("https://bsky.social/xrpc/com.atproto.sync.getBlob?did=" + DID + "&cid=" + CID, {method: "GET"}); let body = await request.then((response) => response.blob()); return body; } // Gets a record. The repo is the account DID, the collection is the type of record, and the key is the little bit at the end. export async function GetRecord(Repo, Collection, RKey) { if (Token == null) { return ""; } let RequestBody = { "repo": Repo, "collection": Collection, "rkey": RKey } let FetchThing = localStorage.getItem(Variables.BlueskyPDS) + "/xrpc/com.atproto.repo.getRecord?repo=" + Repo + "&collection=" + Collection + "&rkey=" + RKey; let DPoP = await ClientDPoPPDS("GET", FetchThing); let request = fetch(FetchThing, { method: "GET", headers: {"Content-Type": "application/json", "Authorization": "DPoP " + Token, "DPoP": DPoP}}); let body = await request.then((response) => response.json()); let status = await request.then((response) => response.status); let header = await request.then((response) => response.headers.get("dpop-nonce")); if (status == 401) { await HandleError(body, header); body = await GetRecord(Repo, Collection, RKey); } return body; } // Creators // This creates a post. Requires the DID of the account and the Text for the record. export async function CreatePost(DID, Text, ContentWarning = undefined, Tags = [], ReplyID = undefined, RootID = undefined) { if (Token == null) { return ""; } let Record = { "$type": "app.bsky.feed.post", "text": Text, "createdAt": new Date(Date.now()).toISOString() }; // Content warning stuff. if (ContentWarning != undefined) { Record.labels = { "$type": "com.atproto.label.defs#selfLabels", "values": [{ "val": ContentWarning }] }; } // ReplyID and RootID simultaniously. if (ReplyID != undefined && RootID != undefined) { Record.reply = { "parent": { "uri": ReplyID.uri, "cid": ReplyID.cid }, "root": { "uri": RootID.uri, "cid": RootID.cid } }; } // Tags if (Tags.length != 0) { Record.tags = Tags; } let body = await CreateRecord(DID, "app.bsky.feed.post", Record, undefined); if (body.hasOwnProperty("error") && body.error == "InvalidRequest") { let matches = body.message.match(/(\d+)/); Record.text = Text.slice(0, matches[0] - 1); body = await CreateRecord(DID, "app.bsky.feed.post", Record, undefined); if (ReplyID == undefined && RootID == undefined) { await CreatePost(DID, Text.slice(matches[0] - 1, Text.length), ContentWarning, Tags, body, body); } else { await CreatePost(DID, Text.slice(matches[0] - 1, Text.length), ContentWarning, Tags, body, RootID); } } return body; } // Creates a Thread Gate for who can reply. Requires the account DID, a post, and a list of who is allowed. export async function CreateThreadGate(DID, Post, VisibilitySettings) { if (Token == null) { return ""; } let Record = { "$type": "app.bsky.feed.threadgate", "post": Post, "allow": VisibilitySettings, "createdAt": new Date(Date.now()).toISOString() } let body = CreateRecord(DID, "app.bsky.feed.threadgate", Record, undefined); return body; } // Create a like and send it to the server. export async function CreateLike(DID, RefURI, RefCID) { if (Token == null) { return ""; } let StrongRef = { "$type": "com.atproto.repo.strongRef", "uri": RefURI, "cid": RefCID } let Record = { "$type": "app.bsky.feed.like", "subject": StrongRef, "createdAt": new Date(Date.now()).toISOString() } let body = await GetRecord(DID, "app.bsky.feed.like", RefURI.split("/")[RefURI.split("/").length - 1]); // We might have a record. If we do, delete the thing. If we don't, create the thing. if (body.hasOwnProperty("error") && body.error == "RecordNotFound") { body = await CreateRecord(DID, "app.bsky.feed.like", Record, RefURI.split("/")[RefURI.split("/").length - 1]); } else { body = await DeleteRecord(DID, "app.bsky.feed.like", RefURI.split("/")[RefURI.split("/").length - 1]); } return body; } export async function CreateFollow(DID, SubjectDID) { if (Token == null) { return ""; } let Record = { "$type": "app.bsky.graph.follow", "subject": SubjectDID, "createdAt": new Date(Date.now()).toISOString() } let body = await GetRecord(DID, "app.bsky.graph.follow", SubjectDID); // We might have a record. If we do, delete the thing. If we don't, create the thing. if (body.hasOwnProperty("error") && body.error == "RecordNotFound") { body = await CreateRecord(DID, "app.bsky.graph.follow", Record, SubjectDID); } else { body = await DeleteRecord(DID, "app.bsky.graph.follow", SubjectDID); } return body; } export async function CreateBlock(DID, SubjectDID) { if (Token == null) { return ""; } let Record = { "$type": "app.bsky.graph.block", "subject": SubjectDID, "createdAt": new Date(Date.now()).toISOString() } let body = await GetRecord(DID, "app.bsky.graph.block", SubjectDID); // We might have a record. If we do, delete the thing. If we don't, create the thing. if (body.hasOwnProperty("error") && body.error == "RecordNotFound") { body = await CreateRecord(DID, "app.bsky.graph.block", Record, SubjectDID); } else { body = await DeleteRecord(DID, "app.bsky.graph.block", SubjectDID); } return body; } // Create a repost and send it to the server. export async function CreateRepost(DID, RefURI, RefCID) { if (localStorage.getItem(Variables.BlueskyAccessToken) == null) { console.log("No access token!"); return ""; } let StrongRef = { "$type": "com.atproto.repo.strongRef", "uri": RefURI, "cid": RefCID } let Record = { "$type": "app.bsky.feed.repost", "subject": StrongRef, "createdAt": new Date(Date.now()).toISOString() } let body = await GetRecord(DID, "app.bsky.feed.repost", RefURI.split("/")[RefURI.split("/").length - 1]); // We might have a record. If we do, delete the thing. If we don't, create the thing. if (body.hasOwnProperty("error") && body.error == "RecordNotFound") { body = await CreateRecord(DID, "app.bsky.feed.repost", Record, RefURI.split("/")[RefURI.split("/").length - 1]); } else { body = await DeleteRecord(DID, "app.bsky.feed.repost", RefURI.split("/")[RefURI.split("/").length - 1]); } return body; } // Put record updates a record. export async function PutRecord(Repo, Collection, Record, RKey) { if (Token == null) { return ""; } let RequestBody = { "repo": Repo, "collection": Collection, "record": Record, "rkey": RKey } let FetchThing = localStorage.getItem(Variables.BlueskyPDS) + "/xrpc/com.atproto.repo.putRecord"; let DPoP = await ClientDPoPPDS("POST", FetchThing); let request = fetch(FetchThing, { body: JSON.stringify(RequestBody), method: "POST", headers: {"Content-Type": "application/json", "Authorization": "DPoP " + Token, "DPoP": DPoP}}); let body = await request.then((response) => response.json()); let status = await request.then((response) => response.status); let header = await request.then((response) => response.headers.get("dpop-nonce")); if (status == 401) { await HandleError(body, header); body = await PutRecord(Repo, Collection, Record, RKey); } return body; } // Creates a record. Universal way of making things. export async function CreateRecord(Repo, Collection, Record, RKey) { if (Token == null) { return ""; } let RequestBody = { "repo": Repo, "collection": Collection, "record": Record } if (RKey != undefined) { RequestBody.rkey = RKey; } let FetchThing = localStorage.getItem(Variables.BlueskyPDS) + "/xrpc/com.atproto.repo.createRecord"; let DPoP = await ClientDPoPPDS("POST", FetchThing); let request = fetch(FetchThing, { body: JSON.stringify(RequestBody), method: "POST", headers: {"Content-Type": "application/json", "Authorization": "DPoP " + Token, "DPoP": DPoP}}); let body = await request.then((response) => response.json()); let status = await request.then((response) => response.status); let header = await request.then((response) => response.headers.get("dpop-nonce")); if (status == 401) { await HandleError(body, header); body = await CreateRecord(Repo, Collection, Record, RKey); } return body; } // Applyers // Currently this only applies to the link Facets. export function ApplyFacets(record, text) { let StringArray = []; let SplitAreas = [0]; let Hrefs = []; let TempText = ""; if (record.hasOwnProperty("facets")) { // First, append split areas. for (let i of record.facets) { if (i.features[0].$type == "app.bsky.richtext.facet#link") { SplitAreas.push(i.index.byteStart); SplitAreas.push(i.index.byteEnd); Hrefs.push(i.features[0].uri); Hrefs.push(""); } } // Last minute append. SplitAreas.push(text.length); // Remove emoji regex let EmojiRegex = /\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F/gu; let EmojiObjects = text.match(EmojiRegex); let SubtractNumber = 0; if (EmojiObjects != null) { SubtractNumber = EmojiObjects.length * 2; } // Now we split the string for (let i = 1; i < SplitAreas.length; i++) { StringArray.push(text.slice(SplitAreas[i - 1] - SubtractNumber, SplitAreas[i] - SubtractNumber)); } // Finally, we append the string with for (let i = 1; i < StringArray.length; i += 2) { TempText += StringArray[i - 1] + "" + StringArray[i] + ""; } } if (TempText == "") { return text; } return TempText; } // Deleters // Removes a record. Can be a like, a repost, a post, etc. export async function DeleteRecord(Repo, Collection, RKey) { if (Token == null) { return ""; } let RequestBody = { "repo": Repo, "collection": Collection, "rkey": RKey } let FetchThing = localStorage.getItem(Variables.BlueskyPDS) + "/xrpc/com.atproto.repo.deleteRecord"; let DPoP = await ClientDPoPPDS("POST", FetchThing); let request = fetch(FetchThing, { body: JSON.stringify(RequestBody), method: "POST", headers: {"Content-Type": "application/json", "Authorization": "DPoP " + Token, "DPoP": DPoP}}); let body = await request.then((response) => response.json()); let status = await request.then((response) => response.status); let header = await request.then((response) => response.headers.get("dpop-nonce")); if (status == 401) { await HandleError(body, header); body = await DeleteRecord(Repo, Collection, RKey); } return body; } // Things to get this to work. // Component 1/4 export async function GetPDSWellKnown() { return fetch("https://bsky.social/.well-known/oauth-authorization-server", {method: "GET"}); } // Component 2/4 // Many thanks to https://github.com/tonyxu-io/pkce-generator. It was the base for this code. export async function CreatePKCECodeVerifier() { // Generate some Numbers let Numbers = new Uint8Array(32); crypto.getRandomValues(Numbers); // Generate a random string of characters. let CodeVerifier = ""; for (let i in Numbers) { CodeVerifier += String.fromCharCode(Numbers[i]); } // Put this random string into Base64URL format. CodeVerifier = btoa(CodeVerifier).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); return CodeVerifier; } export async function CreatePKCECodeChallenge(CodeVerifier) { // Generate a code challenge with the code verifier. // This is done by first SHA256 encrypting the CodeVerifier, then putting the outputted string into Base64URL format. let CodeChallenge = btoa(await sha256(CodeVerifier)).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); return CodeChallenge; } // Component 3/4 export async function PARrequest(PAREndpoint, State, Challenge) { return fetch(PAREndpoint, {method: "POST", body: new URLSearchParams({ response_type: "code", code_challenge_method: "S256", scope: "atproto transition:generic", client_id: "https://fedi.crowdedgames.group/oauth/client-metadata.json", redirect_uri: "https://fedi.crowdedgames.group/HTML/setting.html", code_challenge: Challenge, state: State, login_hint: "crowdedgames.group" }), headers: {"Content-Type": "application/x-www-form-urlencoded"}}); } async function AuthRequest(TokenEndpoint, code, DPoP, Verify) { return fetch(TokenEndpoint, {method: "POST", body: new URLSearchParams({ grant_type: "authorization_code", code: code, client_id: "https://fedi.crowdedgames.group/oauth/client-metadata.json", redirect_uri: "https://fedi.crowdedgames.group/HTML/setting.html", code_verifier: Verify}), headers: { "DPoP": DPoP, "Content-Type": "application/x-www-form-urlencoded"}}) .then((response) => response.json()); } async function ReauthRequest(TokenEndpoint, Token, DPoP) { return fetch(TokenEndpoint, {method: "POST", body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: Token, client_id: "https://fedi.crowdedgames.group/oauth/client-metadata.json"}), headers: { "DPoP": DPoP, "Content-Type": "application/x-www-form-urlencoded"}}); } // Component 4/4 export async function ClientDPoPToken(POSTorGET, RequestURL) { let PublicKey = await crypto.subtle.importKey("jwk", JSON.parse(localStorage.getItem(Variables.BlueskyPublicKey)), {name: "ECDSA", namedCurve: "P-256"}, true, ["verify"]); let PrivateKey = await crypto.subtle.importKey("jwk", JSON.parse(localStorage.getItem(Variables.BlueskyPrivateKey)), {name: "ECDSA", namedCurve: "P-256"}, true, ["sign"]); // Header let Header = {typ: "dpop+jwt", alg: "ES256", jwk: await crypto.subtle.exportKey("jwk", PublicKey) .then(function(response) { delete response["key_ops"]; delete response["ext"]; delete response["alg"]; return response}) }; // Payload let Payload = {}; Payload.iss = "https://fedi.crowdedgames.group/oauth/client-metadata.json"; Payload.jti = crypto.randomUUID(); Payload.htm = POSTorGET; Payload.htu = RequestURL; Payload.iat = Math.floor(new Date(Date.now()).getTime() / 1000); Payload.nonce = localStorage.getItem(Variables.BlueskyNonce); let sHeader = JSON.stringify(Header); let sPayload = JSON.stringify(Payload); let JWT = KJUR.jws.JWS.sign("ES256", sHeader, sPayload, await crypto.subtle.exportKey("jwk", PrivateKey) .then(function(response) { delete response["key_ops"]; delete response["ext"]; delete response["alg"]; return response}) ); return JWT; } export async function ClientDPoPPDS(POSTorGET, RequestURL) { let PublicKey = await crypto.subtle.importKey("jwk", JSON.parse(localStorage.getItem(Variables.BlueskyPublicKey)), {name: "ECDSA", namedCurve: "P-256"}, true, ["verify"]); let PrivateKey = await crypto.subtle.importKey("jwk", JSON.parse(localStorage.getItem(Variables.BlueskyPrivateKey)), {name: "ECDSA", namedCurve: "P-256"}, true, ["sign"]); // Header let Header = {typ: "dpop+jwt", alg: "ES256", jwk: await crypto.subtle.exportKey("jwk", PublicKey) .then(function(response) { delete response["key_ops"]; delete response["ext"]; delete response["alg"]; return response}) }; // Payload let Payload = {}; Payload.iss = "https://fedi.crowdedgames.group/oauth/client-metadata.json"; Payload.jti = crypto.randomUUID(); Payload.htm = POSTorGET; Payload.htu = RequestURL; Payload.iat = Math.floor(new Date(Date.now()).getTime() / 1000); Payload.nonce = localStorage.getItem(Variables.BlueskyNonce); Payload.ath = await CreatePKCECodeChallenge(localStorage.getItem(Variables.BlueskyAccessToken)); let sHeader = JSON.stringify(Header); let sPayload = JSON.stringify(Payload); let JWT = KJUR.jws.JWS.sign("ES256", sHeader, sPayload, await crypto.subtle.exportKey("jwk", PrivateKey) .then(function(response) { delete response["key_ops"]; delete response["ext"]; delete response["alg"]; return response}) ); return JWT; } export async function HandleAuthorization(Website) { // Quickly check to see if it has something before :// so it doesn't screw the link. if (Website.toLowerCase().split("://").length > 1) { Website = "https://" + Website.split("://")[1]; } else { Website = "https://" + Website; } // Declare Variables let KeyPair = await crypto.subtle.generateKey({name: "ECDSA", namedCurve: "P-256"}, true, ["sign", "verify"]); let WellKnown = await GetPDSWellKnown().then((response) => response.json()); let State = crypto.randomUUID(); let PKCEverifier = await CreatePKCECodeVerifier(); let PKCEchallenge = await CreatePKCECodeChallenge(PKCEverifier); // Save these localStorage.setItem(Variables.BlueskyPKCEVerifier, PKCEverifier); localStorage.setItem(Variables.BlueskyPKCEChallenge, PKCEchallenge); // PAR request (beginning) let PAR = PARrequest(WellKnown.pushed_authorization_request_endpoint, State, PKCEchallenge); let body = await PAR.then((response) => response.json()); let nonce = await PAR.then((response) => response.headers.get("dpop-nonce")); // Save nonce localStorage.setItem(Variables.BlueskyNonce, nonce); // Export keys let ExportedKey1 = await crypto.subtle.exportKey("jwk", KeyPair.publicKey); let ExportedKey2 = await crypto.subtle.exportKey("jwk", KeyPair.privateKey); // Convert them into a good format. localStorage.setItem(Variables.BlueskyPublicKey, JSON.stringify(ExportedKey1)); localStorage.setItem(Variables.BlueskyPrivateKey, JSON.stringify(ExportedKey2)); // Now we need to authenticate. Make sure the State stays the same throughout this whole process :] document.location.href = Website + "/oauth/authorize?client_id=https://fedi.crowdedgames.group/oauth/client-metadata.json&request_uri=" + body.request_uri; } export async function GainTokens() { let WellKnown = await GetPDSWellKnown().then((response) => response.json()); // Check to see if something's a miss... if ((document.location.href.split("state=").length > 1 && document.location.href.split("iss=").length > 1 && document.location.href.split("code=").length > 1) && localStorage.getItem(Variables.BlueskyPKCEVerifier) != null && localStorage.getItem(Variables.BlueskyAccessToken) == null) { // Create varaibles, be aware of waits because of internet. let DPoP = await ClientDPoPToken("POST", WellKnown.token_endpoint); let code = document.location.href.split("code=")[1]; // Authentication let Var = await localStorage.getItem(Variables.BlueskyPKCEVerifier); let Auth = await AuthRequest(WellKnown.token_endpoint, code, DPoP, Var); // Save the tokens and be done await localStorage.setItem(Variables.BlueskyAccessToken, Auth.access_token); await localStorage.setItem(Variables.BlueskyRefreshToken, Auth.refresh_token); // That long string just gets the payload // aud = PDS server we are communicating with; sub = user DID await localStorage.setItem(Variables.BlueskyPDS, "https://" + KJUR.jws.JWS.readSafeJSONString(b64utoutf8(localStorage.getItem(Variables.BlueskyAccessToken).split(".")[1])).aud.split(":")[2]); await localStorage.setItem(Variables.BlueskyDID, KJUR.jws.JWS.readSafeJSONString(b64utoutf8(localStorage.getItem(Variables.BlueskyAccessToken).split(".")[1])).sub); } } // Refreshing tokens is an integral part of auth. export async function RefreshTokens() { let WellKnown = await GetPDSWellKnown().then((response) => response.json()); // Fake PAR request to get the nonce. let State = crypto.randomUUID(); let PKCEverifier = await CreatePKCECodeVerifier(); let PKCEchallenge = await CreatePKCECodeChallenge(PKCEverifier); let PAR = PARrequest(WellKnown.pushed_authorization_request_endpoint, State, PKCEchallenge); let nonce = await PAR.then((response) => response.headers.get("dpop-nonce")); // Save nonce await localStorage.setItem(Variables.BlueskyNonce, nonce); // Create varaibles, be aware of waits because of internet. let DPoP = await ClientDPoPToken("POST", WellKnown.token_endpoint); // Token refresh let Var = await localStorage.getItem(Variables.BlueskyRefreshToken); let Auth = await ReauthRequest(WellKnown.token_endpoint, Var, DPoP).then((response) => response.json()); // Save the tokens and be done await localStorage.setItem(Variables.BlueskyAccessToken, Auth.access_token); await localStorage.setItem(Variables.BlueskyRefreshToken, Auth.refresh_token); } async function HandleError(body, header) { await localStorage.setItem(Variables.BlueskyNonce, header); if (body.message != undefined && body.message.includes("claim timestamp check failed")) { await RefreshTokens(); Token = localStorage.getItem(Variables.BlueskyAccessToken); } } // Stolen from elsewhere. // Firefox snippet; Slightly edited. async function sha256(message) { // encode as UTF-8 const MessageBuffer = new TextEncoder().encode(message); // hash the message const HashBuffer = await crypto.subtle.digest('SHA-256', MessageBuffer); // convert ArrayBuffer to Array const HashArray = Array.from(new Uint8Array(HashBuffer)); // convert this hashArray to a string let string = ""; for (let i in HashArray) { string += String.fromCharCode(HashArray[i]); } return string; } // Firefox snippet. function ab2str(buf) { return String.fromCharCode.apply(null, new Uint8Array(buf)); } // Firefox snippet. function str2ab(str) { const buf = new ArrayBuffer(str.length); const bufView = new Uint8Array(buf); for (let i = 0, strLen = str.length; i < strLen; i++) { bufView[i] = str.charCodeAt(i); } return buf; }