summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpolwex <polwex@sortug.com>2025-09-11 01:50:29 +0700
committerpolwex <polwex@sortug.com>2025-09-11 01:50:29 +0700
commit91b15ad49092c314dd6d3483aec47f0be7a37506 (patch)
tree9a0b040ed6e1c2793e4f9fc269a5d6118b16e453
parentb1d68ac307ed87d63e83820cbdf843fff0fd9f7f (diff)
ihategit
-rw-r--r--shim/.envrc10
-rw-r--r--shim/.gitignore9
-rw-r--r--shim/devenv.lock103
-rw-r--r--shim/devenv.nix57
-rw-r--r--shim/devenv.yaml15
l---------shim/http-api1
-rw-r--r--shim/ws-shim/.gitignore34
-rw-r--r--shim/ws-shim/CLAUDE.md107
-rw-r--r--shim/ws-shim/README.md15
-rw-r--r--shim/ws-shim/bun.lock57
-rw-r--r--shim/ws-shim/index.ts1
-rw-r--r--shim/ws-shim/package.json15
-rw-r--r--shim/ws-shim/src/client.ts209
-rw-r--r--shim/ws-shim/src/nostr.ts24
-rw-r--r--shim/ws-shim/src/server.ts157
-rw-r--r--shim/ws-shim/src/test.ts44
-rw-r--r--shim/ws-shim/src/types.ts77
-rw-r--r--shim/ws-shim/tsconfig.json29
m---------wss-shim0
19 files changed, 964 insertions, 0 deletions
diff --git a/shim/.envrc b/shim/.envrc
new file mode 100644
index 0000000..7e9a2d6
--- /dev/null
+++ b/shim/.envrc
@@ -0,0 +1,10 @@
+export DIRENV_WARN_TIMEOUT=20s
+
+eval "$(devenv direnvrc)"
+
+# `use devenv` supports the same options as the `devenv shell` command.
+#
+# To silence the output, use `--quiet`.
+#
+# Example usage: use devenv --quiet --impure --option services.postgres.enable:bool true
+use devenv
diff --git a/shim/.gitignore b/shim/.gitignore
new file mode 100644
index 0000000..4d058db
--- /dev/null
+++ b/shim/.gitignore
@@ -0,0 +1,9 @@
+# Devenv
+.devenv*
+devenv.local.nix
+
+# direnv
+.direnv
+
+# pre-commit
+.pre-commit-config.yaml
diff --git a/shim/devenv.lock b/shim/devenv.lock
new file mode 100644
index 0000000..973ead1
--- /dev/null
+++ b/shim/devenv.lock
@@ -0,0 +1,103 @@
+{
+ "nodes": {
+ "devenv": {
+ "locked": {
+ "dir": "src/modules",
+ "lastModified": 1756101922,
+ "owner": "cachix",
+ "repo": "devenv",
+ "rev": "372c975fd0d5b7fc1ffbb15c75a21d7f9ea97603",
+ "type": "github"
+ },
+ "original": {
+ "dir": "src/modules",
+ "owner": "cachix",
+ "repo": "devenv",
+ "type": "github"
+ }
+ },
+ "flake-compat": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1747046372,
+ "owner": "edolstra",
+ "repo": "flake-compat",
+ "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
+ "type": "github"
+ },
+ "original": {
+ "owner": "edolstra",
+ "repo": "flake-compat",
+ "type": "github"
+ }
+ },
+ "git-hooks": {
+ "inputs": {
+ "flake-compat": "flake-compat",
+ "gitignore": "gitignore",
+ "nixpkgs": [
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1755960406,
+ "owner": "cachix",
+ "repo": "git-hooks.nix",
+ "rev": "e891a93b193fcaf2fc8012d890dc7f0befe86ec2",
+ "type": "github"
+ },
+ "original": {
+ "owner": "cachix",
+ "repo": "git-hooks.nix",
+ "type": "github"
+ }
+ },
+ "gitignore": {
+ "inputs": {
+ "nixpkgs": [
+ "git-hooks",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1709087332,
+ "owner": "hercules-ci",
+ "repo": "gitignore.nix",
+ "rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
+ "type": "github"
+ },
+ "original": {
+ "owner": "hercules-ci",
+ "repo": "gitignore.nix",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1755783167,
+ "owner": "cachix",
+ "repo": "devenv-nixpkgs",
+ "rev": "4a880fb247d24fbca57269af672e8f78935b0328",
+ "type": "github"
+ },
+ "original": {
+ "owner": "cachix",
+ "ref": "rolling",
+ "repo": "devenv-nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "devenv": "devenv",
+ "git-hooks": "git-hooks",
+ "nixpkgs": "nixpkgs",
+ "pre-commit-hooks": [
+ "git-hooks"
+ ]
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/shim/devenv.nix b/shim/devenv.nix
new file mode 100644
index 0000000..5821877
--- /dev/null
+++ b/shim/devenv.nix
@@ -0,0 +1,57 @@
+{
+ pkgs,
+ lib,
+ config,
+ inputs,
+ ...
+}: {
+ # https://devenv.sh/basics/
+ env.GREET = "devenv";
+
+ # https://devenv.sh/packages/
+ packages = with pkgs; [
+ git
+ nodePackages.prettier
+ nodePackages.typescript-language-server
+ ];
+
+ # https://devenv.sh/languages/
+ languages.rust.enable = true;
+ languages.javascript = {
+ enable = true;
+ bun.enable = true;
+ };
+
+ # https://devenv.sh/processes/
+ # processes.cargo-watch.exec = "cargo-watch";
+
+ # https://devenv.sh/services/
+ # services.postgres.enable = true;
+
+ # https://devenv.sh/scripts/
+ scripts.hello.exec = ''
+ echo hello from $GREET
+ '';
+
+ enterShell = ''
+ hello
+ git --version
+ '';
+
+ # https://devenv.sh/tasks/
+ # tasks = {
+ # "myproj:setup".exec = "mytool build";
+ # "devenv:enterShell".after = [ "myproj:setup" ];
+ # };
+
+ # https://devenv.sh/tests/
+ enterTest = ''
+ echo "Running tests"
+ git --version | grep --color=auto "${pkgs.git.version}"
+ '';
+
+ # https://devenv.sh/git-hooks/
+ # git-hooks.hooks.shellcheck.enable = true;
+
+ # See full reference at https://devenv.sh/reference/options/
+}
diff --git a/shim/devenv.yaml b/shim/devenv.yaml
new file mode 100644
index 0000000..116a2ad
--- /dev/null
+++ b/shim/devenv.yaml
@@ -0,0 +1,15 @@
+# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json
+inputs:
+ nixpkgs:
+ url: github:cachix/devenv-nixpkgs/rolling
+
+# If you're using non-OSS software, you can set allowUnfree to true.
+# allowUnfree: true
+
+# If you're willing to use a package that's vulnerable
+# permittedInsecurePackages:
+# - "openssl-1.1.1w"
+
+# If you have more than one devenv you can merge them
+#imports:
+# - ./backend
diff --git a/shim/http-api b/shim/http-api
new file mode 120000
index 0000000..757dc82
--- /dev/null
+++ b/shim/http-api
@@ -0,0 +1 @@
+/home/y/code/urbit/bun/http-api/ \ No newline at end of file
diff --git a/shim/ws-shim/.gitignore b/shim/ws-shim/.gitignore
new file mode 100644
index 0000000..a14702c
--- /dev/null
+++ b/shim/ws-shim/.gitignore
@@ -0,0 +1,34 @@
+# dependencies (bun install)
+node_modules
+
+# output
+out
+dist
+*.tgz
+
+# code coverage
+coverage
+*.lcov
+
+# logs
+logs
+_.log
+report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# caches
+.eslintcache
+.cache
+*.tsbuildinfo
+
+# IntelliJ based IDEs
+.idea
+
+# Finder (MacOS) folder config
+.DS_Store
diff --git a/shim/ws-shim/CLAUDE.md b/shim/ws-shim/CLAUDE.md
new file mode 100644
index 0000000..630114b
--- /dev/null
+++ b/shim/ws-shim/CLAUDE.md
@@ -0,0 +1,107 @@
+---
+
+Default to using Bun instead of Node.js.
+
+- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
+- Use `bun test` instead of `jest` or `vitest`
+- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
+- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
+- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
+- Bun automatically loads .env, so don't use dotenv.
+
+## APIs
+
+- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
+- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
+- `Bun.redis` for Redis. Don't use `ioredis`.
+- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
+- `WebSocket` is built-in. Don't use `ws`.
+- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
+- Bun.$`ls` instead of execa.
+
+## Testing
+
+Use `bun test` to run tests.
+
+```ts#index.test.ts
+import { test, expect } from "bun:test";
+
+test("hello world", () => {
+ expect(1).toBe(1);
+});
+```
+
+## Frontend
+
+Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
+
+Server:
+
+```ts#index.ts
+import index from "./index.html"
+
+Bun.serve({
+ routes: {
+ "/": index,
+ "/api/users/:id": {
+ GET: (req) => {
+ return new Response(JSON.stringify({ id: req.params.id }));
+ },
+ },
+ },
+ // optional websocket support
+ websocket: {
+ open: (ws) => {
+ ws.send("Hello, world!");
+ },
+ message: (ws, message) => {
+ ws.send(message);
+ },
+ close: (ws) => {
+ // handle close
+ }
+ },
+ development: {
+ hmr: true,
+ console: true,
+ }
+})
+```
+
+HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
+
+```html#index.html
+<html>
+ <body>
+ <h1>Hello, world!</h1>
+ <script type="module" src="./frontend.tsx"></script>
+ </body>
+</html>
+```
+
+With the following `frontend.tsx`:
+
+```tsx#frontend.tsx
+import React from "react";
+
+// import .css files directly and it works
+import './index.css';
+
+import { createRoot } from "react-dom/client";
+
+const root = createRoot(document.body);
+
+export default function Frontend() {
+ return <h1>Hello, world!</h1>;
+}
+
+root.render(<Frontend />);
+```
+
+Then, run index.ts
+
+```sh
+bun --hot ./index.ts
+```
+
+For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`.
diff --git a/shim/ws-shim/README.md b/shim/ws-shim/README.md
new file mode 100644
index 0000000..c1effde
--- /dev/null
+++ b/shim/ws-shim/README.md
@@ -0,0 +1,15 @@
+# ws-shim
+
+To install dependencies:
+
+```bash
+bun install
+```
+
+To run:
+
+```bash
+bun run index.ts
+```
+
+This project was created using `bun init` in bun v1.2.19. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
diff --git a/shim/ws-shim/bun.lock b/shim/ws-shim/bun.lock
new file mode 100644
index 0000000..af9c2cb
--- /dev/null
+++ b/shim/ws-shim/bun.lock
@@ -0,0 +1,57 @@
+{
+ "lockfileVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "ws-shim",
+ "dependencies": {
+ "nostr-tools": "^2.16.2",
+ "urbit-http": "file:../http-api",
+ },
+ "devDependencies": {
+ "@types/bun": "latest",
+ },
+ "peerDependencies": {
+ "typescript": "^5",
+ },
+ },
+ },
+ "packages": {
+ "@noble/ciphers": ["@noble/ciphers@0.5.3", "", {}, "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w=="],
+
+ "@noble/curves": ["@noble/curves@1.2.0", "", { "dependencies": { "@noble/hashes": "1.3.2" } }, "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw=="],
+
+ "@noble/hashes": ["@noble/hashes@1.3.1", "", {}, "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA=="],
+
+ "@scure/base": ["@scure/base@1.1.1", "", {}, "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="],
+
+ "@scure/bip32": ["@scure/bip32@1.3.1", "", { "dependencies": { "@noble/curves": "~1.1.0", "@noble/hashes": "~1.3.1", "@scure/base": "~1.1.0" } }, "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A=="],
+
+ "@scure/bip39": ["@scure/bip39@1.2.1", "", { "dependencies": { "@noble/hashes": "~1.3.0", "@scure/base": "~1.1.0" } }, "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg=="],
+
+ "@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="],
+
+ "@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="],
+
+ "@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="],
+
+ "bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="],
+
+ "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
+
+ "nostr-tools": ["nostr-tools@2.16.2", "", { "dependencies": { "@noble/ciphers": "^0.5.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.1", "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", "@scure/bip39": "1.2.1", "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-ZxH9EbSt5ypURZj2TGNJxZd0Omb5ag5KZSu8IyJMCdLyg2KKz+2GA0sP/cSawCQEkyviIN4eRT4G2gB/t9lMRw=="],
+
+ "nostr-wasm": ["nostr-wasm@0.1.0", "", {}, "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA=="],
+
+ "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
+
+ "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
+
+ "urbit-http": ["@urbit/http-api@file:../http-api", { "devDependencies": { "@types/bun": "latest", "typescript": "^5" } }],
+
+ "@noble/curves/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="],
+
+ "@scure/bip32/@noble/curves": ["@noble/curves@1.1.0", "", { "dependencies": { "@noble/hashes": "1.3.1" } }, "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA=="],
+
+ "@scure/bip39/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="],
+ }
+}
diff --git a/shim/ws-shim/index.ts b/shim/ws-shim/index.ts
new file mode 100644
index 0000000..f67b2c6
--- /dev/null
+++ b/shim/ws-shim/index.ts
@@ -0,0 +1 @@
+console.log("Hello via Bun!"); \ No newline at end of file
diff --git a/shim/ws-shim/package.json b/shim/ws-shim/package.json
new file mode 100644
index 0000000..ff2d0c4
--- /dev/null
+++ b/shim/ws-shim/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "ws-shim",
+ "module": "index.ts",
+ "type": "module",
+ "dependencies": {
+ "nostr-tools": "^2.16.2",
+ "urbit-http": "file:../http-api"
+ },
+ "devDependencies": {
+ "@types/bun": "latest"
+ },
+ "peerDependencies": {
+ "typescript": "^5"
+ }
+}
diff --git a/shim/ws-shim/src/client.ts b/shim/ws-shim/src/client.ts
new file mode 100644
index 0000000..b7df3e9
--- /dev/null
+++ b/shim/ws-shim/src/client.ts
@@ -0,0 +1,209 @@
+import type {
+ NostrEvent,
+ ClientMessage,
+ RelayMessage,
+ Filter,
+ Subscription,
+} from "./types";
+
+export class Relay {
+ private url: string;
+ private ws: WebSocket | null = null;
+ private subscriptions: Map<string, Subscription> = new Map();
+ private messageQueue: ClientMessage[] = [];
+ private reconnectTimer: Timer | null = null;
+ private reconnectAttempts = 0;
+ private maxReconnectAttempts = 5;
+ private reconnectDelay = 1000;
+
+ public status: "connecting" | "connected" | "disconnected" | "error" =
+ "disconnected";
+
+ public onconnect?: () => void;
+ public ondisconnect?: () => void;
+ public onerror?: (error: Error) => void;
+ public onnotice?: (message: string) => void;
+
+ constructor(url: string) {
+ this.url = url;
+ }
+
+ async connect(): Promise<void> {
+ return new Promise((resolve, reject) => {
+ if (this.ws?.readyState === WebSocket.OPEN) {
+ resolve();
+ return;
+ }
+
+ this.status = "connecting";
+ this.ws = new WebSocket(this.url);
+
+ this.ws.onopen = () => {
+ this.status = "connected";
+ this.reconnectAttempts = 0;
+ this.flushMessageQueue();
+ this.onconnect?.();
+ resolve();
+ };
+
+ this.ws.onclose = () => {
+ this.status = "disconnected";
+ this.ondisconnect?.();
+ this.attemptReconnect();
+ };
+
+ this.ws.onerror = (event) => {
+ this.status = "error";
+ const error = new Error(`WebSocket error: ${event.type}`);
+ this.onerror?.(error);
+ reject(error);
+ };
+
+ this.ws.onmessage = (event) => {
+ this.handleMessage(event.data);
+ };
+ });
+ }
+
+ disconnect(): void {
+ if (this.reconnectTimer) {
+ clearTimeout(this.reconnectTimer);
+ this.reconnectTimer = null;
+ }
+
+ if (this.ws) {
+ this.ws.close();
+ this.ws = null;
+ }
+
+ this.status = "disconnected";
+ this.subscriptions.clear();
+ this.messageQueue = [];
+ }
+
+ private attemptReconnect(): void {
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
+ this.status = "error";
+ this.onerror?.(new Error("Max reconnection attempts reached"));
+ return;
+ }
+
+ this.reconnectAttempts++;
+ const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
+
+ this.reconnectTimer = setTimeout(() => {
+ this.connect().catch((error) => {
+ console.error("Reconnection failed:", error);
+ });
+ }, delay);
+ }
+
+ private flushMessageQueue(): void {
+ while (this.messageQueue.length > 0) {
+ const message = this.messageQueue.shift();
+ if (message) {
+ this.send(message);
+ }
+ }
+ }
+
+ private send(message: ClientMessage): void {
+ if (this.ws?.readyState === WebSocket.OPEN) {
+ this.ws.send(JSON.stringify(message));
+ } else {
+ this.messageQueue.push(message);
+ }
+ }
+
+ private handleMessage(data: string): void {
+ try {
+ const message = JSON.parse(data) as RelayMessage;
+
+ switch (message[0]) {
+ case "EVENT": {
+ const [, subscriptionId, event] = message;
+ const subscription = this.subscriptions.get(subscriptionId);
+
+ if (subscription) {
+ subscription.onevent?.(event);
+ }
+ break;
+ }
+
+ case "OK": {
+ const [, eventId, success, messag] = message;
+ if (!success) {
+ console.error(`Event ${eventId} rejected: ${messag}`);
+ }
+ break;
+ }
+
+ case "EOSE": {
+ const [, subscriptionId] = message;
+ const subscription = this.subscriptions.get(subscriptionId);
+ subscription?.oneose?.();
+ break;
+ }
+
+ case "CLOSED": {
+ const [, subscriptionId, messag] = message;
+ this.subscriptions.delete(subscriptionId);
+ console.log(`Subscription ${subscriptionId} closed: ${messag}`);
+ break;
+ }
+
+ case "NOTICE": {
+ const [, messag] = message;
+ this.onnotice?.(messag);
+ break;
+ }
+
+ case "AUTH": {
+ console.warn("AUTH not implemented");
+ break;
+ }
+ }
+ } catch (error) {
+ console.error("Failed to handle message:", error);
+ }
+ }
+
+ publishEvent(event: NostrEvent): void {
+ this.send(["EVENT", event]);
+ }
+
+ subscribe(
+ id: string,
+ filters: Filter[],
+ handlers: {
+ onevent?: (event: NostrEvent) => void;
+ oneose?: () => void;
+ },
+ ): () => void {
+ const subscription: Subscription = {
+ id,
+ filters,
+ ...handlers,
+ };
+
+ this.subscriptions.set(id, subscription);
+ this.send(["REQ", id, ...filters]);
+
+ return () => {
+ this.unsubscribe(id);
+ };
+ }
+
+ unsubscribe(id: string): void {
+ this.subscriptions.delete(id);
+ this.send(["CLOSE", id]);
+ }
+
+ getStatus(): string {
+ return this.status;
+ }
+
+ getUrl(): string {
+ return this.url;
+ }
+}
diff --git a/shim/ws-shim/src/nostr.ts b/shim/ws-shim/src/nostr.ts
new file mode 100644
index 0000000..0b084b6
--- /dev/null
+++ b/shim/ws-shim/src/nostr.ts
@@ -0,0 +1,24 @@
+import { finalizeEvent, validateEvent, verifyEvent } from "nostr-tools";
+import type { NostrEvent } from "./types";
+import { hexToBytes } from "nostr-tools/utils";
+
+export function validate(event: NostrEvent) {
+ console.log("constructing event in js");
+ const priv =
+ "d862c25aacfae2f66380448eafdeefeccb970a382f2ff185f3e0c5a538d60e35";
+ const sk = hexToBytes(priv);
+ const raw = {
+ kind: event.kind,
+ created_at: event.created_at,
+ tags: event.tags,
+ content: event.content,
+ };
+ const ev = finalizeEvent(raw, sk);
+ console.log("js event", ev);
+ console.log("validating my event", event);
+ const ok = validateEvent(event);
+ console.log("is valid?", ok);
+ const ok2 = verifyEvent(event);
+ console.log("is verified?", ok2);
+ return ok && ok2;
+}
diff --git a/shim/ws-shim/src/server.ts b/shim/ws-shim/src/server.ts
new file mode 100644
index 0000000..0b807aa
--- /dev/null
+++ b/shim/ws-shim/src/server.ts
@@ -0,0 +1,157 @@
+import EventEmitter from "events";
+import Urbit from "urbit-http";
+
+const SHIP_URL = "http://localhost:8080";
+const api = new Urbit(SHIP_URL, "");
+let sub: number;
+// const emitter = new EventEmitter();
+
+// //github.com/oven-sh/bun/issues/13811
+// function sse(req: Request, channel: string): Response {
+// const stream = new ReadableStream({
+// type: "direct",
+// pull(controller: ReadableStreamDirectController) {
+// let id = +(req.headers.get("last-event-id") ?? 1);
+// const handler = async (event: string, data: unknown): Promise<void> => {
+// await controller.write(`id:${id}\n`);
+// await controller.write(`event:${event}\n`);
+// if (data) await controller.write(`data:${JSON.stringify(data)}\n\n`);
+// await controller.flush();
+// id++;
+// emitter.on(channel, handler);
+// if (req.signal.aborted) {
+// emitter.off(channel, handler);
+// controller.close();
+// }
+// };
+// return new Promise(() => void 0);
+// },
+// });
+// return new Response(stream, {
+// status: 200,
+// headers: { "Content-Type": "text/event-stream" },
+// });
+// }
+function emit(channel: string, url: string, event?: any): void {
+ // emitter.emit(channel, event, data);
+ const body = JSON.stringify({ event, relay: url });
+ fetch(SHIP_URL + "/nostr-shim", {
+ method: "PUT",
+ headers: { "Content-type": "application/json" },
+ body,
+ });
+}
+const sockets: Map<string, Relay> = new Map();
+
+const server = Bun.serve({
+ //http
+ routes: {
+ "/shim": async (req: Request) => {
+ const data = (await req.json()) as ShimRequest;
+ console.log("request data", data);
+ if ("get" in data) {
+ for (const req of data.get) {
+ startWSClient(req.relay, req.filters);
+ }
+ }
+ if ("post" in data) {
+ const ok = validate(data.post.event);
+ if (!ok) return;
+ for (const relay of data.post.relays) {
+ const socket = sockets.get(relay);
+ if (socket) socket.publishEvent(data.post.event);
+ else {
+ await startWSClient(relay, []);
+
+ const socket = sockets.get(relay);
+ if (socket) socket.publishEvent(data.post.event);
+ else console.error("wtf man");
+ }
+ }
+ }
+ // server.publish("shim", data);
+ // return sse(req, "all");
+ return new Response("OK");
+ },
+ },
+ fetch(req, server) {
+ const upgraded = server.upgrade(req, { data: { createdAt: Date.now() } });
+ if (upgraded) return undefined;
+ return new Response("henlo");
+ },
+ websocket: {
+ // Maximum message size (in bytes)
+ maxPayloadLength: 64 * 1024,
+
+ // Backpressure limit before messages are dropped
+ backpressureLimit: 1024 * 1024,
+
+ // Close connection if backpressure limit is hit
+ closeOnBackpressureLimit: true,
+
+ // Handler called when backpressure is relieved
+ drain(ws) {
+ console.log("Backpressure relieved");
+ },
+
+ // Enable per-message deflate compression
+ perMessageDeflate: {
+ compress: true,
+ decompress: true,
+ },
+
+ // Send ping frames to keep connection alive
+ sendPings: true,
+
+ // Handlers for ping/pong frames
+ ping(ws, data) {
+ console.log("Received ping");
+ },
+ pong(ws, data) {
+ console.log("Received pong");
+ },
+
+ // Whether server receives its own published messages
+ publishToSelf: false,
+ // handlers
+ async open(ws) {
+ //
+ // ws.subscribe("shim");
+ // console.log(ws, "hey someone subscribed here");
+ // if (sub) {
+ // await api.unsubscribe(sub);
+ // sub = await api.subscribe({ app: "nostril", path: "/ws" });
+ // } else sub = await api.subscribe({ app: "nostril", path: "/ws" });
+ },
+ async close(ws) {
+ //
+ // ws.unsubscribe("shim");
+ // if (sub) await api.unsubscribe(sub);
+ },
+ async message(ws) {
+ // api.poke({ app: "nostril", mark: "json", json: { ws: ws.data } });
+ // server.publish("chat", "henlo");
+ },
+ },
+ port: 8888,
+});
+
+import { Relay } from "./client";
+import type { Filter, ShimRequest } from "./types";
+import { validate } from "./nostr";
+async function startWSClient(url: string, filters: Filter[]) {
+ console.log("connecting to relay...");
+ const relay = new Relay(url);
+ await relay.connect();
+ const id = crypto.randomUUID();
+ relay.subscribe(id, filters, {
+ oneose: () => {
+ console.log("oneose");
+ },
+ onevent(event) {
+ console.log("relay event", { url, event });
+ emit("all", url, event);
+ },
+ });
+ sockets.set(url, relay);
+}
diff --git a/shim/ws-shim/src/test.ts b/shim/ws-shim/src/test.ts
new file mode 100644
index 0000000..fb87555
--- /dev/null
+++ b/shim/ws-shim/src/test.ts
@@ -0,0 +1,44 @@
+import { Relay } from "./client";
+const ids = [
+ "1a4f2d987384a33753e777138586b1f9b3b62eb0f6e54ca1cdb42859de5625bc",
+];
+async function wsClient(url: string) {
+ console.log("connecting to relae", url);
+ const relay = new Relay(url);
+ await relay.connect();
+ const id = crypto.randomUUID();
+ relay.subscribe(id, [{ ids, limit: 50 }], {
+ oneose: () => {
+ console.log("oneose");
+ },
+ onevent(event) {
+ console.log("relay event", { url, event });
+ },
+ });
+ // const socket = new WebSocket(url);
+ // socket.addEventListener("open", (event) => {
+ // //
+ // console.log("socket client open", event);
+ // });
+ // socket.addEventListener("close", (event) => {
+ // //
+ // console.log("socket client close", event);
+ // });
+ // socket.addEventListener("error", (event) => {
+ // //
+ // console.log("socket client error", event);
+ // });
+ // socket.addEventListener("message", (event) => {
+ // //
+ // console.log("socket client msg", event);
+ // });
+ // return socket;
+}
+
+const relays = ["wss://nos.lol", "wss://relay.damus.io"];
+
+async function run() {
+ console.log("wth");
+ await wsClient(relays[0]!);
+}
+run();
diff --git a/shim/ws-shim/src/types.ts b/shim/ws-shim/src/types.ts
new file mode 100644
index 0000000..4063772
--- /dev/null
+++ b/shim/ws-shim/src/types.ts
@@ -0,0 +1,77 @@
+// Shim types
+export type ShimRequest = {
+ get: Array<{ relay: string; filters: Filter[] }>;
+ post: { event: NostrEvent; relays: string[] };
+};
+// NOSTR official
+export interface NostrEvent {
+ id: string;
+ pubkey: string;
+ created_at: number;
+ kind: number;
+ tags: string[][];
+ content: string;
+ sig: string;
+}
+
+export interface UnsignedEvent {
+ pubkey: string;
+ created_at: number;
+ kind: number;
+ tags: string[][];
+ content: string;
+}
+
+export enum EventKind {
+ Metadata = 0,
+ TextNote = 1,
+ RecommendRelay = 2,
+ Contacts = 3,
+ EncryptedDirectMessage = 4,
+ EventDeletion = 5,
+ Repost = 6,
+ Reaction = 7,
+ BadgeAward = 8,
+ ChannelCreation = 40,
+ ChannelMetadata = 41,
+ ChannelMessage = 42,
+ ChannelHideMessage = 43,
+ ChannelMuteUser = 44,
+}
+
+export interface Filter {
+ ids?: string[];
+ authors?: string[];
+ kinds?: number[];
+ since?: number;
+ until?: number;
+ limit?: number;
+ search?: string;
+ [key: `#${string}`]: string[];
+}
+
+export type ClientMessage =
+ | ["EVENT", NostrEvent]
+ | ["REQ", string, ...Filter[]]
+ | ["CLOSE", string];
+
+export type RelayMessage =
+ | ["EVENT", string, NostrEvent]
+ | ["OK", string, boolean, string]
+ | ["EOSE", string]
+ | ["CLOSED", string, string]
+ | ["NOTICE", string]
+ | ["AUTH", string];
+
+export interface RelayInfo {
+ url: string;
+ status: "connecting" | "connected" | "disconnected" | "error";
+ ws?: WebSocket;
+}
+
+export interface Subscription {
+ id: string;
+ filters: Filter[];
+ oneose?: () => void;
+ onevent?: (event: NostrEvent) => void;
+}
diff --git a/shim/ws-shim/tsconfig.json b/shim/ws-shim/tsconfig.json
new file mode 100644
index 0000000..bfa0fea
--- /dev/null
+++ b/shim/ws-shim/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ // Environment setup & latest features
+ "lib": ["ESNext"],
+ "target": "ESNext",
+ "module": "Preserve",
+ "moduleDetection": "force",
+ "jsx": "react-jsx",
+ "allowJs": true,
+
+ // Bundler mode
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+
+ // Best practices
+ "strict": true,
+ "skipLibCheck": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedIndexedAccess": true,
+ "noImplicitOverride": true,
+
+ // Some stricter flags (disabled by default)
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noPropertyAccessFromIndexSignature": false
+ }
+}
diff --git a/wss-shim b/wss-shim
deleted file mode 160000
-Subproject 823d410ab3961c035f392563dc02b5cfd9a3d7b