Skip to content

Commit 62b9ff1

Browse files
authored
GitHub jsonbin and CDN (#4)
* Added githubPush() in helper.js * Updated githubPush() to githubPushJSON() which also includes timestamp modification now * private POSTs now stored in GitHub * Serving public JSON@github over JSDelivr instead of securelay.github.io/jsonbin * POST at private path also responds with CDN download link
1 parent a282516 commit 62b9ff1

File tree

4 files changed

+78
-9
lines changed

4 files changed

+78
-9
lines changed

api/helper.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ https://upstash.com/docs/redis/sdks/ts/pipelining/auto-pipeline
66
import { hash as cryptoHash, createHmac, getRandomValues, randomUUID } from 'node:crypto';
77
import { Buffer } from "node:buffer";
88
import { Redis } from '@upstash/redis';
9+
import { Octokit } from '@octokit/core';
10+
import { createOrUpdateTextFile } from '@octokit/plugin-create-or-update-text-file';
911

1012
const secret = process.env.SECRET;
1113
const sigLen = parseInt(process.env.SIG_LEN);
@@ -31,6 +33,7 @@ const dbKeyPrefix = {
3133
}
3234
}
3335
}
36+
3437
// Redis client for user database
3538
const redisData = new Redis({
3639
url: process.env.UPSTASH_REDIS_REST_URL,
@@ -46,6 +49,10 @@ const redisRateLimit = new Redis({
4649
enableAutoPipelining: true
4750
})
4851

52+
// Setup Octokit for accessing the GitHub API
53+
const MyOctokit = Octokit.plugin(createOrUpdateTextFile);
54+
const octokit = new MyOctokit({ auth: process.env.GITHUB_PAT });
55+
4956
function hash(str){
5057
return cryptoHash('md5', str, 'base64url').substring(0,hashLen);
5158
// For small size str crypto.hash() is faster than crypto.createHash()
@@ -282,3 +289,43 @@ export async function OneSignalSendPush(app, externalID, data=null){
282289
})
283290
}).then((res) => res.json())
284291
}
292+
293+
// Push JSON (object) to be stored at https://securelay.github.io/jsonbin/{id}/{publicKey}.json
294+
// The function adds metadata using decoratePayload() above.
295+
// Do not pass JSON in order to touch existing data (i.e. update its timestamp).
296+
// Pass null as `json` and true as `remove` for removing the stored data.
297+
// Returns true if data is updated or deleted, false otherwise.
298+
// Ref: https://github.com/octokit/plugin-create-or-update-text-file.js/
299+
export async function githubPushJSON(privateKey, json=null, remove=false){
300+
const publicKey = genPublicKey(privateKey);
301+
const path = id() + '/' + publicKey + '.json';
302+
const touch = Boolean(!(json || remove));
303+
let content, mode;
304+
305+
if (touch) {
306+
// Just update timestamp in metadata when `touch` is true;
307+
mode = 'touched';
308+
content = ({exists, content}) => {
309+
if (!exists) return null;
310+
const json = JSON.parse(content);
311+
json.time = Date.now();
312+
return JSON.stringify(json);
313+
}
314+
} else if (json === null) {
315+
mode = 'deleted';
316+
content = null;
317+
} else {
318+
mode = 'updated';
319+
content = JSON.stringify(decoratePayload(json));
320+
}
321+
322+
const { updated, deleted } = await octokit.createOrUpdateTextFile({
323+
owner: "securelay",
324+
repo: "jsonbin",
325+
path: path,
326+
content: content,
327+
message: mode + ' ' + path,
328+
});
329+
330+
return updated || deleted;
331+
}

api/index.js

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as helper from './helper.js';
22
import Fastify from 'fastify';
33

4+
const endpointID = helper.id();
5+
const cdnUrlBase = `https://cdn.jsdelivr.net/gh/securelay/jsonbin@main/${endpointID}`;
46
const bodyLimit = parseInt(process.env.BODYLIMIT);
57
const fieldLimit = parseInt(process.env.FIELDLIMIT);
68
const webhookTimeout = parseInt(process.env.WEBHOOK_TIMEOUT);
@@ -42,7 +44,7 @@ fastify.get('/', (request, reply) => {
4244
fastify.get('/id', (request, reply) => {
4345
const app = request.query.app;
4446
if (app == null) {
45-
reply.send(helper.id());
47+
reply.send(endpointID);
4648
} else {
4749
const OneSignalID = helper.OneSignalID(app);
4850
if (OneSignalID) {
@@ -172,9 +174,15 @@ fastify.post('/private/:privateKey', async (request, reply) => {
172174
const redirectOnErr = request.query.err;
173175
try {
174176
if (helper.validate(privateKey) !== 'private') throw 401;
175-
await helper.privateProduce(privateKey, JSON.stringify(helper.decoratePayload(request.body)));
177+
//await helper.privateProduce(privateKey, JSON.stringify(helper.decoratePayload(request.body)));
178+
if (! await helper.githubPushJSON(privateKey, request.body)) throw 500;
176179
if (redirectOnOk == null) {
177-
reply.send({message: "Done", error: "Ok", statusCode: reply.statusCode});
180+
reply.send({
181+
message: "Done",
182+
error: "Ok",
183+
statusCode: reply.statusCode,
184+
cdn: `${cdnUrlBase}/${helper.genPublicKey(privateKey)}.json`
185+
});
178186
} else {
179187
reply.redirect(redirectOnOk, 303);
180188
}
@@ -195,7 +203,8 @@ fastify.delete('/private/:privateKey', async (request, reply) => {
195203
const { privateKey } = request.params;
196204
try {
197205
if (helper.validate(privateKey) !== 'private') throw 401;
198-
await helper.privateDelete(privateKey);
206+
//await helper.privateDelete(privateKey);
207+
if (! await helper.githubPushJSON(privateKey, null, true)) throw 500;
199208
reply.code(204);
200209
} catch (err) {
201210
if (err == 401) {
@@ -210,8 +219,14 @@ fastify.patch('/private/:privateKey', async (request, reply) => {
210219
const { privateKey } = request.params;
211220
try {
212221
if (helper.validate(privateKey) !== 'private') throw 401;
213-
await helper.privateRefresh(privateKey);
214-
reply.send({message: "Done", error: "Ok", statusCode: reply.statusCode});
222+
//await helper.privateRefresh(privateKey);
223+
if (! await helper.githubPushJSON(privateKey)) throw 500;
224+
reply.send({
225+
message: "Done",
226+
error: "Ok",
227+
statusCode: reply.statusCode,
228+
cdn: `${cdnUrlBase}/${helper.genPublicKey(privateKey)}.json`
229+
});
215230
} catch (err) {
216231
if (err == 401) {
217232
callUnauthorized(reply, 'Provided key is not Private');
@@ -225,9 +240,13 @@ fastify.get('/public/:publicKey', async (request, reply) => {
225240
const { publicKey } = request.params;
226241
try {
227242
if (helper.validate(publicKey) !== 'public') throw 401;
228-
const data = await helper.publicConsume(publicKey);
229-
if (!data) throw 404;
230-
reply.send(data);
243+
244+
//const data = await helper.publicConsume(publicKey);
245+
//if (!data) throw 404;
246+
//reply.send(data);
247+
248+
//reply.redirect(`https://securelay.github.io/jsonbin/${endpointID}/${publicKey}.json`, 301);
249+
reply.redirect(`${cdnUrlBase}/${publicKey}.json`, 301);
231250
} catch (err) {
232251
if (err == 401) {
233252
callUnauthorized(reply, 'Provided key is not Public');

api/test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Run: node --env-file=.env test.js
55

66
import * as helper from './helper.js';
77

8+
// process.exit(); // Use this to exit this script
89

910
console.log('Sending OneSignal Push for formonit app...OneSignal API returns:',
1011
await helper.OneSignalSendPush('formonit', '6kI2oBt2dN', {"hello":"there"}));

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
"dependencies": {
33
"@fastify/cors": "^10.0.1",
44
"@fastify/formbody": "^8.0.1",
5+
"@octokit/core": "^6.1.3",
6+
"@octokit/plugin-create-or-update-text-file": "^5.1.0",
57
"@upstash/ratelimit": "^2.0.3",
68
"@upstash/redis": "^1.34.3",
79
"@vercel/edge": "^1.1.2",

0 commit comments

Comments
 (0)