micro-stacks
Version:
Tiny libraries for building Stacks apps.
29 lines (23 loc) • 19.6 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
var crypto = require('micro-stacks/crypto');
var common = require('micro-stacks/common');
var zoneFile = require('micro-stacks/zone-file');
var ae=Object.defineProperty;var ce=(t,e,r)=>e in t?ae(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r;var z=(t,e,r)=>(ce(t,typeof e!="symbol"?e+"":e,r),r);function de(t){return (t==null?void 0:t.map(e=>({...e,domain:e.path})))??null}function M(t){let{hubInfo:e,privateKey:r,gaiaHubUrl:o,associationToken:n=null,scopes:s}=t,{challenge_text:i}=e,a=common.bytesToHex(crypto.getPublicKey(r,!0)),u=crypto.getRandomBytes(16).toString();return {gaiaChallenge:i,hubUrl:o,iss:a,salt:u,associationToken:n,scopes:de(s)}}async function ge(t){let e=M(t);return `v1:${await new crypto.TokenSigner("ES256K",t.privateKey).sign(e)}`}function ye(t){let e=M(t);return `v1:${new crypto.TokenSigner("ES256K",t.privateKey).signSync(e)}`}var J={challenge_text:'["gaiahub","0","storage2.blockstack.org","blockstack_storage_please_sign"]',latest_auth_version:"v1",max_file_upload_size_megabytes:20,read_url_prefix:"https://gaia.blockstack.org/hub/"};async function ze(t,e=!1){let{gaiaHubUrl:r,privateKey:o,associationToken:n,scopes:s}=t,i=J;e&&(i=await(await common.fetchPrivate(`${r}/hub_info`)).json());let{read_url_prefix:a,max_file_upload_size_megabytes:u}=i,c=await ge({hubInfo:i,privateKey:o,gaiaHubUrl:r,associationToken:n,scopes:s});return {address:crypto.privateKeyToBase58Address(o),url_prefix:a,token:c,server:r,max_file_upload_size_megabytes:u??20}}function je(t){let{gaiaHubUrl:e,privateKey:r,associationToken:o,scopes:n}=t,s=J,{read_url_prefix:i,max_file_upload_size_megabytes:a}=s,u=ye({hubInfo:s,privateKey:r,gaiaHubUrl:e,associationToken:o,scopes:n});return {address:crypto.privateKeyToBase58Address(r),url_prefix:i,token:u,server:e,max_file_upload_size_megabytes:a??20}}var w={MISSING_PARAMETER:"missing_parameter",REMOTE_SERVICE_ERROR:"remote_service_error",INVALID_STATE:"invalid_state",NO_SESSION_DATA:"no_session_data",DOES_NOT_EXIST:"does_not_exist",FAILED_DECRYPTION_ERROR:"failed_decryption_error",INVALID_DID_ERROR:"invalid_did_error",NOT_ENOUGH_FUNDS_ERROR:"not_enough_error",INVALID_AMOUNT_ERROR:"invalid_amount_error",LOGIN_FAILED_ERROR:"login_failed",SIGNATURE_VERIFICATION_ERROR:"signature_verification_failure",CONFLICT_ERROR:"conflict_error",NOT_ENOUGH_PROOF_ERROR:"not_enough_proof_error",BAD_PATH_ERROR:"bad_path_error",VALIDATION_ERROR:"validation_error",PAYLOAD_TOO_LARGE_ERROR:"payload_too_large_error",PRECONDITION_FAILED_ERROR:"precondition_failed_error",UNKNOWN:"unknown"};Object.freeze(w);var k=class extends Error{message;code;parameter;constructor(e){super();let r=e.message,o=`Error Code: ${e.code}`,n=this.stack;if(n)o+=`Stack Trace:
${n}`;else try{throw new Error}catch(s){n=s.stack;}r+=`
If you believe this exception is caused by a bug in blockstack.js,
please file a bug report: https://github.com/blockstack/blockstack.js/issues
${o}`,this.message=r,this.code=e.code,this.parameter=e.parameter?e.parameter:void 0;}toString(){return `${super.toString()}
code: ${this.code} param: ${this.parameter?this.parameter:"n/a"}`}};var y=class extends k{constructor(e){let r=`Failed to verify signature: ${e}`;super({code:w.SIGNATURE_VERIFICATION_ERROR,message:r}),this.message=r,this.name="SignatureVerificationError";}};var b=class extends k{hubError;constructor(e,r){super(e),r&&(this.hubError={statusCode:r.status,statusText:r.statusText},typeof r.body=="string"?this.hubError.message=r.body:typeof r.body=="object"&&Object.assign(this.hubError,r.body));}},x=class extends b{constructor(e,r){super({message:e,code:w.DOES_NOT_EXIST},r),this.name="DoesNotExist";}},P=class extends b{constructor(e,r){super({message:e,code:w.CONFLICT_ERROR},r),this.name="ConflictError";}},v=class extends b{constructor(e,r){super({message:e,code:w.NOT_ENOUGH_PROOF_ERROR},r),this.name="NotEnoughProofError";}},S=class extends b{constructor(e,r){super({message:e,code:w.BAD_PATH_ERROR},r),this.name="BadPathError";}},C=class extends b{constructor(e,r){super({message:e,code:w.VALIDATION_ERROR},r),this.name="ValidationError";}},E=class extends b{hubError;maxUploadByteSize;constructor(e,r,o){super({message:e,code:w.PAYLOAD_TOO_LARGE_ERROR},r),this.name="PayloadTooLargeError",this.maxUploadByteSize=o;}},L=class extends b{constructor(e,r){super({message:e,code:w.PRECONDITION_FAILED_ERROR},r),this.name="PreconditionFailedError";}};async function me(t){let e="",r;try{e=await t.text();try{r=JSON.parse(e);}catch{}}catch(i){console.debug(`Error getting bad http response text: ${i}`);}let o=t.status,n=t.statusText;return {status:o,statusText:n,body:r||e}}function G(t){return Number.isFinite(t)?Math.floor(t*1024*1024):0}async function _(t,e,r){if(t.ok)throw new Error("Cannot get a Stacks from a valid response.");let o=await me(t);if(o.status===401)throw new C(e,o);if(o.status===402)throw new v(e,o);if(o.status===403)throw new S(e,o);if(o.status===404)throw new x(e,o);if(o.status===409)throw new P(e,o);if(o.status===412)throw new L(e,o);if(o.status===413){let n=r&&r.max_file_upload_size_megabytes?G(r.max_file_upload_size_megabytes):0;throw new E(e,o,n)}else throw new Error(e)}function Z(t){if(!t||!t.hubError||!t.hubError.statusCode)return !1;let e=t.hubError.statusCode;return e===401||e===409||e>=500&&e<=599}async function A(t){let{filename:e,contents:r,hubConfig:o,contentType:n="application/octet-stream"}=t,s={"Content-Type":n,Authorization:`bearer ${o.token}`},i=await common.fetchPrivate(`${o.server}/store/${o.address}/${e}`,{method:"POST",headers:s,body:r});if(!i.ok)throw await _(i,"Error when uploading to Gaia hub.",o);let a=await i.text();return JSON.parse(a)}function I(t,e){return Promise.resolve(`${e.url_prefix}${e.address}/${t}`)}var h=".sig",U="https://stacks-node-api.stacks.co/v1/names";var $=class{content;wasString;contentType;contentByteLength;loadedData;constructor(e,r){this.wasString=typeof e=="string",this.content=$.normalizeContentDataType(e,r),this.contentType=r||this.detectContentType(),this.contentByteLength=this.detectContentLength();}static normalizeContentDataType(e,r){try{if(typeof e=="string"){let o=(r||"").toLowerCase().replace("-","");if(o.includes("charset")&&!o.includes("charset=utf8")&&!o.includes("charset=ascii"))throw new Error(`Unable to determine byte length with charset: ${r}`);return new TextEncoder().encode(e)}else {if(e instanceof Uint8Array)return e;if(ArrayBuffer.isView(e))return new Uint8Array(e.buffer,e.byteOffset,e.byteLength);if(typeof Blob<"u"&&e instanceof Blob)return e;if(e instanceof ArrayBuffer)return new Uint8Array(e);if(Array.isArray(e)){if(e.length>0&&(!Number.isInteger(e[0])||e[0]<0||e[0]>255))throw new Error(`Unexpected array values provided as file data: value "${e[0]}" at index 0 is not an octet number. ${this.supportedTypesMsg}`);return new Uint8Array(e)}else {let o=Object.prototype.toString.call(e);throw new Error(`Unexpected type provided as file data: ${o}. ${this.supportedTypesMsg}`)}}}catch(o){throw console.error(o),new Error(`Error processing data: ${o}`)}}detectContentType(){return this.wasString?"text/plain; charset=utf-8":typeof Blob<"u"&&this.content instanceof Blob&&this.content.type?this.content.type:"application/octet-stream"}detectContentLength(){if(ArrayBuffer.isView(this.content)||this.content instanceof Uint8Array)return this.content.byteLength;if(typeof Blob<"u"&&this.content instanceof Blob)return this.content.size;let e=Object.prototype.toString.call(this.content),r=new Error(`Unexpected type "${e}" while detecting content length`);throw console.error(r),r}async loadContent(){try{if(this.content instanceof Uint8Array)return this.content;if(ArrayBuffer.isView(this.content))return new Uint8Array(this.content.buffer,this.content.byteOffset,this.content.byteLength);if(typeof Blob<"u"&&this.content instanceof Blob){let e=new FileReader;return await new Promise((n,s)=>{e.onerror=i=>{s(i);},e.onload=()=>{let i=e.result;n(new Uint8Array(i));},e.readAsArrayBuffer(this.content);})}else {let e=Object.prototype.toString.call(this.content);throw new Error(`Unexpected type ${e}`)}}catch(e){console.error(e);let r=new Error(`Error loading content: ${e}`);throw console.error(r),r}}load(){return this.loadedData===void 0&&(this.loadedData=this.loadContent()),this.loadedData}},O=$;z(O,"supportedTypesMsg","Supported types are: `string` (to be UTF8 encoded), `Uint8Array`, `Blob`, `File`, `ArrayBuffer`, `UInt8Array` or any other typed array buffer. ");async function nt(t,e,r){let{privateKey:o}=r,{encrypt:n,sign:s,gaiaHubConfig:i,cipherTextEncoding:a}=r,{contentType:u=""}=r,c=G(i.max_file_upload_size_megabytes),g=c>0,l=new O(e,u);if(u=l.contentType,!n&&g&&l.contentByteLength>c){let p=`The max file upload size for this hub is ${c} bytes, the given content is ${l.contentByteLength} bytes`,f=new E(p,null,c);throw console.error(f),f}if(n&&g&&a){let p=crypto.eciesGetJsonStringLength({contentLength:l.contentByteLength,wasString:l.wasString,sign:!!s,cipherTextEncoding:a});if(p>c){let f=`The max file upload size for this hub is ${c} bytes, the given content is ${p} bytes after encryption`,d=new E(f,null,c);throw console.error(d),d}}let m;if(!n&&s){let p=await l.load();if(typeof s=="string")o=s;else if(!o)throw Error("Need to pass private key");let f=await crypto.signECDSA({contents:p,privateKey:o});m=async d=>(await Promise.all([A({filename:t,contents:p,hubConfig:d,contentType:u}),A({filename:`${t}${h}`,contents:JSON.stringify(f),hubConfig:d,contentType:"application/json"})]))[0].publicURL;}else {let p;if(!n&&!s)p=l.content;else {let f;if(typeof n=="string")f=n;else if(typeof s=="string")f=common.bytesToHex(crypto.getPublicKey(s,!0));else if(o)f=common.bytesToHex(crypto.getPublicKey(o,!0));else throw new Error("No private key passed");let d=await l.load(),R=await crypto.encryptContent(d,{publicKey:f,wasString:l.wasString,cipherTextEncoding:a,privateKey:o});if(p=JSON.stringify(R),o){let{signature:K,publicKey:T}=await crypto.signECDSA({contents:R,privateKey:o});p=JSON.stringify({signature:K,publicKey:T,cipherText:R});}u="application/json";}m=async f=>(await A({filename:t,contents:p,hubConfig:f,contentType:u})).publicURL;}try{return await m(i)}catch(p){if(Z(p))return console.error(p),console.error("Possible recoverable error during Gaia upload, retrying..."),await m(i);throw p}}function q(t){if(!t.hasOwnProperty("uri")||!Array.isArray(t.uri)||t.uri.length<1)return null;let e=t.uri.filter(n=>n.hasOwnProperty("target")&&n.name==="_http._tcp");if(e.length<1)return null;let r=e[0];if(!r.hasOwnProperty("target"))return null;let o=r.target;return o.startsWith("https")||o.startsWith("http")||(o=`https://${o}`),o}function N(t,e){let r;return e.proof&&e.proof.url&&(r=e.proof.url),{"@type":"Account",service:t,identifier:e.username,proofType:"http",proofUrl:r}}function H(t){let e={"@type":"Person"};if(t){t.name&&t.name.formatted&&(e.name=t.name.formatted),t.bio&&(e.description=t.bio),t.location&&t.location.formatted&&(e.address={"@type":"PostalAddress",addressLocality:t.location.formatted});let r=[];t.avatar&&t.avatar.url&&r.push({"@type":"ImageObject",name:"avatar",contentUrl:t.avatar.url}),t.cover&&t.cover.url&&r.push({"@type":"ImageObject",name:"cover",contentUrl:t.cover.url}),r.length&&(e.image=r),t.website&&(e.website=[{"@type":"WebSite",url:t.website}]);let o=[];t.bitcoin&&t.bitcoin.address&&o.push({"@type":"Account",role:"payment",service:"bitcoin",identifier:t.bitcoin.address}),t.twitter&&t.twitter.username&&o.push(N("twitter",t.twitter)),t.facebook&&t.facebook.username&&o.push(N("facebook",t.facebook)),t.github&&t.github.username&&o.push(N("github",t.github)),t.auth&&t.auth.length>0&&t.auth[0]&&t.auth[0].publicKeychain&&o.push({"@type":"Account",role:"key",service:"bip32",identifier:t.auth[0].publicKeychain}),t.pgp&&t.pgp.url&&o.push({"@type":"Account",role:"key",service:"pgp",identifier:t.pgp.fingerprint,contentUrl:t.pgp.url}),e.account=o;}return e}function Re(t,e){let r=crypto.decodeToken(t);if(!r)throw Error("no decoded token");let o=r.payload;if(typeof o=="string")throw new Error("Unexpected token payload type of string");if(o.hasOwnProperty("subject")&&o.subject){if(!o.subject.hasOwnProperty("publicKey"))throw new Error("Token doesn't have a subject public key")}else throw new Error("Token doesn't have a subject");if(o.hasOwnProperty("issuer")&&o.issuer){if(!o.issuer.hasOwnProperty("publicKey"))throw new Error("Token doesn't have an issuer public key")}else throw new Error("Token doesn't have an issuer");if(!o.hasOwnProperty("claim"))throw new Error("Token doesn't have a claim");let n=o.issuer.publicKey,s=crypto.publicKeyToStxAddress(n);if(e!==n){if(e!==s)throw new Error("Token issuer public key does not match the verifying value")}let i=new crypto.TokenVerifier(r.header.alg,n);if(!i)throw new Error("Invalid token verifier");if(!i.verify(t))throw new Error("Token verification failed");return r}function ee(t,e){let r=e?Re(t,e):crypto.decodeToken(t);if(r&&r.hasOwnProperty("payload")){let o=r.payload;if(typeof o=="string")throw new Error("[micro-stacks] extractProfile: Unexpected token payload type of string");if(o.hasOwnProperty("claim"))return o.claim}return {}}async function te(t,e){let r=zoneFile.parseZoneFile(t);if(r.hasOwnProperty("$origin")||(r=null),!(r&&Object.keys(r).length>0))return H(JSON.parse(t));let n=q(r);if(n)try{let i=await(await common.fetchPrivate(n)).json();return ee(i[0].token,e)}catch(s){throw console.error(`[micro-stacks] resolveZoneFileToProfile: error fetching token file ${n}: ${s}`),s}return console.debug("[micro-stacks] Token file url not found. Resolving to blank profile."),{}}async function re(t){let{username:e,zoneFileLookupURL:r=U}=t;if(!e)return Promise.reject();let o=`${r.replace(/\/$/,"")}/${t.username}`,s=await(await common.fetchPrivate(o)).json();if(s.hasOwnProperty("zonefile")&&s.hasOwnProperty("address"))return await te(s.zonefile,t.verify?s.address:void 0);throw new Error("Invalid zonefile lookup response: did not contain `address` or `zonefile` field")}async function oe(t,e,r,o){let n=await re({username:e,zoneFileLookupURL:o}),s;if(!!n)return n.hasOwnProperty("apps")?n.apps.hasOwnProperty(r)&&(s=`${n.apps[r].replace(/\/?(\?|#|$)/,"/$1")}${t}`):n.hasOwnProperty("appsMeta")&&n.appsMeta.hasOwnProperty(r)&&(s=`${n.appsMeta[r].replace(/\/?(\?|#|$)/,"/$1")}${t}`),s}async function ve(t,e){let r;if(e.username?r=await oe(t,e.username,e.app,e.zoneFileLookupURL):r=await I(t,e.gaiaHubConfig),r)return r;throw new Error("Missing readURL")}async function D(t){let{app:e,username:r,zoneFileLookupURL:o,gaiaHubConfig:n}=t,s;r?s=await oe("/",r,e,o):n&&(s=await I("/",n));let i=/([13][a-km-zA-HJ-NP-Z0-9]{26,35})/.exec(s);if(!i)throw new Error("Failed to parse gaia address");return i[i.length-1]}async function F(t){let{path:e,forceText:r,app:o,username:n,zoneFileLookupURL:s,gaiaHubConfig:i}=t,a=await ve(e,{app:o,username:n,zoneFileLookupURL:s,gaiaHubConfig:i}),u=await common.fetchPrivate(a);if(!u.ok)throw await _(u,`getFile ${e} failed.`,null);let c=u.headers.get("Content-Type");if(typeof c=="string"&&(c=c.toLowerCase()),r||c===null||c.startsWith("text")||c.startsWith("application/json"))return u.text();{let g=await u.arrayBuffer();return common.arrayBufferToUint8(g)}}async function ne(t,e){let{app:r,username:o,zoneFileLookupURL:n,gaiaHubConfig:s}=e,i=`${t}${h}`;try{let[a,u,c]=await Promise.all([F({path:t,app:r,username:o,zoneFileLookupURL:n,forceText:!1,gaiaHubConfig:s}),F({path:i,app:r,username:o,zoneFileLookupURL:n,forceText:!0,gaiaHubConfig:s}),D({app:r,username:o,zoneFileLookupURL:n,gaiaHubConfig:s})]);if(!a)return a;if(!c)throw new y(`Failed to get gaia address for verification of: ${t}`);if(!u||typeof u!="string")throw new y(`Failed to obtain signature for file: ${t} -- looked in ${t}${h}`);let g,l;try{let d=JSON.parse(u);g=d.signature,l=d.publicKey;}catch(d){throw d instanceof SyntaxError?new Error(`Failed to parse signature content JSON (path: ${t}${h}) The content may be corrupted.`):d}let m=crypto.publicKeyToBase58Address(l),p=typeof a=="string"?common.utf8ToBytes(a):a,f=crypto.verifyECDSA({signature:g,contents:p,publicKey:l});if(c!==m)throw new y(`Signer pubkey address (${m}) doesn't match gaia address (${c})`);if(f)return a;throw new y(`Contents do not match ECDSA signature: path: ${t}, signature: ${t}${h}`)}catch(a){throw a instanceof x&&a.message.indexOf(i)>=0?new y(`Failed to obtain signature for file: ${t} -- looked in ${t}${h}`):a}}async function ie(t){let{path:e,storedContents:r,app:o,privateKey:n,username:s,zoneFileLookupURL:i,gaiaHubConfig:a}=t,u=n,c=u?crypto.getPublicKey(u,!0):null,g=null;if(s||a?g=await D({app:o,username:s,zoneFileLookupURL:i,gaiaHubConfig:a}):c&&(g=crypto.publicKeyToBase58Address(c)),!g)throw new y(`Failed to get gaia address for verification of: ${e}`);let l;try{l=JSON.parse(r);}catch(T){throw T instanceof SyntaxError?new Error("Failed to parse encrypted, signed content JSON. The content may not be encrypted. If using getFile, try passing { verify: false, decrypt: false }."):T}let m=l.signature,p=l.publicKey,f=l.cipherText,d=crypto.publicKeyToBase58Address(p);if(!p||!f||!m)throw new y(`Failed to get signature verification data from file: ${e}`);if(d!==g)throw new y(`Signer pubkey address (${d}) doesn't match gaia address (${g})`);if(!crypto.verifyECDSA({signature:m,contents:f,publicKey:p}))throw new y(`Contents do not match ECDSA signature in file: ${e}`);if(!n)throw Error("Private key needs to be passed in order to decrypt content");return crypto.decryptContent(f,{privateKey:n})}async function Bt(t,e){let r={decrypt:!0,verify:!1,app:common.getGlobalObject("location",{returnEmptyObject:!0}).origin,zoneFileLookupURL:U,...e};if(r.verify&&!r.decrypt)return ne(t,r);let o=await F({path:t,app:r.app,username:r.username,zoneFileLookupURL:r.zoneFileLookupURL,forceText:!!r.decrypt,gaiaHubConfig:r.gaiaHubConfig});if(o===null)return o;if(typeof o!="string")throw new Error("[micro-stacks/storage] Expected to get back a string for the cipherText");let n=!!r.verify,s=!!r.decrypt,i=typeof r.decrypt=="string"?r.decrypt:r.privateKey;if(o.includes("signature")&&o.includes("publicKey")&&(n=!0),o.includes("cipherText")&&o.includes("ephemeralPK")&&(s=!0),!n&&!s)return o;let a=!o.includes("cipherText");if(s&&a)throw new Error(`[micro-stacks/storage] Expected to get back a string that includes cipherText, is it encrypted? got back: ${JSON.stringify(o)}`);if(!i)throw new Error("[micro-stacks/storage] No private key was passed to getFile, a private key needs to be passed if decrypt is set to true");if(!n)return crypto.decryptContent(o,{privateKey:i});if(s&&n)return ie({path:t,storedContents:o,app:r.app,privateKey:i,username:r.username,zoneFileLookupURL:r.zoneFileLookupURL,gaiaHubConfig:r.gaiaHubConfig});throw new Error("[micro-stacks/storage] Should be unreachable.")}async function B(t,e){let r=await common.fetchPrivate(`${e.server}/delete/${e.address}/${t}`,{method:"DELETE",headers:{Authorization:`bearer ${e.token}`}});if(!r.ok)throw await _(r,"Error deleting file from Gaia hub.",e)}async function Zt(t,e){let{gaiaHubConfig:r,wasSigned:o}=e,n=[B(t,r)];o&&n.push(B(`${t}${h}`,r)),await Promise.all(n);}
exports.deleteFile = Zt;
exports.deleteFromGaiaHub = B;
exports.generateGaiaHubConfig = ze;
exports.generateGaiaHubConfigSync = je;
exports.getFile = Bt;
exports.getFullReadUrl = I;
exports.lookupProfile = re;
exports.makeScopedGaiaAuthToken = ge;
exports.makeScopedGaiaAuthTokenPayload = M;
exports.makeScopedGaiaAuthTokenSync = ye;
exports.putFile = nt;
exports.uploadToGaiaHub = A;