From 98399e994b60a28b732e57154234e9d977386129 Mon Sep 17 00:00:00 2001 From: Seth Etter Date: Tue, 13 Jan 2026 06:00:13 -0600 Subject: [PATCH] Rename to std.pub, eject from exe.dev --- CLAUDE.md | 17 +- README.md | 2 +- bun.lock | 502 +++++++++++++++++++++++++++++++------- logrotate.conf | 11 - package.json | 2 +- scripts/cleanup.ts | 32 +-- src/lib/oauth.ts | 81 +++--- src/routes/auth.ts | 147 ++++++----- src/routes/documents.ts | 480 +++++++++++++++++++++++------------- src/routes/publication.ts | 159 +++++++----- src/views/home.ts | 23 +- src/views/layouts/main.ts | 75 +++--- stdeditor.service | 17 -- 13 files changed, 1022 insertions(+), 526 deletions(-) delete mode 100644 logrotate.conf delete mode 100644 stdeditor.service diff --git a/CLAUDE.md b/CLAUDE.md index 10fce71..be854c2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,4 +1,4 @@ -# stdeditor +# std.pub A minimal web UI for managing standard.site publications and documents in a Bluesky PDS. @@ -41,21 +41,6 @@ bun run start - `PUBLIC_URL` - Public URL for OAuth callbacks (MUST be HTTPS without custom port for production) - `DATA_DIR` - Directory for persistent data (default: ./data) -## Production Deployment - -**IMPORTANT**: Bluesky's OAuth server does not allow custom HTTPS ports. You must: - -1. Configure exe.dev proxy to forward port 8000: - ```bash - ssh exe.dev share port stdeditor 8000 - ssh exe.dev share set-public stdeditor - ``` - -2. Set PUBLIC_URL to the standard HTTPS URL (no port): - ``` - PUBLIC_URL=https://stdeditor.exe.xyz - ``` - ## ATProto OAuth Implementation This app is a **confidential client** using: diff --git a/README.md b/README.md index e9db583..ee1e438 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# stdeditor +# std.pub To install dependencies: diff --git a/bun.lock b/bun.lock index b0c84ba..b26ae31 100644 --- a/bun.lock +++ b/bun.lock @@ -3,7 +3,7 @@ "configVersion": 1, "workspaces": { "": { - "name": "stdeditor", + "name": "std.pub", "dependencies": { "@atproto/api": "^0.18.13", "@atproto/jwk-jose": "^0.1.11", @@ -19,88 +19,422 @@ }, }, "packages": { - "@atproto-labs/did-resolver": ["@atproto-labs/did-resolver@0.2.5", "", { "dependencies": { "@atproto-labs/fetch": "0.2.3", "@atproto-labs/pipe": "0.1.1", "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.4", "zod": "^3.23.8" } }, "sha512-he7EC6OMSifNs01a4RT9mta/yYitoKDzlK9ty2TFV5Uj/+HpB4vYMRdIDFrRW0Hcsehy90E2t/dw0t7361MEKQ=="], - - "@atproto-labs/fetch": ["@atproto-labs/fetch@0.2.3", "", { "dependencies": { "@atproto-labs/pipe": "0.1.1" } }, "sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw=="], - - "@atproto-labs/fetch-node": ["@atproto-labs/fetch-node@0.2.0", "", { "dependencies": { "@atproto-labs/fetch": "0.2.3", "@atproto-labs/pipe": "0.1.1", "ipaddr.js": "^2.1.0", "undici": "^6.14.1" } }, "sha512-Krq09nH/aeoiU2s9xdHA0FjTEFWG9B5FFenipv1iRixCcPc7V3DhTNDawxG9gI8Ny0k4dBVS9WTRN/IDzBx86Q=="], - - "@atproto-labs/handle-resolver": ["@atproto-labs/handle-resolver@0.3.5", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.4", "zod": "^3.23.8" } }, "sha512-r3b+plCh/0arN535Aool9gL6yTSbAPDOyReURbA2TWAaeW4vrSJPwR6yYUx0k0vmVPjkZPIdUVd63bG/+VG5MA=="], - - "@atproto-labs/handle-resolver-node": ["@atproto-labs/handle-resolver-node@0.1.24", "", { "dependencies": { "@atproto-labs/fetch-node": "0.2.0", "@atproto-labs/handle-resolver": "0.3.5", "@atproto/did": "0.2.4" } }, "sha512-w/zvktigmRQpOLQQclp48tbb2K/2XW8j1szoIpT8T8v6P5dZ8GGVDIEF142xQMX9vWToFqMTu1P2yOuz8e3Ilg=="], - - "@atproto-labs/identity-resolver": ["@atproto-labs/identity-resolver@0.3.5", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.5", "@atproto-labs/handle-resolver": "0.3.5" } }, "sha512-kSxnreUSPhKL77doUbSl/9I6Y9qpkpD7MMJoYFQVU/WG0PB90tzfIb6DNuWsjbU2I5Q91Nzc4Tm4VJMV+OPKGQ=="], - - "@atproto-labs/pipe": ["@atproto-labs/pipe@0.1.1", "", {}, "sha512-hdNw2oUs2B6BN1lp+32pF7cp8EMKuIN5Qok2Vvv/aOpG/3tNSJ9YkvfI0k6Zd188LeDDYRUpYpxcoFIcGH/FNg=="], - - "@atproto-labs/simple-store": ["@atproto-labs/simple-store@0.3.0", "", {}, "sha512-nOb6ONKBRJHRlukW1sVawUkBqReLlLx6hT35VS3imaNPwiXDxLnTK7lxw3Lrl9k5yugSBDQAkZAq3MPTEFSUBQ=="], - - "@atproto-labs/simple-store-memory": ["@atproto-labs/simple-store-memory@0.1.4", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "lru-cache": "^10.2.0" } }, "sha512-3mKY4dP8I7yKPFj9VKpYyCRzGJOi5CEpOLPlRhoJyLmgs3J4RzDrjn323Oakjz2Aj2JzRU/AIvWRAZVhpYNJHw=="], - - "@atproto/api": ["@atproto/api@0.18.13", "", { "dependencies": { "@atproto/common-web": "^0.4.11", "@atproto/lexicon": "^0.6.0", "@atproto/syntax": "^0.4.2", "@atproto/xrpc": "^0.7.7", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", "zod": "^3.23.8" } }, "sha512-CULZ01pSJDltLS/Gc9MMrhFzB6OM3ezyZw7KoeLT/sBfwgA1ddA4mWdTh7DIRosPRigXtA05bnoiCutZbQDo+Q=="], - - "@atproto/common-web": ["@atproto/common-web@0.4.11", "", { "dependencies": { "@atproto/lex-data": "0.0.7", "@atproto/lex-json": "0.0.7", "zod": "^3.23.8" } }, "sha512-VHejNmSABU8/03VrQ3e36AmT5U3UIeio+qSUqCrO1oNgrJcWfGy1rpj0FVtUugWF8Un29+yzkukzWGZfXL70rQ=="], - - "@atproto/did": ["@atproto/did@0.2.4", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-nxNiCgXeo7pfjojq9fpfZxCO0X0xUipNVKW+AHNZwQKiUDt6zYL0VXEfm8HBUwQOCmKvj2pRRSM1Cur+tUWk3g=="], - - "@atproto/jwk": ["@atproto/jwk@0.6.0", "", { "dependencies": { "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-bDoJPvt7TrQVi/rBfBrSSpGykhtIriKxeYCYQTiPRKFfyRhbgpElF0wPXADjIswnbzZdOwbY63az4E/CFVT3Tw=="], - - "@atproto/jwk-jose": ["@atproto/jwk-jose@0.1.11", "", { "dependencies": { "@atproto/jwk": "0.6.0", "jose": "^5.2.0" } }, "sha512-i4Fnr2sTBYmMmHXl7NJh8GrCH+tDQEVWrcDMDnV5DjJfkgT17wIqvojIw9SNbSL4Uf0OtfEv6AgG0A+mgh8b5Q=="], - - "@atproto/jwk-webcrypto": ["@atproto/jwk-webcrypto@0.2.0", "", { "dependencies": { "@atproto/jwk": "0.6.0", "@atproto/jwk-jose": "0.1.11", "zod": "^3.23.8" } }, "sha512-UmgRrrEAkWvxwhlwe30UmDOdTEFidlIzBC7C3cCbeJMcBN1x8B3KH+crXrsTqfWQBG58mXgt8wgSK3Kxs2LhFg=="], - - "@atproto/lex-data": ["@atproto/lex-data@0.0.7", "", { "dependencies": { "@atproto/syntax": "0.4.2", "multiformats": "^9.9.0", "tslib": "^2.8.1", "uint8arrays": "3.0.0", "unicode-segmenter": "^0.14.0" } }, "sha512-W/Q5o9o7n2Sv3UywckChu01X5lwQUtaiiOkGJLnRsdkQTyC6813nPgY+p2sG7NwwM+82lu+FUV9fE/Ul3VqaJw=="], - - "@atproto/lex-json": ["@atproto/lex-json@0.0.7", "", { "dependencies": { "@atproto/lex-data": "0.0.7", "tslib": "^2.8.1" } }, "sha512-bjNPD5M/MhLfjNM7tcxuls80UgXpHqxdOxDXEUouAtZQV/nIDhGjmNUvKxOmOgnDsiZRnT2g5y3onrnjH3a44g=="], - - "@atproto/lexicon": ["@atproto/lexicon@0.6.0", "", { "dependencies": { "@atproto/common-web": "^0.4.7", "@atproto/syntax": "^0.4.2", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-5veb8aD+J5M0qszLJ+73KSFsFrJBgAY/nM1TSAJvGY7fNc9ZAT+PSUlmIyrdye9YznAZ07yktalls/TwNV7cHQ=="], - - "@atproto/oauth-client": ["@atproto/oauth-client@0.5.13", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.5", "@atproto-labs/fetch": "0.2.3", "@atproto-labs/handle-resolver": "0.3.5", "@atproto-labs/identity-resolver": "0.3.5", "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.4", "@atproto/jwk": "0.6.0", "@atproto/oauth-types": "0.6.1", "@atproto/xrpc": "0.7.7", "core-js": "^3", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-FLbqHkC7BAVZ90LHVzSxQf+s8ZNIQI4TsDuhYDyzi7lYtktFHDbgd88KuM2ClJFOtGCsSS17yR1Joy925tDSaA=="], - - "@atproto/oauth-client-node": ["@atproto/oauth-client-node@0.3.15", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.5", "@atproto-labs/handle-resolver-node": "0.1.24", "@atproto-labs/simple-store": "0.3.0", "@atproto/did": "0.2.4", "@atproto/jwk": "0.6.0", "@atproto/jwk-jose": "0.1.11", "@atproto/jwk-webcrypto": "0.2.0", "@atproto/oauth-client": "0.5.13", "@atproto/oauth-types": "0.6.1" } }, "sha512-iuT7QrLli7IyB4px1+lHvm/YoIRfNRpbNG9seJRtu5eX4N5aLsBP6vpXs9rCygd1+/15LcLRAAGKVEcrLT9tXA=="], - - "@atproto/oauth-types": ["@atproto/oauth-types@0.6.1", "", { "dependencies": { "@atproto/did": "0.2.4", "@atproto/jwk": "0.6.0", "zod": "^3.23.8" } }, "sha512-3z92GN/6zCq9E2GTTfZM27tWEbvi1qwFSA7KoS5+wqBC4kSsLvnLxmbKH402Z40DfWS4YWqw0DkHsgP0LNFDEA=="], - - "@atproto/syntax": ["@atproto/syntax@0.4.2", "", {}, "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA=="], - - "@atproto/xrpc": ["@atproto/xrpc@0.7.7", "", { "dependencies": { "@atproto/lexicon": "^0.6.0", "zod": "^3.23.8" } }, "sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA=="], - - "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], - - "@types/node": ["@types/node@25.0.6", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-NNu0sjyNxpoiW3YuVFfNz7mxSQ+S4X2G28uqg2s+CzoqoQjLPsWSbsFFyztIAqt2vb8kfEAsJNepMGPTxFDx3Q=="], - - "await-lock": ["await-lock@2.2.2", "", {}, "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw=="], - - "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], - - "core-js": ["core-js@3.47.0", "", {}, "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg=="], - - "hono": ["hono@4.11.3", "", {}, "sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w=="], - - "ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="], - - "iso-datestring-validator": ["iso-datestring-validator@2.2.2", "", {}, "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA=="], - - "jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="], - - "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - - "multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], - - "tlds": ["tlds@1.261.0", "", { "bin": { "tlds": "bin.js" } }, "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA=="], - - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - - "uint8arrays": ["uint8arrays@3.0.0", "", { "dependencies": { "multiformats": "^9.4.2" } }, "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA=="], - - "undici": ["undici@6.23.0", "", {}, "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g=="], - - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "unicode-segmenter": ["unicode-segmenter@0.14.5", "", {}, "sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g=="], - - "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@atproto-labs/did-resolver": [ + "@atproto-labs/did-resolver@0.2.5", + "", + { + "dependencies": { + "@atproto-labs/fetch": "0.2.3", + "@atproto-labs/pipe": "0.1.1", + "@atproto-labs/simple-store": "0.3.0", + "@atproto-labs/simple-store-memory": "0.1.4", + "@atproto/did": "0.2.4", + "zod": "^3.23.8" + } + }, + "sha512-he7EC6OMSifNs01a4RT9mta/yYitoKDzlK9ty2TFV5Uj/+HpB4vYMRdIDFrRW0Hcsehy90E2t/dw0t7361MEKQ==" + ], + "@atproto-labs/fetch": [ + "@atproto-labs/fetch@0.2.3", + "", + { + "dependencies": { + "@atproto-labs/pipe": "0.1.1" + } + }, + "sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw==" + ], + "@atproto-labs/fetch-node": [ + "@atproto-labs/fetch-node@0.2.0", + "", + { + "dependencies": { + "@atproto-labs/fetch": "0.2.3", + "@atproto-labs/pipe": "0.1.1", + "ipaddr.js": "^2.1.0", + "undici": "^6.14.1" + } + }, + "sha512-Krq09nH/aeoiU2s9xdHA0FjTEFWG9B5FFenipv1iRixCcPc7V3DhTNDawxG9gI8Ny0k4dBVS9WTRN/IDzBx86Q==" + ], + "@atproto-labs/handle-resolver": [ + "@atproto-labs/handle-resolver@0.3.5", + "", + { + "dependencies": { + "@atproto-labs/simple-store": "0.3.0", + "@atproto-labs/simple-store-memory": "0.1.4", + "@atproto/did": "0.2.4", + "zod": "^3.23.8" + } + }, + "sha512-r3b+plCh/0arN535Aool9gL6yTSbAPDOyReURbA2TWAaeW4vrSJPwR6yYUx0k0vmVPjkZPIdUVd63bG/+VG5MA==" + ], + "@atproto-labs/handle-resolver-node": [ + "@atproto-labs/handle-resolver-node@0.1.24", + "", + { + "dependencies": { + "@atproto-labs/fetch-node": "0.2.0", + "@atproto-labs/handle-resolver": "0.3.5", + "@atproto/did": "0.2.4" + } + }, + "sha512-w/zvktigmRQpOLQQclp48tbb2K/2XW8j1szoIpT8T8v6P5dZ8GGVDIEF142xQMX9vWToFqMTu1P2yOuz8e3Ilg==" + ], + "@atproto-labs/identity-resolver": [ + "@atproto-labs/identity-resolver@0.3.5", + "", + { + "dependencies": { + "@atproto-labs/did-resolver": "0.2.5", + "@atproto-labs/handle-resolver": "0.3.5" + } + }, + "sha512-kSxnreUSPhKL77doUbSl/9I6Y9qpkpD7MMJoYFQVU/WG0PB90tzfIb6DNuWsjbU2I5Q91Nzc4Tm4VJMV+OPKGQ==" + ], + "@atproto-labs/pipe": [ + "@atproto-labs/pipe@0.1.1", + "", + {}, + "sha512-hdNw2oUs2B6BN1lp+32pF7cp8EMKuIN5Qok2Vvv/aOpG/3tNSJ9YkvfI0k6Zd188LeDDYRUpYpxcoFIcGH/FNg==" + ], + "@atproto-labs/simple-store": [ + "@atproto-labs/simple-store@0.3.0", + "", + {}, + "sha512-nOb6ONKBRJHRlukW1sVawUkBqReLlLx6hT35VS3imaNPwiXDxLnTK7lxw3Lrl9k5yugSBDQAkZAq3MPTEFSUBQ==" + ], + "@atproto-labs/simple-store-memory": [ + "@atproto-labs/simple-store-memory@0.1.4", + "", + { + "dependencies": { + "@atproto-labs/simple-store": "0.3.0", + "lru-cache": "^10.2.0" + } + }, + "sha512-3mKY4dP8I7yKPFj9VKpYyCRzGJOi5CEpOLPlRhoJyLmgs3J4RzDrjn323Oakjz2Aj2JzRU/AIvWRAZVhpYNJHw==" + ], + "@atproto/api": [ + "@atproto/api@0.18.13", + "", + { + "dependencies": { + "@atproto/common-web": "^0.4.11", + "@atproto/lexicon": "^0.6.0", + "@atproto/syntax": "^0.4.2", + "@atproto/xrpc": "^0.7.7", + "await-lock": "^2.2.2", + "multiformats": "^9.9.0", + "tlds": "^1.234.0", + "zod": "^3.23.8" + } + }, + "sha512-CULZ01pSJDltLS/Gc9MMrhFzB6OM3ezyZw7KoeLT/sBfwgA1ddA4mWdTh7DIRosPRigXtA05bnoiCutZbQDo+Q==" + ], + "@atproto/common-web": [ + "@atproto/common-web@0.4.11", + "", + { + "dependencies": { + "@atproto/lex-data": "0.0.7", + "@atproto/lex-json": "0.0.7", + "zod": "^3.23.8" + } + }, + "sha512-VHejNmSABU8/03VrQ3e36AmT5U3UIeio+qSUqCrO1oNgrJcWfGy1rpj0FVtUugWF8Un29+yzkukzWGZfXL70rQ==" + ], + "@atproto/did": [ + "@atproto/did@0.2.4", + "", + { + "dependencies": { + "zod": "^3.23.8" + } + }, + "sha512-nxNiCgXeo7pfjojq9fpfZxCO0X0xUipNVKW+AHNZwQKiUDt6zYL0VXEfm8HBUwQOCmKvj2pRRSM1Cur+tUWk3g==" + ], + "@atproto/jwk": [ + "@atproto/jwk@0.6.0", + "", + { + "dependencies": { + "multiformats": "^9.9.0", + "zod": "^3.23.8" + } + }, + "sha512-bDoJPvt7TrQVi/rBfBrSSpGykhtIriKxeYCYQTiPRKFfyRhbgpElF0wPXADjIswnbzZdOwbY63az4E/CFVT3Tw==" + ], + "@atproto/jwk-jose": [ + "@atproto/jwk-jose@0.1.11", + "", + { + "dependencies": { + "@atproto/jwk": "0.6.0", + "jose": "^5.2.0" + } + }, + "sha512-i4Fnr2sTBYmMmHXl7NJh8GrCH+tDQEVWrcDMDnV5DjJfkgT17wIqvojIw9SNbSL4Uf0OtfEv6AgG0A+mgh8b5Q==" + ], + "@atproto/jwk-webcrypto": [ + "@atproto/jwk-webcrypto@0.2.0", + "", + { + "dependencies": { + "@atproto/jwk": "0.6.0", + "@atproto/jwk-jose": "0.1.11", + "zod": "^3.23.8" + } + }, + "sha512-UmgRrrEAkWvxwhlwe30UmDOdTEFidlIzBC7C3cCbeJMcBN1x8B3KH+crXrsTqfWQBG58mXgt8wgSK3Kxs2LhFg==" + ], + "@atproto/lex-data": [ + "@atproto/lex-data@0.0.7", + "", + { + "dependencies": { + "@atproto/syntax": "0.4.2", + "multiformats": "^9.9.0", + "tslib": "^2.8.1", + "uint8arrays": "3.0.0", + "unicode-segmenter": "^0.14.0" + } + }, + "sha512-W/Q5o9o7n2Sv3UywckChu01X5lwQUtaiiOkGJLnRsdkQTyC6813nPgY+p2sG7NwwM+82lu+FUV9fE/Ul3VqaJw==" + ], + "@atproto/lex-json": [ + "@atproto/lex-json@0.0.7", + "", + { + "dependencies": { + "@atproto/lex-data": "0.0.7", + "tslib": "^2.8.1" + } + }, + "sha512-bjNPD5M/MhLfjNM7tcxuls80UgXpHqxdOxDXEUouAtZQV/nIDhGjmNUvKxOmOgnDsiZRnT2g5y3onrnjH3a44g==" + ], + "@atproto/lexicon": [ + "@atproto/lexicon@0.6.0", + "", + { + "dependencies": { + "@atproto/common-web": "^0.4.7", + "@atproto/syntax": "^0.4.2", + "iso-datestring-validator": "^2.2.2", + "multiformats": "^9.9.0", + "zod": "^3.23.8" + } + }, + "sha512-5veb8aD+J5M0qszLJ+73KSFsFrJBgAY/nM1TSAJvGY7fNc9ZAT+PSUlmIyrdye9YznAZ07yktalls/TwNV7cHQ==" + ], + "@atproto/oauth-client": [ + "@atproto/oauth-client@0.5.13", + "", + { + "dependencies": { + "@atproto-labs/did-resolver": "0.2.5", + "@atproto-labs/fetch": "0.2.3", + "@atproto-labs/handle-resolver": "0.3.5", + "@atproto-labs/identity-resolver": "0.3.5", + "@atproto-labs/simple-store": "0.3.0", + "@atproto-labs/simple-store-memory": "0.1.4", + "@atproto/did": "0.2.4", + "@atproto/jwk": "0.6.0", + "@atproto/oauth-types": "0.6.1", + "@atproto/xrpc": "0.7.7", + "core-js": "^3", + "multiformats": "^9.9.0", + "zod": "^3.23.8" + } + }, + "sha512-FLbqHkC7BAVZ90LHVzSxQf+s8ZNIQI4TsDuhYDyzi7lYtktFHDbgd88KuM2ClJFOtGCsSS17yR1Joy925tDSaA==" + ], + "@atproto/oauth-client-node": [ + "@atproto/oauth-client-node@0.3.15", + "", + { + "dependencies": { + "@atproto-labs/did-resolver": "0.2.5", + "@atproto-labs/handle-resolver-node": "0.1.24", + "@atproto-labs/simple-store": "0.3.0", + "@atproto/did": "0.2.4", + "@atproto/jwk": "0.6.0", + "@atproto/jwk-jose": "0.1.11", + "@atproto/jwk-webcrypto": "0.2.0", + "@atproto/oauth-client": "0.5.13", + "@atproto/oauth-types": "0.6.1" + } + }, + "sha512-iuT7QrLli7IyB4px1+lHvm/YoIRfNRpbNG9seJRtu5eX4N5aLsBP6vpXs9rCygd1+/15LcLRAAGKVEcrLT9tXA==" + ], + "@atproto/oauth-types": [ + "@atproto/oauth-types@0.6.1", + "", + { + "dependencies": { + "@atproto/did": "0.2.4", + "@atproto/jwk": "0.6.0", + "zod": "^3.23.8" + } + }, + "sha512-3z92GN/6zCq9E2GTTfZM27tWEbvi1qwFSA7KoS5+wqBC4kSsLvnLxmbKH402Z40DfWS4YWqw0DkHsgP0LNFDEA==" + ], + "@atproto/syntax": [ + "@atproto/syntax@0.4.2", + "", + {}, + "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA==" + ], + "@atproto/xrpc": [ + "@atproto/xrpc@0.7.7", + "", + { + "dependencies": { + "@atproto/lexicon": "^0.6.0", + "zod": "^3.23.8" + } + }, + "sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA==" + ], + "@types/bun": [ + "@types/bun@1.3.5", + "", + { + "dependencies": { + "bun-types": "1.3.5" + } + }, + "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w==" + ], + "@types/node": [ + "@types/node@25.0.6", + "", + { + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "sha512-NNu0sjyNxpoiW3YuVFfNz7mxSQ+S4X2G28uqg2s+CzoqoQjLPsWSbsFFyztIAqt2vb8kfEAsJNepMGPTxFDx3Q==" + ], + "await-lock": [ + "await-lock@2.2.2", + "", + {}, + "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==" + ], + "bun-types": [ + "bun-types@1.3.5", + "", + { + "dependencies": { + "@types/node": "*" + } + }, + "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw==" + ], + "core-js": [ + "core-js@3.47.0", + "", + {}, + "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==" + ], + "hono": [ + "hono@4.11.3", + "", + {}, + "sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w==" + ], + "ipaddr.js": [ + "ipaddr.js@2.3.0", + "", + {}, + "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==" + ], + "iso-datestring-validator": [ + "iso-datestring-validator@2.2.2", + "", + {}, + "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==" + ], + "jose": [ + "jose@5.10.0", + "", + {}, + "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==" + ], + "lru-cache": [ + "lru-cache@10.4.3", + "", + {}, + "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + ], + "multiformats": [ + "multiformats@9.9.0", + "", + {}, + "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==" + ], + "tlds": [ + "tlds@1.261.0", + "", + { + "bin": { + "tlds": "bin.js" + } + }, + "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==" + ], + "tslib": [ + "tslib@2.8.1", + "", + {}, + "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + ], + "typescript": [ + "typescript@5.9.3", + "", + { + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + } + }, + "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==" + ], + "uint8arrays": [ + "uint8arrays@3.0.0", + "", + { + "dependencies": { + "multiformats": "^9.4.2" + } + }, + "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==" + ], + "undici": [ + "undici@6.23.0", + "", + {}, + "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==" + ], + "undici-types": [ + "undici-types@7.16.0", + "", + {}, + "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==" + ], + "unicode-segmenter": [ + "unicode-segmenter@0.14.5", + "", + {}, + "sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==" + ], + "zod": [ + "zod@3.25.76", + "", + {}, + "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==" + ], } } diff --git a/logrotate.conf b/logrotate.conf deleted file mode 100644 index e989b0b..0000000 --- a/logrotate.conf +++ /dev/null @@ -1,11 +0,0 @@ -/home/exedev/stdeditor/data/app.log /home/exedev/stdeditor/data/cleanup.log { - daily - rotate 14 - compress - delaycompress - missingok - notifempty - create 640 exedev exedev - dateext - dateformat -%Y%m%d -} diff --git a/package.json b/package.json index 1cdc4a4..ca99138 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "stdeditor", + "name": "std.pub", "module": "src/server.ts", "type": "module", "private": true, diff --git a/scripts/cleanup.ts b/scripts/cleanup.ts index 105b2d6..dfb68ec 100755 --- a/scripts/cleanup.ts +++ b/scripts/cleanup.ts @@ -2,36 +2,38 @@ /** * Database cleanup script * Removes expired OAuth states and optionally old sessions - * Run via cron: 0 * * * * /home/exedev/.bun/bin/bun /home/exedev/stdeditor/scripts/cleanup.ts + * Run via cron: 0 * * * * /home/exedev/.bun/bin/bun /home/exedev/std.pub/scripts/cleanup.ts */ -import { Database } from 'bun:sqlite'; -import * as path from 'path'; +import { Database } from "bun:sqlite"; +import * as path from "path"; -const DATA_DIR = process.env.DATA_DIR || './data'; -const DB_PATH = path.join(DATA_DIR, 'oauth.db'); +const DATA_DIR = process.env.DATA_DIR || "./data"; +const DB_PATH = path.join(DATA_DIR, "oauth.db"); try { const db = new Database(DB_PATH); - + // Clean up OAuth states older than 1 hour const statesResult = db.run( - `DELETE FROM oauth_states WHERE created_at < strftime('%s', 'now') - 3600` + `DELETE FROM oauth_states WHERE created_at < strftime('%s', 'now') - 3600`, ); - + // Clean up sessions older than 30 days (optional - sessions may still be valid) const sessionsResult = db.run( - `DELETE FROM oauth_sessions WHERE updated_at < strftime('%s', 'now') - 2592000` + `DELETE FROM oauth_sessions WHERE updated_at < strftime('%s', 'now') - 2592000`, ); - + // Vacuum the database to reclaim space - db.run('VACUUM'); - + db.run("VACUUM"); + const timestamp = new Date().toISOString(); - console.log(`[${timestamp}] Cleanup complete: removed old states and sessions, vacuumed database`); - + console.log( + `[${timestamp}] Cleanup complete: removed old states and sessions, vacuumed database`, + ); + db.close(); } catch (error) { - console.error('Cleanup failed:', error); + console.error("Cleanup failed:", error); process.exit(1); } diff --git a/src/lib/oauth.ts b/src/lib/oauth.ts index 9ea910d..0c7c9d5 100644 --- a/src/lib/oauth.ts +++ b/src/lib/oauth.ts @@ -1,16 +1,19 @@ -import { NodeOAuthClient } from '@atproto/oauth-client-node'; -import type { NodeSavedSession, NodeSavedState } from '@atproto/oauth-client-node'; -import { JoseKey } from '@atproto/jwk-jose'; -import { Agent } from '@atproto/api'; -import { Database } from 'bun:sqlite'; -import * as fs from 'fs'; -import * as path from 'path'; +import { NodeOAuthClient } from "@atproto/oauth-client-node"; +import type { + NodeSavedSession, + NodeSavedState, +} from "@atproto/oauth-client-node"; +import { JoseKey } from "@atproto/jwk-jose"; +import { Agent } from "@atproto/api"; +import { Database } from "bun:sqlite"; +import * as fs from "fs"; +import * as path from "path"; // Constants -const PUBLIC_URL = process.env.PUBLIC_URL || 'http://localhost:8000'; -const DATA_DIR = process.env.DATA_DIR || './data'; -const DB_PATH = path.join(DATA_DIR, 'oauth.db'); -const KEYS_PATH = path.join(DATA_DIR, 'private-key.json'); +const PUBLIC_URL = process.env.PUBLIC_URL || "http://localhost:8000"; +const DATA_DIR = process.env.DATA_DIR || "./data"; +const DB_PATH = path.join(DATA_DIR, "oauth.db"); +const KEYS_PATH = path.join(DATA_DIR, "private-key.json"); // Ensure data directory exists if (!fs.existsSync(DATA_DIR)) { @@ -38,7 +41,9 @@ db.run(` `); // Clean up old states (older than 1 hour) -db.run(`DELETE FROM oauth_states WHERE created_at < strftime('%s', 'now') - 3600`); +db.run( + `DELETE FROM oauth_states WHERE created_at < strftime('%s', 'now') - 3600`, +); // State store implementation const stateStore = { @@ -46,11 +51,13 @@ const stateStore = { const stateJson = JSON.stringify(state); db.run( `INSERT OR REPLACE INTO oauth_states (key, state, created_at) VALUES (?, ?, strftime('%s', 'now'))`, - [key, stateJson] + [key, stateJson], ); }, async get(key: string): Promise { - const row = db.query(`SELECT state FROM oauth_states WHERE key = ?`).get(key) as { state: string } | null; + const row = db + .query(`SELECT state FROM oauth_states WHERE key = ?`) + .get(key) as { state: string } | null; if (!row) return undefined; return JSON.parse(row.state); }, @@ -65,11 +72,13 @@ const sessionStore = { const sessionJson = JSON.stringify(session); db.run( `INSERT OR REPLACE INTO oauth_sessions (did, session, updated_at) VALUES (?, ?, strftime('%s', 'now'))`, - [did, sessionJson] + [did, sessionJson], ); }, async get(did: string): Promise { - const row = db.query(`SELECT session FROM oauth_sessions WHERE did = ?`).get(did) as { session: string } | null; + const row = db + .query(`SELECT session FROM oauth_sessions WHERE did = ?`) + .get(did) as { session: string } | null; if (!row) return undefined; return JSON.parse(row.session); }, @@ -81,17 +90,17 @@ const sessionStore = { // Generate or load private key for confidential client async function getOrCreatePrivateKey(): Promise { if (fs.existsSync(KEYS_PATH)) { - const keyData = JSON.parse(fs.readFileSync(KEYS_PATH, 'utf-8')); + const keyData = JSON.parse(fs.readFileSync(KEYS_PATH, "utf-8")); return JoseKey.fromJWK(keyData, keyData.kid); } - + // Generate a new ES256 key - const key = await JoseKey.generate(['ES256'], crypto.randomUUID()); + const key = await JoseKey.generate(["ES256"], crypto.randomUUID()); const jwk = key.privateJwk; - + // Save to disk with restrictive permissions (owner read/write only) fs.writeFileSync(KEYS_PATH, JSON.stringify(jwk, null, 2), { mode: 0o600 }); - + return key; } @@ -101,22 +110,22 @@ let initPromise: Promise | null = null; async function initOAuthClient(): Promise { if (oauthClientInstance) return oauthClientInstance; if (initPromise) return initPromise; - + initPromise = (async () => { const privateKey = await getOrCreatePrivateKey(); - + oauthClientInstance = new NodeOAuthClient({ clientMetadata: { client_id: `${PUBLIC_URL}/client-metadata.json`, - client_name: 'stdeditor', + client_name: "std.pub", client_uri: PUBLIC_URL, redirect_uris: [`${PUBLIC_URL}/auth/callback`], - scope: 'atproto transition:generic', - grant_types: ['authorization_code', 'refresh_token'], - response_types: ['code'], - application_type: 'web', - token_endpoint_auth_method: 'private_key_jwt', - token_endpoint_auth_signing_alg: 'ES256', + scope: "atproto transition:generic", + grant_types: ["authorization_code", "refresh_token"], + response_types: ["code"], + application_type: "web", + token_endpoint_auth_method: "private_key_jwt", + token_endpoint_auth_signing_alg: "ES256", dpop_bound_access_tokens: true, jwks_uri: `${PUBLIC_URL}/jwks.json`, }, @@ -145,19 +154,21 @@ export async function getJwks() { return client.jwks; } -export async function getAgentForSession(did: string): Promise<{ agent: Agent; did: string; handle: string }> { +export async function getAgentForSession( + did: string, +): Promise<{ agent: Agent; did: string; handle: string }> { const client = await getOAuthClient(); const oauthSession = await client.restore(did); - + if (!oauthSession) { - throw new Error('Session not found'); + throw new Error("Session not found"); } const agent = new Agent(oauthSession); - + // Fetch profile to get handle const profile = await agent.getProfile({ actor: did }); - + return { agent, did, diff --git a/src/routes/auth.ts b/src/routes/auth.ts index 8650d6d..fd6368b 100644 --- a/src/routes/auth.ts +++ b/src/routes/auth.ts @@ -1,151 +1,166 @@ -import { Hono } from 'hono'; -import { getCookie, setCookie, deleteCookie } from 'hono/cookie'; -import { html } from 'hono/html'; -import { getOAuthClient, getClientMetadata, getJwks, deleteSession } from '../lib/oauth'; -import { layout } from '../views/layouts/main'; -import { csrfField } from '../lib/csrf'; +import { Hono } from "hono"; +import { getCookie, setCookie, deleteCookie } from "hono/cookie"; +import { html } from "hono/html"; +import { + getOAuthClient, + getClientMetadata, + getJwks, + deleteSession, +} from "../lib/oauth"; +import { layout } from "../views/layouts/main"; +import { csrfField } from "../lib/csrf"; export const authRoutes = new Hono(); // Client metadata endpoint (required for OAuth) -authRoutes.get('/client-metadata.json', async (c) => { +authRoutes.get("/client-metadata.json", async (c) => { try { const metadata = await getClientMetadata(); return c.json(metadata); } catch (error) { - console.error('Error getting client metadata:', error); - return c.json({ error: 'Failed to get client metadata' }, 500); + console.error("Error getting client metadata:", error); + return c.json({ error: "Failed to get client metadata" }, 500); } }); // JWKS endpoint (required for confidential clients) -authRoutes.get('/jwks.json', async (c) => { +authRoutes.get("/jwks.json", async (c) => { try { const jwks = await getJwks(); return c.json(jwks); } catch (error) { - console.error('Error getting JWKS:', error); - return c.json({ error: 'Failed to get JWKS' }, 500); + console.error("Error getting JWKS:", error); + return c.json({ error: "Failed to get JWKS" }, 500); } }); // Login page -authRoutes.get('/login', async (c) => { - const error = c.req.query('error'); - const csrfToken = c.get('csrfToken') as string; - +authRoutes.get("/login", async (c) => { + const error = c.req.query("error"); + const csrfToken = c.get("csrfToken") as string; + const content = html`

Login with Bluesky

- - ${error ? html` -
- ${error === 'handle_required' ? 'Please enter your handle or DID.' : - error === 'authorization_failed' ? 'Authorization failed. Please try again.' : - error === 'callback_failed' ? 'Login failed. Please try again.' : - 'An error occurred. Please try again.'} -
- ` : ''} - + + ${error + ? html` +
+ ${error === "handle_required" + ? "Please enter your handle or DID." + : error === "authorization_failed" + ? "Authorization failed. Please try again." + : error === "callback_failed" + ? "Login failed. Please try again." + : "An error occurred. Please try again."} +
+ ` + : ""} +
${csrfField(csrfToken)}
- - Enter your Bluesky handle (e.g., yourname.bsky.social) or DID + Enter your Bluesky handle (e.g., yourname.bsky.social) or + DID
`; - - return c.html(layout(content, { title: 'Login - stdeditor' })); + + return c.html(layout(content, { title: "Login - std.pub" })); }); // Handle login form submission -authRoutes.post('/login', async (c) => { +authRoutes.post("/login", async (c) => { const body = await c.req.parseBody(); let handle = body.handle as string; - + if (!handle) { - return c.redirect('/auth/login?error=handle_required'); + return c.redirect("/auth/login?error=handle_required"); } - + // Trim and normalize handle handle = handle.trim().toLowerCase(); - + // Remove @ prefix if present - if (handle.startsWith('@')) { + if (handle.startsWith("@")) { handle = handle.slice(1); } - + try { const client = await getOAuthClient(); const url = await client.authorize(handle, { - scope: 'atproto transition:generic', + scope: "atproto transition:generic", }); - + return c.redirect(url.toString()); } catch (error) { - console.error('Login error:', error); - return c.redirect('/auth/login?error=authorization_failed'); + console.error("Login error:", error); + return c.redirect("/auth/login?error=authorization_failed"); } }); // OAuth callback -authRoutes.get('/callback', async (c) => { +authRoutes.get("/callback", async (c) => { const url = new URL(c.req.url); const params = url.searchParams; - + // Check for error from authorization server - const error = params.get('error'); + const error = params.get("error"); if (error) { - console.error('OAuth error:', error, params.get('error_description')); - return c.redirect('/auth/login?error=callback_failed'); + console.error("OAuth error:", error, params.get("error_description")); + return c.redirect("/auth/login?error=callback_failed"); } - + try { const client = await getOAuthClient(); const { session } = await client.callback(params); - + // Store the DID in a cookie for session management // The actual OAuth session is stored in the database by the OAuth client - setCookie(c, 'session', session.did, { + setCookie(c, "session", session.did, { httpOnly: true, - secure: process.env.NODE_ENV === 'production' || process.env.PUBLIC_URL?.startsWith('https'), - sameSite: 'Lax', + secure: + process.env.NODE_ENV === "production" || + process.env.PUBLIC_URL?.startsWith("https"), + sameSite: "Lax", maxAge: 60 * 60 * 24 * 7, // 7 days - path: '/', + path: "/", }); - - return c.redirect('/'); + + return c.redirect("/"); } catch (error) { - console.error('Callback error:', error); - return c.redirect('/auth/login?error=callback_failed'); + console.error("Callback error:", error); + return c.redirect("/auth/login?error=callback_failed"); } }); // Logout -authRoutes.get('/logout', async (c) => { - const did = getCookie(c, 'session'); - +authRoutes.get("/logout", async (c) => { + const did = getCookie(c, "session"); + if (did) { try { // Delete the OAuth session from the database await deleteSession(did); } catch (error) { - console.error('Error deleting session:', error); + console.error("Error deleting session:", error); } } - - deleteCookie(c, 'session', { path: '/' }); - return c.redirect('/'); + + deleteCookie(c, "session", { path: "/" }); + return c.redirect("/"); }); diff --git a/src/routes/documents.ts b/src/routes/documents.ts index 80a7d37..a624846 100644 --- a/src/routes/documents.ts +++ b/src/routes/documents.ts @@ -1,25 +1,25 @@ -import { Hono } from 'hono'; -import { html, raw } from 'hono/html'; -import { layout } from '../views/layouts/main'; -import { requireAuth, type Session } from '../lib/session'; -import { csrfField } from '../lib/csrf'; -import { isValidTID } from '../lib/validation'; +import { Hono } from "hono"; +import { html, raw } from "hono/html"; +import { layout } from "../views/layouts/main"; +import { requireAuth, type Session } from "../lib/session"; +import { csrfField } from "../lib/csrf"; +import { isValidTID } from "../lib/validation"; export const documentRoutes = new Hono(); -const DOCUMENT_COLLECTION = 'site.standard.document'; -const PUBLICATION_COLLECTION = 'site.standard.publication'; +const DOCUMENT_COLLECTION = "site.standard.document"; +const PUBLICATION_COLLECTION = "site.standard.publication"; // List all documents -documentRoutes.get('/', async (c) => { +documentRoutes.get("/", async (c) => { let session: Session; try { session = requireAuth(c); } catch { - return c.redirect('/auth/login'); + return c.redirect("/auth/login"); } - const filter = c.req.query('filter') || 'all'; + const filter = c.req.query("filter") || "all"; try { const response = await session.agent!.com.atproto.repo.listRecords({ @@ -31,22 +31,26 @@ documentRoutes.get('/', async (c) => { let documents = response.data.records; // Filter by draft/published status - if (filter === 'drafts') { + if (filter === "drafts") { documents = documents.filter((doc: any) => { const tags = doc.value.tags || []; - return tags.includes('draft'); + return tags.includes("draft"); }); - } else if (filter === 'published') { + } else if (filter === "published") { documents = documents.filter((doc: any) => { const tags = doc.value.tags || []; - return !tags.includes('draft'); + return !tags.includes("draft"); }); } // Sort by publishedAt or updatedAt documents.sort((a: any, b: any) => { - const dateA = new Date(a.value.updatedAt || a.value.publishedAt).getTime(); - const dateB = new Date(b.value.updatedAt || b.value.publishedAt).getTime(); + const dateA = new Date( + a.value.updatedAt || a.value.publishedAt, + ).getTime(); + const dateB = new Date( + b.value.updatedAt || b.value.publishedAt, + ).getTime(); return dateB - dateA; }); @@ -56,59 +60,84 @@ documentRoutes.get('/', async (c) => {

Documents

New Document - + - - ${documents.length === 0 ? html` -

No documents yet. Create your first document.

- ` : html` -
    - ${documents.map((doc: any) => { - const rkey = doc.uri.split('/').pop(); - const value = doc.value; - const isDraft = (value.tags || []).includes('draft'); - const date = value.publishedAt ? new Date(value.publishedAt).toLocaleDateString() : ''; - - return html` -
  • - - ${value.title} - - ${isDraft ? html`Draft` : ''} - ${date} - - -
  • - `; - })} -
- `} + + ${documents.length === 0 + ? html` +

+ No documents yet. + Create your first document. +

+ ` + : html` +
    + ${documents.map((doc: any) => { + const rkey = doc.uri.split("/").pop(); + const value = doc.value; + const isDraft = (value.tags || []).includes("draft"); + const date = value.publishedAt + ? new Date(value.publishedAt).toLocaleDateString() + : ""; + + return html` +
  • + + ${value.title} + + ${isDraft + ? html`Draft` + : ""} + ${date} + + +
  • + `; + })} +
+ `} `; - return c.html(layout(content, { title: 'Documents - stdeditor', session })); + return c.html(layout(content, { title: "Documents - std.pub", session })); } catch (error) { - console.error('Error fetching documents:', error); - const content = html`

Error loading documents. Please try again.

`; - return c.html(layout(content, { title: 'Documents - stdeditor', session })); + console.error("Error fetching documents:", error); + const content = html`

+ Error loading documents. Please try again. +

`; + return c.html(layout(content, { title: "Documents - std.pub", session })); } }); // New document form -documentRoutes.get('/new', async (c) => { +documentRoutes.get("/new", async (c) => { let session: Session; try { session = requireAuth(c); } catch { - return c.redirect('/auth/login'); + return c.redirect("/auth/login"); } // Get publication to use as site reference - let publicationUri = ''; + let publicationUri = ""; try { const response = await session.agent!.com.atproto.repo.listRecords({ repo: session.did!, @@ -122,94 +151,127 @@ documentRoutes.get('/new', async (c) => { // No publication yet, will need URL } - const csrfToken = c.get('csrfToken') as string; + const csrfToken = c.get("csrfToken") as string; const content = html`

New Document

- +
${csrfField(csrfToken)} - +
- +
The URL path for this document (e.g., /my-post)
- +
- +
- +
- +
- +
- +
- +
- - + +
- + `; - return c.html(layout(content, { title: 'New Document - stdeditor', session })); + return c.html(layout(content, { title: "New Document - std.pub", session })); }); // Handle document creation -documentRoutes.post('/new', async (c) => { +documentRoutes.post("/new", async (c) => { let session: Session; try { session = requireAuth(c); } catch { - return c.redirect('/auth/login'); + return c.redirect("/auth/login"); } const body = await c.req.parseBody(); const title = body.title as string; - const path = body.path as string || undefined; - const description = body.description as string || undefined; - const content = body.content as string || undefined; - const tagsStr = body.tags as string || ''; + const path = (body.path as string) || undefined; + const description = (body.description as string) || undefined; + const content = (body.content as string) || undefined; + const tagsStr = (body.tags as string) || ""; const action = body.action as string; const publicationUri = body.publicationUri as string; // Parse tags - let tags = tagsStr.split(',').map(t => t.trim()).filter(t => t); - + let tags = tagsStr + .split(",") + .map((t) => t.trim()) + .filter((t) => t); + // If publishing, remove draft tag - if (action === 'publish') { - tags = tags.filter(t => t !== 'draft'); - } else if (!tags.includes('draft')) { - tags.push('draft'); + if (action === "publish") { + tags = tags.filter((t) => t !== "draft"); + } else if (!tags.includes("draft")) { + tags.push("draft"); } const now = new Date().toISOString(); @@ -228,11 +290,11 @@ documentRoutes.post('/new', async (c) => { $type: DOCUMENT_COLLECTION, title, site, - publishedAt: action === 'publish' ? now : now, + publishedAt: action === "publish" ? now : now, updatedAt: now, }; - if (path) record.path = path.startsWith('/') ? path : `/${path}`; + if (path) record.path = path.startsWith("/") ? path : `/${path}`; if (description) record.description = description; if (content) record.textContent = content; if (tags.length > 0) record.tags = tags; @@ -246,25 +308,25 @@ documentRoutes.post('/new', async (c) => { return c.redirect(`/documents/${rkey}`); } catch (error) { - console.error('Error creating document:', error); - return c.redirect('/documents/new?error=create_failed'); + console.error("Error creating document:", error); + return c.redirect("/documents/new?error=create_failed"); } }); // View single document -documentRoutes.get('/:rkey', async (c) => { +documentRoutes.get("/:rkey", async (c) => { let session: Session; try { session = requireAuth(c); } catch { - return c.redirect('/auth/login'); + return c.redirect("/auth/login"); } - const rkey = c.req.param('rkey'); - + const rkey = c.req.param("rkey"); + // Validate rkey format if (!isValidTID(rkey)) { - return c.redirect('/documents'); + return c.redirect("/documents"); } try { @@ -275,40 +337,66 @@ documentRoutes.get('/:rkey', async (c) => { }); const doc = response.data.value as any; - const isDraft = (doc.tags || []).includes('draft'); - const csrfToken = c.get('csrfToken') as string; + const isDraft = (doc.tags || []).includes("draft"); + const csrfToken = c.get("csrfToken") as string; const content = html`

${doc.title}

- ${isDraft ? html`Draft` : html`Published`} - ${doc.publishedAt ? html`Published: ${new Date(doc.publishedAt).toLocaleDateString()}` : ''} - ${doc.path ? html`Path: ${doc.path}` : ''} + ${isDraft + ? html`Draft` + : html`Published`} + ${doc.publishedAt + ? html`Published: + ${new Date(doc.publishedAt).toLocaleDateString()}` + : ""} + ${doc.path ? html`Path: ${doc.path}` : ""}
- - ${doc.description ? html`

${doc.description}

` : ''} - + + ${doc.description + ? html`

${doc.description}

` + : ""} +
-
${doc.textContent || '(No content)'}
+
${doc.textContent || "(No content)"}
- +
Edit - ${isDraft ? html` -
- ${csrfField(csrfToken)} - -
- ` : html` -
- ${csrfField(csrfToken)} - -
- `} -
+ ${isDraft + ? html` + + ${csrfField(csrfToken)} + +
+ ` + : html` +
+ ${csrfField(csrfToken)} + +
+ `} +
${csrfField(csrfToken)}
@@ -317,26 +405,28 @@ documentRoutes.get('/:rkey', async (c) => {
`; - return c.html(layout(content, { title: `${doc.title} - stdeditor`, session })); + return c.html( + layout(content, { title: `${doc.title} - std.pub`, session }), + ); } catch (error) { - console.error('Error fetching document:', error); - return c.redirect('/documents'); + console.error("Error fetching document:", error); + return c.redirect("/documents"); } }); // Edit document form -documentRoutes.get('/:rkey/edit', async (c) => { +documentRoutes.get("/:rkey/edit", async (c) => { let session: Session; try { session = requireAuth(c); } catch { - return c.redirect('/auth/login'); + return c.redirect("/auth/login"); } - const rkey = c.req.param('rkey'); - + const rkey = c.req.param("rkey"); + if (!isValidTID(rkey)) { - return c.redirect('/documents'); + return c.redirect("/documents"); } try { @@ -347,39 +437,74 @@ documentRoutes.get('/:rkey/edit', async (c) => { }); const doc = response.data.value as any; - const csrfToken = c.get('csrfToken') as string; + const csrfToken = c.get("csrfToken") as string; const content = html`

Edit Document

- -
+ + ${csrfField(csrfToken)}
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
Cancel @@ -388,28 +513,30 @@ documentRoutes.get('/:rkey/edit', async (c) => {
`; - return c.html(layout(content, { title: `Edit: ${doc.title} - stdeditor`, session })); + return c.html( + layout(content, { title: `Edit: ${doc.title} - std.pub`, session }), + ); } catch (error) { - console.error('Error fetching document:', error); - return c.redirect('/documents'); + console.error("Error fetching document:", error); + return c.redirect("/documents"); } }); // Handle document update -documentRoutes.post('/:rkey/edit', async (c) => { +documentRoutes.post("/:rkey/edit", async (c) => { let session: Session; try { session = requireAuth(c); } catch { - return c.redirect('/auth/login'); + return c.redirect("/auth/login"); } - const rkey = c.req.param('rkey'); - + const rkey = c.req.param("rkey"); + if (!isValidTID(rkey)) { - return c.redirect('/documents'); + return c.redirect("/documents"); } - + const body = await c.req.parseBody(); try { @@ -423,11 +550,14 @@ documentRoutes.post('/:rkey/edit', async (c) => { const oldDoc = existing.data.value as any; const title = body.title as string; - const path = body.path as string || undefined; - const description = body.description as string || undefined; - const content = body.content as string || undefined; - const tagsStr = body.tags as string || ''; - const tags = tagsStr.split(',').map(t => t.trim()).filter(t => t); + const path = (body.path as string) || undefined; + const description = (body.description as string) || undefined; + const content = (body.content as string) || undefined; + const tagsStr = (body.tags as string) || ""; + const tags = tagsStr + .split(",") + .map((t) => t.trim()) + .filter((t) => t); const record: Record = { $type: DOCUMENT_COLLECTION, @@ -437,7 +567,7 @@ documentRoutes.post('/:rkey/edit', async (c) => { updatedAt: new Date().toISOString(), }; - if (path) record.path = path.startsWith('/') ? path : `/${path}`; + if (path) record.path = path.startsWith("/") ? path : `/${path}`; if (description) record.description = description; if (content) record.textContent = content; if (tags.length > 0) record.tags = tags; @@ -451,24 +581,24 @@ documentRoutes.post('/:rkey/edit', async (c) => { return c.redirect(`/documents/${rkey}`); } catch (error) { - console.error('Error updating document:', error); + console.error("Error updating document:", error); return c.redirect(`/documents/${rkey}/edit?error=update_failed`); } }); // Publish document -documentRoutes.post('/:rkey/publish', async (c) => { +documentRoutes.post("/:rkey/publish", async (c) => { let session: Session; try { session = requireAuth(c); } catch { - return c.redirect('/auth/login'); + return c.redirect("/auth/login"); } - const rkey = c.req.param('rkey'); - + const rkey = c.req.param("rkey"); + if (!isValidTID(rkey)) { - return c.redirect('/documents'); + return c.redirect("/documents"); } try { @@ -479,7 +609,7 @@ documentRoutes.post('/:rkey/publish', async (c) => { }); const doc = existing.data.value as any; - const tags = (doc.tags || []).filter((t: string) => t !== 'draft'); + const tags = (doc.tags || []).filter((t: string) => t !== "draft"); const record = { ...doc, @@ -497,24 +627,24 @@ documentRoutes.post('/:rkey/publish', async (c) => { return c.redirect(`/documents/${rkey}`); } catch (error) { - console.error('Error publishing document:', error); + console.error("Error publishing document:", error); return c.redirect(`/documents/${rkey}?error=publish_failed`); } }); // Unpublish document (add draft tag) -documentRoutes.post('/:rkey/unpublish', async (c) => { +documentRoutes.post("/:rkey/unpublish", async (c) => { let session: Session; try { session = requireAuth(c); } catch { - return c.redirect('/auth/login'); + return c.redirect("/auth/login"); } - const rkey = c.req.param('rkey'); - + const rkey = c.req.param("rkey"); + if (!isValidTID(rkey)) { - return c.redirect('/documents'); + return c.redirect("/documents"); } try { @@ -525,7 +655,7 @@ documentRoutes.post('/:rkey/unpublish', async (c) => { }); const doc = existing.data.value as any; - const tags = [...(doc.tags || []), 'draft']; + const tags = [...(doc.tags || []), "draft"]; const record = { ...doc, @@ -542,24 +672,24 @@ documentRoutes.post('/:rkey/unpublish', async (c) => { return c.redirect(`/documents/${rkey}`); } catch (error) { - console.error('Error unpublishing document:', error); + console.error("Error unpublishing document:", error); return c.redirect(`/documents/${rkey}?error=unpublish_failed`); } }); // Delete document -documentRoutes.post('/:rkey/delete', async (c) => { +documentRoutes.post("/:rkey/delete", async (c) => { let session: Session; try { session = requireAuth(c); } catch { - return c.redirect('/auth/login'); + return c.redirect("/auth/login"); } - const rkey = c.req.param('rkey'); - + const rkey = c.req.param("rkey"); + if (!isValidTID(rkey)) { - return c.redirect('/documents'); + return c.redirect("/documents"); } try { @@ -569,9 +699,9 @@ documentRoutes.post('/:rkey/delete', async (c) => { rkey, }); - return c.redirect('/documents'); + return c.redirect("/documents"); } catch (error) { - console.error('Error deleting document:', error); + console.error("Error deleting document:", error); return c.redirect(`/documents/${rkey}?error=delete_failed`); } }); @@ -581,5 +711,5 @@ function generateTID(): string { const now = Date.now() * 1000; const clockId = Math.floor(Math.random() * 1024); const tid = (BigInt(now) << 10n) | BigInt(clockId); - return tid.toString(36).padStart(13, '0'); + return tid.toString(36).padStart(13, "0"); } diff --git a/src/routes/publication.ts b/src/routes/publication.ts index c3bfb55..37d5322 100644 --- a/src/routes/publication.ts +++ b/src/routes/publication.ts @@ -1,20 +1,20 @@ -import { Hono } from 'hono'; -import { html } from 'hono/html'; -import { layout } from '../views/layouts/main'; -import { requireAuth, type Session } from '../lib/session'; -import { csrfField } from '../lib/csrf'; +import { Hono } from "hono"; +import { html } from "hono/html"; +import { layout } from "../views/layouts/main"; +import { requireAuth, type Session } from "../lib/session"; +import { csrfField } from "../lib/csrf"; export const publicationRoutes = new Hono(); -const PUBLICATION_COLLECTION = 'site.standard.publication'; +const PUBLICATION_COLLECTION = "site.standard.publication"; // View/manage publication -publicationRoutes.get('/', async (c) => { +publicationRoutes.get("/", async (c) => { let session: Session; try { session = requireAuth(c); } catch { - return c.redirect('/auth/login'); + return c.redirect("/auth/login"); } try { @@ -32,83 +32,108 @@ publicationRoutes.get('/', async (c) => { const content = html`

Your Publication

- +

${pub.name}

-

${pub.url}

- ${pub.description ? html`

${pub.description}

` : ''} +

+ ${pub.url} +

+ ${pub.description + ? html`

${pub.description}

` + : ""}
- +
`; - return c.html(layout(content, { title: 'Publication - stdeditor', session })); + return c.html( + layout(content, { title: "Publication - std.pub", session }), + ); } // No publication exists, show create form - return c.redirect('/publication/new'); + return c.redirect("/publication/new"); } catch (error) { - console.error('Error fetching publication:', error); - return c.redirect('/publication/new'); + console.error("Error fetching publication:", error); + return c.redirect("/publication/new"); } }); // New publication form -publicationRoutes.get('/new', async (c) => { +publicationRoutes.get("/new", async (c) => { let session: Session; try { session = requireAuth(c); } catch { - return c.redirect('/auth/login'); + return c.redirect("/auth/login"); } - const csrfToken = c.get('csrfToken') as string; + const csrfToken = c.get("csrfToken") as string; const content = html`

Create Publication

- + ${csrfField(csrfToken)}
- +
- - The base URL of your publication (without trailing slash) + + The base URL of your publication (without trailing slash)
- +
- +
- - + +
`; - return c.html(layout(content, { title: 'New Publication - stdeditor', session })); + return c.html( + layout(content, { title: "New Publication - std.pub", session }), + ); }); // Handle publication creation -publicationRoutes.post('/new', async (c) => { +publicationRoutes.post("/new", async (c) => { let session: Session; try { session = requireAuth(c); } catch { - return c.redirect('/auth/login'); + return c.redirect("/auth/login"); } const body = await c.req.parseBody(); const name = body.name as string; - const url = (body.url as string).replace(/\/$/, ''); // Remove trailing slash - const description = body.description as string || undefined; + const url = (body.url as string).replace(/\/$/, ""); // Remove trailing slash + const description = (body.description as string) || undefined; try { // Generate a TID for the record key @@ -126,20 +151,20 @@ publicationRoutes.post('/new', async (c) => { }, }); - return c.redirect('/publication'); + return c.redirect("/publication"); } catch (error) { - console.error('Error creating publication:', error); - return c.redirect('/publication/new?error=create_failed'); + console.error("Error creating publication:", error); + return c.redirect("/publication/new?error=create_failed"); } }); // Edit publication form -publicationRoutes.get('/edit', async (c) => { +publicationRoutes.get("/edit", async (c) => { let session: Session; try { session = requireAuth(c); } catch { - return c.redirect('/auth/login'); + return c.redirect("/auth/login"); } try { @@ -151,64 +176,80 @@ publicationRoutes.get('/edit', async (c) => { const publication = response.data.records[0]; if (!publication) { - return c.redirect('/publication/new'); + return c.redirect("/publication/new"); } const pub = publication.value as any; - const rkey = publication.uri.split('/').pop(); + const rkey = publication.uri.split("/").pop(); - const csrfToken = c.get('csrfToken') as string; + const csrfToken = c.get("csrfToken") as string; const content = html`

Edit Publication

- +
${csrfField(csrfToken)} - +
- +
- +
- +
- +
- + Cancel
`; - return c.html(layout(content, { title: 'Edit Publication - stdeditor', session })); + return c.html( + layout(content, { title: "Edit Publication - std.pub", session }), + ); } catch (error) { - console.error('Error fetching publication:', error); - return c.redirect('/publication'); + console.error("Error fetching publication:", error); + return c.redirect("/publication"); } }); // Handle publication update -publicationRoutes.post('/edit', async (c) => { +publicationRoutes.post("/edit", async (c) => { let session: Session; try { session = requireAuth(c); } catch { - return c.redirect('/auth/login'); + return c.redirect("/auth/login"); } const body = await c.req.parseBody(); const rkey = body.rkey as string; const name = body.name as string; - const url = (body.url as string).replace(/\/$/, ''); - const description = body.description as string || undefined; + const url = (body.url as string).replace(/\/$/, ""); + const description = (body.description as string) || undefined; try { await session.agent!.com.atproto.repo.putRecord({ @@ -223,10 +264,10 @@ publicationRoutes.post('/edit', async (c) => { }, }); - return c.redirect('/publication'); + return c.redirect("/publication"); } catch (error) { - console.error('Error updating publication:', error); - return c.redirect('/publication/edit?error=update_failed'); + console.error("Error updating publication:", error); + return c.redirect("/publication/edit?error=update_failed"); } }); @@ -235,5 +276,5 @@ function generateTID(): string { const now = Date.now() * 1000; // microseconds const clockId = Math.floor(Math.random() * 1024); const tid = (BigInt(now) << 10n) | BigInt(clockId); - return tid.toString(36).padStart(13, '0'); + return tid.toString(36).padStart(13, "0"); } diff --git a/src/views/home.ts b/src/views/home.ts index b7d54eb..464769a 100644 --- a/src/views/home.ts +++ b/src/views/home.ts @@ -1,5 +1,5 @@ -import { html } from 'hono/html'; -import type { Session } from '../lib/session'; +import { html } from "hono/html"; +import type { Session } from "../lib/session"; export function homePage(session: Session) { if (session.did) { @@ -7,7 +7,7 @@ export function homePage(session: Session) {

Welcome, @${session.handle}

Manage your standard.site publication and documents.

- +
Manage Publication View Documents @@ -16,12 +16,15 @@ export function homePage(session: Session) {
`; } - + return html`
-

stdeditor

-

A minimal web UI for managing standard.site publications and documents in your Bluesky PDS.

- +

std.pub

+

+ A minimal web UI for managing standard.site publications and documents + in your Bluesky PDS. +

+

📝 Publish

@@ -36,8 +39,10 @@ export function homePage(session: Session) {

Uses the standard.site lexicon for interoperability

- - Login with Bluesky + + Login with Bluesky
`; } diff --git a/src/views/layouts/main.ts b/src/views/layouts/main.ts index d6df964..24240ba 100644 --- a/src/views/layouts/main.ts +++ b/src/views/layouts/main.ts @@ -1,5 +1,5 @@ -import { html } from 'hono/html'; -import type { Session } from '../../lib/session'; +import { html } from "hono/html"; +import type { Session } from "../../lib/session"; interface LayoutOptions { title?: string; @@ -8,40 +8,41 @@ interface LayoutOptions { } export function layout(content: string, options: LayoutOptions = {}) { - const { title = 'stdeditor', session } = options; - + const { title = "std.pub", session } = options; + return html` - - - - - - ${title} - - - -
- -
-
- ${content} -
- - - -`; + + + + + + ${title} + + + +
+ +
+
${content}
+ + + + `; } diff --git a/stdeditor.service b/stdeditor.service deleted file mode 100644 index 4498a46..0000000 --- a/stdeditor.service +++ /dev/null @@ -1,17 +0,0 @@ -[Unit] -Description=stdeditor - standard.site editor -After=network.target - -[Service] -Type=simple -User=exedev -WorkingDirectory=/home/exedev/stdeditor -Environment=PATH=/home/exedev/.bun/bin:/usr/local/bin:/usr/bin:/bin -Environment=PUBLIC_URL=https://stdeditor.exe.xyz -Environment=PORT=8000 -ExecStart=/home/exedev/.bun/bin/bun run src/server.ts -Restart=on-failure -RestartSec=5 - -[Install] -WantedBy=multi-user.target -- 2.51.2