tangled
alpha
login
or
join now
seth.computer
/
sitebase
0
fork
atom
this repo has no description
0
fork
atom
overview
issues
pulls
pipelines
Rename: sitebase
seth.computer
1 month ago
52815573
d77a09bf
verified
This commit was signed with the committer's
known signature
.
seth.computer
SSH Key Fingerprint:
SHA256:utUtG8j2hgvZ0Rnm/rPJiqFu4NT5bjOnC26AUIBh500=
+1520
-990
17 changed files
expand all
collapse all
unified
split
README.md
bun.lock
fly.toml
package.json
packages
cli
package.json
src
index.ts
core
package.json
web
CLAUDE.md
bun.lock
package.json
scripts
cleanup.ts
src
lib
oauth.ts
routes
auth.ts
documents.ts
publication.ts
views
home.ts
layouts
main.ts
+1
-1
README.md
···
1
1
-
# std.pub
1
1
+
# sitebase
2
2
3
3
To install dependencies:
4
4
+594
-130
bun.lock
···
3
3
"configVersion": 1,
4
4
"workspaces": {
5
5
"": {
6
6
-
"name": "site-editor",
6
6
+
"name": "sitebase",
7
7
"devDependencies": {
8
8
"@biomejs/biome": "^2.3.11",
9
9
},
10
10
},
11
11
"packages/cli": {
12
12
-
"name": "@stdsite/cli",
12
12
+
"name": "@sitebase/cli",
13
13
"version": "0.0.1",
14
14
"bin": {
15
15
-
"stdsite": "./src/index.ts",
15
15
+
"sitebase": "./src/index.ts",
16
16
},
17
17
"dependencies": {
18
18
-
"@stdsite/cli": ".",
19
19
-
"@stdsite/core": "workspace:*",
18
18
+
"@sitebase/cli": ".",
19
19
+
"@sitebase/core": "workspace:*",
20
20
"commander": "^12.1.0",
21
21
},
22
22
},
23
23
"packages/core": {
24
24
-
"name": "@stdsite/core",
24
24
+
"name": "@sitebase/core",
25
25
"version": "0.0.1",
26
26
"dependencies": {
27
27
"handlebars": "^4.7.8",
28
28
},
29
29
},
30
30
"packages/web": {
31
31
-
"name": "@site-editor/web",
31
31
+
"name": "@sitebase/web",
32
32
"dependencies": {
33
33
"@atproto/api": "^0.18.13",
34
34
"@atproto/jwk-jose": "^0.1.11",
···
45
45
},
46
46
},
47
47
"packages": {
48
48
-
"@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=="],
49
49
-
50
50
-
"@atproto-labs/fetch": ["@atproto-labs/fetch@0.2.3", "", { "dependencies": { "@atproto-labs/pipe": "0.1.1" } }, "sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw=="],
51
51
-
52
52
-
"@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=="],
53
53
-
54
54
-
"@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=="],
55
55
-
56
56
-
"@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=="],
57
57
-
58
58
-
"@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=="],
59
59
-
60
60
-
"@atproto-labs/pipe": ["@atproto-labs/pipe@0.1.1", "", {}, "sha512-hdNw2oUs2B6BN1lp+32pF7cp8EMKuIN5Qok2Vvv/aOpG/3tNSJ9YkvfI0k6Zd188LeDDYRUpYpxcoFIcGH/FNg=="],
61
61
-
62
62
-
"@atproto-labs/simple-store": ["@atproto-labs/simple-store@0.3.0", "", {}, "sha512-nOb6ONKBRJHRlukW1sVawUkBqReLlLx6hT35VS3imaNPwiXDxLnTK7lxw3Lrl9k5yugSBDQAkZAq3MPTEFSUBQ=="],
63
63
-
64
64
-
"@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=="],
65
65
-
66
66
-
"@atproto/api": ["@atproto/api@0.18.14", "", { "dependencies": { "@atproto/common-web": "^0.4.12", "@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-1pWAPbuG3RA1o8uOAwYWZOddvNjuweYOxwTvys1q/r9NCjoGkZY0uJUy1dr6LKFaDk8bjikd2O1cgsRwFfv6Fw=="],
67
67
-
68
68
-
"@atproto/common-web": ["@atproto/common-web@0.4.12", "", { "dependencies": { "@atproto/lex-data": "0.0.8", "@atproto/lex-json": "0.0.8", "zod": "^3.23.8" } }, "sha512-3aCJemqM/fkHQrVPbTCHCdiVstKFI+2LkFLvUhO6XZP0EqUZa/rg/CIZBKTFUWu9I5iYiaEiXL9VwcDRpEevSw=="],
69
69
-
70
70
-
"@atproto/did": ["@atproto/did@0.2.4", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-nxNiCgXeo7pfjojq9fpfZxCO0X0xUipNVKW+AHNZwQKiUDt6zYL0VXEfm8HBUwQOCmKvj2pRRSM1Cur+tUWk3g=="],
71
71
-
72
72
-
"@atproto/jwk": ["@atproto/jwk@0.6.0", "", { "dependencies": { "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-bDoJPvt7TrQVi/rBfBrSSpGykhtIriKxeYCYQTiPRKFfyRhbgpElF0wPXADjIswnbzZdOwbY63az4E/CFVT3Tw=="],
73
73
-
74
74
-
"@atproto/jwk-jose": ["@atproto/jwk-jose@0.1.11", "", { "dependencies": { "@atproto/jwk": "0.6.0", "jose": "^5.2.0" } }, "sha512-i4Fnr2sTBYmMmHXl7NJh8GrCH+tDQEVWrcDMDnV5DjJfkgT17wIqvojIw9SNbSL4Uf0OtfEv6AgG0A+mgh8b5Q=="],
75
75
-
76
76
-
"@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=="],
77
77
-
78
78
-
"@atproto/lex-data": ["@atproto/lex-data@0.0.8", "", { "dependencies": { "@atproto/syntax": "0.4.2", "multiformats": "^9.9.0", "tslib": "^2.8.1", "uint8arrays": "3.0.0", "unicode-segmenter": "^0.14.0" } }, "sha512-1Y5tz7BkS7380QuLNXaE8GW8Xba+mRWugt8BKM4BUFYjjUZdmirU8lr72iM4XlEBrzRu8Cfvj+MbsbYaZv+IgA=="],
79
79
-
80
80
-
"@atproto/lex-json": ["@atproto/lex-json@0.0.8", "", { "dependencies": { "@atproto/lex-data": "0.0.8", "tslib": "^2.8.1" } }, "sha512-w1Qmkae1QhmNz+i1Zm3xr3jp0UPPRENmdlpU0qIrdxWDo9W4Mzkeyc3eSoa+Zs+zN8xkRSQw7RLZte/B7Ipdwg=="],
81
81
-
82
82
-
"@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=="],
83
83
-
84
84
-
"@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=="],
85
85
-
86
86
-
"@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=="],
87
87
-
88
88
-
"@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=="],
89
89
-
90
90
-
"@atproto/syntax": ["@atproto/syntax@0.4.2", "", {}, "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA=="],
91
91
-
92
92
-
"@atproto/xrpc": ["@atproto/xrpc@0.7.7", "", { "dependencies": { "@atproto/lexicon": "^0.6.0", "zod": "^3.23.8" } }, "sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA=="],
93
93
-
94
94
-
"@biomejs/biome": ["@biomejs/biome@2.3.11", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.11", "@biomejs/cli-darwin-x64": "2.3.11", "@biomejs/cli-linux-arm64": "2.3.11", "@biomejs/cli-linux-arm64-musl": "2.3.11", "@biomejs/cli-linux-x64": "2.3.11", "@biomejs/cli-linux-x64-musl": "2.3.11", "@biomejs/cli-win32-arm64": "2.3.11", "@biomejs/cli-win32-x64": "2.3.11" }, "bin": { "biome": "bin/biome" } }, "sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ=="],
95
95
-
96
96
-
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA=="],
97
97
-
98
98
-
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg=="],
99
99
-
100
100
-
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g=="],
101
101
-
102
102
-
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg=="],
103
103
-
104
104
-
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg=="],
105
105
-
106
106
-
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw=="],
107
107
-
108
108
-
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw=="],
109
109
-
110
110
-
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.11", "", { "os": "win32", "cpu": "x64" }, "sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg=="],
111
111
-
112
112
-
"@site-editor/web": ["@site-editor/web@workspace:packages/web"],
113
113
-
114
114
-
"@stdsite/cli": ["@stdsite/cli@workspace:packages/cli"],
115
115
-
116
116
-
"@stdsite/core": ["@stdsite/core@workspace:packages/core"],
117
117
-
118
118
-
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
119
119
-
120
120
-
"@types/node": ["@types/node@25.0.8", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg=="],
121
121
-
122
122
-
"await-lock": ["await-lock@2.2.2", "", {}, "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw=="],
123
123
-
124
124
-
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
125
125
-
126
126
-
"commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
127
127
-
128
128
-
"core-js": ["core-js@3.47.0", "", {}, "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg=="],
129
129
-
130
130
-
"handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="],
131
131
-
132
132
-
"hono": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="],
133
133
-
134
134
-
"ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
135
135
-
136
136
-
"iso-datestring-validator": ["iso-datestring-validator@2.2.2", "", {}, "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA=="],
137
137
-
138
138
-
"jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="],
139
139
-
140
140
-
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
141
141
-
142
142
-
"marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="],
143
143
-
144
144
-
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
145
145
-
146
146
-
"multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],
147
147
-
148
148
-
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
149
149
-
150
150
-
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
151
151
-
152
152
-
"tlds": ["tlds@1.261.0", "", { "bin": { "tlds": "bin.js" } }, "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA=="],
153
153
-
154
154
-
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
155
155
-
156
156
-
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
157
157
-
158
158
-
"uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="],
159
159
-
160
160
-
"uint8arrays": ["uint8arrays@3.0.0", "", { "dependencies": { "multiformats": "^9.4.2" } }, "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA=="],
161
161
-
162
162
-
"undici": ["undici@6.23.0", "", {}, "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g=="],
163
163
-
164
164
-
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
165
165
-
166
166
-
"unicode-segmenter": ["unicode-segmenter@0.14.5", "", {}, "sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g=="],
167
167
-
168
168
-
"wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="],
169
169
-
170
170
-
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
48
48
+
"@atproto-labs/did-resolver": [
49
49
+
"@atproto-labs/did-resolver@0.2.5",
50
50
+
"",
51
51
+
{
52
52
+
"dependencies": {
53
53
+
"@atproto-labs/fetch": "0.2.3",
54
54
+
"@atproto-labs/pipe": "0.1.1",
55
55
+
"@atproto-labs/simple-store": "0.3.0",
56
56
+
"@atproto-labs/simple-store-memory": "0.1.4",
57
57
+
"@atproto/did": "0.2.4",
58
58
+
"zod": "^3.23.8"
59
59
+
}
60
60
+
},
61
61
+
"sha512-he7EC6OMSifNs01a4RT9mta/yYitoKDzlK9ty2TFV5Uj/+HpB4vYMRdIDFrRW0Hcsehy90E2t/dw0t7361MEKQ=="
62
62
+
],
63
63
+
"@atproto-labs/fetch": [
64
64
+
"@atproto-labs/fetch@0.2.3",
65
65
+
"",
66
66
+
{
67
67
+
"dependencies": {
68
68
+
"@atproto-labs/pipe": "0.1.1"
69
69
+
}
70
70
+
},
71
71
+
"sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw=="
72
72
+
],
73
73
+
"@atproto-labs/fetch-node": [
74
74
+
"@atproto-labs/fetch-node@0.2.0",
75
75
+
"",
76
76
+
{
77
77
+
"dependencies": {
78
78
+
"@atproto-labs/fetch": "0.2.3",
79
79
+
"@atproto-labs/pipe": "0.1.1",
80
80
+
"ipaddr.js": "^2.1.0",
81
81
+
"undici": "^6.14.1"
82
82
+
}
83
83
+
},
84
84
+
"sha512-Krq09nH/aeoiU2s9xdHA0FjTEFWG9B5FFenipv1iRixCcPc7V3DhTNDawxG9gI8Ny0k4dBVS9WTRN/IDzBx86Q=="
85
85
+
],
86
86
+
"@atproto-labs/handle-resolver": [
87
87
+
"@atproto-labs/handle-resolver@0.3.5",
88
88
+
"",
89
89
+
{
90
90
+
"dependencies": {
91
91
+
"@atproto-labs/simple-store": "0.3.0",
92
92
+
"@atproto-labs/simple-store-memory": "0.1.4",
93
93
+
"@atproto/did": "0.2.4",
94
94
+
"zod": "^3.23.8"
95
95
+
}
96
96
+
},
97
97
+
"sha512-r3b+plCh/0arN535Aool9gL6yTSbAPDOyReURbA2TWAaeW4vrSJPwR6yYUx0k0vmVPjkZPIdUVd63bG/+VG5MA=="
98
98
+
],
99
99
+
"@atproto-labs/handle-resolver-node": [
100
100
+
"@atproto-labs/handle-resolver-node@0.1.24",
101
101
+
"",
102
102
+
{
103
103
+
"dependencies": {
104
104
+
"@atproto-labs/fetch-node": "0.2.0",
105
105
+
"@atproto-labs/handle-resolver": "0.3.5",
106
106
+
"@atproto/did": "0.2.4"
107
107
+
}
108
108
+
},
109
109
+
"sha512-w/zvktigmRQpOLQQclp48tbb2K/2XW8j1szoIpT8T8v6P5dZ8GGVDIEF142xQMX9vWToFqMTu1P2yOuz8e3Ilg=="
110
110
+
],
111
111
+
"@atproto-labs/identity-resolver": [
112
112
+
"@atproto-labs/identity-resolver@0.3.5",
113
113
+
"",
114
114
+
{
115
115
+
"dependencies": {
116
116
+
"@atproto-labs/did-resolver": "0.2.5",
117
117
+
"@atproto-labs/handle-resolver": "0.3.5"
118
118
+
}
119
119
+
},
120
120
+
"sha512-kSxnreUSPhKL77doUbSl/9I6Y9qpkpD7MMJoYFQVU/WG0PB90tzfIb6DNuWsjbU2I5Q91Nzc4Tm4VJMV+OPKGQ=="
121
121
+
],
122
122
+
"@atproto-labs/pipe": [
123
123
+
"@atproto-labs/pipe@0.1.1",
124
124
+
"",
125
125
+
{},
126
126
+
"sha512-hdNw2oUs2B6BN1lp+32pF7cp8EMKuIN5Qok2Vvv/aOpG/3tNSJ9YkvfI0k6Zd188LeDDYRUpYpxcoFIcGH/FNg=="
127
127
+
],
128
128
+
"@atproto-labs/simple-store": [
129
129
+
"@atproto-labs/simple-store@0.3.0",
130
130
+
"",
131
131
+
{},
132
132
+
"sha512-nOb6ONKBRJHRlukW1sVawUkBqReLlLx6hT35VS3imaNPwiXDxLnTK7lxw3Lrl9k5yugSBDQAkZAq3MPTEFSUBQ=="
133
133
+
],
134
134
+
"@atproto-labs/simple-store-memory": [
135
135
+
"@atproto-labs/simple-store-memory@0.1.4",
136
136
+
"",
137
137
+
{
138
138
+
"dependencies": {
139
139
+
"@atproto-labs/simple-store": "0.3.0",
140
140
+
"lru-cache": "^10.2.0"
141
141
+
}
142
142
+
},
143
143
+
"sha512-3mKY4dP8I7yKPFj9VKpYyCRzGJOi5CEpOLPlRhoJyLmgs3J4RzDrjn323Oakjz2Aj2JzRU/AIvWRAZVhpYNJHw=="
144
144
+
],
145
145
+
"@atproto/api": [
146
146
+
"@atproto/api@0.18.14",
147
147
+
"",
148
148
+
{
149
149
+
"dependencies": {
150
150
+
"@atproto/common-web": "^0.4.12",
151
151
+
"@atproto/lexicon": "^0.6.0",
152
152
+
"@atproto/syntax": "^0.4.2",
153
153
+
"@atproto/xrpc": "^0.7.7",
154
154
+
"await-lock": "^2.2.2",
155
155
+
"multiformats": "^9.9.0",
156
156
+
"tlds": "^1.234.0",
157
157
+
"zod": "^3.23.8"
158
158
+
}
159
159
+
},
160
160
+
"sha512-1pWAPbuG3RA1o8uOAwYWZOddvNjuweYOxwTvys1q/r9NCjoGkZY0uJUy1dr6LKFaDk8bjikd2O1cgsRwFfv6Fw=="
161
161
+
],
162
162
+
"@atproto/common-web": [
163
163
+
"@atproto/common-web@0.4.12",
164
164
+
"",
165
165
+
{
166
166
+
"dependencies": {
167
167
+
"@atproto/lex-data": "0.0.8",
168
168
+
"@atproto/lex-json": "0.0.8",
169
169
+
"zod": "^3.23.8"
170
170
+
}
171
171
+
},
172
172
+
"sha512-3aCJemqM/fkHQrVPbTCHCdiVstKFI+2LkFLvUhO6XZP0EqUZa/rg/CIZBKTFUWu9I5iYiaEiXL9VwcDRpEevSw=="
173
173
+
],
174
174
+
"@atproto/did": [
175
175
+
"@atproto/did@0.2.4",
176
176
+
"",
177
177
+
{
178
178
+
"dependencies": {
179
179
+
"zod": "^3.23.8"
180
180
+
}
181
181
+
},
182
182
+
"sha512-nxNiCgXeo7pfjojq9fpfZxCO0X0xUipNVKW+AHNZwQKiUDt6zYL0VXEfm8HBUwQOCmKvj2pRRSM1Cur+tUWk3g=="
183
183
+
],
184
184
+
"@atproto/jwk": [
185
185
+
"@atproto/jwk@0.6.0",
186
186
+
"",
187
187
+
{
188
188
+
"dependencies": {
189
189
+
"multiformats": "^9.9.0",
190
190
+
"zod": "^3.23.8"
191
191
+
}
192
192
+
},
193
193
+
"sha512-bDoJPvt7TrQVi/rBfBrSSpGykhtIriKxeYCYQTiPRKFfyRhbgpElF0wPXADjIswnbzZdOwbY63az4E/CFVT3Tw=="
194
194
+
],
195
195
+
"@atproto/jwk-jose": [
196
196
+
"@atproto/jwk-jose@0.1.11",
197
197
+
"",
198
198
+
{
199
199
+
"dependencies": {
200
200
+
"@atproto/jwk": "0.6.0",
201
201
+
"jose": "^5.2.0"
202
202
+
}
203
203
+
},
204
204
+
"sha512-i4Fnr2sTBYmMmHXl7NJh8GrCH+tDQEVWrcDMDnV5DjJfkgT17wIqvojIw9SNbSL4Uf0OtfEv6AgG0A+mgh8b5Q=="
205
205
+
],
206
206
+
"@atproto/jwk-webcrypto": [
207
207
+
"@atproto/jwk-webcrypto@0.2.0",
208
208
+
"",
209
209
+
{
210
210
+
"dependencies": {
211
211
+
"@atproto/jwk": "0.6.0",
212
212
+
"@atproto/jwk-jose": "0.1.11",
213
213
+
"zod": "^3.23.8"
214
214
+
}
215
215
+
},
216
216
+
"sha512-UmgRrrEAkWvxwhlwe30UmDOdTEFidlIzBC7C3cCbeJMcBN1x8B3KH+crXrsTqfWQBG58mXgt8wgSK3Kxs2LhFg=="
217
217
+
],
218
218
+
"@atproto/lex-data": [
219
219
+
"@atproto/lex-data@0.0.8",
220
220
+
"",
221
221
+
{
222
222
+
"dependencies": {
223
223
+
"@atproto/syntax": "0.4.2",
224
224
+
"multiformats": "^9.9.0",
225
225
+
"tslib": "^2.8.1",
226
226
+
"uint8arrays": "3.0.0",
227
227
+
"unicode-segmenter": "^0.14.0"
228
228
+
}
229
229
+
},
230
230
+
"sha512-1Y5tz7BkS7380QuLNXaE8GW8Xba+mRWugt8BKM4BUFYjjUZdmirU8lr72iM4XlEBrzRu8Cfvj+MbsbYaZv+IgA=="
231
231
+
],
232
232
+
"@atproto/lex-json": [
233
233
+
"@atproto/lex-json@0.0.8",
234
234
+
"",
235
235
+
{
236
236
+
"dependencies": {
237
237
+
"@atproto/lex-data": "0.0.8",
238
238
+
"tslib": "^2.8.1"
239
239
+
}
240
240
+
},
241
241
+
"sha512-w1Qmkae1QhmNz+i1Zm3xr3jp0UPPRENmdlpU0qIrdxWDo9W4Mzkeyc3eSoa+Zs+zN8xkRSQw7RLZte/B7Ipdwg=="
242
242
+
],
243
243
+
"@atproto/lexicon": [
244
244
+
"@atproto/lexicon@0.6.0",
245
245
+
"",
246
246
+
{
247
247
+
"dependencies": {
248
248
+
"@atproto/common-web": "^0.4.7",
249
249
+
"@atproto/syntax": "^0.4.2",
250
250
+
"iso-datestring-validator": "^2.2.2",
251
251
+
"multiformats": "^9.9.0",
252
252
+
"zod": "^3.23.8"
253
253
+
}
254
254
+
},
255
255
+
"sha512-5veb8aD+J5M0qszLJ+73KSFsFrJBgAY/nM1TSAJvGY7fNc9ZAT+PSUlmIyrdye9YznAZ07yktalls/TwNV7cHQ=="
256
256
+
],
257
257
+
"@atproto/oauth-client": [
258
258
+
"@atproto/oauth-client@0.5.13",
259
259
+
"",
260
260
+
{
261
261
+
"dependencies": {
262
262
+
"@atproto-labs/did-resolver": "0.2.5",
263
263
+
"@atproto-labs/fetch": "0.2.3",
264
264
+
"@atproto-labs/handle-resolver": "0.3.5",
265
265
+
"@atproto-labs/identity-resolver": "0.3.5",
266
266
+
"@atproto-labs/simple-store": "0.3.0",
267
267
+
"@atproto-labs/simple-store-memory": "0.1.4",
268
268
+
"@atproto/did": "0.2.4",
269
269
+
"@atproto/jwk": "0.6.0",
270
270
+
"@atproto/oauth-types": "0.6.1",
271
271
+
"@atproto/xrpc": "0.7.7",
272
272
+
"core-js": "^3",
273
273
+
"multiformats": "^9.9.0",
274
274
+
"zod": "^3.23.8"
275
275
+
}
276
276
+
},
277
277
+
"sha512-FLbqHkC7BAVZ90LHVzSxQf+s8ZNIQI4TsDuhYDyzi7lYtktFHDbgd88KuM2ClJFOtGCsSS17yR1Joy925tDSaA=="
278
278
+
],
279
279
+
"@atproto/oauth-client-node": [
280
280
+
"@atproto/oauth-client-node@0.3.15",
281
281
+
"",
282
282
+
{
283
283
+
"dependencies": {
284
284
+
"@atproto-labs/did-resolver": "0.2.5",
285
285
+
"@atproto-labs/handle-resolver-node": "0.1.24",
286
286
+
"@atproto-labs/simple-store": "0.3.0",
287
287
+
"@atproto/did": "0.2.4",
288
288
+
"@atproto/jwk": "0.6.0",
289
289
+
"@atproto/jwk-jose": "0.1.11",
290
290
+
"@atproto/jwk-webcrypto": "0.2.0",
291
291
+
"@atproto/oauth-client": "0.5.13",
292
292
+
"@atproto/oauth-types": "0.6.1"
293
293
+
}
294
294
+
},
295
295
+
"sha512-iuT7QrLli7IyB4px1+lHvm/YoIRfNRpbNG9seJRtu5eX4N5aLsBP6vpXs9rCygd1+/15LcLRAAGKVEcrLT9tXA=="
296
296
+
],
297
297
+
"@atproto/oauth-types": [
298
298
+
"@atproto/oauth-types@0.6.1",
299
299
+
"",
300
300
+
{
301
301
+
"dependencies": {
302
302
+
"@atproto/did": "0.2.4",
303
303
+
"@atproto/jwk": "0.6.0",
304
304
+
"zod": "^3.23.8"
305
305
+
}
306
306
+
},
307
307
+
"sha512-3z92GN/6zCq9E2GTTfZM27tWEbvi1qwFSA7KoS5+wqBC4kSsLvnLxmbKH402Z40DfWS4YWqw0DkHsgP0LNFDEA=="
308
308
+
],
309
309
+
"@atproto/syntax": [
310
310
+
"@atproto/syntax@0.4.2",
311
311
+
"",
312
312
+
{},
313
313
+
"sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA=="
314
314
+
],
315
315
+
"@atproto/xrpc": [
316
316
+
"@atproto/xrpc@0.7.7",
317
317
+
"",
318
318
+
{
319
319
+
"dependencies": {
320
320
+
"@atproto/lexicon": "^0.6.0",
321
321
+
"zod": "^3.23.8"
322
322
+
}
323
323
+
},
324
324
+
"sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA=="
325
325
+
],
326
326
+
"@biomejs/biome": [
327
327
+
"@biomejs/biome@2.3.11",
328
328
+
"",
329
329
+
{
330
330
+
"optionalDependencies": {
331
331
+
"@biomejs/cli-darwin-arm64": "2.3.11",
332
332
+
"@biomejs/cli-darwin-x64": "2.3.11",
333
333
+
"@biomejs/cli-linux-arm64": "2.3.11",
334
334
+
"@biomejs/cli-linux-arm64-musl": "2.3.11",
335
335
+
"@biomejs/cli-linux-x64": "2.3.11",
336
336
+
"@biomejs/cli-linux-x64-musl": "2.3.11",
337
337
+
"@biomejs/cli-win32-arm64": "2.3.11",
338
338
+
"@biomejs/cli-win32-x64": "2.3.11"
339
339
+
},
340
340
+
"bin": {
341
341
+
"biome": "bin/biome"
342
342
+
}
343
343
+
},
344
344
+
"sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ=="
345
345
+
],
346
346
+
"@biomejs/cli-darwin-arm64": [
347
347
+
"@biomejs/cli-darwin-arm64@2.3.11",
348
348
+
"",
349
349
+
{
350
350
+
"os": "darwin",
351
351
+
"cpu": "arm64"
352
352
+
},
353
353
+
"sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA=="
354
354
+
],
355
355
+
"@biomejs/cli-darwin-x64": [
356
356
+
"@biomejs/cli-darwin-x64@2.3.11",
357
357
+
"",
358
358
+
{
359
359
+
"os": "darwin",
360
360
+
"cpu": "x64"
361
361
+
},
362
362
+
"sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg=="
363
363
+
],
364
364
+
"@biomejs/cli-linux-arm64": [
365
365
+
"@biomejs/cli-linux-arm64@2.3.11",
366
366
+
"",
367
367
+
{
368
368
+
"os": "linux",
369
369
+
"cpu": "arm64"
370
370
+
},
371
371
+
"sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g=="
372
372
+
],
373
373
+
"@biomejs/cli-linux-arm64-musl": [
374
374
+
"@biomejs/cli-linux-arm64-musl@2.3.11",
375
375
+
"",
376
376
+
{
377
377
+
"os": "linux",
378
378
+
"cpu": "arm64"
379
379
+
},
380
380
+
"sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg=="
381
381
+
],
382
382
+
"@biomejs/cli-linux-x64": [
383
383
+
"@biomejs/cli-linux-x64@2.3.11",
384
384
+
"",
385
385
+
{
386
386
+
"os": "linux",
387
387
+
"cpu": "x64"
388
388
+
},
389
389
+
"sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg=="
390
390
+
],
391
391
+
"@biomejs/cli-linux-x64-musl": [
392
392
+
"@biomejs/cli-linux-x64-musl@2.3.11",
393
393
+
"",
394
394
+
{
395
395
+
"os": "linux",
396
396
+
"cpu": "x64"
397
397
+
},
398
398
+
"sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw=="
399
399
+
],
400
400
+
"@biomejs/cli-win32-arm64": [
401
401
+
"@biomejs/cli-win32-arm64@2.3.11",
402
402
+
"",
403
403
+
{
404
404
+
"os": "win32",
405
405
+
"cpu": "arm64"
406
406
+
},
407
407
+
"sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw=="
408
408
+
],
409
409
+
"@biomejs/cli-win32-x64": [
410
410
+
"@biomejs/cli-win32-x64@2.3.11",
411
411
+
"",
412
412
+
{
413
413
+
"os": "win32",
414
414
+
"cpu": "x64"
415
415
+
},
416
416
+
"sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg=="
417
417
+
],
418
418
+
"@sitebase/web": [
419
419
+
"@sitebase/web@workspace:packages/web"
420
420
+
],
421
421
+
"@sitebase/cli": [
422
422
+
"@sitebase/cli@workspace:packages/cli"
423
423
+
],
424
424
+
"@sitebase/core": [
425
425
+
"@sitebase/core@workspace:packages/core"
426
426
+
],
427
427
+
"@types/bun": [
428
428
+
"@types/bun@1.3.6",
429
429
+
"",
430
430
+
{
431
431
+
"dependencies": {
432
432
+
"bun-types": "1.3.6"
433
433
+
}
434
434
+
},
435
435
+
"sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="
436
436
+
],
437
437
+
"@types/node": [
438
438
+
"@types/node@25.0.8",
439
439
+
"",
440
440
+
{
441
441
+
"dependencies": {
442
442
+
"undici-types": "~7.16.0"
443
443
+
}
444
444
+
},
445
445
+
"sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg=="
446
446
+
],
447
447
+
"await-lock": [
448
448
+
"await-lock@2.2.2",
449
449
+
"",
450
450
+
{},
451
451
+
"sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw=="
452
452
+
],
453
453
+
"bun-types": [
454
454
+
"bun-types@1.3.6",
455
455
+
"",
456
456
+
{
457
457
+
"dependencies": {
458
458
+
"@types/node": "*"
459
459
+
}
460
460
+
},
461
461
+
"sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="
462
462
+
],
463
463
+
"commander": [
464
464
+
"commander@12.1.0",
465
465
+
"",
466
466
+
{},
467
467
+
"sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="
468
468
+
],
469
469
+
"core-js": [
470
470
+
"core-js@3.47.0",
471
471
+
"",
472
472
+
{},
473
473
+
"sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg=="
474
474
+
],
475
475
+
"handlebars": [
476
476
+
"handlebars@4.7.8",
477
477
+
"",
478
478
+
{
479
479
+
"dependencies": {
480
480
+
"minimist": "^1.2.5",
481
481
+
"neo-async": "^2.6.2",
482
482
+
"source-map": "^0.6.1",
483
483
+
"wordwrap": "^1.0.0"
484
484
+
},
485
485
+
"optionalDependencies": {
486
486
+
"uglify-js": "^3.1.4"
487
487
+
},
488
488
+
"bin": {
489
489
+
"handlebars": "bin/handlebars"
490
490
+
}
491
491
+
},
492
492
+
"sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="
493
493
+
],
494
494
+
"hono": [
495
495
+
"hono@4.11.4",
496
496
+
"",
497
497
+
{},
498
498
+
"sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="
499
499
+
],
500
500
+
"ipaddr.js": [
501
501
+
"ipaddr.js@2.3.0",
502
502
+
"",
503
503
+
{},
504
504
+
"sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="
505
505
+
],
506
506
+
"iso-datestring-validator": [
507
507
+
"iso-datestring-validator@2.2.2",
508
508
+
"",
509
509
+
{},
510
510
+
"sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA=="
511
511
+
],
512
512
+
"jose": [
513
513
+
"jose@5.10.0",
514
514
+
"",
515
515
+
{},
516
516
+
"sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="
517
517
+
],
518
518
+
"lru-cache": [
519
519
+
"lru-cache@10.4.3",
520
520
+
"",
521
521
+
{},
522
522
+
"sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
523
523
+
],
524
524
+
"marked": [
525
525
+
"marked@15.0.12",
526
526
+
"",
527
527
+
{
528
528
+
"bin": {
529
529
+
"marked": "bin/marked.js"
530
530
+
}
531
531
+
},
532
532
+
"sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="
533
533
+
],
534
534
+
"minimist": [
535
535
+
"minimist@1.2.8",
536
536
+
"",
537
537
+
{},
538
538
+
"sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="
539
539
+
],
540
540
+
"multiformats": [
541
541
+
"multiformats@9.9.0",
542
542
+
"",
543
543
+
{},
544
544
+
"sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="
545
545
+
],
546
546
+
"neo-async": [
547
547
+
"neo-async@2.6.2",
548
548
+
"",
549
549
+
{},
550
550
+
"sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
551
551
+
],
552
552
+
"source-map": [
553
553
+
"source-map@0.6.1",
554
554
+
"",
555
555
+
{},
556
556
+
"sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
557
557
+
],
558
558
+
"tlds": [
559
559
+
"tlds@1.261.0",
560
560
+
"",
561
561
+
{
562
562
+
"bin": {
563
563
+
"tlds": "bin.js"
564
564
+
}
565
565
+
},
566
566
+
"sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA=="
567
567
+
],
568
568
+
"tslib": [
569
569
+
"tslib@2.8.1",
570
570
+
"",
571
571
+
{},
572
572
+
"sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
573
573
+
],
574
574
+
"typescript": [
575
575
+
"typescript@5.9.3",
576
576
+
"",
577
577
+
{
578
578
+
"bin": {
579
579
+
"tsc": "bin/tsc",
580
580
+
"tsserver": "bin/tsserver"
581
581
+
}
582
582
+
},
583
583
+
"sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="
584
584
+
],
585
585
+
"uglify-js": [
586
586
+
"uglify-js@3.19.3",
587
587
+
"",
588
588
+
{
589
589
+
"bin": {
590
590
+
"uglifyjs": "bin/uglifyjs"
591
591
+
}
592
592
+
},
593
593
+
"sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="
594
594
+
],
595
595
+
"uint8arrays": [
596
596
+
"uint8arrays@3.0.0",
597
597
+
"",
598
598
+
{
599
599
+
"dependencies": {
600
600
+
"multiformats": "^9.4.2"
601
601
+
}
602
602
+
},
603
603
+
"sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA=="
604
604
+
],
605
605
+
"undici": [
606
606
+
"undici@6.23.0",
607
607
+
"",
608
608
+
{},
609
609
+
"sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g=="
610
610
+
],
611
611
+
"undici-types": [
612
612
+
"undici-types@7.16.0",
613
613
+
"",
614
614
+
{},
615
615
+
"sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="
616
616
+
],
617
617
+
"unicode-segmenter": [
618
618
+
"unicode-segmenter@0.14.5",
619
619
+
"",
620
620
+
{},
621
621
+
"sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g=="
622
622
+
],
623
623
+
"wordwrap": [
624
624
+
"wordwrap@1.0.0",
625
625
+
"",
626
626
+
{},
627
627
+
"sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="
628
628
+
],
629
629
+
"zod": [
630
630
+
"zod@3.25.76",
631
631
+
"",
632
632
+
{},
633
633
+
"sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="
634
634
+
],
171
635
}
172
636
}
+3
-3
fly.toml
···
1
1
-
# fly.toml app configuration file generated for stdpub-web on 2026-01-13T21:16:04-06:00
1
1
+
# fly.toml app configuration file generated for sitebase-web on 2026-01-13T21:16:04-06:00
2
2
#
3
3
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
4
4
#
5
5
6
6
-
app = 'stdpub-web'
6
6
+
app = 'sitebase-web'
7
7
primary_region = 'ord'
8
8
9
9
[build]
10
10
dockerfile = "Dockerfile"
11
11
12
12
[env]
13
13
-
PUBLIC_URL = "https://std.pub"
13
13
+
PUBLIC_URL = "https://sitebase"
14
14
15
15
[http_service]
16
16
internal_port = 8080
+1
-1
package.json
···
1
1
{
2
2
-
"name": "site-editor",
2
2
+
"name": "sitebase",
3
3
"private": true,
4
4
"workspaces": [
5
5
"packages/*"
+11
-11
packages/cli/package.json
···
1
1
{
2
2
-
"name": "@stdsite/cli",
3
3
-
"type": "module",
4
4
-
"version": "0.0.1",
5
5
-
"bin": {
6
6
-
"stdsite": "./src/index.ts"
7
7
-
},
8
8
-
"dependencies": {
9
9
-
"@stdsite/cli": "workspace:*",
10
10
-
"@stdsite/core": "workspace:*",
11
11
-
"commander": "^12.1.0"
12
12
-
}
2
2
+
"name": "@sitebase/cli",
3
3
+
"type": "module",
4
4
+
"version": "0.0.1",
5
5
+
"bin": {
6
6
+
"sitebase": "./src/index.ts"
7
7
+
},
8
8
+
"dependencies": {
9
9
+
"@sitebase/cli": "workspace:*",
10
10
+
"@sitebase/core": "workspace:*",
11
11
+
"commander": "^12.1.0"
12
12
+
}
13
13
}
+80
-83
packages/cli/src/index.ts
···
2
2
import { readFile } from "node:fs/promises";
3
3
import { resolve } from "node:path";
4
4
import { Command } from "commander";
5
5
-
import {
6
6
-
DEFAULT_FILENAME_TEMPLATE,
7
7
-
exportPublication,
8
8
-
} from "@stdsite/core";
5
5
+
import { DEFAULT_FILENAME_TEMPLATE, exportPublication } from "@sitebase/core";
9
6
10
7
const program = new Command();
11
8
12
9
program
13
13
-
.name("stdsite")
14
14
-
.description("CLI tools for standard.site publications")
15
15
-
.version("0.0.1");
10
10
+
.name("sitebase")
11
11
+
.description("CLI tools for standard.site publications")
12
12
+
.version("0.0.1");
16
13
17
14
program
18
18
-
.command("export")
19
19
-
.description("Export a publication to markdown files")
20
20
-
.argument("<at-uri>", "AT URI of the publication to export")
21
21
-
.requiredOption("-o, --output <dir>", "Output directory for markdown files")
22
22
-
.option("-t, --template <file>", "Path to custom content template file")
23
23
-
.option("--filename-template <template>", "Handlebars template for filenames")
24
24
-
.option(
25
25
-
"--include-tags <tags>",
26
26
-
"Only include documents with these tags (comma-separated)",
27
27
-
)
28
28
-
.option(
29
29
-
"--exclude-tags <tags>",
30
30
-
"Exclude documents with these tags (comma-separated)",
31
31
-
)
32
32
-
.action(
33
33
-
async (
34
34
-
atUri: string,
35
35
-
options: {
36
36
-
output: string;
37
37
-
template?: string;
38
38
-
filenameTemplate?: string;
39
39
-
includeTags?: string;
40
40
-
excludeTags?: string;
41
41
-
},
42
42
-
) => {
43
43
-
try {
44
44
-
// Parse tag options
45
45
-
const includeTags = options.includeTags
46
46
-
? options.includeTags.split(",").map((t) => t.trim())
47
47
-
: undefined;
48
48
-
const excludeTags = options.excludeTags
49
49
-
? options.excludeTags.split(",").map((t) => t.trim())
50
50
-
: undefined;
15
15
+
.command("export")
16
16
+
.description("Export a publication to markdown files")
17
17
+
.argument("<at-uri>", "AT URI of the publication to export")
18
18
+
.requiredOption("-o, --output <dir>", "Output directory for markdown files")
19
19
+
.option("-t, --template <file>", "Path to custom content template file")
20
20
+
.option("--filename-template <template>", "Handlebars template for filenames")
21
21
+
.option(
22
22
+
"--include-tags <tags>",
23
23
+
"Only include documents with these tags (comma-separated)",
24
24
+
)
25
25
+
.option(
26
26
+
"--exclude-tags <tags>",
27
27
+
"Exclude documents with these tags (comma-separated)",
28
28
+
)
29
29
+
.action(
30
30
+
async (
31
31
+
atUri: string,
32
32
+
options: {
33
33
+
output: string;
34
34
+
template?: string;
35
35
+
filenameTemplate?: string;
36
36
+
includeTags?: string;
37
37
+
excludeTags?: string;
38
38
+
},
39
39
+
) => {
40
40
+
try {
41
41
+
// Parse tag options
42
42
+
const includeTags = options.includeTags
43
43
+
? options.includeTags.split(",").map((t) => t.trim())
44
44
+
: undefined;
45
45
+
const excludeTags = options.excludeTags
46
46
+
? options.excludeTags.split(",").map((t) => t.trim())
47
47
+
: undefined;
51
48
52
52
-
// Load custom content template if provided
53
53
-
let contentTemplate: string | undefined;
54
54
-
if (options.template) {
55
55
-
const templatePath = resolve(options.template);
56
56
-
contentTemplate = await readFile(templatePath, "utf-8");
57
57
-
}
49
49
+
// Load custom content template if provided
50
50
+
let contentTemplate: string | undefined;
51
51
+
if (options.template) {
52
52
+
const templatePath = resolve(options.template);
53
53
+
contentTemplate = await readFile(templatePath, "utf-8");
54
54
+
}
58
55
59
59
-
// Use custom filename template or default
60
60
-
const filenameTemplate =
61
61
-
options.filenameTemplate || DEFAULT_FILENAME_TEMPLATE;
56
56
+
// Use custom filename template or default
57
57
+
const filenameTemplate =
58
58
+
options.filenameTemplate || DEFAULT_FILENAME_TEMPLATE;
62
59
63
63
-
console.log(`Exporting publication: ${atUri}`);
64
64
-
console.log(`Output directory: ${options.output}`);
60
60
+
console.log(`Exporting publication: ${atUri}`);
61
61
+
console.log(`Output directory: ${options.output}`);
65
62
66
66
-
const result = await exportPublication({
67
67
-
publicationUri: atUri,
68
68
-
outputDir: resolve(options.output),
69
69
-
contentTemplate,
70
70
-
filenameTemplate,
71
71
-
includeTags,
72
72
-
excludeTags,
73
73
-
});
63
63
+
const result = await exportPublication({
64
64
+
publicationUri: atUri,
65
65
+
outputDir: resolve(options.output),
66
66
+
contentTemplate,
67
67
+
filenameTemplate,
68
68
+
includeTags,
69
69
+
excludeTags,
70
70
+
});
74
71
75
75
-
console.log(`\nExport complete:`);
76
76
-
console.log(` Documents processed: ${result.documentsProcessed}`);
77
77
-
console.log(` Documents skipped: ${result.documentsSkipped}`);
78
78
-
console.log(` Files written: ${result.filesWritten.length}`);
72
72
+
console.log(`\nExport complete:`);
73
73
+
console.log(` Documents processed: ${result.documentsProcessed}`);
74
74
+
console.log(` Documents skipped: ${result.documentsSkipped}`);
75
75
+
console.log(` Files written: ${result.filesWritten.length}`);
79
76
80
80
-
if (result.warnings.length > 0) {
81
81
-
console.log(`\nWarnings:`);
82
82
-
for (const warning of result.warnings) {
83
83
-
console.log(` - ${warning}`);
84
84
-
}
85
85
-
}
77
77
+
if (result.warnings.length > 0) {
78
78
+
console.log(`\nWarnings:`);
79
79
+
for (const warning of result.warnings) {
80
80
+
console.log(` - ${warning}`);
81
81
+
}
82
82
+
}
86
83
87
87
-
if (result.filesWritten.length > 0) {
88
88
-
console.log(`\nFiles:`);
89
89
-
for (const file of result.filesWritten) {
90
90
-
console.log(` - ${file}`);
91
91
-
}
92
92
-
}
93
93
-
} catch (error) {
94
94
-
console.error(
95
95
-
`Error: ${error instanceof Error ? error.message : String(error)}`,
96
96
-
);
97
97
-
process.exit(1);
98
98
-
}
99
99
-
},
100
100
-
);
84
84
+
if (result.filesWritten.length > 0) {
85
85
+
console.log(`\nFiles:`);
86
86
+
for (const file of result.filesWritten) {
87
87
+
console.log(` - ${file}`);
88
88
+
}
89
89
+
}
90
90
+
} catch (error) {
91
91
+
console.error(
92
92
+
`Error: ${error instanceof Error ? error.message : String(error)}`,
93
93
+
);
94
94
+
process.exit(1);
95
95
+
}
96
96
+
},
97
97
+
);
101
98
102
99
program.parse();
+9
-9
packages/core/package.json
···
1
1
{
2
2
-
"name": "@stdsite/core",
3
3
-
"type": "module",
4
4
-
"version": "0.0.1",
5
5
-
"exports": {
6
6
-
".": "./src/index.ts"
7
7
-
},
8
8
-
"dependencies": {
9
9
-
"handlebars": "^4.7.8"
10
10
-
}
2
2
+
"name": "@sitebase/core",
3
3
+
"type": "module",
4
4
+
"version": "0.0.1",
5
5
+
"exports": {
6
6
+
".": "./src/index.ts"
7
7
+
},
8
8
+
"dependencies": {
9
9
+
"handlebars": "^4.7.8"
10
10
+
}
11
11
}
+1
-1
packages/web/CLAUDE.md
···
1
1
-
# std.pub
1
1
+
# sitebase
2
2
3
3
A minimal web UI for managing standard.site publications and documents in a Bluesky PDS.
4
4
+1
-1
packages/web/bun.lock
···
3
3
"configVersion": 1,
4
4
"workspaces": {
5
5
"": {
6
6
-
"name": "std.pub",
6
6
+
"name": "sitebase",
7
7
"dependencies": {
8
8
"@atproto/api": "^0.18.13",
9
9
"@atproto/jwk-jose": "^0.1.11",
+21
-21
packages/web/package.json
···
1
1
{
2
2
-
"name": "@stdpub/web",
3
3
-
"module": "src/server.ts",
4
4
-
"type": "module",
5
5
-
"private": true,
6
6
-
"scripts": {
7
7
-
"dev": "bun --hot run src/server.ts",
8
8
-
"start": "bun run src/server.ts"
9
9
-
},
10
10
-
"devDependencies": {
11
11
-
"@types/bun": "latest"
12
12
-
},
13
13
-
"peerDependencies": {
14
14
-
"typescript": "^5"
15
15
-
},
16
16
-
"dependencies": {
17
17
-
"@atproto/api": "^0.18.13",
18
18
-
"@atproto/jwk-jose": "^0.1.11",
19
19
-
"@atproto/oauth-client-node": "^0.3.15",
20
20
-
"hono": "^4.11.3",
21
21
-
"marked": "^15.0.0"
22
22
-
}
2
2
+
"name": "@sitebase/web",
3
3
+
"module": "src/server.ts",
4
4
+
"type": "module",
5
5
+
"private": true,
6
6
+
"scripts": {
7
7
+
"dev": "bun --hot run src/server.ts",
8
8
+
"start": "bun run src/server.ts"
9
9
+
},
10
10
+
"devDependencies": {
11
11
+
"@types/bun": "latest"
12
12
+
},
13
13
+
"peerDependencies": {
14
14
+
"typescript": "^5"
15
15
+
},
16
16
+
"dependencies": {
17
17
+
"@atproto/api": "^0.18.13",
18
18
+
"@atproto/jwk-jose": "^0.1.11",
19
19
+
"@atproto/oauth-client-node": "^0.3.15",
20
20
+
"hono": "^4.11.3",
21
21
+
"marked": "^15.0.0"
22
22
+
}
23
23
}
+19
-19
packages/web/scripts/cleanup.ts
···
2
2
/**
3
3
* Database cleanup script
4
4
* Removes expired OAuth states and optionally old sessions
5
5
-
* Run via cron: 0 * * * * /home/exedev/.bun/bin/bun /home/exedev/std.pub/scripts/cleanup.ts
5
5
+
* Run via cron: 0 * * * * /home/exedev/.bun/bin/bun /home/exedev/sitebase/scripts/cleanup.ts
6
6
*/
7
7
8
8
import { Database } from "bun:sqlite";
···
12
12
const DB_PATH = path.join(DATA_DIR, "oauth.db");
13
13
14
14
try {
15
15
-
const db = new Database(DB_PATH);
15
15
+
const db = new Database(DB_PATH);
16
16
17
17
-
// Clean up OAuth states older than 1 hour
18
18
-
const statesResult = db.run(
19
19
-
`DELETE FROM oauth_states WHERE created_at < strftime('%s', 'now') - 3600`,
20
20
-
);
17
17
+
// Clean up OAuth states older than 1 hour
18
18
+
const statesResult = db.run(
19
19
+
`DELETE FROM oauth_states WHERE created_at < strftime('%s', 'now') - 3600`,
20
20
+
);
21
21
22
22
-
// Clean up sessions older than 30 days (optional - sessions may still be valid)
23
23
-
const sessionsResult = db.run(
24
24
-
`DELETE FROM oauth_sessions WHERE updated_at < strftime('%s', 'now') - 2592000`,
25
25
-
);
22
22
+
// Clean up sessions older than 30 days (optional - sessions may still be valid)
23
23
+
const sessionsResult = db.run(
24
24
+
`DELETE FROM oauth_sessions WHERE updated_at < strftime('%s', 'now') - 2592000`,
25
25
+
);
26
26
27
27
-
// Vacuum the database to reclaim space
28
28
-
db.run("VACUUM");
27
27
+
// Vacuum the database to reclaim space
28
28
+
db.run("VACUUM");
29
29
30
30
-
const timestamp = new Date().toISOString();
31
31
-
console.log(
32
32
-
`[${timestamp}] Cleanup complete: removed old states and sessions, vacuumed database`,
33
33
-
);
30
30
+
const timestamp = new Date().toISOString();
31
31
+
console.log(
32
32
+
`[${timestamp}] Cleanup complete: removed old states and sessions, vacuumed database`,
33
33
+
);
34
34
35
35
-
db.close();
35
35
+
db.close();
36
36
} catch (error) {
37
37
-
console.error("Cleanup failed:", error);
38
38
-
process.exit(1);
37
37
+
console.error("Cleanup failed:", error);
38
38
+
process.exit(1);
39
39
}
+94
-94
packages/web/src/lib/oauth.ts
···
1
1
import { NodeOAuthClient } from "@atproto/oauth-client-node";
2
2
import type {
3
3
-
NodeSavedSession,
4
4
-
NodeSavedState,
3
3
+
NodeSavedSession,
4
4
+
NodeSavedState,
5
5
} from "@atproto/oauth-client-node";
6
6
import { JoseKey } from "@atproto/jwk-jose";
7
7
import { Agent } from "@atproto/api";
···
17
17
18
18
// Ensure data directory exists
19
19
if (!fs.existsSync(DATA_DIR)) {
20
20
-
fs.mkdirSync(DATA_DIR, { recursive: true });
20
20
+
fs.mkdirSync(DATA_DIR, { recursive: true });
21
21
}
22
22
23
23
// Initialize SQLite database
···
42
42
43
43
// Clean up old states (older than 1 hour)
44
44
db.run(
45
45
-
`DELETE FROM oauth_states WHERE created_at < strftime('%s', 'now') - 3600`,
45
45
+
`DELETE FROM oauth_states WHERE created_at < strftime('%s', 'now') - 3600`,
46
46
);
47
47
48
48
// State store implementation
49
49
const stateStore = {
50
50
-
async set(key: string, state: NodeSavedState): Promise<void> {
51
51
-
const stateJson = JSON.stringify(state);
52
52
-
db.run(
53
53
-
`INSERT OR REPLACE INTO oauth_states (key, state, created_at) VALUES (?, ?, strftime('%s', 'now'))`,
54
54
-
[key, stateJson],
55
55
-
);
56
56
-
},
57
57
-
async get(key: string): Promise<NodeSavedState | undefined> {
58
58
-
const row = db
59
59
-
.query(`SELECT state FROM oauth_states WHERE key = ?`)
60
60
-
.get(key) as { state: string } | null;
61
61
-
if (!row) return undefined;
62
62
-
return JSON.parse(row.state);
63
63
-
},
64
64
-
async del(key: string): Promise<void> {
65
65
-
db.run(`DELETE FROM oauth_states WHERE key = ?`, [key]);
66
66
-
},
50
50
+
async set(key: string, state: NodeSavedState): Promise<void> {
51
51
+
const stateJson = JSON.stringify(state);
52
52
+
db.run(
53
53
+
`INSERT OR REPLACE INTO oauth_states (key, state, created_at) VALUES (?, ?, strftime('%s', 'now'))`,
54
54
+
[key, stateJson],
55
55
+
);
56
56
+
},
57
57
+
async get(key: string): Promise<NodeSavedState | undefined> {
58
58
+
const row = db
59
59
+
.query(`SELECT state FROM oauth_states WHERE key = ?`)
60
60
+
.get(key) as { state: string } | null;
61
61
+
if (!row) return undefined;
62
62
+
return JSON.parse(row.state);
63
63
+
},
64
64
+
async del(key: string): Promise<void> {
65
65
+
db.run(`DELETE FROM oauth_states WHERE key = ?`, [key]);
66
66
+
},
67
67
};
68
68
69
69
// Session store implementation
70
70
const sessionStore = {
71
71
-
async set(did: string, session: NodeSavedSession): Promise<void> {
72
72
-
const sessionJson = JSON.stringify(session);
73
73
-
db.run(
74
74
-
`INSERT OR REPLACE INTO oauth_sessions (did, session, updated_at) VALUES (?, ?, strftime('%s', 'now'))`,
75
75
-
[did, sessionJson],
76
76
-
);
77
77
-
},
78
78
-
async get(did: string): Promise<NodeSavedSession | undefined> {
79
79
-
const row = db
80
80
-
.query(`SELECT session FROM oauth_sessions WHERE did = ?`)
81
81
-
.get(did) as { session: string } | null;
82
82
-
if (!row) return undefined;
83
83
-
return JSON.parse(row.session);
84
84
-
},
85
85
-
async del(did: string): Promise<void> {
86
86
-
db.run(`DELETE FROM oauth_sessions WHERE did = ?`, [did]);
87
87
-
},
71
71
+
async set(did: string, session: NodeSavedSession): Promise<void> {
72
72
+
const sessionJson = JSON.stringify(session);
73
73
+
db.run(
74
74
+
`INSERT OR REPLACE INTO oauth_sessions (did, session, updated_at) VALUES (?, ?, strftime('%s', 'now'))`,
75
75
+
[did, sessionJson],
76
76
+
);
77
77
+
},
78
78
+
async get(did: string): Promise<NodeSavedSession | undefined> {
79
79
+
const row = db
80
80
+
.query(`SELECT session FROM oauth_sessions WHERE did = ?`)
81
81
+
.get(did) as { session: string } | null;
82
82
+
if (!row) return undefined;
83
83
+
return JSON.parse(row.session);
84
84
+
},
85
85
+
async del(did: string): Promise<void> {
86
86
+
db.run(`DELETE FROM oauth_sessions WHERE did = ?`, [did]);
87
87
+
},
88
88
};
89
89
90
90
// Generate or load private key for confidential client
91
91
async function getOrCreatePrivateKey(): Promise<JoseKey> {
92
92
-
if (fs.existsSync(KEYS_PATH)) {
93
93
-
const keyData = JSON.parse(fs.readFileSync(KEYS_PATH, "utf-8"));
94
94
-
return JoseKey.fromJWK(keyData, keyData.kid);
95
95
-
}
92
92
+
if (fs.existsSync(KEYS_PATH)) {
93
93
+
const keyData = JSON.parse(fs.readFileSync(KEYS_PATH, "utf-8"));
94
94
+
return JoseKey.fromJWK(keyData, keyData.kid);
95
95
+
}
96
96
97
97
-
// Generate a new ES256 key
98
98
-
const key = await JoseKey.generate(["ES256"], crypto.randomUUID());
99
99
-
const jwk = key.privateJwk;
97
97
+
// Generate a new ES256 key
98
98
+
const key = await JoseKey.generate(["ES256"], crypto.randomUUID());
99
99
+
const jwk = key.privateJwk;
100
100
101
101
-
// Save to disk with restrictive permissions (owner read/write only)
102
102
-
fs.writeFileSync(KEYS_PATH, JSON.stringify(jwk, null, 2), { mode: 0o600 });
101
101
+
// Save to disk with restrictive permissions (owner read/write only)
102
102
+
fs.writeFileSync(KEYS_PATH, JSON.stringify(jwk, null, 2), { mode: 0o600 });
103
103
104
104
-
return key;
104
104
+
return key;
105
105
}
106
106
107
107
let oauthClientInstance: NodeOAuthClient | null = null;
108
108
let initPromise: Promise<NodeOAuthClient> | null = null;
109
109
110
110
async function initOAuthClient(): Promise<NodeOAuthClient> {
111
111
-
if (oauthClientInstance) return oauthClientInstance;
112
112
-
if (initPromise) return initPromise;
111
111
+
if (oauthClientInstance) return oauthClientInstance;
112
112
+
if (initPromise) return initPromise;
113
113
114
114
-
initPromise = (async () => {
115
115
-
const privateKey = await getOrCreatePrivateKey();
114
114
+
initPromise = (async () => {
115
115
+
const privateKey = await getOrCreatePrivateKey();
116
116
117
117
-
oauthClientInstance = new NodeOAuthClient({
118
118
-
clientMetadata: {
119
119
-
client_id: `${PUBLIC_URL}/client-metadata.json`,
120
120
-
client_name: "std.pub",
121
121
-
client_uri: PUBLIC_URL,
122
122
-
redirect_uris: [`${PUBLIC_URL}/auth/callback`],
123
123
-
scope: "atproto transition:generic",
124
124
-
grant_types: ["authorization_code", "refresh_token"],
125
125
-
response_types: ["code"],
126
126
-
application_type: "web",
127
127
-
token_endpoint_auth_method: "private_key_jwt",
128
128
-
token_endpoint_auth_signing_alg: "ES256",
129
129
-
dpop_bound_access_tokens: true,
130
130
-
jwks_uri: `${PUBLIC_URL}/jwks.json`,
131
131
-
},
132
132
-
keyset: [privateKey],
133
133
-
stateStore,
134
134
-
sessionStore,
135
135
-
});
117
117
+
oauthClientInstance = new NodeOAuthClient({
118
118
+
clientMetadata: {
119
119
+
client_id: `${PUBLIC_URL}/client-metadata.json`,
120
120
+
client_name: "sitebase",
121
121
+
client_uri: PUBLIC_URL,
122
122
+
redirect_uris: [`${PUBLIC_URL}/auth/callback`],
123
123
+
scope: "atproto transition:generic",
124
124
+
grant_types: ["authorization_code", "refresh_token"],
125
125
+
response_types: ["code"],
126
126
+
application_type: "web",
127
127
+
token_endpoint_auth_method: "private_key_jwt",
128
128
+
token_endpoint_auth_signing_alg: "ES256",
129
129
+
dpop_bound_access_tokens: true,
130
130
+
jwks_uri: `${PUBLIC_URL}/jwks.json`,
131
131
+
},
132
132
+
keyset: [privateKey],
133
133
+
stateStore,
134
134
+
sessionStore,
135
135
+
});
136
136
137
137
-
return oauthClientInstance;
138
138
-
})();
137
137
+
return oauthClientInstance;
138
138
+
})();
139
139
140
140
-
return initPromise;
140
140
+
return initPromise;
141
141
}
142
142
143
143
export async function getOAuthClient(): Promise<NodeOAuthClient> {
144
144
-
return initOAuthClient();
144
144
+
return initOAuthClient();
145
145
}
146
146
147
147
export async function getClientMetadata() {
148
148
-
const client = await getOAuthClient();
149
149
-
return client.clientMetadata;
148
148
+
const client = await getOAuthClient();
149
149
+
return client.clientMetadata;
150
150
}
151
151
152
152
export async function getJwks() {
153
153
-
const client = await getOAuthClient();
154
154
-
return client.jwks;
153
153
+
const client = await getOAuthClient();
154
154
+
return client.jwks;
155
155
}
156
156
157
157
export async function getAgentForSession(
158
158
-
did: string,
158
158
+
did: string,
159
159
): Promise<{ agent: Agent; did: string; handle: string }> {
160
160
-
const client = await getOAuthClient();
161
161
-
const oauthSession = await client.restore(did);
160
160
+
const client = await getOAuthClient();
161
161
+
const oauthSession = await client.restore(did);
162
162
163
163
-
if (!oauthSession) {
164
164
-
throw new Error("Session not found");
165
165
-
}
163
163
+
if (!oauthSession) {
164
164
+
throw new Error("Session not found");
165
165
+
}
166
166
167
167
-
const agent = new Agent(oauthSession);
167
167
+
const agent = new Agent(oauthSession);
168
168
169
169
-
// Fetch profile to get handle
170
170
-
const profile = await agent.getProfile({ actor: did });
169
169
+
// Fetch profile to get handle
170
170
+
const profile = await agent.getProfile({ actor: did });
171
171
172
172
-
return {
173
173
-
agent,
174
174
-
did,
175
175
-
handle: profile.data.handle,
176
176
-
};
172
172
+
return {
173
173
+
agent,
174
174
+
did,
175
175
+
handle: profile.data.handle,
176
176
+
};
177
177
}
178
178
179
179
export async function deleteSession(did: string): Promise<void> {
180
180
-
await sessionStore.del(did);
180
180
+
await sessionStore.del(did);
181
181
}
+91
-95
packages/web/src/routes/auth.ts
···
2
2
import { getCookie, setCookie, deleteCookie } from "hono/cookie";
3
3
import { html } from "hono/html";
4
4
import {
5
5
-
getOAuthClient,
6
6
-
getClientMetadata,
7
7
-
getJwks,
8
8
-
deleteSession,
5
5
+
getOAuthClient,
6
6
+
getClientMetadata,
7
7
+
getJwks,
8
8
+
deleteSession,
9
9
} from "../lib/oauth";
10
10
import { layout } from "../views/layouts/main";
11
11
import { csrfField } from "../lib/csrf";
···
14
14
15
15
// Client metadata endpoint (required for OAuth)
16
16
authRoutes.get("/client-metadata.json", async (c) => {
17
17
-
try {
18
18
-
const metadata = await getClientMetadata();
19
19
-
return c.json(metadata);
20
20
-
} catch (error) {
21
21
-
console.error("Error getting client metadata:", error);
22
22
-
return c.json({ error: "Failed to get client metadata" }, 500);
23
23
-
}
17
17
+
try {
18
18
+
const metadata = await getClientMetadata();
19
19
+
return c.json(metadata);
20
20
+
} catch (error) {
21
21
+
console.error("Error getting client metadata:", error);
22
22
+
return c.json({ error: "Failed to get client metadata" }, 500);
23
23
+
}
24
24
});
25
25
26
26
// JWKS endpoint (required for confidential clients)
27
27
authRoutes.get("/jwks.json", async (c) => {
28
28
-
try {
29
29
-
const jwks = await getJwks();
30
30
-
return c.json(jwks);
31
31
-
} catch (error) {
32
32
-
console.error("Error getting JWKS:", error);
33
33
-
return c.json({ error: "Failed to get JWKS" }, 500);
34
34
-
}
28
28
+
try {
29
29
+
const jwks = await getJwks();
30
30
+
return c.json(jwks);
31
31
+
} catch (error) {
32
32
+
console.error("Error getting JWKS:", error);
33
33
+
return c.json({ error: "Failed to get JWKS" }, 500);
34
34
+
}
35
35
});
36
36
37
37
// Login page
38
38
authRoutes.get("/login", async (c) => {
39
39
-
const error = c.req.query("error");
40
40
-
const csrfToken = c.get("csrfToken") as string;
39
39
+
const error = c.req.query("error");
40
40
+
const csrfToken = c.get("csrfToken") as string;
41
41
42
42
-
const content = html`
42
42
+
const content = html`
43
43
<div class="auth-form">
44
44
<h1>Login with Bluesky</h1>
45
45
46
46
-
${
47
47
-
error
48
48
-
? html`
46
46
+
${error
47
47
+
? html`
49
48
<div class="error-message">
50
50
-
${
51
51
-
error === "handle_required"
52
52
-
? "Please enter your handle or DID."
53
53
-
: error === "authorization_failed"
54
54
-
? "Authorization failed. Please try again."
55
55
-
: error === "callback_failed"
56
56
-
? "Login failed. Please try again."
57
57
-
: "An error occurred. Please try again."
58
58
-
}
49
49
+
${error === "handle_required"
50
50
+
? "Please enter your handle or DID."
51
51
+
: error === "authorization_failed"
52
52
+
? "Authorization failed. Please try again."
53
53
+
: error === "callback_failed"
54
54
+
? "Login failed. Please try again."
55
55
+
: "An error occurred. Please try again."}
59
56
</div>
60
57
`
61
61
-
: ""
62
62
-
}
58
58
+
: ""}
63
59
64
60
<form action="/auth/login" method="POST">
65
61
${csrfField(csrfToken)}
···
84
80
</div>
85
81
`;
86
82
87
87
-
return c.html(layout(content, { title: "Login - std.pub" }));
83
83
+
return c.html(layout(content, { title: "Login - sitebase" }));
88
84
});
89
85
90
86
// Handle login form submission
91
87
authRoutes.post("/login", async (c) => {
92
92
-
const body = await c.req.parseBody();
93
93
-
let handle = body.handle as string;
88
88
+
const body = await c.req.parseBody();
89
89
+
let handle = body.handle as string;
94
90
95
95
-
if (!handle) {
96
96
-
return c.redirect("/auth/login?error=handle_required");
97
97
-
}
91
91
+
if (!handle) {
92
92
+
return c.redirect("/auth/login?error=handle_required");
93
93
+
}
98
94
99
99
-
// Trim and normalize handle
100
100
-
handle = handle.trim().toLowerCase();
95
95
+
// Trim and normalize handle
96
96
+
handle = handle.trim().toLowerCase();
101
97
102
102
-
// Remove @ prefix if present
103
103
-
if (handle.startsWith("@")) {
104
104
-
handle = handle.slice(1);
105
105
-
}
98
98
+
// Remove @ prefix if present
99
99
+
if (handle.startsWith("@")) {
100
100
+
handle = handle.slice(1);
101
101
+
}
106
102
107
107
-
try {
108
108
-
const client = await getOAuthClient();
109
109
-
const url = await client.authorize(handle, {
110
110
-
scope: "atproto transition:generic",
111
111
-
});
103
103
+
try {
104
104
+
const client = await getOAuthClient();
105
105
+
const url = await client.authorize(handle, {
106
106
+
scope: "atproto transition:generic",
107
107
+
});
112
108
113
113
-
return c.redirect(url.toString());
114
114
-
} catch (error) {
115
115
-
console.error("Login error:", error);
116
116
-
return c.redirect("/auth/login?error=authorization_failed");
117
117
-
}
109
109
+
return c.redirect(url.toString());
110
110
+
} catch (error) {
111
111
+
console.error("Login error:", error);
112
112
+
return c.redirect("/auth/login?error=authorization_failed");
113
113
+
}
118
114
});
119
115
120
116
// OAuth callback
121
117
authRoutes.get("/callback", async (c) => {
122
122
-
const url = new URL(c.req.url);
123
123
-
const params = url.searchParams;
118
118
+
const url = new URL(c.req.url);
119
119
+
const params = url.searchParams;
124
120
125
125
-
// Check for error from authorization server
126
126
-
const error = params.get("error");
127
127
-
if (error) {
128
128
-
console.error("OAuth error:", error, params.get("error_description"));
129
129
-
return c.redirect("/auth/login?error=callback_failed");
130
130
-
}
121
121
+
// Check for error from authorization server
122
122
+
const error = params.get("error");
123
123
+
if (error) {
124
124
+
console.error("OAuth error:", error, params.get("error_description"));
125
125
+
return c.redirect("/auth/login?error=callback_failed");
126
126
+
}
131
127
132
132
-
try {
133
133
-
const client = await getOAuthClient();
134
134
-
const { session } = await client.callback(params);
128
128
+
try {
129
129
+
const client = await getOAuthClient();
130
130
+
const { session } = await client.callback(params);
135
131
136
136
-
// Store the DID in a cookie for session management
137
137
-
// The actual OAuth session is stored in the database by the OAuth client
138
138
-
setCookie(c, "session", session.did, {
139
139
-
httpOnly: true,
140
140
-
secure:
141
141
-
process.env.NODE_ENV === "production" ||
142
142
-
process.env.PUBLIC_URL?.startsWith("https"),
143
143
-
sameSite: "Lax",
144
144
-
maxAge: 60 * 60 * 24 * 7, // 7 days
145
145
-
path: "/",
146
146
-
});
132
132
+
// Store the DID in a cookie for session management
133
133
+
// The actual OAuth session is stored in the database by the OAuth client
134
134
+
setCookie(c, "session", session.did, {
135
135
+
httpOnly: true,
136
136
+
secure:
137
137
+
process.env.NODE_ENV === "production" ||
138
138
+
process.env.PUBLIC_URL?.startsWith("https"),
139
139
+
sameSite: "Lax",
140
140
+
maxAge: 60 * 60 * 24 * 7, // 7 days
141
141
+
path: "/",
142
142
+
});
147
143
148
148
-
return c.redirect("/");
149
149
-
} catch (error) {
150
150
-
console.error("Callback error:", error);
151
151
-
return c.redirect("/auth/login?error=callback_failed");
152
152
-
}
144
144
+
return c.redirect("/");
145
145
+
} catch (error) {
146
146
+
console.error("Callback error:", error);
147
147
+
return c.redirect("/auth/login?error=callback_failed");
148
148
+
}
153
149
});
154
150
155
151
// Logout
156
152
authRoutes.get("/logout", async (c) => {
157
157
-
const did = getCookie(c, "session");
153
153
+
const did = getCookie(c, "session");
158
154
159
159
-
if (did) {
160
160
-
try {
161
161
-
// Delete the OAuth session from the database
162
162
-
await deleteSession(did);
163
163
-
} catch (error) {
164
164
-
console.error("Error deleting session:", error);
165
165
-
}
166
166
-
}
155
155
+
if (did) {
156
156
+
try {
157
157
+
// Delete the OAuth session from the database
158
158
+
await deleteSession(did);
159
159
+
} catch (error) {
160
160
+
console.error("Error deleting session:", error);
161
161
+
}
162
162
+
}
167
163
168
168
-
deleteCookie(c, "session", { path: "/" });
169
169
-
return c.redirect("/");
164
164
+
deleteCookie(c, "session", { path: "/" });
165
165
+
return c.redirect("/");
170
166
});
+450
-373
packages/web/src/routes/documents.ts
···
5
5
import { csrfField } from "../lib/csrf";
6
6
import { isValidTID } from "../lib/validation";
7
7
import {
8
8
-
createMarkdownContent,
9
9
-
getDocumentContentText,
8
8
+
createMarkdownContent,
9
9
+
getDocumentContentText,
10
10
} from "../lib/content-types";
11
11
import { marked } from "marked";
12
12
···
17
17
18
18
// List all documents
19
19
documentRoutes.get("/", async (c) => {
20
20
-
let session: Session;
21
21
-
try {
22
22
-
session = requireAuth(c);
23
23
-
} catch {
24
24
-
return c.redirect("/auth/login");
25
25
-
}
20
20
+
let session: Session;
21
21
+
try {
22
22
+
session = requireAuth(c);
23
23
+
} catch {
24
24
+
return c.redirect("/auth/login");
25
25
+
}
26
26
27
27
-
const filter = c.req.query("filter") || "all";
27
27
+
const filter = c.req.query("filter") || "all";
28
28
29
29
-
try {
30
30
-
const response = await session.agent!.com.atproto.repo.listRecords({
31
31
-
repo: session.did!,
32
32
-
collection: DOCUMENT_COLLECTION,
33
33
-
limit: 100,
34
34
-
});
29
29
+
try {
30
30
+
const response = await session.agent!.com.atproto.repo.listRecords({
31
31
+
repo: session.did!,
32
32
+
collection: DOCUMENT_COLLECTION,
33
33
+
limit: 100,
34
34
+
});
35
35
36
36
-
let documents = response.data.records;
36
36
+
let documents = response.data.records;
37
37
38
38
-
// Filter by draft/published status
39
39
-
if (filter === "drafts") {
40
40
-
documents = documents.filter((doc: any) => {
41
41
-
const tags = doc.value.tags || [];
42
42
-
return tags.includes("draft");
43
43
-
});
44
44
-
} else if (filter === "published") {
45
45
-
documents = documents.filter((doc: any) => {
46
46
-
const tags = doc.value.tags || [];
47
47
-
return !tags.includes("draft");
48
48
-
});
49
49
-
}
38
38
+
// Filter by draft/published status
39
39
+
if (filter === "drafts") {
40
40
+
documents = documents.filter((doc: any) => {
41
41
+
const tags = doc.value.tags || [];
42
42
+
return tags.includes("draft");
43
43
+
});
44
44
+
} else if (filter === "published") {
45
45
+
documents = documents.filter((doc: any) => {
46
46
+
const tags = doc.value.tags || [];
47
47
+
return !tags.includes("draft");
48
48
+
});
49
49
+
}
50
50
51
51
-
// Sort by publishedAt or updatedAt
52
52
-
documents.sort((a: any, b: any) => {
53
53
-
const dateA = new Date(
54
54
-
a.value.updatedAt || a.value.publishedAt,
55
55
-
).getTime();
56
56
-
const dateB = new Date(
57
57
-
b.value.updatedAt || b.value.publishedAt,
58
58
-
).getTime();
59
59
-
return dateB - dateA;
60
60
-
});
51
51
+
// Sort by publishedAt or updatedAt
52
52
+
documents.sort((a: any, b: any) => {
53
53
+
const dateA = new Date(
54
54
+
a.value.updatedAt || a.value.publishedAt,
55
55
+
).getTime();
56
56
+
const dateB = new Date(
57
57
+
b.value.updatedAt || b.value.publishedAt,
58
58
+
).getTime();
59
59
+
return dateB - dateA;
60
60
+
});
61
61
62
62
-
const content = html`
62
62
+
const content = html`
63
63
<div class="documents">
64
64
<div class="documents-header">
65
65
<h1>Documents</h1>
···
84
84
>
85
85
</div>
86
86
87
87
-
${
88
88
-
documents.length === 0
89
89
-
? html`
87
87
+
${documents.length === 0
88
88
+
? html`
90
89
<p class="empty">
91
90
No documents yet.
92
91
<a href="/documents/new">Create your first document</a>.
93
92
</p>
94
93
`
95
95
-
: html`
94
94
+
: html`
96
95
<ul class="document-list">
97
96
${documents.map((doc: any) => {
98
98
-
const rkey = doc.uri.split("/").pop();
99
99
-
const value = doc.value;
100
100
-
const isDraft = (value.tags || []).includes("draft");
101
101
-
const date = value.publishedAt
102
102
-
? new Date(value.publishedAt).toLocaleDateString()
103
103
-
: "";
97
97
+
const rkey = doc.uri.split("/").pop();
98
98
+
const value = doc.value;
99
99
+
const isDraft = (value.tags || []).includes("draft");
100
100
+
const date = value.publishedAt
101
101
+
? new Date(value.publishedAt).toLocaleDateString()
102
102
+
: "";
104
103
105
105
-
return html`
104
104
+
return html`
106
105
<li
107
106
class="document-item ${isDraft ? "draft" : "published"}"
108
107
>
109
108
<a href="/documents/${rkey}">
110
109
<span class="title">${value.title}</span>
111
110
<span class="meta">
112
112
-
${
113
113
-
isDraft
114
114
-
? html`<span class="badge badge-draft">Draft</span>`
115
115
-
: ""
116
116
-
}
111
111
+
${isDraft
112
112
+
? html`<span class="badge badge-draft">Draft</span>`
113
113
+
: ""}
117
114
<span class="date">${date}</span>
118
115
</span>
119
116
</a>
120
117
</li>
121
118
`;
122
122
-
})}
119
119
+
})}
123
120
</ul>
124
124
-
`
125
125
-
}
121
121
+
`}
126
122
</div>
127
123
`;
128
124
129
129
-
return c.html(layout(content, { title: "Documents - std.pub", session }));
130
130
-
} catch (error) {
131
131
-
console.error("Error fetching documents:", error);
132
132
-
const content = html`<p class="error">
125
125
+
return c.html(layout(content, { title: "Documents - sitebase", session }));
126
126
+
} catch (error) {
127
127
+
console.error("Error fetching documents:", error);
128
128
+
const content = html`<p class="error">
133
129
Error loading documents. Please try again.
134
130
</p>`;
135
135
-
return c.html(layout(content, { title: "Documents - std.pub", session }));
136
136
-
}
131
131
+
return c.html(layout(content, { title: "Documents - sitebase", session }));
132
132
+
}
137
133
});
138
134
139
135
// New document form
140
136
documentRoutes.get("/new", async (c) => {
141
141
-
let session: Session;
142
142
-
try {
143
143
-
session = requireAuth(c);
144
144
-
} catch {
145
145
-
return c.redirect("/auth/login");
146
146
-
}
137
137
+
let session: Session;
138
138
+
try {
139
139
+
session = requireAuth(c);
140
140
+
} catch {
141
141
+
return c.redirect("/auth/login");
142
142
+
}
147
143
148
148
-
// Get publication to use as site reference
149
149
-
let publicationUri = "";
150
150
-
try {
151
151
-
const response = await session.agent!.com.atproto.repo.listRecords({
152
152
-
repo: session.did!,
153
153
-
collection: PUBLICATION_COLLECTION,
154
154
-
limit: 1,
155
155
-
});
156
156
-
if (response.data.records[0]) {
157
157
-
publicationUri = response.data.records[0].uri;
158
158
-
}
159
159
-
} catch (e) {
160
160
-
// No publication yet, will need URL
161
161
-
}
144
144
+
// Get publication to use as site reference
145
145
+
let publicationUri = "";
146
146
+
try {
147
147
+
const response = await session.agent!.com.atproto.repo.listRecords({
148
148
+
repo: session.did!,
149
149
+
collection: PUBLICATION_COLLECTION,
150
150
+
limit: 1,
151
151
+
});
152
152
+
if (response.data.records[0]) {
153
153
+
publicationUri = response.data.records[0].uri;
154
154
+
}
155
155
+
} catch (e) {
156
156
+
// No publication yet, will need URL
157
157
+
}
162
158
163
163
-
const csrfToken = c.get("csrfToken") as string;
159
159
+
const csrfToken = c.get("csrfToken") as string;
164
160
165
165
-
const content = html`
161
161
+
const content = html`
166
162
<div class="form-page">
167
163
<h1>New Document</h1>
168
164
···
202
198
</div>
203
199
204
200
<div class="form-group">
201
201
+
<label for="publishDate">Publish Date</label>
202
202
+
<input type="datetime-local" id="publishDate" name="publishDate" />
203
203
+
<small
204
204
+
>Only past dates allowed. Leave empty to use current date when
205
205
+
publishing.</small
206
206
+
>
207
207
+
</div>
208
208
+
209
209
+
<div class="form-group">
205
210
<label for="tags">Tags (comma-separated)</label>
206
211
<input
207
212
type="text"
···
234
239
</div>
235
240
236
241
<script>
242
242
+
// Set default publish date to current date/time
243
243
+
const publishDateInput = document.getElementById("publishDate");
244
244
+
if (publishDateInput) {
245
245
+
const now = new Date();
246
246
+
now.setMinutes(now.getMinutes() - now.getTimezoneOffset());
247
247
+
publishDateInput.value = now.toISOString().slice(0, 16);
248
248
+
}
249
249
+
237
250
// Auto-save functionality
238
251
const form = document.querySelector(".document-form");
239
252
const contentField = document.getElementById("content");
···
246
259
console.log("Would auto-save...");
247
260
}, 2000);
248
261
});
262
262
+
263
263
+
// Validate publish date is not in the future
264
264
+
form.addEventListener("submit", (e) => {
265
265
+
const publishDate = publishDateInput?.value;
266
266
+
if (publishDate) {
267
267
+
const selectedDate = new Date(publishDate);
268
268
+
const now = new Date();
269
269
+
if (selectedDate > now) {
270
270
+
e.preventDefault();
271
271
+
alert("Publish date must be in the past or present.");
272
272
+
publishDateInput.focus();
273
273
+
}
274
274
+
}
275
275
+
});
249
276
</script>
250
277
`;
251
278
252
252
-
return c.html(layout(content, { title: "New Document - std.pub", session }));
279
279
+
return c.html(layout(content, { title: "New Document - sitebase", session }));
253
280
});
254
281
255
282
// Handle document creation
256
283
documentRoutes.post("/new", async (c) => {
257
257
-
let session: Session;
258
258
-
try {
259
259
-
session = requireAuth(c);
260
260
-
} catch {
261
261
-
return c.redirect("/auth/login");
262
262
-
}
284
284
+
let session: Session;
285
285
+
try {
286
286
+
session = requireAuth(c);
287
287
+
} catch {
288
288
+
return c.redirect("/auth/login");
289
289
+
}
263
290
264
264
-
const body = await c.req.parseBody();
265
265
-
const title = body.title as string;
266
266
-
const path = (body.path as string) || undefined;
267
267
-
const description = (body.description as string) || undefined;
268
268
-
const content = (body.content as string) || undefined;
269
269
-
const tagsStr = (body.tags as string) || "";
270
270
-
const action = body.action as string;
271
271
-
const publicationUri = body.publicationUri as string;
291
291
+
const body = await c.req.parseBody();
292
292
+
const title = body.title as string;
293
293
+
const path = (body.path as string) || undefined;
294
294
+
const description = (body.description as string) || undefined;
295
295
+
const content = (body.content as string) || undefined;
296
296
+
const tagsStr = (body.tags as string) || "";
297
297
+
const action = body.action as string;
298
298
+
const publicationUri = body.publicationUri as string;
299
299
+
const publishDateStr = body.publishDate as string;
272
300
273
273
-
// Parse tags
274
274
-
let tags = tagsStr
275
275
-
.split(",")
276
276
-
.map((t) => t.trim())
277
277
-
.filter((t) => t);
301
301
+
// Parse tags
302
302
+
let tags = tagsStr
303
303
+
.split(",")
304
304
+
.map((t) => t.trim())
305
305
+
.filter((t) => t);
278
306
279
279
-
// If publishing, remove draft tag
280
280
-
if (action === "publish") {
281
281
-
tags = tags.filter((t) => t !== "draft");
282
282
-
} else if (!tags.includes("draft")) {
283
283
-
tags.push("draft");
284
284
-
}
307
307
+
// If publishing, remove draft tag
308
308
+
if (action === "publish") {
309
309
+
tags = tags.filter((t) => t !== "draft");
310
310
+
} else if (!tags.includes("draft")) {
311
311
+
tags.push("draft");
312
312
+
}
285
313
286
286
-
const now = new Date().toISOString();
314
314
+
const now = new Date().toISOString();
287
315
288
288
-
try {
289
289
-
const rkey = generateTID();
316
316
+
// Determine publish date
317
317
+
let publishedAt: string | undefined;
318
318
+
if (action === "publish") {
319
319
+
if (publishDateStr) {
320
320
+
const parsedDate = new Date(publishDateStr);
321
321
+
if (!isNaN(parsedDate.getTime())) {
322
322
+
publishedAt = parsedDate.toISOString();
323
323
+
}
324
324
+
}
325
325
+
if (!publishedAt) {
326
326
+
publishedAt = now;
327
327
+
}
328
328
+
}
290
329
291
291
-
// Determine site reference
292
292
-
let site = publicationUri;
293
293
-
if (!site) {
294
294
-
// Fall back to a URL if no publication
295
295
-
site = `https://${session.handle}.bsky.social`;
296
296
-
}
330
330
+
try {
331
331
+
const rkey = generateTID();
297
332
298
298
-
const record: Record<string, any> = {
299
299
-
$type: DOCUMENT_COLLECTION,
300
300
-
title,
301
301
-
site,
302
302
-
publishedAt: action === "publish" ? now : now,
303
303
-
updatedAt: now,
304
304
-
};
333
333
+
// Determine site reference
334
334
+
let site = publicationUri;
335
335
+
if (!site) {
336
336
+
// Fall back to a URL if no publication
337
337
+
site = `https://${session.handle}.bsky.social`;
338
338
+
}
305
339
306
306
-
if (path) record.path = path.startsWith("/") ? path : `/${path}`;
307
307
-
if (description) record.description = description;
308
308
-
if (content) {
309
309
-
record.content = createMarkdownContent(content);
310
310
-
record.textContent = content;
311
311
-
}
312
312
-
if (tags.length > 0) record.tags = tags;
340
340
+
const record: Record<string, any> = {
341
341
+
$type: DOCUMENT_COLLECTION,
342
342
+
title,
343
343
+
site,
344
344
+
publishedAt,
345
345
+
updatedAt: now,
346
346
+
};
313
347
314
314
-
await session.agent!.com.atproto.repo.createRecord({
315
315
-
repo: session.did!,
316
316
-
collection: DOCUMENT_COLLECTION,
317
317
-
rkey,
318
318
-
record,
319
319
-
});
348
348
+
if (path) record.path = path.startsWith("/") ? path : `/${path}`;
349
349
+
if (description) record.description = description;
350
350
+
if (content) {
351
351
+
record.content = createMarkdownContent(content);
352
352
+
record.textContent = content;
353
353
+
}
354
354
+
if (tags.length > 0) record.tags = tags;
320
355
321
321
-
return c.redirect(`/documents/${rkey}`);
322
322
-
} catch (error) {
323
323
-
console.error("Error creating document:", error);
324
324
-
return c.redirect("/documents/new?error=create_failed");
325
325
-
}
356
356
+
await session.agent!.com.atproto.repo.createRecord({
357
357
+
repo: session.did!,
358
358
+
collection: DOCUMENT_COLLECTION,
359
359
+
rkey,
360
360
+
record,
361
361
+
});
362
362
+
363
363
+
return c.redirect(`/documents/${rkey}`);
364
364
+
} catch (error) {
365
365
+
console.error("Error creating document:", error);
366
366
+
return c.redirect("/documents/new?error=create_failed");
367
367
+
}
326
368
});
327
369
328
370
// View single document
329
371
documentRoutes.get("/:rkey", async (c) => {
330
330
-
let session: Session;
331
331
-
try {
332
332
-
session = requireAuth(c);
333
333
-
} catch {
334
334
-
return c.redirect("/auth/login");
335
335
-
}
372
372
+
let session: Session;
373
373
+
try {
374
374
+
session = requireAuth(c);
375
375
+
} catch {
376
376
+
return c.redirect("/auth/login");
377
377
+
}
336
378
337
337
-
const rkey = c.req.param("rkey");
379
379
+
const rkey = c.req.param("rkey");
338
380
339
339
-
// Validate rkey format
340
340
-
if (!isValidTID(rkey)) {
341
341
-
return c.redirect("/documents");
342
342
-
}
381
381
+
// Validate rkey format
382
382
+
if (!isValidTID(rkey)) {
383
383
+
return c.redirect("/documents");
384
384
+
}
343
385
344
344
-
try {
345
345
-
const response = await session.agent!.com.atproto.repo.getRecord({
346
346
-
repo: session.did!,
347
347
-
collection: DOCUMENT_COLLECTION,
348
348
-
rkey,
349
349
-
});
386
386
+
try {
387
387
+
const response = await session.agent!.com.atproto.repo.getRecord({
388
388
+
repo: session.did!,
389
389
+
collection: DOCUMENT_COLLECTION,
390
390
+
rkey,
391
391
+
});
350
392
351
351
-
const doc = response.data.value as any;
352
352
-
const isDraft = (doc.tags || []).includes("draft");
353
353
-
const csrfToken = c.get("csrfToken") as string;
393
393
+
const doc = response.data.value as any;
394
394
+
const isDraft = (doc.tags || []).includes("draft");
395
395
+
const csrfToken = c.get("csrfToken") as string;
354
396
355
355
-
const content = html`
397
397
+
const content = html`
356
398
<div class="document-view">
357
399
<div class="document-header">
358
400
<h1>${doc.title}</h1>
359
401
<div class="document-meta">
360
360
-
${
361
361
-
isDraft
362
362
-
? html`<span class="badge badge-draft">Draft</span>`
363
363
-
: html`<span class="badge badge-published">Published</span>`
364
364
-
}
365
365
-
${
366
366
-
doc.publishedAt
367
367
-
? html`<span class="date"
402
402
+
${isDraft
403
403
+
? html`<span class="badge badge-draft">Draft</span>`
404
404
+
: html`<span class="badge badge-published">Published</span>`}
405
405
+
${doc.publishedAt
406
406
+
? html`<span class="date"
368
407
>Published:
369
408
${new Date(doc.publishedAt).toLocaleDateString()}</span
370
409
>`
371
371
-
: ""
372
372
-
}
410
410
+
: ""}
373
411
${doc.path ? html`<span class="path">Path: ${doc.path}</span>` : ""}
374
412
</div>
375
413
</div>
376
414
377
377
-
${
378
378
-
doc.description
379
379
-
? html`<p class="description">${doc.description}</p>`
380
380
-
: ""
381
381
-
}
415
415
+
${doc.description
416
416
+
? html`<p class="description">${doc.description}</p>`
417
417
+
: ""}
382
418
383
419
<div class="document-content">
384
420
${(() => {
385
385
-
const text = getDocumentContentText(doc);
386
386
-
if (!text) return html`<p class="empty">(No content)</p>`;
387
387
-
const htmlContent = marked.parse(text) as string;
388
388
-
return html`<div class="markdown-body">${raw(htmlContent)}</div>`;
389
389
-
})()}
421
421
+
const text = getDocumentContentText(doc);
422
422
+
if (!text) return html`<p class="empty">(No content)</p>`;
423
423
+
const htmlContent = marked.parse(text) as string;
424
424
+
return html`<div class="markdown-body">${raw(htmlContent)}</div>`;
425
425
+
})()}
390
426
</div>
391
427
392
428
<div class="actions">
393
429
<a href="/documents/${rkey}/edit" class="btn btn-primary">Edit</a>
394
394
-
${
395
395
-
isDraft
396
396
-
? html`
430
430
+
${isDraft
431
431
+
? html`
397
432
<form
398
433
action="/documents/${rkey}/publish"
399
434
method="POST"
···
403
438
<button type="submit" class="btn btn-success">Publish</button>
404
439
</form>
405
440
`
406
406
-
: html`
441
441
+
: html`
407
442
<form
408
443
action="/documents/${rkey}/unpublish"
409
444
method="POST"
···
414
449
Unpublish
415
450
</button>
416
451
</form>
417
417
-
`
418
418
-
}
452
452
+
`}
419
453
<form
420
454
action="/documents/${rkey}/delete"
421
455
method="POST"
···
430
464
</div>
431
465
`;
432
466
433
433
-
return c.html(
434
434
-
layout(content, { title: `${doc.title} - std.pub`, session }),
435
435
-
);
436
436
-
} catch (error) {
437
437
-
console.error("Error fetching document:", error);
438
438
-
return c.redirect("/documents");
439
439
-
}
467
467
+
return c.html(
468
468
+
layout(content, { title: `${doc.title} - sitebase`, session }),
469
469
+
);
470
470
+
} catch (error) {
471
471
+
console.error("Error fetching document:", error);
472
472
+
return c.redirect("/documents");
473
473
+
}
440
474
});
441
475
442
476
// Edit document form
443
477
documentRoutes.get("/:rkey/edit", async (c) => {
444
444
-
let session: Session;
445
445
-
try {
446
446
-
session = requireAuth(c);
447
447
-
} catch {
448
448
-
return c.redirect("/auth/login");
449
449
-
}
478
478
+
let session: Session;
479
479
+
try {
480
480
+
session = requireAuth(c);
481
481
+
} catch {
482
482
+
return c.redirect("/auth/login");
483
483
+
}
450
484
451
451
-
const rkey = c.req.param("rkey");
485
485
+
const rkey = c.req.param("rkey");
452
486
453
453
-
if (!isValidTID(rkey)) {
454
454
-
return c.redirect("/documents");
455
455
-
}
487
487
+
if (!isValidTID(rkey)) {
488
488
+
return c.redirect("/documents");
489
489
+
}
456
490
457
457
-
try {
458
458
-
const response = await session.agent!.com.atproto.repo.getRecord({
459
459
-
repo: session.did!,
460
460
-
collection: DOCUMENT_COLLECTION,
461
461
-
rkey,
462
462
-
});
491
491
+
try {
492
492
+
const response = await session.agent!.com.atproto.repo.getRecord({
493
493
+
repo: session.did!,
494
494
+
collection: DOCUMENT_COLLECTION,
495
495
+
rkey,
496
496
+
});
463
497
464
464
-
const doc = response.data.value as any;
465
465
-
const csrfToken = c.get("csrfToken") as string;
498
498
+
const doc = response.data.value as any;
499
499
+
const csrfToken = c.get("csrfToken") as string;
466
500
467
467
-
const content = html`
501
501
+
const content = html`
468
502
<div class="form-page">
469
503
<h1>Edit Document</h1>
470
504
···
530
564
/>
531
565
</div>
532
566
567
567
+
<div class="form-group">
568
568
+
<label for="publishDate">Publish Date</label>
569
569
+
<input
570
570
+
type="datetime-local"
571
571
+
id="publishDate"
572
572
+
name="publishDate"
573
573
+
value="${doc.publishedAt
574
574
+
? new Date(doc.publishedAt).toISOString().slice(0, 16)
575
575
+
: ""}"
576
576
+
/>
577
577
+
<small
578
578
+
>Only past dates allowed. Set to change the published date.</small
579
579
+
>
580
580
+
</div>
581
581
+
533
582
<div class="form-actions">
534
583
<button type="submit" class="btn btn-primary">Save Changes</button>
535
584
<a href="/documents/${rkey}" class="btn btn-secondary">Cancel</a>
536
585
</div>
537
586
</form>
538
587
</div>
588
588
+
589
589
+
<script>
590
590
+
const form = document.querySelector(".document-form");
591
591
+
const publishDateInput = document.getElementById("publishDate");
592
592
+
593
593
+
form.addEventListener("submit", (e) => {
594
594
+
const publishDate = publishDateInput?.value;
595
595
+
if (publishDate) {
596
596
+
const selectedDate = new Date(publishDate);
597
597
+
const now = new Date();
598
598
+
if (selectedDate > now) {
599
599
+
e.preventDefault();
600
600
+
alert("Publish date must be in the past or present.");
601
601
+
publishDateInput.focus();
602
602
+
}
603
603
+
}
604
604
+
});
605
605
+
</script>
539
606
`;
540
607
541
541
-
return c.html(
542
542
-
layout(content, { title: `Edit: ${doc.title} - std.pub`, session }),
543
543
-
);
544
544
-
} catch (error) {
545
545
-
console.error("Error fetching document:", error);
546
546
-
return c.redirect("/documents");
547
547
-
}
608
608
+
return c.html(
609
609
+
layout(content, { title: `Edit: ${doc.title} - sitebase`, session }),
610
610
+
);
611
611
+
} catch (error) {
612
612
+
console.error("Error fetching document:", error);
613
613
+
return c.redirect("/documents");
614
614
+
}
548
615
});
549
616
550
617
// Handle document update
551
618
documentRoutes.post("/:rkey/edit", async (c) => {
552
552
-
let session: Session;
553
553
-
try {
554
554
-
session = requireAuth(c);
555
555
-
} catch {
556
556
-
return c.redirect("/auth/login");
557
557
-
}
619
619
+
let session: Session;
620
620
+
try {
621
621
+
session = requireAuth(c);
622
622
+
} catch {
623
623
+
return c.redirect("/auth/login");
624
624
+
}
558
625
559
559
-
const rkey = c.req.param("rkey");
626
626
+
const rkey = c.req.param("rkey");
560
627
561
561
-
if (!isValidTID(rkey)) {
562
562
-
return c.redirect("/documents");
563
563
-
}
628
628
+
if (!isValidTID(rkey)) {
629
629
+
return c.redirect("/documents");
630
630
+
}
564
631
565
565
-
const body = await c.req.parseBody();
632
632
+
const body = await c.req.parseBody();
566
633
567
567
-
try {
568
568
-
// Get existing record
569
569
-
const existing = await session.agent!.com.atproto.repo.getRecord({
570
570
-
repo: session.did!,
571
571
-
collection: DOCUMENT_COLLECTION,
572
572
-
rkey,
573
573
-
});
634
634
+
try {
635
635
+
// Get existing record
636
636
+
const existing = await session.agent!.com.atproto.repo.getRecord({
637
637
+
repo: session.did!,
638
638
+
collection: DOCUMENT_COLLECTION,
639
639
+
rkey,
640
640
+
});
574
641
575
575
-
const oldDoc = existing.data.value as any;
642
642
+
const oldDoc = existing.data.value as any;
643
643
+
644
644
+
const title = body.title as string;
645
645
+
const path = (body.path as string) || undefined;
646
646
+
const description = (body.description as string) || undefined;
647
647
+
const content = (body.content as string) || undefined;
648
648
+
const tagsStr = (body.tags as string) || "";
649
649
+
const publishDateStr = body.publishDate as string;
650
650
+
const tags = tagsStr
651
651
+
.split(",")
652
652
+
.map((t) => t.trim())
653
653
+
.filter((t) => t);
576
654
577
577
-
const title = body.title as string;
578
578
-
const path = (body.path as string) || undefined;
579
579
-
const description = (body.description as string) || undefined;
580
580
-
const content = (body.content as string) || undefined;
581
581
-
const tagsStr = (body.tags as string) || "";
582
582
-
const tags = tagsStr
583
583
-
.split(",")
584
584
-
.map((t) => t.trim())
585
585
-
.filter((t) => t);
655
655
+
// Determine publishedAt
656
656
+
let publishedAt = oldDoc.publishedAt;
657
657
+
if (publishDateStr) {
658
658
+
const parsedDate = new Date(publishDateStr);
659
659
+
if (!isNaN(parsedDate.getTime())) {
660
660
+
publishedAt = parsedDate.toISOString();
661
661
+
}
662
662
+
}
586
663
587
587
-
const record: Record<string, any> = {
588
588
-
$type: DOCUMENT_COLLECTION,
589
589
-
title,
590
590
-
site: oldDoc.site,
591
591
-
publishedAt: oldDoc.publishedAt,
592
592
-
updatedAt: new Date().toISOString(),
593
593
-
};
664
664
+
const record: Record<string, any> = {
665
665
+
$type: DOCUMENT_COLLECTION,
666
666
+
title,
667
667
+
site: oldDoc.site,
668
668
+
publishedAt,
669
669
+
updatedAt: new Date().toISOString(),
670
670
+
};
594
671
595
595
-
if (path) record.path = path.startsWith("/") ? path : `/${path}`;
596
596
-
if (description) record.description = description;
597
597
-
if (content) {
598
598
-
record.content = createMarkdownContent(content);
599
599
-
record.textContent = content;
600
600
-
}
601
601
-
if (tags.length > 0) record.tags = tags;
672
672
+
if (path) record.path = path.startsWith("/") ? path : `/${path}`;
673
673
+
if (description) record.description = description;
674
674
+
if (content) {
675
675
+
record.content = createMarkdownContent(content);
676
676
+
record.textContent = content;
677
677
+
}
678
678
+
if (tags.length > 0) record.tags = tags;
602
679
603
603
-
await session.agent!.com.atproto.repo.putRecord({
604
604
-
repo: session.did!,
605
605
-
collection: DOCUMENT_COLLECTION,
606
606
-
rkey,
607
607
-
record,
608
608
-
});
680
680
+
await session.agent!.com.atproto.repo.putRecord({
681
681
+
repo: session.did!,
682
682
+
collection: DOCUMENT_COLLECTION,
683
683
+
rkey,
684
684
+
record,
685
685
+
});
609
686
610
610
-
return c.redirect(`/documents/${rkey}`);
611
611
-
} catch (error) {
612
612
-
console.error("Error updating document:", error);
613
613
-
return c.redirect(`/documents/${rkey}/edit?error=update_failed`);
614
614
-
}
687
687
+
return c.redirect(`/documents/${rkey}`);
688
688
+
} catch (error) {
689
689
+
console.error("Error updating document:", error);
690
690
+
return c.redirect(`/documents/${rkey}/edit?error=update_failed`);
691
691
+
}
615
692
});
616
693
617
694
// Publish document
618
695
documentRoutes.post("/:rkey/publish", async (c) => {
619
619
-
let session: Session;
620
620
-
try {
621
621
-
session = requireAuth(c);
622
622
-
} catch {
623
623
-
return c.redirect("/auth/login");
624
624
-
}
696
696
+
let session: Session;
697
697
+
try {
698
698
+
session = requireAuth(c);
699
699
+
} catch {
700
700
+
return c.redirect("/auth/login");
701
701
+
}
625
702
626
626
-
const rkey = c.req.param("rkey");
703
703
+
const rkey = c.req.param("rkey");
627
704
628
628
-
if (!isValidTID(rkey)) {
629
629
-
return c.redirect("/documents");
630
630
-
}
705
705
+
if (!isValidTID(rkey)) {
706
706
+
return c.redirect("/documents");
707
707
+
}
631
708
632
632
-
try {
633
633
-
const existing = await session.agent!.com.atproto.repo.getRecord({
634
634
-
repo: session.did!,
635
635
-
collection: DOCUMENT_COLLECTION,
636
636
-
rkey,
637
637
-
});
709
709
+
try {
710
710
+
const existing = await session.agent!.com.atproto.repo.getRecord({
711
711
+
repo: session.did!,
712
712
+
collection: DOCUMENT_COLLECTION,
713
713
+
rkey,
714
714
+
});
638
715
639
639
-
const doc = existing.data.value as any;
640
640
-
const tags = (doc.tags || []).filter((t: string) => t !== "draft");
716
716
+
const doc = existing.data.value as any;
717
717
+
const tags = (doc.tags || []).filter((t: string) => t !== "draft");
641
718
642
642
-
const record = {
643
643
-
...doc,
644
644
-
tags: tags.length > 0 ? tags : undefined,
645
645
-
publishedAt: doc.publishedAt || new Date().toISOString(),
646
646
-
updatedAt: new Date().toISOString(),
647
647
-
};
719
719
+
const record = {
720
720
+
...doc,
721
721
+
tags: tags.length > 0 ? tags : undefined,
722
722
+
publishedAt: doc.publishedAt || new Date().toISOString(),
723
723
+
updatedAt: new Date().toISOString(),
724
724
+
};
648
725
649
649
-
await session.agent!.com.atproto.repo.putRecord({
650
650
-
repo: session.did!,
651
651
-
collection: DOCUMENT_COLLECTION,
652
652
-
rkey,
653
653
-
record,
654
654
-
});
726
726
+
await session.agent!.com.atproto.repo.putRecord({
727
727
+
repo: session.did!,
728
728
+
collection: DOCUMENT_COLLECTION,
729
729
+
rkey,
730
730
+
record,
731
731
+
});
655
732
656
656
-
return c.redirect(`/documents/${rkey}`);
657
657
-
} catch (error) {
658
658
-
console.error("Error publishing document:", error);
659
659
-
return c.redirect(`/documents/${rkey}?error=publish_failed`);
660
660
-
}
733
733
+
return c.redirect(`/documents/${rkey}`);
734
734
+
} catch (error) {
735
735
+
console.error("Error publishing document:", error);
736
736
+
return c.redirect(`/documents/${rkey}?error=publish_failed`);
737
737
+
}
661
738
});
662
739
663
740
// Unpublish document (add draft tag)
664
741
documentRoutes.post("/:rkey/unpublish", async (c) => {
665
665
-
let session: Session;
666
666
-
try {
667
667
-
session = requireAuth(c);
668
668
-
} catch {
669
669
-
return c.redirect("/auth/login");
670
670
-
}
742
742
+
let session: Session;
743
743
+
try {
744
744
+
session = requireAuth(c);
745
745
+
} catch {
746
746
+
return c.redirect("/auth/login");
747
747
+
}
671
748
672
672
-
const rkey = c.req.param("rkey");
749
749
+
const rkey = c.req.param("rkey");
673
750
674
674
-
if (!isValidTID(rkey)) {
675
675
-
return c.redirect("/documents");
676
676
-
}
751
751
+
if (!isValidTID(rkey)) {
752
752
+
return c.redirect("/documents");
753
753
+
}
677
754
678
678
-
try {
679
679
-
const existing = await session.agent!.com.atproto.repo.getRecord({
680
680
-
repo: session.did!,
681
681
-
collection: DOCUMENT_COLLECTION,
682
682
-
rkey,
683
683
-
});
755
755
+
try {
756
756
+
const existing = await session.agent!.com.atproto.repo.getRecord({
757
757
+
repo: session.did!,
758
758
+
collection: DOCUMENT_COLLECTION,
759
759
+
rkey,
760
760
+
});
684
761
685
685
-
const doc = existing.data.value as any;
686
686
-
const tags = [...(doc.tags || []), "draft"];
762
762
+
const doc = existing.data.value as any;
763
763
+
const tags = [...(doc.tags || []), "draft"];
687
764
688
688
-
const record = {
689
689
-
...doc,
690
690
-
tags,
691
691
-
updatedAt: new Date().toISOString(),
692
692
-
};
765
765
+
const record = {
766
766
+
...doc,
767
767
+
tags,
768
768
+
updatedAt: new Date().toISOString(),
769
769
+
};
693
770
694
694
-
await session.agent!.com.atproto.repo.putRecord({
695
695
-
repo: session.did!,
696
696
-
collection: DOCUMENT_COLLECTION,
697
697
-
rkey,
698
698
-
record,
699
699
-
});
771
771
+
await session.agent!.com.atproto.repo.putRecord({
772
772
+
repo: session.did!,
773
773
+
collection: DOCUMENT_COLLECTION,
774
774
+
rkey,
775
775
+
record,
776
776
+
});
700
777
701
701
-
return c.redirect(`/documents/${rkey}`);
702
702
-
} catch (error) {
703
703
-
console.error("Error unpublishing document:", error);
704
704
-
return c.redirect(`/documents/${rkey}?error=unpublish_failed`);
705
705
-
}
778
778
+
return c.redirect(`/documents/${rkey}`);
779
779
+
} catch (error) {
780
780
+
console.error("Error unpublishing document:", error);
781
781
+
return c.redirect(`/documents/${rkey}?error=unpublish_failed`);
782
782
+
}
706
783
});
707
784
708
785
// Delete document
709
786
documentRoutes.post("/:rkey/delete", async (c) => {
710
710
-
let session: Session;
711
711
-
try {
712
712
-
session = requireAuth(c);
713
713
-
} catch {
714
714
-
return c.redirect("/auth/login");
715
715
-
}
787
787
+
let session: Session;
788
788
+
try {
789
789
+
session = requireAuth(c);
790
790
+
} catch {
791
791
+
return c.redirect("/auth/login");
792
792
+
}
716
793
717
717
-
const rkey = c.req.param("rkey");
794
794
+
const rkey = c.req.param("rkey");
718
795
719
719
-
if (!isValidTID(rkey)) {
720
720
-
return c.redirect("/documents");
721
721
-
}
796
796
+
if (!isValidTID(rkey)) {
797
797
+
return c.redirect("/documents");
798
798
+
}
722
799
723
723
-
try {
724
724
-
await session.agent!.com.atproto.repo.deleteRecord({
725
725
-
repo: session.did!,
726
726
-
collection: DOCUMENT_COLLECTION,
727
727
-
rkey,
728
728
-
});
800
800
+
try {
801
801
+
await session.agent!.com.atproto.repo.deleteRecord({
802
802
+
repo: session.did!,
803
803
+
collection: DOCUMENT_COLLECTION,
804
804
+
rkey,
805
805
+
});
729
806
730
730
-
return c.redirect("/documents");
731
731
-
} catch (error) {
732
732
-
console.error("Error deleting document:", error);
733
733
-
return c.redirect(`/documents/${rkey}?error=delete_failed`);
734
734
-
}
807
807
+
return c.redirect("/documents");
808
808
+
} catch (error) {
809
809
+
console.error("Error deleting document:", error);
810
810
+
return c.redirect(`/documents/${rkey}?error=delete_failed`);
811
811
+
}
735
812
});
736
813
737
814
// Generate a TID (timestamp-based ID)
738
815
function generateTID(): string {
739
739
-
const now = Date.now() * 1000;
740
740
-
const clockId = Math.floor(Math.random() * 1024);
741
741
-
const tid = (BigInt(now) << 10n) | BigInt(clockId);
742
742
-
return tid.toString(36).padStart(13, "0");
816
816
+
const now = Date.now() * 1000;
817
817
+
const clockId = Math.floor(Math.random() * 1024);
818
818
+
const tid = (BigInt(now) << 10n) | BigInt(clockId);
819
819
+
return tid.toString(36).padStart(13, "0");
743
820
}
+129
-131
packages/web/src/routes/publication.ts
···
10
10
11
11
// View/manage publication
12
12
publicationRoutes.get("/", async (c) => {
13
13
-
let session: Session;
14
14
-
try {
15
15
-
session = requireAuth(c);
16
16
-
} catch {
17
17
-
return c.redirect("/auth/login");
18
18
-
}
13
13
+
let session: Session;
14
14
+
try {
15
15
+
session = requireAuth(c);
16
16
+
} catch {
17
17
+
return c.redirect("/auth/login");
18
18
+
}
19
19
20
20
-
try {
21
21
-
// Fetch existing publication
22
22
-
const response = await session.agent!.com.atproto.repo.listRecords({
23
23
-
repo: session.did!,
24
24
-
collection: PUBLICATION_COLLECTION,
25
25
-
limit: 1,
26
26
-
});
20
20
+
try {
21
21
+
// Fetch existing publication
22
22
+
const response = await session.agent!.com.atproto.repo.listRecords({
23
23
+
repo: session.did!,
24
24
+
collection: PUBLICATION_COLLECTION,
25
25
+
limit: 1,
26
26
+
});
27
27
28
28
-
const publication = response.data.records[0];
28
28
+
const publication = response.data.records[0];
29
29
30
30
-
if (publication) {
31
31
-
const pub = publication.value as any;
32
32
-
const content = html`
30
30
+
if (publication) {
31
31
+
const pub = publication.value as any;
32
32
+
const content = html`
33
33
<div class="publication">
34
34
<h1>Your Publication</h1>
35
35
···
38
38
<p class="url">
39
39
<a href="${pub.url}" target="_blank">${pub.url}</a>
40
40
</p>
41
41
-
${
42
42
-
pub.description
43
43
-
? html`<p class="description">${pub.description}</p>`
44
44
-
: ""
45
45
-
}
41
41
+
${pub.description
42
42
+
? html`<p class="description">${pub.description}</p>`
43
43
+
: ""}
46
44
</div>
47
45
48
46
<div class="actions">
···
52
50
</div>
53
51
</div>
54
52
`;
55
55
-
return c.html(
56
56
-
layout(content, { title: "Publication - std.pub", session }),
57
57
-
);
58
58
-
}
53
53
+
return c.html(
54
54
+
layout(content, { title: "Publication - sitebase", session }),
55
55
+
);
56
56
+
}
59
57
60
60
-
// No publication exists, show create form
61
61
-
return c.redirect("/publication/new");
62
62
-
} catch (error) {
63
63
-
console.error("Error fetching publication:", error);
64
64
-
return c.redirect("/publication/new");
65
65
-
}
58
58
+
// No publication exists, show create form
59
59
+
return c.redirect("/publication/new");
60
60
+
} catch (error) {
61
61
+
console.error("Error fetching publication:", error);
62
62
+
return c.redirect("/publication/new");
63
63
+
}
66
64
});
67
65
68
66
// New publication form
69
67
publicationRoutes.get("/new", async (c) => {
70
70
-
let session: Session;
71
71
-
try {
72
72
-
session = requireAuth(c);
73
73
-
} catch {
74
74
-
return c.redirect("/auth/login");
75
75
-
}
68
68
+
let session: Session;
69
69
+
try {
70
70
+
session = requireAuth(c);
71
71
+
} catch {
72
72
+
return c.redirect("/auth/login");
73
73
+
}
76
74
77
77
-
const csrfToken = c.get("csrfToken") as string;
75
75
+
const csrfToken = c.get("csrfToken") as string;
78
76
79
79
-
const content = html`
77
77
+
const content = html`
80
78
<div class="form-page">
81
79
<h1>Create Publication</h1>
82
80
···
118
116
</div>
119
117
`;
120
118
121
121
-
return c.html(
122
122
-
layout(content, { title: "New Publication - std.pub", session }),
123
123
-
);
119
119
+
return c.html(
120
120
+
layout(content, { title: "New Publication - sitebase", session }),
121
121
+
);
124
122
});
125
123
126
124
// Handle publication creation
127
125
publicationRoutes.post("/new", async (c) => {
128
128
-
let session: Session;
129
129
-
try {
130
130
-
session = requireAuth(c);
131
131
-
} catch {
132
132
-
return c.redirect("/auth/login");
133
133
-
}
126
126
+
let session: Session;
127
127
+
try {
128
128
+
session = requireAuth(c);
129
129
+
} catch {
130
130
+
return c.redirect("/auth/login");
131
131
+
}
134
132
135
135
-
const body = await c.req.parseBody();
136
136
-
const name = body.name as string;
137
137
-
const url = (body.url as string).replace(/\/$/, ""); // Remove trailing slash
138
138
-
const description = (body.description as string) || undefined;
133
133
+
const body = await c.req.parseBody();
134
134
+
const name = body.name as string;
135
135
+
const url = (body.url as string).replace(/\/$/, ""); // Remove trailing slash
136
136
+
const description = (body.description as string) || undefined;
139
137
140
140
-
try {
141
141
-
// Generate a TID for the record key
142
142
-
const rkey = generateTID();
138
138
+
try {
139
139
+
// Generate a TID for the record key
140
140
+
const rkey = generateTID();
143
141
144
144
-
await session.agent!.com.atproto.repo.createRecord({
145
145
-
repo: session.did!,
146
146
-
collection: PUBLICATION_COLLECTION,
147
147
-
rkey,
148
148
-
record: {
149
149
-
$type: PUBLICATION_COLLECTION,
150
150
-
name,
151
151
-
url,
152
152
-
...(description && { description }),
153
153
-
},
154
154
-
});
142
142
+
await session.agent!.com.atproto.repo.createRecord({
143
143
+
repo: session.did!,
144
144
+
collection: PUBLICATION_COLLECTION,
145
145
+
rkey,
146
146
+
record: {
147
147
+
$type: PUBLICATION_COLLECTION,
148
148
+
name,
149
149
+
url,
150
150
+
...(description && { description }),
151
151
+
},
152
152
+
});
155
153
156
156
-
return c.redirect("/publication");
157
157
-
} catch (error) {
158
158
-
console.error("Error creating publication:", error);
159
159
-
return c.redirect("/publication/new?error=create_failed");
160
160
-
}
154
154
+
return c.redirect("/publication");
155
155
+
} catch (error) {
156
156
+
console.error("Error creating publication:", error);
157
157
+
return c.redirect("/publication/new?error=create_failed");
158
158
+
}
161
159
});
162
160
163
161
// Edit publication form
164
162
publicationRoutes.get("/edit", async (c) => {
165
165
-
let session: Session;
166
166
-
try {
167
167
-
session = requireAuth(c);
168
168
-
} catch {
169
169
-
return c.redirect("/auth/login");
170
170
-
}
163
163
+
let session: Session;
164
164
+
try {
165
165
+
session = requireAuth(c);
166
166
+
} catch {
167
167
+
return c.redirect("/auth/login");
168
168
+
}
171
169
172
172
-
try {
173
173
-
const response = await session.agent!.com.atproto.repo.listRecords({
174
174
-
repo: session.did!,
175
175
-
collection: PUBLICATION_COLLECTION,
176
176
-
limit: 1,
177
177
-
});
170
170
+
try {
171
171
+
const response = await session.agent!.com.atproto.repo.listRecords({
172
172
+
repo: session.did!,
173
173
+
collection: PUBLICATION_COLLECTION,
174
174
+
limit: 1,
175
175
+
});
178
176
179
179
-
const publication = response.data.records[0];
180
180
-
if (!publication) {
181
181
-
return c.redirect("/publication/new");
182
182
-
}
177
177
+
const publication = response.data.records[0];
178
178
+
if (!publication) {
179
179
+
return c.redirect("/publication/new");
180
180
+
}
183
181
184
184
-
const pub = publication.value as any;
185
185
-
const rkey = publication.uri.split("/").pop();
182
182
+
const pub = publication.value as any;
183
183
+
const rkey = publication.uri.split("/").pop();
186
184
187
187
-
const csrfToken = c.get("csrfToken") as string;
185
185
+
const csrfToken = c.get("csrfToken") as string;
188
186
189
189
-
const content = html`
187
187
+
const content = html`
190
188
<div class="form-page">
191
189
<h1>Edit Publication</h1>
192
190
···
229
227
</div>
230
228
`;
231
229
232
232
-
return c.html(
233
233
-
layout(content, { title: "Edit Publication - std.pub", session }),
234
234
-
);
235
235
-
} catch (error) {
236
236
-
console.error("Error fetching publication:", error);
237
237
-
return c.redirect("/publication");
238
238
-
}
230
230
+
return c.html(
231
231
+
layout(content, { title: "Edit Publication - sitebase", session }),
232
232
+
);
233
233
+
} catch (error) {
234
234
+
console.error("Error fetching publication:", error);
235
235
+
return c.redirect("/publication");
236
236
+
}
239
237
});
240
238
241
239
// Handle publication update
242
240
publicationRoutes.post("/edit", async (c) => {
243
243
-
let session: Session;
244
244
-
try {
245
245
-
session = requireAuth(c);
246
246
-
} catch {
247
247
-
return c.redirect("/auth/login");
248
248
-
}
241
241
+
let session: Session;
242
242
+
try {
243
243
+
session = requireAuth(c);
244
244
+
} catch {
245
245
+
return c.redirect("/auth/login");
246
246
+
}
249
247
250
250
-
const body = await c.req.parseBody();
251
251
-
const rkey = body.rkey as string;
252
252
-
const name = body.name as string;
253
253
-
const url = (body.url as string).replace(/\/$/, "");
254
254
-
const description = (body.description as string) || undefined;
248
248
+
const body = await c.req.parseBody();
249
249
+
const rkey = body.rkey as string;
250
250
+
const name = body.name as string;
251
251
+
const url = (body.url as string).replace(/\/$/, "");
252
252
+
const description = (body.description as string) || undefined;
255
253
256
256
-
try {
257
257
-
await session.agent!.com.atproto.repo.putRecord({
258
258
-
repo: session.did!,
259
259
-
collection: PUBLICATION_COLLECTION,
260
260
-
rkey,
261
261
-
record: {
262
262
-
$type: PUBLICATION_COLLECTION,
263
263
-
name,
264
264
-
url,
265
265
-
...(description && { description }),
266
266
-
},
267
267
-
});
254
254
+
try {
255
255
+
await session.agent!.com.atproto.repo.putRecord({
256
256
+
repo: session.did!,
257
257
+
collection: PUBLICATION_COLLECTION,
258
258
+
rkey,
259
259
+
record: {
260
260
+
$type: PUBLICATION_COLLECTION,
261
261
+
name,
262
262
+
url,
263
263
+
...(description && { description }),
264
264
+
},
265
265
+
});
268
266
269
269
-
return c.redirect("/publication");
270
270
-
} catch (error) {
271
271
-
console.error("Error updating publication:", error);
272
272
-
return c.redirect("/publication/edit?error=update_failed");
273
273
-
}
267
267
+
return c.redirect("/publication");
268
268
+
} catch (error) {
269
269
+
console.error("Error updating publication:", error);
270
270
+
return c.redirect("/publication/edit?error=update_failed");
271
271
+
}
274
272
});
275
273
276
274
// Generate a TID (timestamp-based ID)
277
275
function generateTID(): string {
278
278
-
const now = Date.now() * 1000; // microseconds
279
279
-
const clockId = Math.floor(Math.random() * 1024);
280
280
-
const tid = (BigInt(now) << 10n) | BigInt(clockId);
281
281
-
return tid.toString(36).padStart(13, "0");
276
276
+
const now = Date.now() * 1000; // microseconds
277
277
+
const clockId = Math.floor(Math.random() * 1024);
278
278
+
const tid = (BigInt(now) << 10n) | BigInt(clockId);
279
279
+
return tid.toString(36).padStart(13, "0");
282
280
}
+5
-5
packages/web/src/views/home.ts
···
2
2
import type { Session } from "../lib/session";
3
3
4
4
export function homePage(session: Session) {
5
5
-
if (session.did) {
6
6
-
return html`
5
5
+
if (session.did) {
6
6
+
return html`
7
7
<div class="dashboard">
8
8
<h1>Welcome, @${session.handle}</h1>
9
9
<p>Manage your standard.site publication and documents.</p>
···
15
15
</div>
16
16
</div>
17
17
`;
18
18
-
}
18
18
+
}
19
19
20
20
-
return html`
20
20
+
return html`
21
21
<div class="hero">
22
22
-
<h1>std.pub</h1>
22
22
+
<h1>sitebase</h1>
23
23
<p>
24
24
A minimal web UI for managing standard.site publications and documents
25
25
in your Bluesky PDS.
+10
-12
packages/web/src/views/layouts/main.ts
···
2
2
import type { Session } from "../../lib/session";
3
3
4
4
interface LayoutOptions {
5
5
-
title?: string;
6
6
-
session?: Session;
7
7
-
csrfToken?: string;
5
5
+
title?: string;
6
6
+
session?: Session;
7
7
+
csrfToken?: string;
8
8
}
9
9
10
10
export function layout(content: string, options: LayoutOptions = {}) {
11
11
-
const { title = "std.pub", session } = options;
11
11
+
const { title = "sitebase", session } = options;
12
12
13
13
-
return html`
13
13
+
return html`
14
14
<!DOCTYPE html>
15
15
<html lang="en">
16
16
<head>
···
22
22
<body>
23
23
<header class="header">
24
24
<nav class="nav">
25
25
-
<a href="/" class="logo">std.pub</a>
25
25
+
<a href="/" class="logo">sitebase</a>
26
26
<div class="nav-links">
27
27
-
${
28
28
-
session?.did
29
29
-
? html`
27
27
+
${session?.did
28
28
+
? html`
30
29
<a href="/publication">Publication</a>
31
30
<a href="/documents">Documents</a>
32
31
<span class="handle">@${session.handle}</span>
33
32
<a href="/auth/logout">Logout</a>
34
33
`
35
35
-
: html` <a href="/auth/login">Login with Bluesky</a> `
36
36
-
}
34
34
+
: html` <a href="/auth/login">Login with Bluesky</a> `}
37
35
</div>
38
36
</nav>
39
37
</header>
40
38
<main class="main">${content}</main>
41
39
<footer class="footer">
42
40
<p>
43
43
-
std.pub - Manage your
41
41
+
sitebase - Manage your
44
42
<a href="https://standard.site">standard.site</a> content
45
43
</p>
46
44
</footer>