Skip to content

Commit f895a68

Browse files
authored
Optimize (#6)
* Replaced validate(key) === type with assert(key, type). Disabled upstash SDK telemetry * Comments/docs in api/helper.js * Memory optimization: store only hash part of key as redis-key
1 parent 79968b2 commit f895a68

File tree

4 files changed

+52
-36
lines changed

4 files changed

+52
-36
lines changed

api/helper.js

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export function id(){
7676
return sign('id');
7777
}
7878

79+
// Check if given key is valid, and if it is, return its type
7980
export function validate(key, silent=true){
8081
const sig = key.substring(0, sigLen);
8182
const hash = key.substring(sigLen);
@@ -89,18 +90,33 @@ export function validate(key, silent=true){
8990
}
9091
}
9192

93+
// Assert if given key is of the given type (private | public)
94+
// This is computationally favorable to the Boolean: validate(key) === type
95+
export function assert(key, type){
96+
const sig = key.substring(0, sigLen);
97+
const hash = key.substring(sigLen);
98+
return sig === sign(hash + type);
99+
}
100+
92101
export function genPublicKey(privateOrPublicKey){
93-
if (validate(privateOrPublicKey) === 'public') return privateOrPublicKey;
102+
if (assert(privateOrPublicKey, 'public')) return privateOrPublicKey;
94103
const privateKey = privateOrPublicKey;
95104
const privateHash = privateKey.substring(sigLen);
96105
const publicHash = hash(privateHash);
97106
const publicKey = sign(publicHash + 'public') + publicHash;
98107
return publicKey;
99108
}
100109

110+
export function genKeyPair(seed = randomUUID()){
111+
const privateHash = hash(seed);
112+
const privateKey = sign(privateHash + 'private') + privateHash;
113+
const publicKey = genPublicKey(privateKey);
114+
return {private: privateKey, public: publicKey};
115+
}
116+
101117
export function cacheSet(privateKey, obj){
102118
const publicKey = genPublicKey(privateKey);
103-
const dbKey = dbKeyPrefix.cache + publicKey;
119+
const dbKey = dbKeyPrefix.cache + publicKey.substring(sigLen);
104120
// Promise.all below enables both commands to be executed in a single http request (using same pipeline)
105121
// As Redis is single-threaded, the commands are executed in order
106122
// See https://upstash.com/docs/redis/sdks/ts/pipelining/auto-pipeline
@@ -111,23 +127,16 @@ export function cacheSet(privateKey, obj){
111127
}
112128

113129
export function cacheGet(publicKey, key){
114-
const dbKey = dbKeyPrefix.cache + publicKey;
130+
const dbKey = dbKeyPrefix.cache + publicKey.substring(sigLen);
115131
return redisRateLimit.hget(dbKey, key);
116132
}
117133

118134
export function cacheDel(privateOrPublicKey, key){
119135
const publicKey = genPublicKey(privateOrPublicKey);
120-
const dbKey = dbKeyPrefix.cache + publicKey;
136+
const dbKey = dbKeyPrefix.cache + publicKey.substring(sigLen);
121137
return redisRateLimit.hdel(dbKey, key);
122138
}
123139

124-
export function genKeyPair(seed = randomUUID()){
125-
const privateHash = hash(seed);
126-
const privateKey = sign(privateHash + 'private') + privateHash;
127-
const publicKey = genPublicKey(privateKey);
128-
return {private: privateKey, public: publicKey};
129-
}
130-
131140
// Add metadata to payload (which must be a JSON object)
132141
// Some metadata, such as `id` to uniquely identify a payload and timestamp, are generated
133142
// Other metadata may be provided as the `fields` JSON object.
@@ -151,7 +160,7 @@ export function unlockJSON(json, passwd){
151160
}
152161

153162
export async function publicProduce(publicKey, data){
154-
const dbKey = dbKeyPrefix.manyToOne + publicKey;
163+
const dbKey = dbKeyPrefix.manyToOne + publicKey.substring(sigLen);
155164
return Promise.all([
156165
redisData.rpush(dbKey, data),
157166
redisData.expire(dbKey, ttl)
@@ -160,7 +169,7 @@ export async function publicProduce(publicKey, data){
160169

161170
export async function privateConsume(privateKey){
162171
const publicKey = genPublicKey(privateKey);
163-
const dbKey = dbKeyPrefix.manyToOne + publicKey;
172+
const dbKey = dbKeyPrefix.manyToOne + publicKey.substring(sigLen);
164173
const atomicTransaction = redisData.multi();
165174
atomicTransaction.lrange(dbKey, 0, -1);
166175
atomicTransaction.del(dbKey);
@@ -170,26 +179,26 @@ export async function privateConsume(privateKey){
170179

171180
export async function privateProduce(privateKey, data){
172181
const publicKey = genPublicKey(privateKey);
173-
const dbKey = dbKeyPrefix.oneToMany + publicKey;
182+
const dbKey = dbKeyPrefix.oneToMany + publicKey.substring(sigLen);
174183
return redisData.set(dbKey, data, { ex: ttl });
175184
}
176185

177186
export async function privateDelete(privateKey){
178187
const publicKey = genPublicKey(privateKey);
179-
const dbKey = dbKeyPrefix.oneToMany + publicKey;
188+
const dbKey = dbKeyPrefix.oneToMany + publicKey.substring(sigLen);
180189
return redisData.del(dbKey);
181190
}
182191

183192
export async function privateRefresh(privateKey){
184193
const publicKey = genPublicKey(privateKey);
185-
const dbKey = dbKeyPrefix.oneToMany + publicKey;
194+
const dbKey = dbKeyPrefix.oneToMany + publicKey.substring(sigLen);
186195
return redisData.expire(dbKey, ttl);
187196
}
188197

189198
export async function privateStats(privateKey){
190199
const publicKey = genPublicKey(privateKey);
191-
const dbKeyConsume = dbKeyPrefix.manyToOne + publicKey;
192-
const dbKeyPublish = dbKeyPrefix.oneToMany + publicKey;
200+
const dbKeyConsume = dbKeyPrefix.manyToOne + publicKey.substring(sigLen);
201+
const dbKeyPublish = dbKeyPrefix.oneToMany + publicKey.substring(sigLen);
193202
const [ countConsume, ttlConsume ] = await Promise.all([
194203
redisData.llen(dbKeyConsume),
195204
redisData.ttl(dbKeyConsume)
@@ -202,7 +211,7 @@ export async function privateStats(privateKey){
202211

203212
// Demand for data also refreshes its expiry
204213
export async function publicConsume(publicKey){
205-
const dbKey = dbKeyPrefix.oneToMany + publicKey;
214+
const dbKey = dbKeyPrefix.oneToMany + publicKey.substring(sigLen);
206215
// Ideally there should be getex() in Upstash's Redis SDK.
207216
// Until it's available, we make do with pipelining as follows.
208217
const [ data, _ ] = await Promise.all([
@@ -214,16 +223,18 @@ export async function publicConsume(publicKey){
214223

215224
export async function oneToOneProduce(privateKey, key, data){
216225
const publicKey = genPublicKey(privateKey);
217-
const dbKey = dbKeyPrefix.oneToOne + publicKey;
226+
const dbKey = dbKeyPrefix.oneToOne + publicKey.substring(sigLen);
218227
const field = {[hash(key)]: data};
228+
// Ideally there should be hexpire() in Upstash's Redis SDK.
229+
// Until it's available, we expire the containing key as follows.
219230
return Promise.all([
220231
redisData.hset(dbKey, field),
221232
redisData.expire(dbKey, ttl)
222233
])
223234
}
224235

225236
export async function oneToOneConsume(publicKey, key){
226-
const dbKey = dbKeyPrefix.oneToOne + publicKey;
237+
const dbKey = dbKeyPrefix.oneToOne + publicKey.substring(sigLen);
227238
const field = hash(key);
228239
const atomicTransaction = redisData.multi();
229240
atomicTransaction.hget(dbKey, field);
@@ -234,8 +245,10 @@ export async function oneToOneConsume(publicKey, key){
234245

235246
export async function oneToOneTTL(privateKey, key){
236247
const publicKey = genPublicKey(privateKey);
237-
const dbKey = dbKeyPrefix.oneToOne + publicKey;
248+
const dbKey = dbKeyPrefix.oneToOne + publicKey.substring(sigLen);
238249
const field = hash(key);
250+
// Ideally there should be httl() in Upstash's Redis SDK.
251+
// Until it's available, we use ttl of the containing key as follows.
239252
const [ bool, ttl ] = await Promise.all([
240253
redisData.hexists(dbKey, field),
241254
redisData.ttl(dbKey)
@@ -244,13 +257,14 @@ export async function oneToOneTTL(privateKey, key){
244257
}
245258

246259
// Tokens are stored in LIFO stacks. Old and unused tokens are trimmed.
260+
// Timestamps are stored with the tokens using string concatenation
247261
export async function streamToken(privateOrPublicKey, receive=true){
248262
const type = validate(privateOrPublicKey, false);
249263
const typeComplement = (type == 'private') ? 'public' : 'private';
250264
const publicKey = genPublicKey(privateOrPublicKey);
251265
const mode = receive ? "receive" : "send";
252266
const modeComplement = receive ? "send" : "receive";
253-
const existing = await redisData.lpop(dbKeyPrefix.stream[typeComplement][modeComplement] + publicKey);
267+
const existing = await redisData.lpop(dbKeyPrefix.stream[typeComplement][modeComplement] + publicKey.substring(sigLen));
254268
const timeNow = Math.round(Date.now()/1000);
255269
if (existing) {
256270
const [token, timestamp] = existing.split('@');
@@ -259,7 +273,7 @@ export async function streamToken(privateOrPublicKey, receive=true){
259273
if ((timeNow - timestamp) < streamTimeout) return token;
260274
}
261275
const token = randStr();
262-
const dbKey = dbKeyPrefix.stream[type][mode] + publicKey;
276+
const dbKey = dbKeyPrefix.stream[type][mode] + publicKey.substring(sigLen);
263277
await Promise.all([
264278
redisData.lpush(dbKey, token + '@' + timeNow),
265279
redisData.expire(dbKey, streamTimeout),

api/index.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ fastify.post('/public/:publicKey', async (request, reply) => {
8383
const redirectOnErr = request.query.err;
8484

8585
try {
86-
if (helper.validate(publicKey) !== 'public') throw 401;
86+
if (!helper.assert(publicKey, 'public')) throw 401;
8787

8888
const app = request.query.app;
8989
const data = helper.decoratePayload(request.body);
@@ -142,7 +142,7 @@ fastify.get('/private/:privateKey', async (request, reply) => {
142142
webhookHandler = statsHandler = dataHandler = () => null; // default
143143

144144
try {
145-
if (helper.validate(privateKey) !== 'private') throw 401;
145+
if (!helper.assert(privateKey, 'private')) throw 401;
146146
if (webhook == null) {
147147
webhookHandler = () => helper.cacheDel(privateKey, 'hook');
148148
} else {
@@ -180,7 +180,7 @@ fastify.post('/private/:privateKey', async (request, reply) => {
180180
const redirectOnErr = request.query.err;
181181
const passwd = request.query.password;
182182
try {
183-
if (helper.validate(privateKey) !== 'private') throw 401;
183+
if (!helper.assert(privateKey, 'private')) throw 401;
184184

185185
const cdn = {};
186186
if (passwd == null) {
@@ -218,7 +218,7 @@ fastify.delete('/private/:privateKey', async (request, reply) => {
218218
const { privateKey } = request.params;
219219
const passwdPresent = 'password' in request.query;
220220
try {
221-
if (helper.validate(privateKey) !== 'private') throw 401;
221+
if (!helper.assert(privateKey, 'private')) throw 401;
222222
if (passwdPresent) {
223223
await helper.privateDelete(privateKey);
224224
} else {
@@ -238,7 +238,7 @@ fastify.patch('/private/:privateKey', async (request, reply) => {
238238
const { privateKey } = request.params;
239239
const passwdPresent = 'password' in request.query;
240240
try {
241-
if (helper.validate(privateKey) !== 'private') throw 401;
241+
if (!helper.assert(privateKey, 'private')) throw 401;
242242

243243
const cdn = {};
244244
if (passwdPresent) {
@@ -267,7 +267,7 @@ fastify.get('/public/:publicKey', async (request, reply) => {
267267
const { publicKey } = request.params;
268268
const passwd = request.query.password;
269269
try {
270-
if (helper.validate(publicKey) !== 'public') throw 401;
270+
if (!helper.assert(publicKey, 'public')) throw 401;
271271

272272
if (passwd == null) {
273273
reply.redirect(`${cdnUrlBase}/${publicKey}.json`, 301);
@@ -297,7 +297,7 @@ fastify.post('/private/:privateKey/:key', async (request, reply) => {
297297
const redirectOnOk = request.query.ok;
298298
const redirectOnErr = request.query.err;
299299
try {
300-
if (helper.validate(privateKey) !== 'private') throw 401;
300+
if (!helper.assert(privateKey, 'private')) throw 401;
301301
await helper.oneToOneProduce(privateKey, key, JSON.stringify(helper.decoratePayload(request.body)));
302302
if (redirectOnOk == null) {
303303
reply.send({message: "Done", error: "Ok", statusCode: reply.statusCode});
@@ -322,7 +322,7 @@ fastify.post('/private/:privateKey/:key', async (request, reply) => {
322322
fastify.get('/public/:publicKey/:key', async (request, reply) => {
323323
const { publicKey, key } = request.params;
324324
try {
325-
if (helper.validate(publicKey) !== 'public') throw 401;
325+
if (!helper.assert(publicKey, 'public')) throw 401;
326326
const data = await helper.oneToOneConsume(publicKey, key);
327327
if (!data) throw 404;
328328
reply.send(data);
@@ -342,7 +342,7 @@ fastify.get('/public/:publicKey/:key', async (request, reply) => {
342342
fastify.get('/private/:privateKey/:key', async (request, reply) => {
343343
const { privateKey, key } = request.params;
344344
try {
345-
if (helper.validate(privateKey) !== 'private') throw 401;
345+
if (!helper.assert(privateKey, 'private')) throw 401;
346346
return helper.oneToOneTTL(privateKey, key);
347347
} catch (err) {
348348
if (err == 400) {

api/test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ const key = helper.genKeyPair();
1414

1515
console.log(JSON.stringify(key));
1616

17-
console.log('This should show public: ' + helper.validate(key.public));
18-
console.log('This should show private: ' + helper.validate(key.private));
19-
console.log('This should show false: ' + helper.validate('random'));
17+
console.log('This should show public: ', helper.validate(key.public), helper.assert(key.public, 'public'));
18+
console.log('This should show private: ', helper.validate(key.private), helper.assert(key.private, 'private'));
19+
console.log('This should show false: ', helper.validate('random'), helper.assert('random', 'private') || helper.assert('random', 'public'));
2020

2121
console.log('Stream token private POST:', await helper.streamToken(key.private, false));
2222
console.log('Stream token public GET:', await helper.streamToken(key.public));

example.env

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ KV_REST_API_TOKEN=
2323
# Redis credentials to be used for database
2424
UPSTASH_REDIS_REST_URL=
2525
UPSTASH_REDIS_REST_TOKEN=
26+
# Disable upstash SDK telemetry for performance
27+
UPSTASH_DISABLE_TELEMETRY=1
2628

2729
# OneSignal credentials for web-push for each app registered @ https://github.com/securelay/apps
2830
ONESIGNAL_API_KEY_FORMONIT=

0 commit comments

Comments
 (0)