diff --git a/utils/GunSmith/GunT.ts b/utils/GunSmith/GunT.ts new file mode 100644 index 00000000..b3a74409 --- /dev/null +++ b/utils/GunSmith/GunT.ts @@ -0,0 +1,154 @@ +/** + * @prettier + */ +namespace GunT { + export type Primitive = boolean | string | number + + export interface Data { + [K: string]: ValidDataValue + } + + export type ValidDataValue = Primitive | null | Data + + export interface Ack { + err: string | undefined + } + + type ListenerObjSoul = { + '#': string + } + + export type ListenerObj = Record< + string, + ListenerObjSoul | Primitive | null + > & { + _: ListenerObjSoul + } + + export type ListenerData = Primitive | null | ListenerObj | undefined + + interface OpenListenerDataObj { + [k: string]: OpenListenerData + } + + export type Listener = (data: ListenerData, key: string) => void + export type Callback = (ack: Ack) => void + + export interface Peer { + url: string + id: string + wire?: { + readyState: number + } + } + + export interface Soul { + get: string + put: Primitive | null | object | undefined + opt: { + peers: Record + } + } + + export type OpenListenerData = Primitive | null | OpenListenerDataObj + export type OpenListener = (data: OpenListenerData, key: string) => void + + export type LoadListenerData = OpenListenerData + export type LoadListener = (data: LoadListenerData, key: string) => void + + export interface GUNNodeBase { + _: Soul + + map(): GUNNode + + on(this: GUNNode, cb: Listener): void + once(this: GUNNode, cb?: Listener, opts?: { wait: number }): GUNNode + + // open(this: GUNNode, cb?: OpenListener): GUNNode + // load(this: GUNNode, cb?: OpenListener): GUNNode + + // load(this: GUNNode, cb?: LoadListener): GUNNode + + off(): void + user(): UserGUNNode + user(pub: string): GUNNode + + then(): Promise + then(cb: (v: ListenerData) => T): Promise + } + + export interface GUNNode extends GUNNodeBase { + get(key: string): GUNNode + put(data: ValidDataValue, cb?: Callback): GUNNode + set(data: ValidDataValue, cb?: Callback): GUNNode + } + + export interface CreateAck { + pub: string | undefined + err: string | undefined + } + + export type CreateCB = (ack: CreateAck) => void + + export interface AuthAck { + err: string | undefined + sea: + | { + pub: string + } + | undefined + } + + export type AuthCB = (ack: AuthAck) => void + + export interface UserPair { + epriv: string + epub: string + priv: string + pub: string + } + + export interface UserSoul extends Soul { + sea: UserPair + } + + export interface UserGUNNode extends GUNNode { + _: UserSoul + auth(user: string, pass: string, cb: AuthCB): void + is?: { + alias: string + pub: string + } + create(user: string, pass: string, cb: CreateCB): void + leave(): void + } + + export interface ISEA { + encrypt( + message: string | number | boolean, + senderSecret: string + ): Promise + decrypt(encryptedMessage: string, recipientSecret: string): Promise + decryptNumber( + encryptedMessage: string, + recipientSecret: string + ): Promise + decryptBoolean( + encryptedMessage: string, + recipientSecret: string + ): Promise + secret( + recipientOrSenderEpub: string, + recipientOrSenderUserPair: UserPair + ): Promise + } + + export interface MySEA { + encrypt(message: string, senderSecret: string): Promise + decrypt(encryptedMessage: string, recipientSecret: string): Promise + secret( + recipientOrSenderEpub: string, + recipientOrSenderUserPair: UserPair + ): Promise + } +} diff --git a/utils/GunSmith/Smith.ts b/utils/GunSmith/Smith.ts new file mode 100644 index 00000000..e17910e7 --- /dev/null +++ b/utils/GunSmith/Smith.ts @@ -0,0 +1,50 @@ +/** + * @format + */ +/// +namespace Smith { + export interface SmithMsgOn { + path: string + type: 'on' + } + + export interface SmithMsgOnce { + id: string + path: string + type: 'once' + } + + export interface SmithMsgPut { + id: string + path: string + type: 'put' + value: any + } + + export type SmithMsg = SmithMsgOn | SmithMsgOnce | SmithMsgPut | BatchSmithMsg + + export type BatchSmithMsg = SmithMsg[] + + export interface GunMsgOn { + data: any + key: string + path: string + type: 'on' + } + + export interface GunMsgOnce { + data: any + id: string + key: string + type: 'once' + } + + export interface GunMsgPut { + ack: GunT.Ack + id: string + path: string + type: 'put' + } + + export type GunMsg = GunMsgOn | GunMsgOnce | GunMsgPut +} diff --git a/utils/GunSmith/config.js b/utils/GunSmith/config.js new file mode 100644 index 00000000..44dab137 --- /dev/null +++ b/utils/GunSmith/config.js @@ -0,0 +1,25 @@ +/** + * @format + */ +/* eslint-disable no-process-env */ + +const dotenv = require('dotenv') +const defaults = require('../../config/defaults')(false) + +dotenv.config() + +// @ts-ignore Let it crash if undefined +exports.DATA_FILE_NAME = process.env.DATA_FILE_NAME || defaults.dataFileName + +/** + * @type {string[]} + */ +exports.PEERS = process.env.PEERS + ? JSON.parse(process.env.PEERS) + : defaults.peers + +exports.MS_TO_TOKEN_EXPIRATION = Number( + process.env.MS_TO_TOKEN_EXPIRATION || defaults.tokenExpirationMS +) + +exports.SHOW_LOG = process.env.SHOW_GUN_DB_LOG === 'true' diff --git a/utils/GunSmith/gun.js b/utils/GunSmith/gun.js new file mode 100644 index 00000000..112cb72a --- /dev/null +++ b/utils/GunSmith/gun.js @@ -0,0 +1,133 @@ +/** + * @format + */ +// @ts-check +/// +/// +const Gun = require('gun') +// @ts-ignore +require('gun/nts') + +const Config = require('./config') + +// @ts-ignore +Gun.log = () => {} + +/** + * This var is just to please typescript's casting rules. + */ +const _gun = /** @type {any} */ (new Gun({ + axe: false, + multicast: false, + peers: Config.PEERS +})) + +/** + * @type {GunT.GUNNode} + */ +const gun = _gun + +const user = gun.user() + +/** @type {Set} */ +const pendingOnces = new Set() + +/** + * @returns {Promise} + */ +const waitForAuth = async () => { + if (user.is?.pub) { + return Promise.resolve() + } + + await new Promise(res => setTimeout(res, 1000)) + + return waitForAuth() +} + +/** + * @param {Smith.SmithMsg} msg + */ +const handleMsg = msg => { + if (Array.isArray(msg)) { + msg.forEach(handleMsg) + return + } + if (msg.type === 'on') { + const [root, ...keys] = msg.path.split('>') + + /** @type {GunT.GUNNode} */ + let node = + { + $root: gun, + $user: user + }[root] || gun.user(root) + + for (const key of keys) { + node = node.get(key) + } + node.on((data, key) => { + /** @type {Smith.GunMsgOn} */ + const res = { + data, + key, + path: msg.path, + type: 'on' + } + // @ts-expect-error + process.send(res) + }) + } + if (msg.type === 'once') { + const [root, ...keys] = msg.path.split('>') + + /** @type {GunT.GUNNode} */ + let node = + { + $root: gun, + $user: user + }[root] || gun.user(root) + + for (const key of keys) { + node = node.get(key) + } + node.once((data, key) => { + /** @type {Smith.GunMsgOnce} */ + const res = { + data, + id: msg.id, + key, + type: 'once' + } + // @ts-expect-error + process.send(res) + }) + } + if (msg.type === 'put') { + const [root, ...keys] = msg.path.split('>') + + /** @type {GunT.GUNNode} */ + let node = + { + $root: gun, + $user: user + }[root] || gun.user(root) + + for (const key of keys) { + node = node.get(key) + } + node.on((data, key) => { + /** @type {Smith.GunMsgOn} */ + const res = { + data, + key, + path: msg.path, + type: 'on' + } + // @ts-expect-error + process.send(res) + }) + } +} + +process.on('message', handleMsg) diff --git a/utils/GunSmith/gunSmith.js b/utils/GunSmith/gunSmith.js new file mode 100644 index 00000000..9c1e2d14 --- /dev/null +++ b/utils/GunSmith/gunSmith.js @@ -0,0 +1,150 @@ +/** + * @format + */ +// @ts-check +/// +/// +const { fork } = require('child_process') + +const Config = require('./config') + +/** + * Maps a path to `on()` listeners + * @type {Record>} + */ +const pathToListeners = { + 'x>x>x>': new WeakSet() +} + +/** + * @param {Smith.GunMsg} msg + */ +const handleMsg = msg => {} + +let currentGun = fork('./gun') + +const forge = () => { + currentGun.kill() + currentGun = fork('./gun') + + currentGun.on('message', handleMsg) + + const msgs = Object.keys(pathToListeners).map(path => { + /** @type {Smith.SmithMsgOn} */ + const msg = { + path, + type: 'on' + } + return msg + }) + + currentGun.send(msgs) +} + +/** + * @param {string} path + * @param {boolean=} afterMap + * @returns {GunT.GUNNode} + */ +const createReplica = (path, afterMap = false) => { + /** @type {GunT.Listener[]} */ + const listenersForThisRef = [] + + return { + _: { + get: '', + opt: { + peers: {} + }, + put: {} + }, + get(key) { + return createReplica(path + '>' + key) + }, + map() { + return createReplica(path, true) + }, + off() { + for (const l of listenersForThisRef) { + pathToListeners[path].delete(l) + } + }, + on(cb) { + listenersForThisRef.push(cb) + /** @type {Smith.SmithMsgOn} */ + const msg = { + path, + type: 'on' + } + currentGun.send(msg) + return this + }, + once(cb, opts) { + return this + }, + put(data, cb) { + return this + }, + set(data, cb) { + return this + }, + then() { + return Promise.resolve(null) + }, + user(pub) { + if (path !== '$root') { + throw new ReferenceError( + `Do not call user() on a non-root GunSmith node` + ) + } + if (!pub) { + throw new Error( + 'Do not use `user()` (without providing a pub) on a GunSmith node, use getUser()' + ) + } + const replica = createReplica(pub) + return { + ...replica, + _: { + ...replica._, + sea: { + epriv: 'Do not use ._.sea on a GunSmith node, use getUser()', + epub: 'Do not use ._.sea on a GunSmith node, use getUser()', + priv: 'Do not use ._.sea on a GunSmith node, use getUser()', + pub: 'Do not use ._.sea on a GunSmith node, use getUser()' + } + }, + auth() { + throw new Error( + 'Do not call auth() on a GunSmith node, use getUser()' + ) + }, + create() { + throw new Error( + 'Do not call create() on a GunSmith node, use getUser()' + ) + }, + leave() { + throw new Error( + 'Do not call leave() on a GunSmith node, use getUser()' + ) + } + } + } + } +} + +/** + * @returns {GunT.GUNNode} + */ +const getGun = () => createReplica('$root') + +/** + * @returns {GunT.UserGUNNode} + */ +const getUser = createReplica('$user') + +module.exports = { + getGun, + getUser +} diff --git a/utils/GunSmith/index.js b/utils/GunSmith/index.js new file mode 100644 index 00000000..e9024186 --- /dev/null +++ b/utils/GunSmith/index.js @@ -0,0 +1 @@ +module.exports = require('./GunSmith') \ No newline at end of file