├── .gitignore ├── README.md ├── envelope └── handler.js └── terraform ├── apigw.tf ├── iam.tf ├── lambda.tf ├── policies └── lambda_role.json ├── provider.tf ├── templates ├── list_letters.json └── read_letter.json └── variables.tf /.gitignore: -------------------------------------------------------------------------------- 1 | *.tfstate 2 | *.tfstate.backup 3 | *.zip 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Signed Blogs 2 | ============ 3 | 4 | This is a basic proof of concept cobbled together after reading about [the awesome new Keybase filesystem](https://keybase.io/introducing-the-keybase-filesystem). 5 | 6 | It's a simple user search, and view onto plaintext and markdown formatted files stored publicly on Keybase. 7 | 8 | ## What/Why/How 9 | There's very little to this - all the heavy-lifting is done by genii at [Keybase.io](http://keybase.io); to whom I'm not affiliated. 10 | 11 | I just thought this was maybe an interesting use for their new filesystem, and threw this together stylesheet-less to toy with it. 12 | 13 | Feel free to fork/PR/host your own, whatever. 14 | 15 | Files are pulled in from Keybase every time - no storage here (would sort of defeat the point anyway). 16 | 17 | ## Try it out: 18 | The Terraform's all ready to go, just supply AWS keys and it'll provision a Lambda, and API Gateway routing. 19 | 20 | Then you can test it out with `${STAGE_URL}/${KEYBASE_USER}[/${POST_TITLE}`. 21 | -------------------------------------------------------------------------------- /envelope/handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const async = require('async'); 3 | const cheerio = require('cheerio'); 4 | const request = require('request'); 5 | 6 | const list_response = (user) => ` 7 | 8 | 9 | 10 | ${user}'s Signed Blog 11 | 12 | 13 | 14 | 15 |

${user}\'s Signed Blog

16 |
17 | `; 18 | 19 | const list_item = (title) => ` 20 |
21 | 22 |

${title}

23 |
24 |
25 | ` 26 | 27 | const end_list = (response) => ` 28 | ${response} 29 |
30 | 31 | 32 | `; 33 | 34 | const single_response = (user, title, file) => ` 35 | 36 | 37 | 38 | ${user} | ${title} 39 | 40 | 41 | 42 | 43 | 51 | 52 | 53 |
54 | 55 | 56 | ` 57 | 58 | function list_posts(user, callback) { 59 | request(`https://keybase.pub/${user}/envelope`, function(err, _, html){ 60 | if(!err){ 61 | const $ = cheerio.load(html); 62 | let files = $('.directory table').find('td.name-col a').slice(1); 63 | 64 | async.reduce(files, list_response(user), function(memo, el, done){ 65 | let fname = $(el).text(); 66 | 67 | if(fname.startsWith('.')){ 68 | done(null, memo); 69 | } else { 70 | done(null, memo + list_item(fname)); 71 | } 72 | }, (err, response) => callback(err, end_list(response))); 73 | } 74 | else{ 75 | callback(err); 76 | } 77 | }); 78 | } 79 | 80 | function single_post(user, fname, callback) { 81 | let url = `https://${user}.keybase.pub/envelope/${fname}`; 82 | request(url, (err, _, data) => callback(err, single_response(user, fname, data))); 83 | } 84 | 85 | function lookup_user(id, callback) { 86 | if(id.includes('@')){ 87 | let username = id.split('@')[0]; 88 | let service = id.split('@')[1]; 89 | let url = `https://keybase.io/_/api/1.0/user/lookup.json?${service}=${username}`; 90 | 91 | request(url, function(err, _, body){ 92 | if(err){ 93 | callback(err); 94 | } else { 95 | callback(null, JSON.parse(body).them[0].basics.username); 96 | } 97 | }); 98 | } else { 99 | callback(null, id); 100 | } 101 | } 102 | 103 | exports.view = function(event, context, callback) { 104 | lookup_user(event.user, function(err, user){ 105 | if(event.title !== undefined){ 106 | single_post(user, title, callback); 107 | } else { 108 | list_posts(user, callback); 109 | } 110 | }); 111 | } 112 | -------------------------------------------------------------------------------- /terraform/apigw.tf: -------------------------------------------------------------------------------- 1 | resource "aws_api_gateway_rest_api" "envelope" { 2 | name = "Envelope" 3 | } 4 | 5 | resource "aws_api_gateway_resource" "Letters" { 6 | rest_api_id = "${aws_api_gateway_rest_api.envelope.id}" 7 | parent_id = "${aws_api_gateway_rest_api.envelope.root_resource_id}" 8 | path_part = "{user}" 9 | } 10 | 11 | resource "aws_api_gateway_method" "list_letters" { 12 | rest_api_id = "${aws_api_gateway_rest_api.envelope.id}" 13 | resource_id = "${aws_api_gateway_resource.Letters.id}" 14 | http_method = "GET" 15 | authorization = "NONE" 16 | } 17 | 18 | resource "aws_api_gateway_integration" "list_letters" { 19 | rest_api_id = "${aws_api_gateway_rest_api.envelope.id}" 20 | resource_id = "${aws_api_gateway_resource.Letters.id}" 21 | http_method = "${aws_api_gateway_method.list_letters.http_method}" 22 | uri = "arn:aws:apigateway:${var.region}:lambda:path/2015-03-31/functions/${aws_lambda_function.envelope.arn}/invocations" 23 | type = "AWS" 24 | integration_http_method = "POST" 25 | passthrough_behavior = "NEVER" 26 | request_templates = { 27 | "application/json"="${file("${path.module}/templates/list_letters.json")}" 28 | } 29 | } 30 | 31 | resource "aws_api_gateway_method_response" "list_letters" { 32 | rest_api_id = "${aws_api_gateway_rest_api.envelope.id}" 33 | resource_id = "${aws_api_gateway_resource.Letters.id}" 34 | http_method = "${aws_api_gateway_method.list_letters.http_method}" 35 | status_code = "200" 36 | } 37 | 38 | resource "aws_api_gateway_integration_response" "list_letters" { 39 | depends_on = [ 40 | "aws_api_gateway_integration.list_letters", 41 | ] 42 | rest_api_id = "${aws_api_gateway_rest_api.envelope.id}" 43 | resource_id = "${aws_api_gateway_resource.Letters.id}" 44 | http_method = "${aws_api_gateway_method.list_letters.http_method}" 45 | status_code = "${aws_api_gateway_method_response.list_letters.status_code}" 46 | response_templates = { 47 | "text/html"="$input.path('$')" 48 | } 49 | response_parameters = { 50 | "method.response.header.Content-Type"="'text/html'" 51 | } 52 | } 53 | 54 | resource "aws_api_gateway_resource" "Letter" { 55 | rest_api_id = "${aws_api_gateway_rest_api.envelope.id}" 56 | parent_id = "${aws_api_gateway_resource.Letters.id}" 57 | path_part = "{title}" 58 | } 59 | 60 | resource "aws_api_gateway_method" "read_letter" { 61 | rest_api_id = "${aws_api_gateway_rest_api.envelope.id}" 62 | resource_id = "${aws_api_gateway_resource.Letter.id}" 63 | http_method = "GET" 64 | authorization = "NONE" 65 | } 66 | 67 | resource "aws_api_gateway_integration" "read_letter" { 68 | rest_api_id = "${aws_api_gateway_rest_api.envelope.id}" 69 | resource_id = "${aws_api_gateway_resource.Letter.id}" 70 | http_method = "${aws_api_gateway_method.read_letter.http_method}" 71 | uri = "arn:aws:apigateway:${var.region}:lambda:path/2015-03-31/functions/${aws_lambda_function.envelope.arn}/invocations" 72 | type = "AWS" 73 | integration_http_method = "POST" 74 | passthrough_behavior = "NEVER" 75 | request_templates = { 76 | "application/json"="${file("${path.module}/templates/read_letter.json")}" 77 | } 78 | } 79 | 80 | resource "aws_api_gateway_method_response" "read_letter" { 81 | rest_api_id = "${aws_api_gateway_rest_api.envelope.id}" 82 | resource_id = "${aws_api_gateway_resource.Letter.id}" 83 | http_method = "${aws_api_gateway_method.read_letter.http_method}" 84 | status_code = "200" 85 | } 86 | 87 | resource "aws_api_gateway_integration_response" "read_letter" { 88 | depends_on = [ 89 | "aws_api_gateway_integration.read_letter" 90 | ] 91 | rest_api_id = "${aws_api_gateway_rest_api.envelope.id}" 92 | resource_id = "${aws_api_gateway_resource.Letter.id}" 93 | http_method = "${aws_api_gateway_method.read_letter.http_method}" 94 | status_code = "${aws_api_gateway_method_response.read_letter.status_code}" 95 | response_templates = { 96 | "text/html"="$input.path('$')" 97 | } 98 | response_parameters = { 99 | "method.response.header.Content-Type"="'text/html'" 100 | } 101 | } 102 | 103 | resource "aws_api_gateway_deployment" "envelope" { 104 | depends_on = [ 105 | "aws_api_gateway_integration.list_letters", 106 | "aws_api_gateway_integration.read_letter" 107 | ] 108 | rest_api_id = "${aws_api_gateway_rest_api.envelope.id}" 109 | stage_name = "${var.deploy_stage}" 110 | } 111 | -------------------------------------------------------------------------------- /terraform/iam.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_role" "lambda" { 2 | name = "Lambda-IAM-role" 3 | assume_role_policy = "${file("${path.module}/policies/lambda_role.json")}" 4 | } 5 | -------------------------------------------------------------------------------- /terraform/lambda.tf: -------------------------------------------------------------------------------- 1 | resource "aws_lambda_function" "envelope" { 2 | filename = "${var.envelope_pkg}" 3 | function_name = "envelope" 4 | role = "${aws_iam_role.lambda.arn}" 5 | handler = "handler.view" 6 | source_code_hash = "${base64sha256(file("${var.envelope_pkg}"))}" 7 | runtime = "nodejs4.3" 8 | } 9 | 10 | resource "aws_lambda_permission" "allow_api_gateway" { 11 | function_name = "${aws_lambda_function.envelope.function_name}" 12 | statement_id = "AllowExecutionFromApiGateway" 13 | action = "lambda:InvokeFunction" 14 | principal = "apigateway.amazonaws.com" 15 | } 16 | -------------------------------------------------------------------------------- /terraform/policies/lambda_role.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Action": "sts:AssumeRole", 6 | "Principal": { 7 | "Service": "lambda.amazonaws.com" 8 | }, 9 | "Effect": "Allow", 10 | "Sid": "" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /terraform/provider.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = "${var.region}" 3 | profile = "${var.aws_profile}" 4 | } 5 | -------------------------------------------------------------------------------- /terraform/templates/list_letters.json: -------------------------------------------------------------------------------- 1 | { 2 | "user": "$input.params('user')" 3 | } 4 | -------------------------------------------------------------------------------- /terraform/templates/read_letter.json: -------------------------------------------------------------------------------- 1 | { 2 | "user": "$input.params('user')", 3 | "title": "$input.params('title')" 4 | } 5 | -------------------------------------------------------------------------------- /terraform/variables.tf: -------------------------------------------------------------------------------- 1 | variable "region" { 2 | description = "AWS region to use" 3 | default = "eu-west-1" 4 | } 5 | 6 | variable "aws_profile" { 7 | description = "Credential profile to use" 8 | default = "default" 9 | } 10 | 11 | variable "envelope_pkg" { 12 | description = "Zip package for deployment to AWS Lambda" 13 | default = "lambda.zip" 14 | } 15 | 16 | variable "deploy_stage" { 17 | description = "Deployment stage - e.g. dev, prod" 18 | default = "dev" 19 | } 20 | --------------------------------------------------------------------------------