import { AxiosPromise , AxiosRequestConfig , AxiosResponse } from "axios" ; import axios from "axios" ; import Cookies from "universal-cookie" ; import { sessions , users , User , setClerkApiKey } from "@clerk/clerk-sdk-node" ; import { APIGatewayProxyEvent , APIGatewayProxyHandler , APIGatewayProxyResult , } from "aws-lambda" ; import OAuth from "oauth-1.0a" ; import crypto from "crypto" ; import AWS from "aws-sdk" ; import Mixpanel from "mixpanel" ; import randomstring from "randomstring" ; import Stripe from "stripe" ; import AES from "crypto-js/aes" ; import encutf8 from "crypto-js/enc-utf8" ; export const stripe = new Stripe ( process . env . STRIPE_SECRET_KEY || "" , { apiVersion : "2020-08-27" , maxNetworkRetries : 3 , } ) ; export const lambda = new AWS . Lambda ( { apiVersion : "2015-03-31" } ) ; export const dynamo = new AWS . DynamoDB ( { apiVersion : "2012-08-10" } ) ; export const s3 = new AWS . S3 ( { apiVersion : "2006-03-01" } ) ; export const ses = new AWS . SES ( { apiVersion : "2010-12-01" } ) ; export const mixpanel = process . env . MIXPANEL_TOKEN ? Mixpanel . init ( process . env . MIXPANEL_TOKEN ) : { track : ( ) => console . log ( "track" ) } ; const ALLOWED_ORIGINS = [ "https://roamjs.com" , "https://roamresearch.com" ] ; type Headers = { [ header ] : boolean | number | string ; } ; export const headers = ( event ) : Headers => { const origin = event . headers . origin || event . headers . Origin ; return { "Access-Control-Allow-Origin" : ALLOWED_ORIGINS . includes ( origin ) ? origin : "*" , "Access-Control-Allow-Methods" : "GET, POST, PUT, DELETE" , "Access-Control-Allow-Credentials" : true , } ; } ; export const wrapAxios = ( req , event ) : Promise < APIGatewayProxyResult > => req . then ( ( r ) => ( { statusCode : 200 , body : JSON . stringify ( r . data ) , headers : headers ( event ) , } ) ) . catch ( ( e ) => ( { statusCode : e . response ?. status || 500 , body : e . response ?. data ? JSON . stringify ( e . response . data ) : e . message , headers : headers ( event ) , } ) ) ; export const userError = ( body , event ) : APIGatewayProxyResult => ( { statusCode : 400 , body , headers : headers ( event ) , } ) ; export const serverError = ( body , event ) : APIGatewayProxyResult => ( { statusCode : 500 , body , headers : headers ( event ) , } ) ; export const emptyResponse = ( event ) : APIGatewayProxyResult => ( { statusCode : 204 , body : JSON . stringify ( { } ) , headers : headers ( event ) , } ) ; export const bareSuccessResponse = ( event ) : APIGatewayProxyResult => ( { statusCode : 200 , body : JSON . stringify ( { success : true } ) , headers : headers ( event ) , } ) ; const personalAccessToken = process . env . GITHUB_PERSONAL_ACCESS_TOKEN || "" ; export const getGithubOpts = ( ) : AxiosRequestConfig => ( { headers : { Accept : "application/vnd.github.inertia-preview+json" , Authorization : `Basic ${ Buffer . from ( `dvargas92495:${ personalAccessToken }` ) . toString ( "base64" ) }` , } , } ) ; const twitterConsumerKey = process . env . TWITTER_CONSUMER_KEY || "" ; const twitterConsumerSecret = process . env . TWITTER_CONSUMER_SECRET || "" ; export const getTwitterOpts = async ( event ) : Promise < AxiosRequestConfig > => { const twitterBearerTokenResponse = await wrapAxios ( axios . post ( `https://api.twitter.com/oauth2/token` , { } , { params : { grant_type : "client_credentials" , } , auth : { username : twitterConsumerKey , password : twitterConsumerSecret , } , headers : { "Content-Type" : "application/x-www-form-urlencoded;charset=UTF-8" , } , } ) , event ) ; const body = JSON . parse ( twitterBearerTokenResponse . body ) ; const twitterBearerToken = body . access_token ; return { headers : { Authorization : `Bearer ${ twitterBearerToken }` , } , } ; } ; export type Contracts = { link : string ; reward : number ; } [ ] ; export const getFlossActiveContracts = ( ) : Promise < { projects : Contracts ; issues : Contracts ; } > => axios . get ( `${ process . env . FLOSS_API_URL }/contracts` ) . then ( ( r ) => r . data ) ; export const twitterOAuth = new OAuth ( { consumer : { key : process . env . TWITTER_CONSUMER_KEY || "" , secret : process . env . TWITTER_CONSUMER_SECRET || "" , } , signature_method : "HMAC-SHA1" , hash_function ( base_string , key ) { return crypto . createHmac ( "sha1" , key ) . update ( base_string ) . digest ( "base64" ) ; } , } ) ; export const getClerkUser = async ( event ) : Promise < User > => { const cookies = new Cookies ( event . headers . Cookie ) ; const sessionToken = cookies . get ( "__session" ) ; if ( ! sessionToken ) { console . warn ( "No cookie found" , JSON . stringify ( event . headers , null , 4 ) ) ; return undefined ; } const sessionId = event . queryStringParameters . _clerk_session_id ; const session = await sessions . verifySession ( sessionId , sessionToken ) ; return await users . getUser ( session . userId ) ; } ; export const getClerkEmail = async ( event ) : Promise < string > => { const user = await getClerkUser ( event ) ; return user ? user . emailAddresses . find ( ( e ) => e . id === user . primaryEmailAddressId ) ?. emailAddress : "" ; } ; export const getClerkOpts = ( email , headers ? ) : AxiosRequestConfig => ( { headers : { Authorization : `Basic ${ Buffer . from ( email ) . toString ( "base64" ) }` , ... headers , } , } ) ; export const generateToken = ( userId ) : string => Buffer . from ( `${ userId . replace ( /^user_/ , "" ) }:${ randomstring . generate ( { length : 16 } ) }` ) . toString ( "base64" ) ; const findUser = async ( predicate ) : Promise < User > => { let offset = 0 ; while ( offset < 10000 ) { const us = await users . getUserList ( { limit : 100 , offset } ) ; const user = us . find ( predicate ) ; if ( user ) { return user ; } if ( us . length < 100 ) { return ; } offset += us . length ; } } ; export const getUserFromEvent = ( Authorization , service , dev ? ) : Promise < User > => { if ( dev ) { setClerkApiKey ( process . env . CLERK_DEV_API_KEY ) ; } else { setClerkApiKey ( process . env . CLERK_API_KEY ) ; } const [ userId , token ] = Authorization . length === 32 || Authorization . includes ( ":" ) ? [ null , Authorization . split ( ":" ) . slice ( - 1 ) [ 0 ] , Authorization . split ( ":" ) . slice ( - 1 ) [ 0 ] , ] : [ Buffer . from ( Authorization , "base64" ) . toString ( ) . split ( ":" ) [ 0 ] , Authorization , Buffer . from ( Authorization , "base64" ) . toString ( ) . split ( ":" ) [ 1 ] , ] ; return userId ? users . getUser ( `user_${ userId }` ) . then ( ( user ) => ( user . publicMetadata as { [ s ] : { token : string ; } ; } ) ?. [ service ] ?. token === token ? user : undefined ) . catch ( ( ) => { return undefined ; } ) : findUser ( ( user ) => ( user . publicMetadata as { [ s ] : { token : string ; } ; } ) ?. [ service ] ?. token === token ) ; } ; export const authenticateUser = ( Authorization , dev ? ) : Promise < User > => { if ( dev ) { setClerkApiKey ( process . env . CLERK_DEV_API_KEY ) ; } else { setClerkApiKey ( process . env . CLERK_API_KEY ) ; } const encryptionSecret = process . env . ENCRYPTION_SECRET ; const [ email , token ] = Buffer . from ( Authorization . replace ( /^Bearer / , "" ) , "base64" ) . toString ( ) . split ( ":" ) ; return users . getUserList ( { emailAddress : [ email ] } ) . then ( ( us ) => us . find ( ( u ) => { const stored = AES . decrypt ( u . privateMetadata . token as string , encryptionSecret ) . toString ( encutf8 ) ; return stored && stored === token ; } ) ) . catch ( ( ) => undefined ) ; } ; export const authenticate = ( handler , inputService ? ) : APIGatewayProxyHandler => ( event , ctx , callback ) => { const service = inputService || event . queryStringParameters . service ; const Authorization = event . headers . Authorization || event . headers . authorization || "" ; const dev = ! ! event . headers [ "x-roamjs-dev" ] ; return Promise . all ( [ authenticateUser ( Authorization , dev ) , getUserFromEvent ( Authorization , service , dev ) , ] ) . then ( ( [ userv2 , legacyUser ] ) => { const user = userv2 || legacyUser ; if ( ! user ) { console . log ( "Failed to authenticate" , Authorization . slice ( - 5 ) , service , dev ) ; return { statusCode : 401 , body : "Invalid token" , headers : headers ( event ) , } ; } const publicMetadata = user . publicMetadata ; const serviceData = ( publicMetadata as { [ s ] : { authenticated : boolean ; } ; } ) [ service ] ; if ( ! serviceData . authenticated ) { users . updateUser ( user . id , { publicMetadata : { ... publicMetadata , [ service ] : { ... serviceData , authenticated : true , } , } , } ) ; } event . headers . Authorization = user . id ; const result = handler ( event , ctx , callback ) ; if ( ! result ) { return emptyResponse ( event ) ; } return result ; } ) ; } ; export const emailError = ( subject , e ) : Promise < string > => ses . sendEmail ( { Destination : { ToAddresses : [ "dvargas92495@gmail.com" ] , } , Message : { Body : { Text : { Charset : "UTF-8" , Data : `An error was thrown in a RoamJS lambda:

${ e . name }: ${ e . message }
${ e . stack }` , } , } , Subject : { Charset : "UTF-8" , Data : `RoamJS Error: ${ subject }` , } , } , Source : "support@roamjs.com" , } ) . promise ( ) . then ( ( r ) => r . MessageId ) ; export const emailCatch = ( subject , event ) => ( e ) : Promise < APIGatewayProxyResult > => emailError ( subject , e ) . then ( ( id ) => ( { statusCode : 500 , body : `Unknown error - Message Id ${ id }` , headers : headers ( event ) , } ) ) ; export const listAll = async ( Prefix ) : Promise < { objects : AWS . S3 . ObjectList ; prefixes : AWS . S3 . CommonPrefixList ; } > => { const objects = [ ] ; const prefixes = [ ] ; let ContinuationToken = undefined ; let isTruncated = true ; while ( isTruncated ) { const res = await s3 . listObjectsV2 ( { Bucket : "roamjs.com" , Prefix , ContinuationToken , Delimiter : "/" , } ) . promise ( ) ; objects . push ( ... res . Contents ) ; prefixes . push ( ... res . CommonPrefixes ) ; ContinuationToken = res . ContinuationToken ; isTruncated = res . IsTruncated ; } return { objects , prefixes } ; } ; export const TableName = process . env . NODE_ENV === "development" ? "RoamJSExtensionsDev" : "RoamJSExtensions" ; export const getStripePriceId = ( service ) : Promise < string > => dynamo . getItem ( { TableName , Key : { id : { S : service } } , } ) . promise ( ) . then ( ( r ) => r . Item . premium ?. S ) ;